commit 44c07b7ab8c85e99ae32ea1e7fe2c586eff92211 Author: cinnaboot Date: Mon Oct 12 14:18:55 2020 -0400 initial commit new file: .gitignore new file: .gitmodules new file: Makefile new file: README.txt new file: examples/Makefile new file: examples/main.cpp new file: ext/stb_libs new file: include/camera.h new file: include/dumbLog.h new file: include/entity.h new file: include/mesh.h new file: include/platform_wait_for_vblank.h new file: include/render_group.h new file: include/renderer.h new file: include/util.h new file: src/camera.cpp new file: src/dumbLog.cpp new file: src/entity.cpp new file: src/mesh.cpp new file: src/render_group.cpp new file: src/renderer.cpp new file: src/util.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43b6705 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +build/ +doc/ +msvc/.vs/ +msvc/x64/ +tags +*.swp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4f320ec --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ext/stb_libs"] + path = ext/stb_libs + url = https://github.com/nothings/stb.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c2a250f --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ + +CXX = g++ +CXXFLAGS = -std=c++11 -g -ggdb3 -Wall -Iinclude -I/usr/include/SDL2 -Iext/stb_libs +EXAMPLEDIR = examples +OBJDIR = build +SRCDIR = src +LIBNAME = libTangerine.a + +RENDER_SOURCES = $(wildcard $(SRCDIR)/*.cpp) +RENDER_OBJECTS = $(patsubst $(SRCDIR)/%.cpp,$(OBJDIR)/%.o,$(RENDER_SOURCES)) + + +all: $(RENDER_OBJECTS) + ar -crs $(OBJDIR)/$(LIBNAME) $(RENDER_OBJECTS) +.PHONY: all + +-include $(OBJDIR)/*.d + +$(RENDER_OBJECTS): $(OBJDIR)/%.o : $(SRCDIR)/%.cpp + $(CXX) $(CXXFLAGS) -c -MMD $< -o $@ + +examples: + $(MAKE) -C $(EXAMPLEDIR) +.PHONY: examples + +clean: + rm $(OBJDIR)/* +.PHONY: clean diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..3816124 --- /dev/null +++ b/README.txt @@ -0,0 +1,11 @@ + +Tangerine + + A small OpenGL 3+ renderer and game engine + NOTE: This is still a work in progress + +Dependencies: + glew + SDL2 + assimp + stb_image diff --git a/examples/Makefile b/examples/Makefile new file mode 100644 index 0000000..14be360 --- /dev/null +++ b/examples/Makefile @@ -0,0 +1,12 @@ + +CXX = g++ +CXXFLAGS = -std=c++11 -g -ggdb3 -Wall -I../include -I/usr/include/SDL2 -I../ext/stb_libs +LDFLAGS = -lSDL2 -lGLEW -lGL -lassimp +OBJDIR = ../build +BINARY = example +BINDIR = bin + +all: + $(CXX) $(CXXFLAGS) -c -MMD main.cpp -o $(OBJDIR)/$(BINARY).o + mkdir -p $(BINDIR) + $(CXX) -o $(BINDIR)/$(BINARY) $(LDFLAGS) $(OBJDIR)/$(BINARY).o $(OBJDIR)/libTangerine.a diff --git a/examples/main.cpp b/examples/main.cpp new file mode 100644 index 0000000..026ea05 --- /dev/null +++ b/examples/main.cpp @@ -0,0 +1,9 @@ + +#include "renderer.h" + + +int +main() +{ + return 0; +} diff --git a/ext/stb_libs b/ext/stb_libs new file mode 160000 index 0000000..b42009b --- /dev/null +++ b/ext/stb_libs @@ -0,0 +1 @@ +Subproject commit b42009b3b9d4ca35bc703f5310eedc74f584be58 diff --git a/include/camera.h b/include/camera.h new file mode 100644 index 0000000..b545645 --- /dev/null +++ b/include/camera.h @@ -0,0 +1,45 @@ + +#pragma once +#include +#include +#include "util.h" + + +struct camera +{ + float hAngle; + float vAngle; + + glm::vec3 position; + glm::vec3 forward; + glm::vec3 up; + glm::vec3 left; + glm::vec3 target; + glm::vec3 world_up; + + glm::mat4 model; + glm::mat4 view; + glm::mat4 projection; + glm::mat4 MVP; +}; + +enum projection_type +{ + PERSPECTIVE, + ORTHOGRAPHIC, +}; + + +v2f cameraUnproject(camera& cam, int x, int y, int vp_width, int vp_height); + +v3f cameraCreateRay(camera& cam, v2i vp_coords, v2i vp_dims); + +bool cameraIntersectPlane(camera& cam, v3f ray, v3f plane_origin, v3f plane_normal, v3f& intersection); + +void cameraInitPerspective(camera& cam, glm::vec3 position, glm::vec3 target, glm::vec3 world_up); + +void cameraMove(camera& cam, bool up, bool left, bool down, bool right, bool forward, bool backward); + +void cameraRotate(camera& cam, int32 xrel, int32 yrel); + +void cameraRoll(camera& cam, bool CW, bool CCW); diff --git a/include/dumbLog.h b/include/dumbLog.h new file mode 100644 index 0000000..44a43a8 --- /dev/null +++ b/include/dumbLog.h @@ -0,0 +1,28 @@ + +#include +#include + + +enum log_level { + Error, + Warning, + Info, + Debug +}; + +struct dumbLog +{ + std::ostream* OUT = &std::cout; + void setOutputStream(std::ostream* out); + const char* logLevelToString(log_level level); + std::tm* getCurrentTime(); + int getCurrentMS(); +}; + +static dumbLog logger; + +#define LOG(level) *logger.OUT \ + << std::put_time(logger.getCurrentTime(), "%F %T.") << logger.getCurrentMS() << " " \ + << "[" << logger.logLevelToString(level) << "] " \ + << "(" << __FUNCTION__ << ") " + diff --git a/include/entity.h b/include/entity.h new file mode 100644 index 0000000..3605345 --- /dev/null +++ b/include/entity.h @@ -0,0 +1,39 @@ + +#pragma once + +#include +#include + +#include "mesh.h" +#include "render_group.h" + +struct Entity +{ + glm::mat4 world_transform; + glm::vec3 scale; + glm::vec3 translation; + glm::vec4 rotation; + + meMeshGroup mesh_group; + render_group* ren_group; + + char model_filename[256]; +}; + +inline void +entTranslate(Entity& e, glm::vec3 v) +{ + e.world_transform = glm::translate(e.world_transform, v); +} + +inline void +entScale(Entity& e, glm::vec3 v) +{ + e.world_transform = glm::scale(e.world_transform, v); +} + +bool entInit(Entity& e, const char* data_dir, rg_shader_program& shader); + +void entFree(Entity& e); + +void entSetWorldPosition(Entity& e, float x, float y, float z); diff --git a/include/mesh.h b/include/mesh.h new file mode 100644 index 0000000..dde1486 --- /dev/null +++ b/include/mesh.h @@ -0,0 +1,43 @@ +/* + * mesh.h + * - wrapper for assimp http://www.assimp.org + */ + +#pragma once + +#include + +#include "util.h" + +struct meMeshInfo +{ + glm::mat4 model_transform; + uint num_vertices; + glm::vec3* vertices; + glm::vec3* normals; + glm::vec3* texture_coords; // NOTE: using vec3 to stay aligned with other props + uint num_indices; + uint* indices; + glm::vec3 diffuse_color; + + bool use_texture; + util_image diffuse_texture; +}; + +struct meMeshGroup +{ + bool use_normals; + uint num_meshes; + meMeshInfo** meshes; + // animation/bonemapping info here... +}; + + +bool meInitAssimp(); + +bool meLoadFromFile(meMeshGroup& mesh_group, const char* data_dir, const char* filename); + +void meFreeMeshGroup(meMeshGroup& mesh_group); + +void meShutdownAssimp(); + diff --git a/include/platform_wait_for_vblank.h b/include/platform_wait_for_vblank.h new file mode 100644 index 0000000..d11a353 --- /dev/null +++ b/include/platform_wait_for_vblank.h @@ -0,0 +1,102 @@ + +// attempt to fix vsync with opengl on windows +// https://bugs.chromium.org/p/chromium/issues/detail?id=467617 +// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/d3dkmthk/nf-d3dkmthk-d3dkmtwaitforverticalblankevent +// NOTE: requires installing windows driver kit addon for msvc +// TODO: not working with hdc provided by SDL, try enumerating display devices as described here: +// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/d3dkmthk/nf-d3dkmthk-d3dkmtopenadapterfromhdc +// NOTE: the above seems to make some difference in lowering cpu usage, but cpu stays at max frequency even when relatively idle. + +// TODO: if d3dkmt... doensn't work, try DWMFlush() https://docs.microsoft.com/en-us/windows/desktop/api/dwmapi/nf-dwmapi-dwmflush +// glfw uses this https://github.com/glfw/glfw/blob/master/src/wgl_context.c +// +// if that doesn't work... https://docs.microsoft.com/en-us/windows/desktop/api/timeapi/nf-timeapi-timebeginperiod +// to increase schedular granularity. can then use sleep(1) for 1ms resolution + +#pragma once + +#include + +#include "SDL_syswm.h" + +#if defined(_WIN32) +#include "windows.h" +#include "d3dkmthk.h" + +typedef uint32_t D3DKMT_HANDLE; +typedef struct { + D3DKMT_HANDLE hAdapter = NULL; + UINT vidID = 0; +} platform_win_device_handles; +platform_win_device_handles g_platform_win_device_handles; + +#endif // _WIN32 + + +inline bool +platform_init(SDL_Window* window) +{ +#if defined(_WIN32) + D3DKMT_HANDLE hAdapter = NULL; + UINT vidID = 0; + D3DKMT_OPENADAPTERFROMHDC OpenAdapterData; + DISPLAY_DEVICE dd; + HDC hdc; + + memset(&dd, 0, sizeof(dd)); + dd.cb = sizeof dd; + + for (int i = 0; EnumDisplayDevicesA(NULL, i, &dd, 0); ++i) { + if (dd.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) + break; + } + + hdc = CreateDC(NULL, dd.DeviceName, NULL, NULL); + if (hdc == NULL) + return false; + + OpenAdapterData.hDc = hdc; + if ((D3DKMTOpenAdapterFromHdc(&OpenAdapterData)) >= 0) { + DeleteDC(hdc); + g_platform_win_device_handles.hAdapter = OpenAdapterData.hAdapter; + g_platform_win_device_handles.vidID = OpenAdapterData.VidPnSourceId; + return true; + } + + DeleteDC(hdc); + return false; +#endif // _WIN32 + + return true; +} + +inline void +platform_wait_for_vblank(bool wait=true) +{ + if (!wait) return; + +#if defined(_WIN32) + _D3DKMT_WAITFORVERTICALBLANKEVENT waitForVBlankData; + memset(&waitForVBlankData, 0, sizeof(waitForVBlankData)); + waitForVBlankData.hAdapter = g_platform_win_device_handles.hAdapter; + waitForVBlankData.VidPnSourceId = g_platform_win_device_handles.vidID; + NTSTATUS ret = D3DKMTWaitForVerticalBlankEvent(&waitForVBlankData); + + switch (ret) { + case 0x00000000: // STATUS_SUCCESS + //OutputDebugString("Success\r\n"); + break; + //case STATUS_DEVICE_REMOVED: + // OutputDebugString("STATUS_DEVICE_REMOVED\r\n"); + // break; + case STATUS_INVALID_PARAMETER: + OutputDebugString("STATUS_INVALID_PARAMETER\r\n"); + break; + default: + OutputDebugString("????"); + } +#endif // _WIN32 + + // TODO: implement/test for linux +} + diff --git a/include/render_group.h b/include/render_group.h new file mode 100644 index 0000000..7aa429b --- /dev/null +++ b/include/render_group.h @@ -0,0 +1,96 @@ + +#pragma once + +#include +#include + +#include "util.h" + + +// TODO: can these structs be used as opaque pointers? +struct gl_buffer +{ + GLuint buffer_id; + uint count; // NOTE: number of elements in buffer + uint max_count; // NOTE: maxium number of elements buffer can fit + GLfloat* buffer; +}; + +struct gl_index_buffer +{ + GLuint buffer_id; + uint count; // NOTE: number of elements in buffer + uint max_count; // NOTE: maxium number of elements buffer can fit + uint* buffer; +}; + +struct render_object +{ + // TODO: can remove these once we're using the global color palatte for all textures + bool use_texture; + GLuint tex_id; + + gl_buffer vertex_buffer; + gl_buffer normal_buffer; + gl_buffer uv_buffer; + gl_index_buffer index_buffer; +}; + +struct rg_shader_program +{ + GLuint program_id; + + GLuint model_matrix_id; + GLuint view_matrix_id; + GLuint projection_matrix_id; + GLuint normal_matrix_id; + + GLuint vertex_array_id; + GLuint sampler_id; + GLuint num_lights_id; +}; + +struct render_group +{ + uint num_objects; + render_object** render_objects; + rg_shader_program shader; + bool use_normals; + bool draw_indexed; + GLenum draw_mode = GL_TRIANGLES; + char name[64]; +}; + +// TODO: maybe pull out to seperate header +struct rg_point_light +{ + uint light_ID; + glm::vec3 position; + glm::vec3 color; + float intensity; +}; + +render_object * rgAllocateRenderObject(uint buffer_len, uint index_len = 0); + +// NOTE: initializes a render_group with 1 render_object allocated +render_group* rgInitSingle(rg_shader_program& sp, uint vertex_buffer_len, + bool use_normals = false, uint index_buffer_len = 0, + GLenum draw_mode = GL_TRIANGLES); + +bool rgInitShaderProgram(rg_shader_program& sp, const char * vertex_code, const char * frag_code); + +bool rgInitGLTexture(GLuint& tex_id, util_image image); + +void rgInitGLFloatBuffer(gl_buffer* buffer, GLenum usage, GLenum target); + +void rgInitGLIndexBuffer(gl_index_buffer* index_buffer, GLenum usage, GLenum target); + +void rgUpdateGLBuffer(gl_buffer& buffer); + +void rgDraw(render_group* rg, glm::mat4 model_matrix, + glm::mat4 view_matrix, glm::mat4 projection_matrix, + rg_point_light* lights, uint num_lights, + bool update_vertex_data = false); + +void rgFree(render_group* rg); + diff --git a/include/renderer.h b/include/renderer.h new file mode 100644 index 0000000..0b27ec8 --- /dev/null +++ b/include/renderer.h @@ -0,0 +1,58 @@ + +#pragma once + +#include + +#if defined (_WIN32) + #include +#else + #include +#endif +#include +#include + +#include "camera.h" +#include "entity.h" +#include "render_group.h" +#include "util.h" + + +struct SDL_Handles +{ + SDL_Window *window; + SDL_GLContext glContext; + SDL_DisplayMode currentDisplayMode; + std::vector texSurfaces; +}; + +struct rg_point_light; + +struct render_state +{ + v2i viewport_dims; + bool is_debug_draw; + SDL_Handles handles; + camera cam; + util_image palette_image; + // TODO: this needs to be initialized outside struct because calloc + GLuint palette_id = UINT_MAX; + + render_group* filled_hex_render_group; + render_group* hex_line_render_group; + render_group* debug_render_group; + // NOTE: entity render groups are stored on the entity object + rg_shader_program default_shader; + + rg_point_light* lights; + uint num_lights; + uint max_lights; +}; + +bool renInit(render_state* rs); + +void renFreeBuffers(render_state* rs); + +void renRenderFrame(render_state* rs, Entity* entities, uint32 entity_count); + +//void renRenderDebug(render_state* rs, std::vector &vertices); + diff --git a/include/util.h b/include/util.h new file mode 100644 index 0000000..263111a --- /dev/null +++ b/include/util.h @@ -0,0 +1,142 @@ + +#pragma once + +#include +#include + + +typedef float real32; +typedef float GLfloat; +typedef double real64; +typedef int32_t bool32; +typedef int32_t int32; +typedef int64_t int64; +typedef uint8_t uint8; +typedef uint32_t uint32; +typedef uint32_t uint; + + +struct v2f +{ + v2f(): x(0), y(0) {} + v2f(real64 a, real64 b): x(a), y(b) {} + real64 x; + real64 y; +}; + +struct v2i +{ + v2i(int a, int b): x(a), y(b) {} + v2i() : x(0), y(0) {} + int32 x; + int32 y; +}; + +struct v3f +{ + v3f(): x(0), y(0), z(0) {} + v3f(real64 a, real64 b, real64 c): x(a), y(b), z(c) {} + real64 x; + real64 y; + real64 z; +}; + +struct v3i +{ + int32 x; + int32 y; + int32 z; +}; + +struct v4i +{ + int32 x0; + int32 y0; + int32 x1; + int32 y1; +}; + +struct util_RGBA +{ + real32 R; + real32 G; + real32 B; + real32 A; +}; + +struct util_image +{ + int32 w; + int32 h; + int32 bits_per_channel; + int32 num_channels; + uint data_len; + uint8* pixels; + char file_path[256]; +}; + +inline real32 +SafeRatio(real32 dividend, real32 divisor) +{ + if (divisor == 0) + // TODO: log warning (don't require aixlog) + return 0; + else + return dividend / divisor; +} + +inline int32 +SafeTruncateToInt32(int64 val) +{ + assert(val <= INT32_MAX && val >= INT32_MIN); + return (int32) val; +} + +inline void +utilConvertColor(GLfloat buf[3], uint32 color) +{ + // NOTE: not using the alpha values + buf[0] = (GLfloat) ((color >> 24) & 0xFF) / (GLfloat) 255; + buf[1] = (GLfloat) ((color >> 16) & 0xFF) / (GLfloat) 255; + buf[2] = (GLfloat) ((color >> 8) & 0xFF) / (GLfloat) 255; +} + +//----------------- +// C Strings + +// NOTE: max_len should be the allocated size of dest +bool utilCopyCStr(char* dest, const char* src, uint max_len); + +// NOTE: returns new string with '/' between +// NOTE: max_len should be the size of return buffer. +bool utilConcatPath(char* out, const char* base_dir, const char* file_name, uint max_len); + +const char* utilBaseName(const char* path_str); + +// NOTE: returns true if the first 'sz' characters from each string match +bool utilMatchPrefix(const char* lhs, const char* rhs, int sz); + +//----------------- +// Memory allocation + +// NOTE: Wrapper for calloc that will send error message on out of memory +#define UTIL_ALLOC(len, type) (type *) utilLogAlloc((len), sizeof(type), __FILE__, __LINE__) +void* utilLogAlloc(uint item_count, uint type_size, const char* file_name, const int line); + +void utilSafeFree(const void* mem); + +//----------------- +// File I/O + +char* utilDumpTextFile(const char* filename); + +bool utilWriteTextFile(const char* filename, const char* text); + +//----------------- +// utilImage + +util_image utilLoadImage(const char* base_dir, const char* filename); + +void utilFreeImage(util_image image); + +v2f utilGetPaletteCoords(util_image& palette_image, uint color_index); diff --git a/src/camera.cpp b/src/camera.cpp new file mode 100644 index 0000000..0fa34f0 --- /dev/null +++ b/src/camera.cpp @@ -0,0 +1,193 @@ + +#include + +#include "camera.h" + +// TODO: add these props to scene json +#define MOVE_SPEED 5.f +#define ROTATE_SPEED 0.005f +#define CAMERA_Z_CLAMP_ANGLE 85.f +#define FOV 60.f +#define ASPECT_RATIO 16.f/9.f +#define NEAR_CLIP_PLANE 20.f + + +// forward declarations +inline glm::vec3 convertv3f(v3f v); + + +// interface + +void +cameraInitPerspective(camera& cam, glm::vec3 position, glm::vec3 target, glm::vec3 world_up) +{ + cam.position = position; + cam.target = target; + cam.world_up = world_up; + cam.projection = glm::infinitePerspective(glm::radians(FOV), ASPECT_RATIO, NEAR_CLIP_PLANE); + + cam.forward = glm::normalize(target - position); + cam.left = glm::normalize(glm::cross(cam.world_up, cam.forward)); + cam.up = glm::normalize(glm::cross(cam.forward, cam.left)); + + cam.hAngle = glm::atan(cam.forward.x, cam.forward.y); + // NOTE: using pythagoras' to get absolute value of relative axis for vAngle component + real32 len = glm::sqrt(glm::pow(cam.forward.y, 2) + glm::pow(cam.forward.x, 2)); + cam.vAngle = glm::atan(cam.forward.z, len); + + cam.view = glm::lookAt(cam.position, cam.position + cam.forward, cam.up); + cam.model = glm::mat4(1.0f); + cam.MVP = cam.projection * cam.view * cam.model; +} + +void +cameraInitOrthographic(/*camera& cam, */) +{ +#if 0 + // left, right, bottom, top, zNear, zFar + cam.projection = glm::ortho(0.f, 1280.0f, 0.f, 720.0f, 0.1f, 100.0f); + cam.view = glm::lookAt( + glm::vec3(0.0f, 0.0f, 1.0f), // camera position + glm::vec3(0.0f, 0.0f, 0.0f), // look at position + glm::vec3(0,1,0) // "up" vector + ); + + cam.model = glm::mat4(1.0f); + cam.MVP = cam.projection * cam.view * cam.model; +#endif +} + +v2f +cameraUnproject(camera& cam, int x, int y, int vp_width, int vp_height) +{ + // NOTE: using depth buffer may not be as accurate as doing ray-cast + GLfloat depth; + glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth); + glm::vec4 viewport = glm::vec4(0, 0, vp_width, vp_height); + glm::vec3 wincoord = glm::vec3(x, y, depth); + glm::vec3 vU = glm::unProject(wincoord, cam.view, cam.projection, viewport); + v2f v(vU.x, vU.y); + + return v; +} + +v3f +cameraCreateRay(camera& cam, v2i vp_coords, v2i vp_dims) +{ + // NOTE: http://antongerdelan.net/opengl/raycasting.html + float x = 2.f * vp_coords.x / vp_dims.x - 1.f; + float y = 2.f * vp_coords.y / vp_dims.y - 1.f; + glm::vec4 ray_clip = glm::vec4(x, y, -1.f, 1.f); + glm::vec4 ray_eye = glm::inverse(cam.projection) * ray_clip; + ray_eye = glm::vec4(ray_eye.x, ray_eye.y, -1.f, 0); // NOTE: reset as ray + glm::vec4 ray_world = glm::normalize(glm::inverse(cam.view) * ray_eye); + + return v3f(ray_world.x, ray_world.y, ray_world.z); +} + +bool +cameraIntersectPlane(camera& cam, v3f ray, v3f plane_origin, v3f plane_normal, v3f& intersection) +{ + // NOTE: https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection + glm::vec3 c_o = cam.position; + glm::vec3 r = convertv3f(ray); + glm::vec3 p_o = convertv3f(plane_origin); + glm::vec3 p_n = convertv3f(plane_normal); + float divisor = glm::dot(r, p_n); + + if (divisor <= 0.000001f && divisor >= -0.000001f) // NOTE: ray and plane are co-planar + return false; + + float distance = glm::dot((p_o - c_o), p_n) / divisor; + glm::vec3 xsect = c_o + (r * distance); + intersection = v3f(xsect.x, xsect.y, xsect.z); + + return true; +} + +void +cameraMove(camera& cam, bool up, bool left, bool down, bool right, bool forward, bool backward) +{ + if (!up && !left && !down && !right && !forward && !backward) + return; + + glm::vec3 f = cam.forward; + glm::vec3 u = cam.up; + glm::vec3 old = cam.position; + glm::vec3 &p = cam.position; + glm::vec3 v(0.f); // normalized direction + + // TODO: still seems like we're adding magnitude when moving in 2 directions +#if 0 + if (forward) v = glm::normalize(v + f); + if (backward) v = glm::normalize(v - f); + if (up) v = glm::normalize(v + u); + if (down) v = glm::normalize(v - u); + if (left) v -= glm::normalize(glm::cross(f, u)); + if (right) v -= glm::normalize(glm::cross(u, f)); +#else + if (forward) v += f; + if (backward) v -= f; + if (up) v += u; + if (down) v -= u; + if (left) v -= glm::cross(f, u); + if (right) v -= glm::cross(u, f); +#endif + + p += (v * MOVE_SPEED); + glm::vec3 diff = old - p; + cam.view = glm::translate(cam.view, diff); + cam.MVP = cam.projection * cam.view * cam.model; +} + +void +cameraRotate(camera& cam, int32 xrel, int32 yrel) +{ + float &h = cam.hAngle; + float &v = cam.vAngle; + h += ROTATE_SPEED * xrel; + v -= ROTATE_SPEED * yrel; + + // clamp vAngle to prevent gimbal lock + float a = glm::radians(CAMERA_Z_CLAMP_ANGLE); + if (v < (-1 * a)) v = (-1 * a); + if (v > a) v = a; + + cam.forward = glm::vec3( + glm::cos(v) * glm::sin(h), + glm::cos(v) * glm::cos(h), + glm::sin(v) + ); + + glm::normalize(cam.forward); + cam.left = glm::normalize(glm::cross(cam.forward, cam.world_up)); + cam.up = glm::normalize(glm::cross(cam.left, cam.forward)); + + cam.view = glm::lookAt(cam.position, cam.position + cam.forward, cam.up); + cam.MVP = cam.projection * cam.view * cam.model; +} + +void +cameraRoll(camera& cam, bool CW, bool CCW) +{ + if ((!CW && !CCW) || (CW && CCW)) + return; + + float a = 0.005f; + if (CW) a *= 1; + if (CCW) a *= -1; + glm::mat4 m = glm::rotate(glm::mat4(1.f), a, cam.forward); + glm::vec4 v(cam.up.x, cam.up.y, cam.up.z, 0); + v = v * m; + cam.up = glm::vec3(v.x, v.y, v.z); + cam.view *= m; + cam.MVP = cam.projection * cam.view * cam.model; +} + +// internal + +inline glm::vec3 +convertv3f(v3f v) +{ + return glm::vec3(v.x, v.y, v.z); +} diff --git a/src/dumbLog.cpp b/src/dumbLog.cpp new file mode 100644 index 0000000..13b7f14 --- /dev/null +++ b/src/dumbLog.cpp @@ -0,0 +1,38 @@ + +#include +#include + +#include "dumbLog.h" + +void dumbLog::setOutputStream(std::ostream* out) +{ + this->OUT = out; +} + +const char* +dumbLog::logLevelToString(log_level level) +{ + switch (level) { + case log_level::Error: return "Error"; + case log_level::Warning: return "Warning"; + case log_level::Info: return "Info"; + default: return "Potato"; + } +}; + +std::tm* +dumbLog::getCurrentTime() +{ + auto now = std::chrono::system_clock::now(); + auto t_c = std::chrono::system_clock::to_time_t(now); + return std::localtime(&t_c); +} + +int +dumbLog::getCurrentMS() +{ + auto now = std::chrono::system_clock::now(); + long long total_ms = std::chrono::duration_cast(now.time_since_epoch()).count(); + return int(total_ms % 1000); +} + diff --git a/src/entity.cpp b/src/entity.cpp new file mode 100644 index 0000000..ca96a16 --- /dev/null +++ b/src/entity.cpp @@ -0,0 +1,164 @@ + +#include "dumbLog.h" +#include "entity.h" + + +void initEntityRG(Entity& e, rg_shader_program& shader, bool use_normals); +bool initEntityROs(Entity& e); +bool convertMeshInfo(meMeshInfo* mesh, render_object* ro, bool use_normals, bool use_texture); + +bool +entInit(Entity& e, const char* data_dir, rg_shader_program& shader) +{ + e.world_transform = glm::mat4(1.0); + entScale(e, e.scale); + uint max_len = 256; // TODO: define max_len for property strings + char full_path[max_len]; + if (utilConcatPath(full_path, data_dir, e.model_filename, max_len)) { + if (meLoadFromFile(e.mesh_group, data_dir, full_path)) { + initEntityRG(e, shader, e.mesh_group.use_normals); + entSetWorldPosition(e, e.translation.x, e.translation.y, e.translation.z); + + if (!initEntityROs(e)) + goto cleanup; + } else { + goto cleanup; + } + } else { + LOG(Error) << data_dir << " + " << e.model_filename << " is too long\n"; + goto cleanup; + } + + return true; + +cleanup: + entFree(e); + return false; +} + +void +entFree(Entity& e) +{ + meFreeMeshGroup(e.mesh_group); + rgFree(e.ren_group); +} + +void +entSetWorldPosition(Entity& e, float x, float y, float z) +{ + e.world_transform[3][0] = e.translation.x = x; + e.world_transform[3][1] = e.translation.y = y; + e.world_transform[3][2] = e.translation.z = z; +} + +void +initEntityRG(Entity& e, rg_shader_program& shader, bool use_normals) +{ + render_group* rg = UTIL_ALLOC(1, render_group); + e.ren_group = rg; + e.ren_group->shader = shader; + rg->draw_indexed = true; + rg->draw_mode = GL_TRIANGLES; + rg->use_normals = use_normals; + uint num_meshes = rg->num_objects = e.mesh_group.num_meshes; + rg->render_objects = UTIL_ALLOC(num_meshes, render_object*); +} + +bool +initEntityROs(Entity& e) +{ + render_group* rg = e.ren_group; + + for (uint i = 0; i < e.mesh_group.num_meshes; i++) + { + meMeshInfo* mesh = e.mesh_group.meshes[i]; + uint buffer_len = mesh->num_vertices * 3; + uint index_len = mesh->num_indices; + + render_object* ro = rgAllocateRenderObject(buffer_len, index_len); + rg->render_objects[i] = ro; + + if (ro == nullptr) + return false; + + if (!convertMeshInfo(mesh, ro, rg->use_normals, mesh->use_texture)) { + rgFree(rg); + return false; + } + + if (mesh->use_texture) { + ro->use_texture = true; + if (!rgInitGLTexture(ro->tex_id, mesh->diffuse_texture)) { + LOG(Error) << "Error initializing GL texture\n"; + return false; + } + } + } + + return true; +} + +bool +convertMeshInfo(meMeshInfo* mesh, render_object* ro, bool use_normals, bool use_texture) +{ + uint vertex_buf_len = mesh->num_vertices * 3; + GLfloat* vertex_buf = ro->vertex_buffer.buffer; + GLfloat* normal_buf = ro->normal_buffer.buffer; + GLfloat* uv_buf = ro->uv_buffer.buffer; + uint* index_buf = ro->index_buffer.buffer; + + if (!vertex_buf || !normal_buf || !index_buf) + return false; + + // dump vertices, colors, normals, and texture coords into render_group buffers + uint vertex_index = 0; + uint vertex_prop_index = 0; + + for (uint i = 0; i < vertex_buf_len; i++) { + const glm::vec3& vertex = mesh->vertices[vertex_index]; + + switch (vertex_prop_index) { + case 0: vertex_buf[i] = vertex.x; break; + case 1: vertex_buf[i] = vertex.y; break; + case 2: vertex_buf[i] = vertex.z; break; + } + + if (use_normals) { + const glm::vec3& normal = mesh->normals[vertex_index]; + switch (vertex_prop_index) { + case 0: normal_buf[i] = normal.x; break; + case 1: normal_buf[i] = normal.y; break; + case 2: normal_buf[i] = normal.z; break; + } + } + + if (use_texture) { + const glm::vec3& uv = mesh->texture_coords[vertex_index]; + switch (vertex_prop_index) { + case 0: uv_buf[i] = uv.x; break; + case 1: uv_buf[i] = uv.y; break; + case 2: uv_buf[i] = uv.z; break; + } + } + + vertex_prop_index++; + + if (vertex_prop_index == 3) { + vertex_prop_index = 0; + vertex_index++; + } + } + + // dump indices + for (uint i = 0; i < mesh->num_indices; i++) + index_buf[i] = mesh->indices[i]; + + rgInitGLFloatBuffer(&ro->vertex_buffer, GL_DYNAMIC_DRAW, GL_ARRAY_BUFFER); + if (use_normals) + rgInitGLFloatBuffer(&ro->normal_buffer, GL_STATIC_DRAW, GL_ARRAY_BUFFER); + if (use_texture) + rgInitGLFloatBuffer(&ro->uv_buffer, GL_STATIC_DRAW, GL_ARRAY_BUFFER); + rgInitGLIndexBuffer(&ro->index_buffer, GL_STATIC_DRAW, GL_ELEMENT_ARRAY_BUFFER); + + return true; +} diff --git a/src/mesh.cpp b/src/mesh.cpp new file mode 100644 index 0000000..40f02a4 --- /dev/null +++ b/src/mesh.cpp @@ -0,0 +1,175 @@ +#include + +#include +#include +#include + +#include +#include +#include + +#include "dumbLog.h" + +#include "mesh.h" + + +// forward declarations +void assimpLogCB(const char* message, char* user); +meMeshInfo* allocateMeshInfo(uint num_vertices, uint num_indices, bool has_normals, bool has_texture); +void freeMesh(meMeshInfo* mesh); +inline glm::vec3 copyVector(aiVector3D v_in, glm::vec3& v_out); +meMeshInfo* copyMeshInfo(const char* data_dir, const aiScene* scene, aiMesh* mesh); + +// interface + +bool +meInitAssimp() +{ + LOG(Info) << "Initializing Assimp\n"; + aiLogStream ls; + ls.callback = assimpLogCB; + aiAttachLogStream(&ls); + + return true; +} + +bool +meLoadFromFile(meMeshGroup& mesh_group, const char* data_dir, const char* filename) +{ + LOG(Info) << "Loading file: " << filename << "\n"; + const aiScene* scene = aiImportFile(filename, aiProcessPreset_TargetRealtime_MaxQuality); + + if (!scene) { + LOG(Error) << "Error loading file: " << filename << "\n"; + return false; + } + + if (scene->mNumMeshes < 1) { + LOG(Error) << "Scene contains no meshes\n"; + return false; + } + + mesh_group.num_meshes = scene->mNumMeshes; + mesh_group.meshes = UTIL_ALLOC(mesh_group.num_meshes, meMeshInfo*); + mesh_group.use_normals = scene->mMeshes[0]->HasNormals(); + + for (uint i = 0; i < scene->mNumMeshes; i++) { + mesh_group.meshes[i] = copyMeshInfo(data_dir, scene, scene->mMeshes[i]); + } + + // free memeory from assimp + aiReleaseImport(scene); + return true; +} + +void +meFreeMeshGroup(meMeshGroup& mesh_group) +{ + for (uint i = 0; i < mesh_group.num_meshes; i++) + freeMesh(mesh_group.meshes[i]); + + utilSafeFree(mesh_group.meshes); +} + +void +meShutdownAssimp() +{ + aiDetachAllLogStreams(); +} + + +// internal + +void +assimpLogCB(const char* message, char* user) +{ + // NOTE: filter 'info' messages from assimp + if (!utilMatchPrefix(message, "Info,", 5)) + LOG(Info) << message << "\n"; +} + +meMeshInfo* +allocateMeshInfo(uint num_vertices, uint num_indices, bool has_normals, bool has_texture) +{ + meMeshInfo* mi = UTIL_ALLOC(1, meMeshInfo); + mi->model_transform = glm::mat4(1); + + // allocate buffers for vertex and index data from mesh + mi->num_vertices = num_vertices; + mi->vertices = UTIL_ALLOC(mi->num_vertices, glm::vec3); + mi->num_indices = num_indices; + mi->indices = UTIL_ALLOC(num_indices, uint); + if (has_normals) mi->normals = UTIL_ALLOC(mi->num_vertices, glm::vec3); + if (has_texture) mi->texture_coords = UTIL_ALLOC(mi->num_vertices, glm::vec3); + + return mi; +} + +void +freeMesh(meMeshInfo* mesh) +{ + utilFreeImage(mesh->diffuse_texture); + utilSafeFree(mesh->vertices); + utilSafeFree(mesh->normals); + utilSafeFree(mesh->texture_coords); + utilSafeFree(mesh->indices); + utilSafeFree(mesh); +} + +inline glm::vec3 +copyVector(aiVector3D v_in, glm::vec3& v_out) +{ + v_out.x = v_in.x; + v_out.y = v_in.y; + v_out.z = v_in.z; + return v_out; +} + +meMeshInfo* +copyMeshInfo(const char* data_dir, const aiScene* scene, aiMesh* mesh) +{ + bool has_tex = mesh->HasTextureCoords(0); + bool has_normals = mesh->HasNormals(); + meMeshInfo* mi = allocateMeshInfo(mesh->mNumVertices, mesh->mNumFaces * 3, has_normals, has_tex); + + // copy vertices, normals, and texture coords + for (uint i = 0; i < mi->num_vertices; i++) { + copyVector(mesh->mVertices[i], mi->vertices[i]); + + if (has_normals) + copyVector(mesh->mNormals[i], mi->normals[i]); + + if (has_tex) { + mi->texture_coords[i].x = mesh->mTextureCoords[0][i].x; + mi->texture_coords[i].y = mesh->mTextureCoords[0][i].y; + } + } + + // copy indices + for (uint i = 0; i < mesh->mNumFaces; i++) + for (uint j = 0; j < 3; j++) + mi->indices[i * 3 + j] = mesh->mFaces[i].mIndices[j]; + + // material + aiMaterial* mat = scene->mMaterials[mesh->mMaterialIndex]; + aiColor3D color(0.f, 0.f, 0.f); + + if (AI_SUCCESS == mat->Get(AI_MATKEY_COLOR_DIFFUSE, color)) { + mi->diffuse_color.r = color.r; + mi->diffuse_color.g = color.g; + mi->diffuse_color.b = color.b; + } + + if (has_tex && mat->GetTextureCount(aiTextureType_DIFFUSE) > 0) { + aiString file_name; + if (AI_SUCCESS != mat->GetTexture(aiTextureType_DIFFUSE, 0, &file_name, NULL, NULL, NULL, NULL, NULL)) { + LOG(Error) << "No diffuse texture from assimp\n"; + } else { + LOG(Info) << "Loading texture file: " << file_name.C_Str() << "\n"; + mi->diffuse_texture = utilLoadImage(data_dir, file_name.C_Str()); + mi->use_texture = true; + } + } + + return mi; +} diff --git a/src/render_group.cpp b/src/render_group.cpp new file mode 100644 index 0000000..be92675 --- /dev/null +++ b/src/render_group.cpp @@ -0,0 +1,277 @@ + +#include // TODO: remove this and make something in util.h +#include + +#include +#include +#include + +#include "dumbLog.h" + +#include "render_group.h" +#include "util.h" + +#define INFO_LOG_MAX_LENGTH 312 + + +// forward declarations + +void freeRenderObject(render_object* ro); +void rgInitGLIndexBuffer(gl_index_buffer* index_buffer, GLenum usage, GLenum target); + + +// interface + +render_object * +rgAllocateRenderObject(uint buffer_len, uint index_len) +{ + render_object* ro = UTIL_ALLOC(1, render_object); + + if (ro == nullptr) + return nullptr; + + ro->vertex_buffer.buffer = UTIL_ALLOC(buffer_len, GLfloat); + ro->vertex_buffer.count = ro->vertex_buffer.max_count = buffer_len; + ro->normal_buffer.buffer = UTIL_ALLOC(buffer_len, GLfloat); + ro->normal_buffer.count = ro->normal_buffer.max_count = buffer_len; + ro->uv_buffer.buffer = UTIL_ALLOC(buffer_len, GLfloat); + ro->uv_buffer.count = ro->uv_buffer.max_count = buffer_len; + + if (index_len > 0) { + ro->index_buffer.buffer = UTIL_ALLOC(index_len, uint); + ro->index_buffer.count = index_len; + } + + if (ro->vertex_buffer.buffer == nullptr || + ro->normal_buffer.buffer == nullptr || + ((index_len > 0) && (ro->vertex_buffer.buffer == nullptr))) + { + freeRenderObject(ro); + return nullptr; + } + + return ro; +} + +render_group* +rgInitSingle(rg_shader_program& sp, uint vertex_buffer_len, + bool use_normals, uint index_buffer_len, + GLenum draw_mode) +{ + render_group* rg = UTIL_ALLOC(1, render_group); + rg->shader = sp; + rg->use_normals = use_normals; + rg->draw_indexed = (index_buffer_len > 0); + rg->draw_mode = draw_mode; + + rg->render_objects = UTIL_ALLOC(1, render_object*); + rg->render_objects[0] = rgAllocateRenderObject(vertex_buffer_len, index_buffer_len); + rg->num_objects = 1; + + if (rg->render_objects[0] == nullptr) { + LOG(Error) << "Allocation Error\n"; + rgFree(rg); + } + + return rg; +} + +bool +rgInitShaderProgram(rg_shader_program& sp, const char * vertex_code, const char * frag_code) +{ + glGenVertexArrays(1, &sp.vertex_array_id); + glBindVertexArray(sp.vertex_array_id); + GLuint vertex_shader_id = glCreateShader(GL_VERTEX_SHADER); + GLuint fragment_shader_id = glCreateShader(GL_FRAGMENT_SHADER); + + glShaderSource(vertex_shader_id, 1, &vertex_code, NULL); + glShaderSource(fragment_shader_id, 1, &frag_code, NULL); + glCompileShader(vertex_shader_id); + glCompileShader(fragment_shader_id); + + sp.program_id = glCreateProgram(); + glAttachShader(sp.program_id, vertex_shader_id); + glAttachShader(sp.program_id, fragment_shader_id); + glLinkProgram(sp.program_id); + + sp.model_matrix_id = glGetUniformLocation(sp.program_id, "model"); + sp.view_matrix_id = glGetUniformLocation(sp.program_id, "view"); + sp.projection_matrix_id = glGetUniformLocation(sp.program_id, "projection"); + sp.normal_matrix_id = glGetUniformLocation(sp.program_id, "normal_matrix"); + sp.num_lights_id = glGetUniformLocation(sp.program_id, "num_lights"); + sp.sampler_id = glGetUniformLocation(sp.program_id, "sampler"); + + glDetachShader(sp.program_id, vertex_shader_id); + glDetachShader(sp.program_id, fragment_shader_id); + glDeleteShader(vertex_shader_id); + glDeleteShader(fragment_shader_id); + + // TODO: quick hack to allow 'dynamic' stack allocation for msvc + // also remove INFO_LOG_MAX_LENGTH define + GLint isLinked = 0; + glGetProgramiv(sp.program_id, GL_LINK_STATUS, &isLinked); + if (isLinked == GL_FALSE) { + GLint maxLength = INFO_LOG_MAX_LENGTH; + glGetProgramiv(sp.program_id, GL_INFO_LOG_LENGTH, &maxLength); +#ifdef _WIN32 + GLchar infoLog[312] = { 0 }; +#else + GLchar infoLog[maxLength] = { 0 }; +#endif + glGetProgramInfoLog(sp.program_id, maxLength, &maxLength, &infoLog[0]); + LOG(Error) << infoLog << "\n"; + glDeleteProgram(sp.program_id); + + return false; + } + + return true; +} + +bool +rgInitGLTexture(GLuint& tex_id, util_image image) +{ + glGenTextures(1, &tex_id); + glBindTexture(GL_TEXTURE_2D, tex_id); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + GLenum pixel_format = (image.num_channels == 3) ? GL_RGB : GL_RGBA; + glTexImage2D(GL_TEXTURE_2D, 0, pixel_format, image.w, image.h, 0, + pixel_format, GL_UNSIGNED_BYTE, image.pixels); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + + return (glGetError() == GL_NO_ERROR); +} + +void +rgInitGLFloatBuffer(gl_buffer* buffer, GLenum usage, GLenum target) +{ + glGenBuffers(1, &buffer->buffer_id); + glBindBuffer(target, buffer->buffer_id); + glBufferData(target, buffer->max_count * sizeof(GLfloat), buffer->buffer, usage); +} + +void +rgInitGLIndexBuffer(gl_index_buffer* index_buffer, GLenum usage, GLenum target) +{ + glGenBuffers(1, &index_buffer->buffer_id); + glBindBuffer(target, index_buffer->buffer_id); + glBufferData(target, index_buffer->count * sizeof(uint), index_buffer->buffer, usage); +} + +void +rgUpdateGLBuffer(gl_buffer& buffer) +{ + glBindBuffer(GL_ARRAY_BUFFER, buffer.buffer_id); + glBufferSubData(GL_ARRAY_BUFFER, 0, buffer.count * sizeof(GLfloat), buffer.buffer); +} + +void +rgDraw(render_group* rg, glm::mat4 model_matrix, + glm::mat4 view_matrix, glm::mat4 projection_matrix, + rg_point_light* lights, uint num_lights, bool update_vertex_data) +{ + for (uint i = 0; i < rg->num_objects; i++) { + render_object* ro = rg->render_objects[i]; + + glUseProgram(rg->shader.program_id); + glUniformMatrix4fv(rg->shader.model_matrix_id, 1, GL_FALSE, &model_matrix[0][0]); + glUniformMatrix4fv(rg->shader.view_matrix_id, 1, GL_FALSE, &view_matrix[0][0]); + glUniformMatrix4fv(rg->shader.projection_matrix_id, 1, GL_FALSE, &projection_matrix[0][0]); + + // 1st attribute buffer : vertices + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, ro->vertex_buffer.buffer_id); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0); + + if (update_vertex_data) + { + glBufferSubData(GL_ARRAY_BUFFER, 0, ro->vertex_buffer.count * sizeof(GLfloat), + ro->vertex_buffer.buffer); + } + + // 2rd attribute buffer: normals + if (rg->use_normals) { + glEnableVertexAttribArray(1); + glBindBuffer(GL_ARRAY_BUFFER, ro->normal_buffer.buffer_id); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0); + + glm::mat3 normal_matrix = glm::transpose(glm::inverse(glm::mat3(model_matrix))); + glUniformMatrix3fv(rg->shader.normal_matrix_id, 1, GL_FALSE, &normal_matrix[0][0]); + + // TODO: use Uniform Buffer Objects to update light data + // https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_uniform_buffer_object.txt + // https://www.khronos.org/opengl/wiki/Uniform_Buffer_Objects + + glUniform1ui(rg->shader.num_lights_id, num_lights); + + for (uint i = 0; i < num_lights; i++) { + // TODO: don't do this every frame * every render_group + std::stringstream ss; + ss << "lights[" << i << "].position"; + int light_pos_loc = glGetUniformLocation(rg->shader.program_id, ss.str().c_str()); + glUniform3fv(light_pos_loc, 1, &lights[i].position[0]); + } + } + + // 3rd attribute buffer: UV coordinates + if (ro->use_texture) { + glEnableVertexAttribArray(2); + glBindBuffer(GL_ARRAY_BUFFER, ro->uv_buffer.buffer_id); + glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0); + glBindTexture(GL_TEXTURE_2D, ro->tex_id); + glUniform1i(rg->shader.sampler_id, 0); + } + + // draw + if (rg->draw_indexed) { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ro->index_buffer.buffer_id); + glDrawElements(rg->draw_mode, ro->index_buffer.count, GL_UNSIGNED_INT, 0); + } else { + glDrawArrays(rg->draw_mode, 0, ro->vertex_buffer.count / 3); + } + + // cleanup + glDisableVertexAttribArray(0); + if (rg->use_normals) + glDisableVertexAttribArray(1); + if (ro->use_texture) + glDisableVertexAttribArray(2); + + glUseProgram(0); + } +} + +void +rgFree(render_group* rg) +{ + if (rg == nullptr) { + LOG(Error) << "tried to free nullptr\n"; + return; + } + + for (uint i = 0; i < rg->num_objects; i++) + freeRenderObject(rg->render_objects[i]); + + utilSafeFree(rg->render_objects); + utilSafeFree(rg); +} + + +// internal + +void +freeRenderObject(render_object* ro) +{ + if (ro == nullptr) { + LOG(Error) << "Tried to free nullptr\n"; + return; + } + + glDeleteTextures(1, &ro->tex_id); + utilSafeFree(ro->vertex_buffer.buffer); + utilSafeFree(ro->normal_buffer.buffer); + utilSafeFree(ro->uv_buffer.buffer); + utilSafeFree(ro->index_buffer.buffer); + utilSafeFree(ro); +} diff --git a/src/renderer.cpp b/src/renderer.cpp new file mode 100644 index 0000000..281cb27 --- /dev/null +++ b/src/renderer.cpp @@ -0,0 +1,232 @@ + +#include +#include // trig functions + +#if defined (_WIN32) + #include +#else + #include +#endif +#include +#include +#include + +#include "dumbLog.h" + +#include "renderer.h" + +#define DEFAULT_VERTEX_SHADER_FILE "../data/default.vs" +#define DEFAULT_FRAGMENT_SHADER_FILE "../data/default.fs" +#define MAX_LIGHTS 10 // NOTE: needs to match the fragment shader source +#define DEFAULT_PALETTE_DIR "../data" +#define DEFAULT_PALETTE_FILE "palette.png" +#define CLEAR_COL_R 55.f / 255.f +#define CLEAR_COL_G 55.f / 255.f +#define CLEAR_COL_B 55.f / 255.f +#define CLEAR_COL_A 1.f +#define USE_SECOND_MONITOR 0 + +// TODO: add this to json scene properties and render_state +util_RGBA g_clear_col = {1.f,1.f,1.f,1.f}; + + +// forward declarations +void openglDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, + GLsizei length, const GLchar* message, const void* userParam); +bool initDebugRenderGroup(render_state* rs); + + +// interface + +bool +renInit(render_state* rs) +{ + SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + SDL_GetCurrentDisplayMode(0, &rs->handles.currentDisplayMode); + + uint display_id = 0; + if (USE_SECOND_MONITOR && SDL_GetNumVideoDisplays() > 1) + display_id = 1; + + rs->handles.window = SDL_CreateWindow( + "hexgame", + SDL_WINDOWPOS_CENTERED_DISPLAY(display_id), + SDL_WINDOWPOS_CENTERED_DISPLAY(display_id), + rs->viewport_dims.x, + rs->viewport_dims.y, + SDL_WINDOW_OPENGL|SDL_WINDOW_RESIZABLE + ); + + if (!rs->handles.window) { + LOG(Error) << "Error creating window: " << SDL_GetError() << "\n"; + return false; + } + + rs->handles.glContext = SDL_GL_CreateContext(rs->handles.window); + + if (!rs->handles.glContext) { + LOG(Error) << "Error creating glContext: " << SDL_GetError() << "\n"; + return false; + } + + if (SDL_GL_SetSwapInterval(1) != 0) { // vsync + LOG(Error) << "SDL Errors: " << SDL_GetError() << "\n"; + return false; + } + + if (glewInit()) { + LOG(Error) << "failed to initialize OpenGL\n"; + return false; + } + + LOG(Info) << "opengl vendor: " << glGetString(GL_VENDOR) << "\n"; + LOG(Info)<< "opengl renderer: " << glGetString(GL_RENDERER) << "\n"; + LOG(Info) << "opengl version: " << glGetString(GL_VERSION) << "\n"; + + glEnable(GL_DEPTH_TEST); + glEnable(GL_LINE_SMOOTH); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +#if 0 + // TODO: blending messes up rendering with mesa on intel graphics 4000 + glEnable(GL_BLEND); + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_SRC_ALPHA); +#endif + + // TODO: glDebugMessageCallback is only availabe from >v4.3 + // check and warn if context doesn't support this function here + glEnable (GL_DEBUG_OUTPUT); + glDebugMessageCallback((GLDEBUGPROC) openglDebugCallback, 0); + // hide VRAM debug messages + glDebugMessageControl(GL_DONT_CARE, 33361, GL_DONT_CARE, 0, 0, GL_FALSE); + + const char* vs_code = utilDumpTextFile(DEFAULT_VERTEX_SHADER_FILE); + const char* fs_code = utilDumpTextFile(DEFAULT_FRAGMENT_SHADER_FILE); + + bool shader_error = !rgInitShaderProgram(rs->default_shader, vs_code, fs_code); + + utilSafeFree(vs_code); + utilSafeFree(fs_code); + + if (shader_error) { + LOG(Error) << "Error initializing shader program\n"; + return false; + } + + rs->max_lights = MAX_LIGHTS; + rs->lights = UTIL_ALLOC(rs->max_lights, rg_point_light); + rs->palette_image = utilLoadImage(DEFAULT_PALETTE_DIR, DEFAULT_PALETTE_FILE); + + if (!rgInitGLTexture(rs->palette_id, rs->palette_image)) { + LOG(Error) << "Error creating texture\n"; + return false; + } + + if (!initDebugRenderGroup(rs)) { + LOG(Error) << "Error initializing debug render group\n"; + return false; + } + + g_clear_col.R = CLEAR_COL_R; + g_clear_col.B = CLEAR_COL_B; + g_clear_col.G = CLEAR_COL_G; + g_clear_col.A = CLEAR_COL_A; + + return true; +} + +void +renFreeBuffers(render_state* rs) +{ + utilSafeFree(rs->lights); + utilFreeImage(rs->palette_image); + glDeleteTextures(1, &rs->palette_id); + rgFree(rs->filled_hex_render_group); + rgFree(rs->hex_line_render_group); + rgFree(rs->debug_render_group); +} + +void +renRenderFrame(render_state* rs, Entity* entities, uint32 entity_count) +{ + glClearColor(g_clear_col.R, g_clear_col.G, g_clear_col.B, g_clear_col.A); + glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); + + //glm::mat4 m_model = rs->cam.model; + glm::mat4 m_view = rs->cam.view; + glm::mat4 m_projection = rs->cam.projection; +#if 0 + // filled hexes + + // get new colors every frame + hgUpdateUVBuffer(hg, rs->filled_hex_render_group); + rgDraw(rs->filled_hex_render_group, m_model, m_view, m_projection, + rs->lights, rs->num_lights); + + // hex lines + rgDraw(rs->hex_line_render_group, m_model, m_view, m_projection, + rs->lights, rs->num_lights); +#endif + // entities + for (uint i = 0; i < entity_count; i++) { + rgDraw(entities[i].ren_group, entities[i].world_transform , m_view, m_projection, + rs->lights, rs->num_lights); + } +} +#if 0 +void +renRenderDebug(render_state* rs, std::vector &vertices) +{ + GLfloat* buf = rs->debug_render_group->render_objects[0]->vertex_buffer.buffer; + buf[0] = vertices[0].x; buf[1] = vertices[0].y; buf[2] = 0; + buf[3] = vertices[1].x; buf[4] = vertices[1].y; buf[2] = 0; + buf[6] = vertices[2].x; buf[7] = vertices[2].y; buf[8] = 0; + buf[9] = vertices[3].x; buf[10] = vertices[3].y; buf[11] = 0; + + rgDraw(rs->debug_render_group, rs->cam.model, rs->cam.view, + rs->cam.projection, rs->lights, rs->num_lights, true); +} +#endif + +// internal + +void +openglDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, + GLsizei length, const GLchar* message, const void* userParam) +{ + LOG((type == GL_DEBUG_TYPE_ERROR) ? Error : Debug) + << (type == GL_DEBUG_TYPE_ERROR ? "** GL Error **" : "") + << ", type: " << type + << ", severity: " << severity + << ", message: " << message << "\n"; +} + +bool +initDebugRenderGroup(render_state* rs) +{ + uint debug_buf_len = 12; // 4 vertices, 3 floats per vertex + rs->debug_render_group = rgInitSingle(rs->default_shader, debug_buf_len, true, 0, GL_LINE_LOOP); + + if (rs->debug_render_group == nullptr) + return false; + + gl_buffer& debug_vertex_buf = rs->debug_render_group->render_objects[0]->vertex_buffer; + gl_buffer& debug_normal_buf = rs->debug_render_group->render_objects[0]->normal_buffer; + + for (uint i = 0; i < debug_buf_len; i += 3) { + debug_normal_buf.buffer[i] = 0.f; + debug_normal_buf.buffer[i + 1] = 0.f; + debug_normal_buf.buffer[i + 2] = 1.f; + } + + rgInitGLFloatBuffer(&debug_vertex_buf, GL_DYNAMIC_DRAW, GL_ARRAY_BUFFER); + rgInitGLFloatBuffer(&debug_normal_buf, GL_STATIC_DRAW, GL_ARRAY_BUFFER); + + return true; +} diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..2e08be2 --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,234 @@ + +#include +#include +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" + +#include "dumbLog.h" +#include "util.h" + + +#define PALETTE_ROWS 16 +#define PALETTE_COLUMNS 16 +#define PALETTE_BORDER 1 +const uint MAX_FILESIZE = 2 * 1024 * 1024; // 2MB +const uint MAX_STRING_LENGTH = 1024; + + +//----------------- +// C Strings + +const char* +utilBaseName(const char* path_str) +{ + assert(std::strlen(path_str) < MAX_STRING_LENGTH); + const char* output = std::strrchr(path_str, '/'); + if (output) + return output; + else + return path_str; +} + +bool +utilCopyCStr(char* dest, const char* src, uint max_len) +{ + assert(std::strlen(src) < MAX_STRING_LENGTH && max_len <= MAX_STRING_LENGTH); + if (std::strlen(src) + 1 > max_len) + return false; + + std::memcpy(dest, src, std::strlen(src) + 1); + return true; +} + +bool +utilConcatPath(char* out, const char* base_dir, const char* file_name, uint max_len) +{ + size_t l1 = std::strlen(base_dir); + size_t l2 = std::strlen(file_name); + size_t padded = l1 + l2 + 2; // NOTE: null term + '/' + + if (padded <= MAX_STRING_LENGTH && padded <= max_len) { + int c = std::snprintf(out, padded, "%s/%s", base_dir, file_name); + if (c > 0) + return true; + else + return false; + } + + return false; +} + +bool +utilMatchPrefix(const char* lhs, const char* rhs, int sz) +{ + int rc = strncmp(lhs, rhs, sz); + return (rc >= 0); +} + +//----------------- +// Memory allocation + +void * +utilLogAlloc(uint item_count, uint type_size, const char* file_name, const int line) +{ + assert(item_count > 0); // that was a fun bug + + void* mem = std::calloc(item_count, type_size); + + if (mem == nullptr) { + LOG(Error) << "Memory allocation failed, called from " + << file_name << ":" << line; + } + + assert(mem != nullptr); // might as well stop execution here + + return mem; +} + +void utilSafeFree(const void* mem) +{ + if (mem != nullptr) { + std::free((void *) mem); + mem = nullptr; + } +} + +//----------------- +// File I/O + +// TODO: don't use ftell() to get filesize +// https://wiki.sei.cmu.edu/confluence/display/c/FIO19-C.+Do+not+use+fseek%28%29+and+ftell%28%29+to+compute+the+size+of+a+regular+file +char * +utilDumpTextFile(const char* filename) +{ + LOG(Info) << "loading filename, " << filename << "\n"; + std::FILE* fp = std::fopen(filename, "rt"); + assert(fp); + + std::fseek(fp, 0, SEEK_END); + uint length = std::ftell(fp); + std::fseek(fp, 0, SEEK_SET); + assert(length < MAX_FILESIZE); + // TODO: check error codes for fseek and ftell + + char* buf = UTIL_ALLOC(length, char); + assert(buf); + + std::fread(buf, sizeof(char), length, fp); + // TODO: check fp w/ ferror() here + + return buf; +} + +// TODO: might want to do the base_dir concat in this function to prevent clobbering +// user files on accident +bool +utilWriteTextFile(const char* filename, const char* text) +{ + size_t text_len = std::strlen(text); + + if (text_len >= MAX_FILESIZE) { + LOG(Error) << "that string is too big\n"; + return false; + } + + std::FILE* fp = fopen(filename, "wt"); + if (fp) { + size_t written = fwrite(text, sizeof(char), text_len, fp); + fclose(fp); + if (written == text_len) { + LOG(Debug) << "successfuly wrote " << written << " bytes\n"; + return true; + } else { + LOG(Error) << "error writing to file: " << filename << "\n"; + return false; + } + } + + LOG(Debug) << text << "\n"; + return false; +} + +//----------------- +// utilImage + +util_image +utilLoadImage(const char* base_dir, const char* filename) +{ + const uint max_path = 256; // NOTE: util_image.file_path is char[256] + const char* base_name = utilBaseName(filename); + // NOTE: +1 for '/' char, +1 for null + uint l = std::strlen(base_dir) + std::strlen(base_name) + 2; + assert(l < max_path); + char full_path[l]; + std::memcpy(full_path, base_dir, std::strlen(base_dir) + 1); + std::strcat(std::strcat(full_path, "/"), base_name); + LOG(Info) << "Loading Image: " << full_path << "\n"; + + util_image image; + std::memcpy(image.file_path, full_path, std::strlen(full_path) + 1); + stbi_set_flip_vertically_on_load(1); + image.pixels = stbi_load(full_path, &image.w, &image.h, &image.num_channels, 0); + image.bits_per_channel = 8; + image.data_len = image.w * image.h * image.num_channels; // NOTE: assumes 8 bits per channel + + if (image.pixels == 0) { + LOG(Error) << stbi_failure_reason() << "\n"; + utilFreeImage(image); + return image; + } + + LOG(Info) << "Image properties, data_len: " << image.data_len + << ", width: " << image.w << ", height: " << image.h << "\n"; + + return image; +} + +void +utilFreeImage(util_image image) +{ + image.w = image.h = image.data_len = 0; + image.bits_per_channel = 8; + image.num_channels = 4; + std::memset(image.file_path, 0, std::strlen(image.file_path) + 1); + utilSafeFree(image.pixels); +} + +// need to init default values from scene file, or finally make default config? +v2f +utilGetPaletteCoords(util_image& palette_image, uint color_index) +{ + v2i tex_coords; + v2f normalized; + + if (PALETTE_ROWS * PALETTE_COLUMNS < color_index) { + LOG(Error) << "index out of range\n"; + return normalized; + } + + if (palette_image.pixels == nullptr) { + LOG(Error) << "palatte image not loaded\n"; + return normalized; + } + + // NOTE: we're aiming to sample near the middle of the palette 'square' so GL_NEAREST + // doesn't try to sample the border color + v2i palette_square; + palette_square.x = palette_image.w / PALETTE_ROWS; + palette_square.y = palette_image.h / PALETTE_COLUMNS; + uint v_row = (uint) std::floor(color_index / PALETTE_ROWS); + uint u_column = color_index % PALETTE_ROWS; + tex_coords.x = u_column * palette_square.x + palette_square.x / 2; + tex_coords.y = v_row * palette_square.y + palette_square.y / 2; + // NOTE: flip y coord + tex_coords.y = palette_image.h - tex_coords.y; + + normalized.x = (float) tex_coords.x / palette_image.w; + normalized.y = (float) tex_coords.y / palette_image.h; + + return normalized; +}