From bd57f345ed9dbed1d81683e48199626de2ea9044 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Fri, 27 Jun 2025 10:18:39 -0700 Subject: Restructure project --- src/asset/asset_cache.c | 252 +++++ src/asset/asset_cache.h | 37 + src/asset/model.c | 1968 ++++++++++++++++++++++++++++++++++++++ src/asset/model.h | 12 + src/asset/texture.c | 177 ++++ src/asset/texture.h | 7 + src/core/buffer.c | 85 ++ src/core/buffer.h | 26 + src/core/constants.h | 9 + src/core/core.c | 429 +++++++++ src/core/core_impl.h | 68 ++ src/core/framebuffer.c | 151 +++ src/core/framebuffer.h | 15 + src/core/geometry.c | 326 +++++++ src/core/geometry.h | 28 + src/core/gl_util.h | 45 + src/core/renderbuffer.c | 35 + src/core/renderbuffer.h | 15 + src/core/shader.c | 92 ++ src/core/shader.h | 17 + src/core/shader_program.c | 291 ++++++ src/core/shader_program.h | 24 + src/core/texture.c | 218 +++++ src/core/texture.h | 35 + src/gfx.c | 73 ++ src/gfx_assert.h | 5 + src/renderer/imm_renderer.c | 260 +++++ src/renderer/imm_renderer_impl.h | 44 + src/renderer/renderer.c | 396 ++++++++ src/renderer/renderer_impl.h | 27 + src/scene/animation.c | 524 ++++++++++ src/scene/animation_impl.h | 98 ++ src/scene/camera.c | 37 + src/scene/camera_impl.h | 12 + src/scene/light.c | 42 + src/scene/light_impl.h | 25 + src/scene/material.c | 57 ++ src/scene/material_impl.h | 16 + src/scene/mesh.c | 24 + src/scene/mesh_impl.h | 12 + src/scene/model.c | 45 + src/scene/model_impl.h | 17 + src/scene/node.c | 409 ++++++++ src/scene/node_impl.h | 40 + src/scene/object.c | 83 ++ src/scene/object_impl.h | 26 + src/scene/scene.c | 25 + src/scene/scene_graph.h | 138 +++ src/scene/scene_impl.h | 13 + src/scene/scene_memory.c | 149 +++ src/scene/scene_memory.h | 39 + src/scene/types.h | 24 + src/util/geometry.c | 44 + src/util/ibl.c | 328 +++++++ src/util/shader.c | 136 +++ src/util/skyquad.c | 161 ++++ 56 files changed, 7691 insertions(+) create mode 100644 src/asset/asset_cache.c create mode 100644 src/asset/asset_cache.h create mode 100644 src/asset/model.c create mode 100644 src/asset/model.h create mode 100644 src/asset/texture.c create mode 100644 src/asset/texture.h create mode 100644 src/core/buffer.c create mode 100644 src/core/buffer.h create mode 100644 src/core/constants.h create mode 100644 src/core/core.c create mode 100644 src/core/core_impl.h create mode 100644 src/core/framebuffer.c create mode 100644 src/core/framebuffer.h create mode 100644 src/core/geometry.c create mode 100644 src/core/geometry.h create mode 100644 src/core/gl_util.h create mode 100644 src/core/renderbuffer.c create mode 100644 src/core/renderbuffer.h create mode 100644 src/core/shader.c create mode 100644 src/core/shader.h create mode 100644 src/core/shader_program.c create mode 100644 src/core/shader_program.h create mode 100644 src/core/texture.c create mode 100644 src/core/texture.h create mode 100644 src/gfx.c create mode 100644 src/gfx_assert.h create mode 100644 src/renderer/imm_renderer.c create mode 100644 src/renderer/imm_renderer_impl.h create mode 100644 src/renderer/renderer.c create mode 100644 src/renderer/renderer_impl.h create mode 100644 src/scene/animation.c create mode 100644 src/scene/animation_impl.h create mode 100644 src/scene/camera.c create mode 100644 src/scene/camera_impl.h create mode 100644 src/scene/light.c create mode 100644 src/scene/light_impl.h create mode 100644 src/scene/material.c create mode 100644 src/scene/material_impl.h create mode 100644 src/scene/mesh.c create mode 100644 src/scene/mesh_impl.h create mode 100644 src/scene/model.c create mode 100644 src/scene/model_impl.h create mode 100644 src/scene/node.c create mode 100644 src/scene/node_impl.h create mode 100644 src/scene/object.c create mode 100644 src/scene/object_impl.h create mode 100644 src/scene/scene.c create mode 100644 src/scene/scene_graph.h create mode 100644 src/scene/scene_impl.h create mode 100644 src/scene/scene_memory.c create mode 100644 src/scene/scene_memory.h create mode 100644 src/scene/types.h create mode 100644 src/util/geometry.c create mode 100644 src/util/ibl.c create mode 100644 src/util/shader.c create mode 100644 src/util/skyquad.c (limited to 'src') diff --git a/src/asset/asset_cache.c b/src/asset/asset_cache.c new file mode 100644 index 0000000..16c4d5c --- /dev/null +++ b/src/asset/asset_cache.c @@ -0,0 +1,252 @@ +#include "asset_cache.h" + +#include "model.h" +#include "scene/animation_impl.h" +#include "scene/model_impl.h" +#include "scene/node_impl.h" +#include "scene/scene_memory.h" +#include "texture.h" + +#include +#include +#include +#include + +#include +#include +#include + +static void log_model_load_failure(const LoadModelCmd* cmd) { + assert(cmd); + + switch (cmd->origin) { + case AssetFromFile: + log_error("Failed to load model: %s", mstring_cstr(&cmd->filepath)); + break; + case AssetFromMemory: + log_error("Failed to load model: %p", cmd->data); + break; + } +} + +static void log_texture_load_failure(const LoadTextureCmd* cmd) { + assert(cmd); + + switch (cmd->origin) { + case AssetFromFile: + switch (cmd->type) { + case LoadTexture: + log_error( + "Failed to load texture: %s", + mstring_cstr(&cmd->data.texture.filepath)); + break; + case LoadCubemap: + log_error( + "Failed to load cubemap texture: %s", + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_pos_x)); + break; + } + break; + case AssetFromMemory: + switch (cmd->type) { + case LoadTexture: + log_error("Failed to load texture: %p", cmd->data.texture.data); + break; + case LoadCubemap: + log_error( + "Failed to load texture: %p", cmd->data.cubemap.buffers.data_pos_x); + break; + } + break; + } +} + +static Hash calc_model_hash(const LoadModelCmd* cmd) { + assert(cmd); + switch (cmd->origin) { + case AssetFromFile: + return cstring_hash(mstring_cstr(&cmd->filepath)); + case AssetFromMemory: + return (Hash)cmd->data; + } + FAIL("Unhandled model asset origin"); + return 0; +} + +static Hash calc_texture_hash(const LoadTextureCmd* cmd) { + assert(cmd); + switch (cmd->origin) { + case AssetFromFile: + switch (cmd->type) { + case LoadTexture: + return cstring_hash(mstring_cstr(&cmd->data.texture.filepath)); + case LoadCubemap: + return cstring_hash( + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_pos_x)) ^ + cstring_hash( + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_neg_x)) ^ + cstring_hash( + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_pos_y)) ^ + cstring_hash( + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_neg_y)) ^ + cstring_hash( + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_pos_z)) ^ + cstring_hash( + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_neg_z)); + } + break; + case AssetFromMemory: + switch (cmd->type) { + case LoadTexture: + return (Hash)cmd->data.texture.data; + case LoadCubemap: + return (Hash)cmd->data.cubemap.buffers.data_pos_x ^ + (Hash)cmd->data.cubemap.buffers.data_neg_x ^ + (Hash)cmd->data.cubemap.buffers.data_pos_y ^ + (Hash)cmd->data.cubemap.buffers.data_neg_y ^ + (Hash)cmd->data.cubemap.buffers.data_pos_z ^ + (Hash)cmd->data.cubemap.buffers.data_neg_z; + } + break; + } + FAIL("Unhandled texture asset origin"); + return 0; +} + +static Asset* lookup_cache(AssetCache* cache, Hash hash) { + assert(cache); + mempool_foreach(&cache->assets, asset, { + if (asset->hash == hash) { + return asset; + } + }); + return 0; +} + +static void log_model_cache_hit(const LoadModelCmd* cmd, Hash hash) { + assert(cmd); + switch (cmd->origin) { + case AssetFromFile: + LOGD( + "Found asset [%s] in cache with hash [%lu]", + mstring_cstr(&cmd->filepath), hash); + break; + case AssetFromMemory: + LOGD("Found asset [%p] in cache with hash [%lu]", cmd->data, hash); + break; + } +} + +static void log_model_loaded(const LoadModelCmd* cmd) { + assert(cmd); + switch (cmd->origin) { + case AssetFromFile: + LOGD("Loaded asset from file: [%s]", mstring_cstr(&cmd->filepath)); + break; + case AssetFromMemory: + LOGD("Loaded asset from memory: [%p]", cmd->data); + break; + } +} + +static Model* clone_model(const Model* model) { + assert(model); + + // Only the Anima needs to be cloned since everything else in the model is + // static. + // + // The Anima can be partially shallow-cloned. Skeletons and animations are + // static and can be shared with the original Anima. Other members are + // deep-cloned. Skeletons in particular point back to their Anima, so need to + // be deep-cloned. + const SceneNode* root = mem_get_node(model->root); + if (gfx_get_node_type(root) == AnimaNode) { + const Anima* anima = gfx_get_node_anima(root); + Anima* anima_copy = mem_alloc_anima(); + *anima_copy = *anima; // Shallow copy. + + SceneNode* root_copy = gfx_clone_scene_shallow(root); + root_copy->anima = mem_get_anima_index(anima_copy); + anima_copy->parent = mem_get_node_index(root_copy); + + Model* copy = mem_alloc_model(); + copy->root = mem_get_node_index(root_copy); + return copy; + } else { + return (Model*)model; // Static model, can't be mutated. + } +} + +void gfx_init_asset_cache(AssetCache* cache) { + assert(cache); + mempool_make(&cache->assets); + + // Allocate a dummy asset at index 0 to guarantee that no assets allocated by + // the caller map to index 0. + const Asset* dummy = mempool_alloc(&cache->assets); + assert(mempool_get_block_index(&cache->assets, dummy) == 0); +} + +void gfx_destroy_asset_cache(AssetCache* cache) { + assert(cache); + mempool_del(&cache->assets); +} + +Model* gfx_load_model(Gfx* gfx, const LoadModelCmd* cmd) { + assert(gfx); + + AssetCache* cache = gfx_get_asset_cache(gfx); + + // First search for the asset in the cache. + const uint64_t hash = calc_model_hash(cmd); + Asset* asset = lookup_cache(cache, hash); + if (asset) { + log_model_cache_hit(cmd, hash); + return clone_model(asset->model); + } + + // Asset not found in the cache. + // Load it, insert it into the cache, and return it. + Model* model = gfx_model_load(gfx, cmd); + if (model) { + *(Asset*)mempool_alloc(&cache->assets) = (Asset){ + .type = ModelAsset, + .hash = hash, + .model = model, + }; + log_model_loaded(cmd); + return clone_model(model); + } else { + log_model_load_failure(cmd); + return 0; + } +} + +const Texture* gfx_load_texture(Gfx* gfx, const LoadTextureCmd* cmd) { + assert(gfx); + assert(cmd); + + AssetCache* cache = gfx_get_asset_cache(gfx); + + // First search for the asset in the cache. + const uint64_t hash = calc_texture_hash(cmd); + Asset* asset = lookup_cache(cache, hash); + if (asset) { + return asset->texture; + } + + // Asset not found in the cache. + // Load it, insert it into the cache, and return it. + GfxCore* gfxcore = gfx_get_core(gfx); + const Texture* texture = gfx_texture_load(gfxcore, cmd); + if (texture) { + *(Asset*)mempool_alloc(&cache->assets) = (Asset){ + .type = TextureAsset, + .hash = hash, + .texture = texture, + }; + } else { + log_texture_load_failure(cmd); + } + return texture; +} diff --git a/src/asset/asset_cache.h b/src/asset/asset_cache.h new file mode 100644 index 0000000..b2a35ed --- /dev/null +++ b/src/asset/asset_cache.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +#include +#include + +typedef struct Model Model; +typedef struct Texture Texture; + +typedef uint64_t Hash; + +typedef enum AssetType { + ModelAsset, + TextureAsset, +} AssetType; + +typedef struct Asset { + AssetType type; + Hash hash; + union { + Model* model; + const Texture* texture; + }; +} Asset; + +DEF_MEMPOOL(asset_pool, Asset, GFX_MAX_NUM_ASSETS) + +typedef struct AssetCache { + asset_pool assets; +} AssetCache; + +/// Create a new asset cache. +void gfx_init_asset_cache(AssetCache*); + +/// Destroy the asset cache. +void gfx_destroy_asset_cache(AssetCache*); diff --git a/src/asset/model.c b/src/asset/model.c new file mode 100644 index 0000000..25f2780 --- /dev/null +++ b/src/asset/model.c @@ -0,0 +1,1968 @@ +/// Loads scenes from memory and files. +/// +/// Only the GLTF scene format is current supported. +/// +/// ---------------------------------------------------------------------------- +/// glTF File Format Documentation +/// ---------------------------------------------------------------------------- +/// +/// cgltf: +/// https://github.com/jkuhlmann/cgltf +/// +/// gltf overview: +/// https://raw.githubusercontent.com/KhronosGroup/glTF/master/specification/2.0/figures/gltfOverview-2.0.0b.png +/// +/// gltf spec: +/// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md +/// +/// Sample models: +/// https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0 +/// https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/2.0/Sponza/glTF/Sponza.gltf +/// https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/2.0/AlphaBlendModeTest/glTF/AlphaBlendModeTest.gltf +/// https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/2.0/Buggy/glTF/Buggy.gltf +/// https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/2.0/AntiqueCamera/glTF/AntiqueCamera.gltf +/// https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/2.0/DamagedHelmet/glTF/DamagedHelmet.gltf +/// +/// ---------------------------------------------------------------------------- +/// Implementation Notes +/// ---------------------------------------------------------------------------- +/// +/// # glTF and the gfx library +/// +/// glTF has concepts that are similar to those in the gfx library, but there +/// isn't an exact 1-1 mapping. Concepts map as follows: +/// +/// glTF gfx +/// ---- --- +/// buffer Buffer +/// accessor + buffer view BufferView +/// mesh primitive (geom + mat) Mesh (also geom + mat) +/// mesh SceneObject +/// node SceneNode +/// +/// glTF buffers map 1-1 with gfx Buffers. glTF scenes make heavy re-use of +/// buffers across views/accessors/meshes, so it is important to make that same +/// re-use in the gfx library to use the data effectively and without +/// duplication. The Sponza scene, for example, has all of its data in one giant +/// buffer. +/// +/// glTF accessors and buffer views are combined and mapped to gfx BufferViews. +/// The glTF buffer view's offset/length/stride are combined with the accessor's +/// offset, and together with the remaining information of both data structures +/// baked into a BufferView. Internally, this information is fed into +/// glVertexAttribPointer() calls, wrapped in a VAO (index view/accessor +/// information is fed into glDrawElements()). This baking should not hurt +/// re-use, at least in the OpenGL world. +/// +/// A glTF mesh primitive contains a piece of geometry and a material. This maps +/// directly to a gfx Mesh. +/// +/// A glTF mesh is a list of mesh primitives. This maps nicely to a gfx +/// SceneObject, with the only inconvenience that terminology gets a little +/// confusing. +/// +/// Finally, glTF nodes map directly to gfx SceneNodes. Both enforce a strict +/// tree hierarchy; DAGs are not supported. +/// +/// # Materials +/// +/// glTF uses the metallic-roughness material model. However, an extension +/// allows a scene to use the specular-glossiness model as well and cgltf +/// supports it: +/// +/// https://kcoley.github.io/glTF/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness/ +/// +/// From the docs, the specular-glossiness model can represent more materials +/// than the metallic-roughness model, but it is also more computationally +/// expensive. Furthermore, a material in glTF can specify parameters for both +/// models, leaving it up to the implementation to decide which one to use. +/// In our case, we use the specular-glosiness model if parameters for it are +/// provided, otherwise we use the metallic-roughness model. + +#include "asset/model.h" + +#include "asset/texture.h" +#include "gfx/core.h" +#include "gfx/gfx.h" +#include "gfx/scene/animation.h" +#include "gfx/scene/camera.h" +#include "gfx/scene/material.h" +#include "gfx/scene/mesh.h" +#include "gfx/scene/node.h" +#include "gfx/scene/object.h" +#include "gfx/scene/scene.h" +#include "gfx/sizes.h" +#include "gfx/util/shader.h" + +#include "gfx_assert.h" +#include "scene/model_impl.h" + +#include "cstring.h" +#include "error.h" +#include "log/log.h" +#include "math/camera.h" +#include "math/defs.h" +#include "math/mat4.h" +#include "math/quat.h" +#include "math/vec2.h" +#include "math/vec3.h" + +#include "cgltf_tangents.h" +#define CGLTF_IMPLEMENTATION +#include "cgltf.h" + +#include +#include + +// Taken from the GL header file. +#define GL_NEAREST 0x2600 +#define GL_LINEAR 0x2601 +#define GL_NEAREST_MIPMAP_NEAREST 0x2700 +#define GL_LINEAR_MIPMAP_NEAREST 0x2701 +#define GL_NEAREST_MIPMAP_LINEAR 0x2702 +#define GL_LINEAR_MIPMAP_LINEAR 0x2703 + +// Uniforms names. Must match the names in shaders. +#define UNIFORM_BASE_COLOR_FACTOR "BaseColorFactor" +#define UNIFORM_METALLIC_FACTOR "MetallicFactor" +#define UNIFORM_ROUGHNESS_FACTOR "RoughnessFactor" +#define UNIFORM_EMISSIVE_FACTOR "EmissiveFactor" +#define UNIFORM_BASE_COLOR_TEXTURE "BaseColorTexture" +#define UNIFORM_METALLIC_ROUGHNESS_TEXTURE "MetallicRoughnessTexture" +#define UNIFORM_EMISSIVE_TEXTURE "EmissiveTexture" +#define UNIFORM_AMBIENT_OCCLUSION_TEXTURE "AmbientOcclusionTexture" +#define UNIFORM_NORMAL_MAP "NormalMap" + +// Shader compiler defines. Must match the names in shaders. +#define DEFINE_HAS_TEXCOORDS "HAS_TEXCOORDS" +#define DEFINE_HAS_NORMALS "HAS_NORMALS" +#define DEFINE_HAS_TANGENTS "HAS_TANGENTS" +#define DEFINE_HAS_ALBEDO_MAP "HAS_ALBEDO_MAP" +#define DEFINE_HAS_METALLIC_ROUGHNESS_MAP "HAS_METALLIC_ROUGHNESS_MAP" +#define DEFINE_HAS_NORMAL_MAP "HAS_NORMAL_MAP" +#define DEFINE_HAS_OCCLUSION_MAP "HAS_OCCLUSION_MAP" +#define DEFINE_HAS_EMISSIVE_MAP "HAS_EMISSIVE_MAP" +#define DEFINE_HAS_JOINTS "HAS_JOINTS" +#define DEFINE_MAX_JOINTS "MAX_JOINTS" + +typedef enum TextureType { + BaseColorTexture, + MetallicRoughnessTexture, + EmissiveTexture, + AmbientOcclusionTexture, + NormalMap, +} TextureType; + +/// Describes the properties of a mesh. +/// This is used to create shader permutations. +typedef struct MeshPermutation { + union { + struct { + // Vertex attributes. + bool has_texcoords : 1; + bool has_normals : 1; + bool has_tangents : 1; + bool has_joints : 1; + bool has_weights : 1; + // Textures. + bool has_albedo_map : 1; + bool has_metallic_roughness_map : 1; + bool has_normal_map : 1; + bool has_occlusion_map : 1; + bool has_emissive_map : 1; + }; + int32_t all; + }; +} MeshPermutation; + +/// Build shader compiler defines from a mesh permutation. +static size_t make_defines( + MeshPermutation perm, ShaderCompilerDefine* defines) { + static const char* str_true = "1"; + size_t next = 0; + +#define check(field, define) \ + if (perm.field) { \ + defines[next].name = sstring_make(define); \ + defines[next].value = sstring_make(str_true); \ + next++; \ + } + check(has_texcoords, DEFINE_HAS_TEXCOORDS); + check(has_normals, DEFINE_HAS_NORMALS); + check(has_tangents, DEFINE_HAS_TANGENTS); + check(has_joints, DEFINE_HAS_JOINTS); + check(has_albedo_map, DEFINE_HAS_ALBEDO_MAP); + check(has_metallic_roughness_map, DEFINE_HAS_METALLIC_ROUGHNESS_MAP); + check(has_normal_map, DEFINE_HAS_NORMAL_MAP); + check(has_occlusion_map, DEFINE_HAS_OCCLUSION_MAP); + check(has_emissive_map, DEFINE_HAS_EMISSIVE_MAP); + + if (perm.has_joints) { + defines[next].name = sstring_make(DEFINE_MAX_JOINTS); + defines[next].value = sstring_itoa(GFX_MAX_NUM_JOINTS); + next++; + } + + return next; +} + +/// Compile a shader permutation. +static ShaderProgram* make_shader_permutation( + GfxCore* gfxcore, MeshPermutation perm) { + LOGD( + "Compiling Cook-Torrance shader permutation: texcoords: %d, normals: " + "%d, tangents: %d, joints: %d, weights: %d, albedo map: %d, " + "metallic-roughness map: " + "%d, normal " + "map: %d, AO map: %d, emissive map: %d", + perm.has_texcoords, perm.has_normals, perm.has_tangents, perm.has_joints, + perm.has_weights, perm.has_albedo_map, perm.has_metallic_roughness_map, + perm.has_normal_map, perm.has_occlusion_map, perm.has_emissive_map); + + ShaderCompilerDefine defines[GFX_MAX_SHADER_COMPILER_DEFINES]; + const size_t num_defines = make_defines(perm, defines); + return gfx_make_cook_torrance_shader_perm(gfxcore, defines, num_defines); +} + +/// Map a texture type to the name of the shader uniform used to access the +/// texture. +static const char* get_texture_uniform_name(TextureType type) { + switch (type) { + case BaseColorTexture: + return UNIFORM_BASE_COLOR_TEXTURE; + case MetallicRoughnessTexture: + return UNIFORM_METALLIC_ROUGHNESS_TEXTURE; + case EmissiveTexture: + return UNIFORM_EMISSIVE_TEXTURE; + case AmbientOcclusionTexture: + return UNIFORM_AMBIENT_OCCLUSION_TEXTURE; + case NormalMap: + return UNIFORM_NORMAL_MAP; + } + assert(false); + return 0; +} + +/// Map a glTF primitive type to a gfx primitive type. +static PrimitiveType from_gltf_primitive_type(cgltf_primitive_type type) { + switch (type) { + case cgltf_primitive_type_triangles: + return Triangles; + case cgltf_primitive_type_triangle_fan: + return TriangleFan; + case cgltf_primitive_type_triangle_strip: + return TriangleStrip; + // Not yet implemented. + case cgltf_primitive_type_lines: + case cgltf_primitive_type_line_loop: + case cgltf_primitive_type_line_strip: + case cgltf_primitive_type_points: + break; + } + LOGE("Unsupported primitive type: %d", type); + assert(false); + return 0; +} + +/// Map a glTF animation path type to its Gfx equivalent. +static ChannelType from_gltf_animation_path_type( + cgltf_animation_path_type type) { + switch (type) { + case cgltf_animation_path_type_translation: + return TranslationChannel; + case cgltf_animation_path_type_rotation: + return RotationChannel; + case cgltf_animation_path_type_scale: + return ScaleChannel; + case cgltf_animation_path_type_weights: + return WeightsChannel; + case cgltf_animation_path_type_invalid: + assert(false); + break; + } + assert(false); + return 0; +} + +/// Map a glTF interpolation to its Gfx equivalent. +static AnimationInterpolation from_gltf_interpolation_type( + cgltf_interpolation_type type) { + switch (type) { + case cgltf_interpolation_type_linear: + return LinearInterpolation; + case cgltf_interpolation_type_step: + return StepInterpolation; + case cgltf_interpolation_type_cubic_spline: + return CubicSplineInterpolation; + } + assert(false); + return 0; +} + +/// Return the component's size in bytes. +static cgltf_size get_component_size(cgltf_component_type type) { + switch (type) { + case cgltf_component_type_r_8: + return 1; + case cgltf_component_type_r_8u: + return 1; + case cgltf_component_type_r_16: + return 2; + case cgltf_component_type_r_16u: + return 2; + case cgltf_component_type_r_32u: + return 4; + case cgltf_component_type_r_32f: + return 4; + case cgltf_component_type_invalid: + assert(false); + break; + } + assert(false); + return 0; +} + +/// Return the number dimensionality of the given data type. +int get_num_dimensions(cgltf_type type) { + switch (type) { + case cgltf_type_scalar: + return 1; + case cgltf_type_vec2: + return 2; + case cgltf_type_vec3: + return 3; + case cgltf_type_vec4: + return 4; + case cgltf_type_mat2: + return 4; // 2x2 + case cgltf_type_mat3: + return 9; // 3x3 + case cgltf_type_mat4: + return 16; // 4x4 + case cgltf_type_invalid: + FAIL(); + break; + } + FAIL(); + return 0; +} + +/// Read an int64 from the given data pointer and accessor. +/// The largest integer in glTF is u32, so we can fit all integers in an int64. +static int64_t read_int(const void* component, const cgltf_accessor* accessor) { + assert(component); + assert(accessor); + + switch (accessor->component_type) { + case cgltf_component_type_r_8: { + const int8_t c = *((int8_t*)component); + return c; + } + case cgltf_component_type_r_8u: { + const uint8_t c = *((uint8_t*)component); + return c; + } + case cgltf_component_type_r_16: { + const int16_t c = *((int16_t*)component); + return c; + } + case cgltf_component_type_r_16u: { + const uint16_t c = *((uint16_t*)component); + return c; + } + case cgltf_component_type_r_32u: { + const uint32_t c = *((uint32_t*)component); + return c; + } + case cgltf_component_type_r_32f: { + const float c = *((float*)component); + return (int64_t)c; + } + case cgltf_component_type_invalid: + FAIL(); + break; + } + FAIL(); + return 0; +} + +/// Read a float from the given data pointer and accessor. +/// +/// This function uses the normalization equations from the spec. See the +/// animation section: +/// +/// https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#animations +static float read_float(const void* component, const cgltf_accessor* accessor) { + assert(component); + assert(accessor); + + switch (accessor->component_type) { + case cgltf_component_type_r_8: { + // assert(accessor->normalized); + const int8_t c = *((int8_t*)component); + return max((float)c / 127.0, -1.0); + } + case cgltf_component_type_r_8u: { + // assert(accessor->normalized); + const uint8_t c = *((uint8_t*)component); + return (float)c / 255.0; + } + case cgltf_component_type_r_16: { + // assert(accessor->normalized); + const int16_t c = *((int16_t*)component); + return max((float)c / 32767.0, -1.0); + } + case cgltf_component_type_r_16u: { + // assert(accessor->normalized); + const uint16_t c = *((uint16_t*)component); + return (float)c / 65535.0; + } + case cgltf_component_type_r_32u: { + // assert(accessor->normalized); + const uint32_t c = *((uint32_t*)component); + return (float)c / 4294967295.0; + } + case cgltf_component_type_r_32f: { + const float c = *((float*)component); + return c; + } + case cgltf_component_type_invalid: + FAIL(); + break; + } + FAIL(); + return 0; +} + +typedef struct AccessorIter { + const cgltf_accessor* accessor; + const uint8_t* next_element; + cgltf_size comp_size; // Component size in bytes. + cgltf_size stride; // ELement stride in bytes. + cgltf_size index; // Index of the next element. + bool is_matrix; +} AccessorIter; + +typedef struct AccessorData { + union { + struct { + float x, y, z, w; // Possibly normalized. + int64_t xi, yi, zi, wi; // Always unnormalized. + }; + const float* floats; + }; +} AccessorData; + +bool accessor_iter_next(AccessorIter* iter, AccessorData* data) { + assert(iter); + assert(data); + + if (iter->index < iter->accessor->count) { + const int dimensions = get_num_dimensions(iter->accessor->type); + const uint8_t* component = iter->next_element; + + // So that the caller can access the element's components as an array. + data->floats = (const float*)component; + + if (!iter->is_matrix) { // Scalar or vector. + // x + data->x = read_float(component, iter->accessor); + data->xi = read_int(component, iter->accessor); + component += iter->comp_size; + // y + if (dimensions > 1) { + data->y = read_float(component, iter->accessor); + data->yi = read_int(component, iter->accessor); + component += iter->comp_size; + } + // z + if (dimensions > 2) { + data->z = read_float(component, iter->accessor); + data->zi = read_int(component, iter->accessor); + component += iter->comp_size; + } + // w + if (dimensions > 3) { + data->w = read_float(component, iter->accessor); + data->wi = read_int(component, iter->accessor); + component += iter->comp_size; + } + } + + iter->next_element += iter->stride; + iter->index++; + return true; + } + + return false; +} + +AccessorIter make_accessor_iter(const cgltf_accessor* accessor) { + assert(accessor); + + const bool is_matrix = (accessor->type == cgltf_type_mat2) || + (accessor->type == cgltf_type_mat3) || + (accessor->type == cgltf_type_mat4); + + const int dimensions = get_num_dimensions(accessor->type); + assert( + ((dimensions == 1) && (accessor->type == cgltf_type_scalar)) || + ((dimensions == 2) && (accessor->type == cgltf_type_vec2)) || + ((dimensions == 3) && (accessor->type == cgltf_type_vec3)) || + ((dimensions == 4) && (accessor->type == cgltf_type_vec4)) || + ((dimensions == 4) && (accessor->type == cgltf_type_mat2)) || + ((dimensions == 9) && (accessor->type == cgltf_type_mat3)) || + ((dimensions == 16) && (accessor->type == cgltf_type_mat4))); + + const cgltf_buffer_view* view = accessor->buffer_view; + const cgltf_buffer* buffer = view->buffer; + const cgltf_size offset = accessor->offset + view->offset; + const uint8_t* bytes = (const uint8_t*)buffer->data + offset; + // Component size in bytes. + const cgltf_size comp_size = get_component_size(accessor->component_type); + // Element size in bytes. + const cgltf_size elem_size = dimensions * comp_size; + // Stride in bytes. If the view stride is 0, then the elements are tightly + // packed. + const cgltf_size stride = view->stride != 0 ? view->stride : elem_size; + + // There isn't an accessor stride in the spec, but cgltf still specifies one. + assert(accessor->stride == elem_size); + + // Accessor data must fit inside the view. + assert(accessor->offset + (accessor->count * accessor->stride) <= view->size); + + // Accessor data must fit inside the buffer. + assert( + (offset + (accessor->count * elem_size) + + ((accessor->count - 1) * view->stride)) <= buffer->size); + + return (AccessorIter){ + .accessor = accessor, + .next_element = bytes, + .comp_size = comp_size, + .stride = stride, + .index = 0, + .is_matrix = is_matrix, + }; +} + +/// Return the total number of primitives in the scene. Each mesh may contain +/// multiple primitives. +/// +/// Note that this function scans all of the scenes in the glTF data. +static size_t get_total_primitives(const cgltf_data* data) { + size_t total = 0; + for (cgltf_size i = 0; i < data->meshes_count; ++i) { + total += data->meshes[i].primitives_count; + } + return total; +} + +/// Load all buffers from the glTF scene. +/// +/// If buffer data is loaded from memory, set filepath = null. +/// +/// Return an array of Buffers such that the index of each glTF buffer in the +/// original array matches the same Buffer in the resulting array. +/// +/// TODO: There is no need to load the inverse bind matrices buffer into the +/// GPU. Might need to lazily load buffers. +static bool load_buffers( + const cgltf_data* data, GfxCore* gfxcore, Buffer** buffers) { + assert(data); + assert(gfxcore); + assert(buffers); + + for (cgltf_size i = 0; i < data->buffers_count; ++i) { + const cgltf_buffer* buffer = &data->buffers[i]; + assert(buffer->data); + buffers[i] = gfx_make_buffer( + gfxcore, &(BufferDesc){ + .usage = BufferStatic, + .type = BufferUntyped, + .data.data = buffer->data, + .data.count = buffer->size}); + if (!buffers[i]) { + return false; + } + } + + return true; +} + +/// Load tangent buffers. +static bool load_tangent_buffers( + const cgltfTangentBuffer* cgltf_tangent_buffers, + cgltf_size num_tangent_buffers, GfxCore* gfxcore, + Buffer** tangent_buffers) { + assert(cgltf_tangent_buffers); + assert(gfxcore); + assert(tangent_buffers); + + for (cgltf_size i = 0; i < num_tangent_buffers; ++i) { + const cgltfTangentBuffer* buffer = &cgltf_tangent_buffers[i]; + assert(buffer->data); + tangent_buffers[i] = gfx_make_buffer( + gfxcore, &(BufferDesc){ + .usage = BufferStatic, + .type = BufferUntyped, + .data.data = buffer->data, + .data.count = buffer->size_bytes}); + if (!tangent_buffers[i]) { + return false; + } + } + + return true; +} + +/// Lazily load all textures from the glTF scene. +/// +/// Colour textures like albedo are in sRGB colour space. Non-colour textures +/// like normal maps are in linear space (e.g. DamagedHelmet sample). Since we +/// don't know how the texture is going to be used at this point, we can't tell +/// what colour space it should be loaded in (ideally this would be part of the +/// image file format, but not all formats specify colour space.) Therefore, we +/// load the textures lazily and don't actually commit them to GPU memory until +/// we know their colour space when loading glTF materials. +/// +/// Return an array of LoadTextureCmds such that the index of each cmd matches +/// the index of each glTF texture in the scene. +static void load_textures_lazy( + const cgltf_data* data, GfxCore* gfxcore, const char* directory, + LoadTextureCmd* load_texture_cmds) { + assert(data); + assert(gfxcore); + assert(load_texture_cmds); + + for (cgltf_size i = 0; i < data->textures_count; ++i) { + const cgltf_texture* texture = &data->textures[i]; + const cgltf_image* image = texture->image; + const cgltf_sampler* sampler = texture->sampler; + + // glTF models might not specify a sampler. In such case, the client can + // pick its own defaults. + // https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#samplers + bool mipmaps = true; + TextureFiltering filtering = LinearFiltering; + TextureWrapping wrap = Repeat; + + if (sampler) { + // The gfx library does not distinguish between sampling the texture and + // combining the mipmap levels. + const cgltf_int filter = + sampler->min_filter == 0 ? sampler->mag_filter : sampler->min_filter; + + switch (filter) { + case GL_NEAREST_MIPMAP_NEAREST: + mipmaps = true; + filtering = NearestFiltering; + break; + case GL_NEAREST_MIPMAP_LINEAR: + case GL_LINEAR_MIPMAP_NEAREST: + case GL_LINEAR_MIPMAP_LINEAR: + mipmaps = true; + filtering = LinearFiltering; + break; + case GL_NEAREST: + filtering = NearestFiltering; + break; + case GL_LINEAR: + filtering = LinearFiltering; + break; + default: + break; + } + } + + // Currently only supporting loading textures from files. + assert(image->uri); + assert(directory); + mstring fullpath = + mstring_concat_path(mstring_make(directory), mstring_make(image->uri)); + + load_texture_cmds[i] = (LoadTextureCmd){ + .origin = AssetFromFile, + .type = LoadTexture, + .colour_space = sRGB, + .filtering = filtering, + .wrap = wrap, + .mipmaps = mipmaps, + .data.texture.filepath = fullpath}; + } +} + +/// Load a texture uniform. +/// +/// This determines a texture's colour space based on its intended use, loads +/// the texture, and then defines the sampler shader uniform. +static bool load_texture_and_uniform( + const cgltf_data* data, Gfx* gfx, const cgltf_texture_view* texture_view, + TextureType texture_type, const Texture** textures, + LoadTextureCmd* load_texture_cmds, int* next_uniform, MaterialDesc* desc) { + assert(data); + assert(gfx); + assert(texture_view); + assert(textures); + assert(next_uniform); + assert(desc); + assert(*next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL); + + const size_t texture_index = texture_view->texture - data->textures; + assert(texture_index < data->textures_count); + + // Here we are assuming that if a texture is re-used, it is re-used with the + // same texture view. This should be fine because, e.g., a normal map would + // not be used as albedo and vice versa. + if (!textures[texture_index]) { + LoadTextureCmd* cmd = &load_texture_cmds[texture_index]; + // TODO: Check for colour textures and default to LinearColourSpace instead. + if (texture_type == NormalMap) { + cmd->colour_space = LinearColourSpace; + } + + LOGD( + "Load texture: %s (mipmaps: %d, filtering: %d)", + mstring_cstr(&cmd->data.texture.filepath), cmd->mipmaps, + cmd->filtering); + + textures[texture_index] = gfx_load_texture(gfx, cmd); + if (!textures[texture_index]) { + log_error( + "Failed to load texture: %s", + mstring_cstr(&cmd->data.texture.filepath)); + return false; + } + } + + assert(*next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc->uniforms[(*next_uniform)++] = (ShaderUniform){ + .name = sstring_make(get_texture_uniform_name(texture_type)), + .type = UniformTexture, + .value.texture = textures[texture_index]}; + + return true; +} + +/// Load all materials from the glTF scene. +/// +/// Return an array of Materials such that the index of each descriptor matches +/// the index of each glTF material in the scene. Also return the number of +/// materials and the textures used by them. +static bool load_materials( + const cgltf_data* data, Gfx* gfx, LoadTextureCmd* load_texture_cmds, + const Texture** textures, Material** materials) { + assert(data); + assert(gfx); + assert(materials); + if (data->textures_count > 0) { + assert(load_texture_cmds); + assert(textures); + } + + for (cgltf_size i = 0; i < data->materials_count; ++i) { + const cgltf_material* mat = &data->materials[i]; + + int next_uniform = 0; + MaterialDesc desc = {0}; + + // TODO: specular/glossiness and other material parameters. + if (mat->has_pbr_metallic_roughness) { + const cgltf_pbr_metallic_roughness* pbr = &mat->pbr_metallic_roughness; + + assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[next_uniform++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_BASE_COLOR_FACTOR), + .type = UniformVec4, + .value.vec4 = vec4_from_array(pbr->base_color_factor)}; + + assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[next_uniform++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_METALLIC_FACTOR), + .type = UniformFloat, + .value.scalar = pbr->metallic_factor}; + + assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[next_uniform++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_ROUGHNESS_FACTOR), + .type = UniformFloat, + .value.scalar = pbr->roughness_factor}; + + assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[next_uniform++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_EMISSIVE_FACTOR), + .type = UniformVec3, + .value.vec3 = vec3_from_array(mat->emissive_factor)}; + + if (pbr->base_color_texture.texture) { + if (!load_texture_and_uniform( + data, gfx, &pbr->base_color_texture, BaseColorTexture, textures, + load_texture_cmds, &next_uniform, &desc)) { + return false; + } + } + + if (pbr->metallic_roughness_texture.texture) { + if (!load_texture_and_uniform( + data, gfx, &pbr->metallic_roughness_texture, + MetallicRoughnessTexture, textures, load_texture_cmds, + &next_uniform, &desc)) { + return false; + } + } + } + + if (mat->emissive_texture.texture) { + if (!load_texture_and_uniform( + data, gfx, &mat->emissive_texture, EmissiveTexture, textures, + load_texture_cmds, &next_uniform, &desc)) { + return false; + } + } + + if (mat->occlusion_texture.texture) { + if (!load_texture_and_uniform( + data, gfx, &mat->occlusion_texture, AmbientOcclusionTexture, + textures, load_texture_cmds, &next_uniform, &desc)) { + return false; + } + } + + if (mat->normal_texture.texture) { + if (!load_texture_and_uniform( + data, gfx, &mat->normal_texture, NormalMap, textures, + load_texture_cmds, &next_uniform, &desc)) { + return false; + } + } + + assert(next_uniform < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.num_uniforms = next_uniform; + + materials[i] = gfx_make_material(&desc); + if (!materials[i]) { + return false; + } + } + + return true; +} + +/// Create a default material for meshes that do not have a material. +static Material* make_default_material() { + MaterialDesc desc = (MaterialDesc){0}; + + assert(desc.num_uniforms < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[desc.num_uniforms++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_BASE_COLOR_FACTOR), + .type = UniformVec4, + .value.vec4 = vec4_make(1, 1, 1, 1)}; + + assert(desc.num_uniforms < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[desc.num_uniforms++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_METALLIC_FACTOR), + .type = UniformFloat, + .value.scalar = 0}; + + assert(desc.num_uniforms < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[desc.num_uniforms++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_ROUGHNESS_FACTOR), + .type = UniformFloat, + .value.scalar = 1}; + + assert(desc.num_uniforms < GFX_MAX_UNIFORMS_PER_MATERIAL); + desc.uniforms[desc.num_uniforms++] = (ShaderUniform){ + .name = sstring_make(UNIFORM_EMISSIVE_FACTOR), + .type = UniformVec3, + .value.vec3 = vec3_make(0, 0, 0)}; + + return gfx_make_material(&desc); +} + +/// Compute the bounding box of the vertices pointed to by the accessor. +/// 'dim' is the dimension of the vertices (2D or 3D). +aabb3 compute_aabb(const cgltf_accessor* accessor) { + aabb3 box = {0}; + if (accessor->has_min && accessor->has_max) { + box = aabb3_make( + vec3_from_array(accessor->min), vec3_from_array(accessor->max)); + } else { + AccessorIter iter = make_accessor_iter(accessor); + AccessorData vertex = {0}; + cgltf_size i = 0; + + while (accessor_iter_next(&iter, &vertex)) { + const vec3 p = vec3_make(vertex.x, vertex.y, vertex.z); + if (i == 0) { + box = aabb3_make(p, p); + } else { + box = aabb3_add(box, p); + } + ++i; + } + } + return box; +} + +/// Load all meshes from the glTF scene. +static bool load_meshes( + const cgltf_data* data, GfxCore* gfxcore, Buffer** buffers, + Buffer** tangent_buffers, const cgltfTangentBuffer* cgltf_tangent_buffers, + cgltf_size num_tangent_buffers, Material** materials, + ShaderProgram* const shader, size_t primitive_count, Geometry** geometries, + Mesh** meshes, SceneObject** scene_objects) { + // Walk through the mesh primitives to create Meshes. A GLTF mesh primitive + // has a material (Mesh) and vertex data (Geometry). A GLTF mesh maps to + // a SceneObject. + // + // glTF gfx + // ---- --- + // Mesh SceneObject + // Mesh primitive Mesh / Geometry + // Accessor + buffer view BufferView + // Buffer Buffer + assert(data); + assert(gfxcore); + assert(buffers); + assert(materials); + assert(geometries); + assert(meshes); + assert(scene_objects); + if (num_tangent_buffers > 0) { + assert(tangent_buffers); + assert(cgltf_tangent_buffers); + } + + // Points to the next available Mesh and also the next available Geometry. + // There is one (Mesh, Geometry) pair per glTF mesh primitive. + size_t next_mesh = 0; + + for (cgltf_size m = 0; m < data->meshes_count; ++m) { + const cgltf_mesh* mesh = &data->meshes[m]; + + ObjectDesc object_desc = {0}; + + for (cgltf_size p = 0; p < mesh->primitives_count; ++p) { + assert(next_mesh < primitive_count); + const cgltf_primitive* prim = &mesh->primitives[p]; + const cgltf_material* mat = prim->material; + + MeshPermutation perm = {0}; + if (mat) { + perm.has_normal_map = mat->normal_texture.texture != 0; + perm.has_occlusion_map = mat->occlusion_texture.texture != 0; + perm.has_emissive_map = mat->emissive_texture.texture != 0; + + if (mat->has_pbr_metallic_roughness) { + const cgltf_pbr_metallic_roughness* pbr = + &mat->pbr_metallic_roughness; + perm.has_albedo_map = pbr->base_color_texture.texture != 0; + perm.has_metallic_roughness_map = + pbr->metallic_roughness_texture.texture != 0; + } else { + // TODO: specular/glossiness and other material parameters. + } + } + + GeometryDesc geometry_desc = { + .type = from_gltf_primitive_type(prim->type), + .buffer_usage = BufferStatic}; + + // Vertex indices. + if (prim->indices) { + const cgltf_accessor* accessor = prim->indices; + const cgltf_buffer_view* view = prim->indices->buffer_view; + const cgltf_size buffer_index = view->buffer - data->buffers; + + assert(buffer_index < data->buffers_count); + Buffer* buffer = buffers[buffer_index]; + + const cgltf_size component_size = + get_component_size(accessor->component_type); + switch (component_size) { + case 1: { + BufferViewIdx8* indices = &geometry_desc.indices8; + // TODO: discards const qualifier. + indices->buffer = buffer; + indices->offset_bytes = accessor->offset + view->offset; + indices->size_bytes = view->size; + indices->stride_bytes = view->stride; + geometry_desc.num_indices = prim->indices->count; + break; + } + case 2: { + BufferViewIdx16* indices = &geometry_desc.indices16; + indices->buffer = buffer; + indices->offset_bytes = accessor->offset + view->offset; + indices->size_bytes = view->size; + indices->stride_bytes = view->stride; + geometry_desc.num_indices = prim->indices->count; + break; + } + default: + // TODO: Handle 32-bit indices. + assert(false); + break; + } + } + + // Vertex attributes. + for (cgltf_size a = 0; a < prim->attributes_count; ++a) { + const cgltf_attribute* attrib = &prim->attributes[a]; + const cgltf_accessor* accessor = attrib->data; + const cgltf_buffer_view* view = accessor->buffer_view; + const cgltf_size offset = accessor->offset + view->offset; + const cgltf_size buffer_index = view->buffer - data->buffers; + + assert(buffer_index < data->buffers_count); + Buffer* buffer = buffers[buffer_index]; + + BufferView2d* buffer_view_2d = 0; + BufferView3d* buffer_view_3d = 0; + BufferView4d* buffer_view_4d = 0; + BufferViewFloat* buffer_view_float = 0; + BufferViewU8* buffer_view_u8 = 0; + BufferViewU16* buffer_view_u16 = 0; + + switch (attrib->type) { + case cgltf_attribute_type_position: { + switch (accessor->type) { + case cgltf_type_vec2: + assert(geometry_desc.positions3d.buffer == 0); + buffer_view_2d = &geometry_desc.positions2d; + geometry_desc.aabb = compute_aabb(accessor); + break; + case cgltf_type_vec3: + assert(geometry_desc.positions2d.buffer == 0); + buffer_view_3d = &geometry_desc.positions3d; + geometry_desc.aabb = compute_aabb(accessor); + break; + default: + FAIL( + "Unhandled accessor type %d in vertex positions", + accessor->type); + assert(false); + return false; + } + // It is assumed that meshes have positions, so there is nothing to + // do for the mesh permutation in this case. + break; + } + case cgltf_attribute_type_normal: + buffer_view_3d = &geometry_desc.normals; + perm.has_normals = true; + break; + case cgltf_attribute_type_tangent: + buffer_view_4d = &geometry_desc.tangents; + perm.has_tangents = true; + break; + case cgltf_attribute_type_texcoord: + buffer_view_2d = &geometry_desc.texcoords; + perm.has_texcoords = true; + break; + case cgltf_attribute_type_color: + // TODO: Add support for color. + break; + case cgltf_attribute_type_joints: + // Joints can be either u8 or u16. + switch (accessor->component_type) { + case cgltf_component_type_r_8u: + buffer_view_u8 = &geometry_desc.joints.u8; + break; + case cgltf_component_type_r_16u: + buffer_view_u16 = &geometry_desc.joints.u16; + break; + default: + assert(false); + return false; + } + perm.has_joints = true; + break; + case cgltf_attribute_type_weights: + // Weights can be either u8, u16, or float. + switch (accessor->component_type) { + case cgltf_component_type_r_8u: + buffer_view_u8 = &geometry_desc.weights.u8; + break; + case cgltf_component_type_r_16u: + buffer_view_u16 = &geometry_desc.weights.u16; + break; + case cgltf_component_type_r_32f: + buffer_view_float = &geometry_desc.weights.floats; + break; + default: + assert(false); + return false; + } + perm.has_weights = true; + break; + case cgltf_attribute_type_invalid: + assert(false); + break; + } + +#define CONFIGURE_BUFFER(buf) \ + if (buf) { \ + buf->buffer = buffer; \ + buf->offset_bytes = offset; \ + buf->size_bytes = view->size; \ + buf->stride_bytes = view->stride; \ + } + CONFIGURE_BUFFER(buffer_view_2d); + CONFIGURE_BUFFER(buffer_view_3d); + CONFIGURE_BUFFER(buffer_view_4d); + CONFIGURE_BUFFER(buffer_view_u8); + CONFIGURE_BUFFER(buffer_view_u16); + CONFIGURE_BUFFER(buffer_view_float); + } // Vertex attributes. + + assert( + (perm.has_joints && perm.has_weights) || + (!perm.has_joints && !perm.has_weights)); + + // If the mesh primitive has no tangents, see if they were computed + // separately. + if (!geometry_desc.tangents.buffer) { + for (cgltf_size t = 0; t < num_tangent_buffers; ++t) { + const cgltfTangentBuffer* cgltf_buffer = &cgltf_tangent_buffers[t]; + + if (cgltf_buffer->primitive == prim) { + BufferView4d* view = &geometry_desc.tangents; + view->buffer = tangent_buffers[t]; + view->offset_bytes = 0; + view->size_bytes = cgltf_buffer->size_bytes; + view->stride_bytes = 0; // Tightly packed. + break; + } + } + } + + // Set the number of vertices in the geometry. Since a geometry can have + // either 2d or 3d positions but not both, here we can perform addition + // to compute the total number of vertices. + geometry_desc.num_verts = + (geometry_desc.positions2d.size_bytes / sizeof(vec2)) + + (geometry_desc.positions3d.size_bytes / sizeof(vec3)); + +#define CHECK_COUNT(buffer_view, type, num_components) \ + if (geometry_desc.buffer_view.buffer) { \ + assert( \ + (geometry_desc.buffer_view.size_bytes / \ + (num_components * sizeof(type))) == geometry_desc.num_verts); \ + } + + // Check that the number of vertices is consistent across all vertex + // attributes. + CHECK_COUNT(normals, vec3, 1); + CHECK_COUNT(tangents, vec4, 1); + CHECK_COUNT(texcoords, vec2, 1); + CHECK_COUNT(joints.u8, uint8_t, 4); + CHECK_COUNT(joints.u16, uint16_t, 4); + CHECK_COUNT(weights.u8, uint8_t, 4); + CHECK_COUNT(weights.u16, uint16_t, 4); + CHECK_COUNT(weights.floats, float, 4); + + Material* material = 0; + if (mat) { + const cgltf_size material_index = mat - data->materials; + assert(material_index < data->materials_count); + material = materials[material_index]; + } else { + // Create a default material for meshes that do not specify one. + material = make_default_material(); + } + assert(material); + + geometries[next_mesh] = gfx_make_geometry(gfxcore, &geometry_desc); + if (!geometries[next_mesh]) { + return false; + } + + // If the user specifies a custom shader, use that instead. Otherwise + // compile a shader based on the mesh's permutation. + // + // Note that Gfx takes care of caching shaders and shader programs. + // + // Caching materials could be useful, but, provided they can share + // shaders, the renderer can check later whether uniforms have the same + // values. Also, changing uniforms is much faster than swapping shaders, + // so shader caching is the most important thing here. + ShaderProgram* mesh_shader = + shader ? shader : make_shader_permutation(gfxcore, perm); + assert(mesh_shader); + + meshes[next_mesh] = gfx_make_mesh(&(MeshDesc){ + .geometry = geometries[next_mesh], + .material = material, + .shader = mesh_shader}); + + if (!meshes[next_mesh]) { + return false; + } + + assert(object_desc.num_meshes < GFX_MAX_NUM_MESHES); + object_desc.meshes[object_desc.num_meshes] = meshes[next_mesh]; + object_desc.num_meshes++; + + ++next_mesh; + } // glTF mesh primitive / gfx Mesh. + + scene_objects[m] = gfx_make_object(&object_desc); + if (!scene_objects[m]) { + return false; + } + } // glTF mesh / gfx SceneObject. + + return true; +} + +/// Compute bounding boxes for the joints in the model. +static void compute_joint_bounding_boxes( + const cgltf_data* data, size_t num_joints, JointDesc* joint_descs) { + assert(data); + assert(joint_descs); + assert(num_joints <= GFX_MAX_NUM_JOINTS); + + // Initialize bounding boxes so that we can compute unions below. + for (size_t i = 0; i < num_joints; ++i) { + joint_descs[i].box = aabb3_make_empty(); + } + + // Iterate over the meshes -> primitives -> vertices -> joint indices, and add + // the vertex to the joint's bounding box. + for (cgltf_size n = 0; n < data->nodes_count; ++n) { + const cgltf_node* node = &data->nodes[n]; + + if (node->skin) { + if (node->mesh) { + const cgltf_mesh* mesh = node->mesh; + + for (cgltf_size pr = 0; pr < mesh->primitives_count; ++pr) { + const cgltf_primitive* prim = &mesh->primitives[pr]; + + // Find the indices of the positions and joints arrays in the + // primitive's attributes. + int positions_index = -1; + int joints_index = -1; + for (int a = 0; a < (int)prim->attributes_count; ++a) { + const cgltf_attribute* attrib = &prim->attributes[a]; + + if (attrib->type == cgltf_attribute_type_position) { + positions_index = a; + } else if (attrib->type == cgltf_attribute_type_joints) { + joints_index = a; + } + } + + if ((positions_index != -1) && (joints_index != -1)) { + const cgltf_accessor* positions = + prim->attributes[positions_index].data; + const cgltf_accessor* joints = prim->attributes[joints_index].data; + + assert(positions->count == joints->count); + + AccessorIter positions_iter = make_accessor_iter(positions); + AccessorIter joints_iter = make_accessor_iter(joints); + AccessorData position = {0}, joint = {0}; + + while (accessor_iter_next(&positions_iter, &position)) { + const bool advance = accessor_iter_next(&joints_iter, &joint); + assert(advance); // Counts should match. + + const vec3 p = vec3_make(position.x, position.y, position.z); + const int64_t j[4] = {joint.xi, joint.yi, joint.wi, joint.zi}; + + for (int i = 0; i < 4; ++i) { + const size_t joint_index = j[i]; + assert((size_t)joint_index < num_joints); + + joint_descs[joint_index].box = + aabb3_add(joint_descs[joint_index].box, p); + } + } + } + } + } + } + } +} + +/// Find the joint node with the smallest index across all skeletons. +/// +/// The channels in glTF may target arbitrary nodes in the scene (those nodes +/// are the joints). However, we want to map the "base joint" (the joint/node +/// with the smallest index) to 0 in the AnimaDesc's joint array. We can do this +/// by subtracting the "base node index" from every joint index or channel +/// target. +/// +/// There is an assumption in the animation library that joints are contiguous +/// anyway, so this "base joint index" works provided the joint nodes are also +/// contiguous in the glTF. The glTF does not guarantee this, but I think it's +/// a reasonable assumption that exporters write glTF files in such a way, and +/// Blender does appear to do so. +cgltf_size find_base_joint_index(const cgltf_data* data) { + assert(data); + + cgltf_size base_joint_index = (cgltf_size)-1; + + for (cgltf_size s = 0; s < data->skins_count; ++s) { + const cgltf_skin* skin = &data->skins[s]; + for (cgltf_size j = 0; j < skin->joints_count; ++j) { + // Joint is an index/pointer into the nodes array. + const cgltf_size node_index = skin->joints[j] - data->nodes; + assert(node_index < data->nodes_count); + // Min. + if (node_index < base_joint_index) { + base_joint_index = node_index; + } + } + } + + return base_joint_index; +} + +/// Load all skins (Gfx skeletons) from the glTF scene. +/// Return the total number of joints. +static size_t load_skins( + const cgltf_data* data, Buffer* const* buffers, cgltf_size base_joint_index, + AnimaDesc* anima_desc) { + assert(data); + assert(buffers); + assert(anima_desc); + assert(base_joint_index < data->nodes_count); + + // Determines whether the ith joint in the node hierarchy is a joint node. + // This is then used to determine whether a joint is a root of the joint + // hierarchy. + bool is_joint_node[GFX_MAX_NUM_JOINTS] = {false}; + + size_t num_joints = 0; + + for (cgltf_size s = 0; s < data->skins_count; ++s) { + const cgltf_skin* skin = &data->skins[s]; + const cgltf_accessor* matrices_accessor = skin->inverse_bind_matrices; + assert(matrices_accessor->count == skin->joints_count); + + num_joints += skin->joints_count; + assert(num_joints < GFX_MAX_NUM_JOINTS); + + SkeletonDesc* skeleton_desc = &anima_desc->skeletons[s]; + *skeleton_desc = (SkeletonDesc){.num_joints = skin->joints_count}; + + // for (cgltf_size j = 0; j < skin->joints_count; ++j) { + AccessorIter iter = make_accessor_iter(matrices_accessor); + AccessorData matrix = {0}; + for (cgltf_size i = 0; accessor_iter_next(&iter, &matrix); ++i) { + const mat4 inv_bind_matrix = mat4_from_array(matrix.floats); + + // Joint is an index/pointer into the nodes array. + const cgltf_size node_index = skin->joints[i] - data->nodes; + assert(node_index < data->nodes_count); + + const cgltf_size parent_node_index = + skin->joints[i]->parent - data->nodes; + assert(parent_node_index < data->nodes_count); + + // Subtract the base index to pack the joints as tightly as possible in + // the AnimaDesc. + assert(node_index >= base_joint_index); + const cgltf_size joint_index = node_index - base_joint_index; + + assert(parent_node_index >= base_joint_index); + const cgltf_size parent_index = parent_node_index - base_joint_index; + + skeleton_desc->joints[i] = joint_index; + + JointDesc* joint_desc = &anima_desc->joints[joint_index]; + joint_desc->parent = parent_index; + joint_desc->inv_bind_matrix = inv_bind_matrix; + + is_joint_node[joint_index] = true; + }; + + // glTF may specify a "skeleton", which is the root of the skin's + // (skeleton's) node hierarchy. + // if (skin->skeleton) { + // // cgltf_size root_index = skin->skeleton - data->nodes; + // // assert(root_index <= data->nodes_count); + // // root_node = nodes[root_index]; + // assert(false); + //} + } + + // Animation library assumes that joints are contiguous. + for (size_t i = 0; i < num_joints; ++i) { + assert(is_joint_node[i]); + } + + // Insert the root joint. + // This is the root of all skeletons. It is, specifically, the root of all + // joints that do not have a parent; skins (skeletons) in glTF are not + // guaranteed to have a common parent, but are generally a set of disjoint + // trees. + const size_t root_index = num_joints; + assert(root_index < GFX_MAX_NUM_JOINTS); + anima_desc->joints[root_index] = (JointDesc){.parent = INDEX_NONE}; + num_joints++; + + // Make root joints point to the root joint at index N. + // The root joints are the ones that have a non-joint node in the glTF as a + // parent. + for (size_t i = 0; i < root_index; ++i) { + JointDesc* joint = &anima_desc->joints[i]; + if ((joint->parent >= root_index) || !is_joint_node[joint->parent]) { + joint->parent = root_index; + } + } + + return num_joints; +} + +/// Load all animations from the glTF scene. +static void load_animations( + const cgltf_data* data, cgltf_size base_joint_index, + AnimaDesc* anima_desc) { + assert(data); + assert(anima_desc); + assert(base_joint_index < data->nodes_count); + assert(data->animations_count <= GFX_MAX_NUM_ANIMATIONS); + + for (cgltf_size a = 0; a < data->animations_count; ++a) { + const cgltf_animation* animation = &data->animations[a]; + AnimationDesc* animation_desc = &anima_desc->animations[a]; + + *animation_desc = (AnimationDesc){ + .name = sstring_make(animation->name), + .num_channels = animation->channels_count}; + + assert(animation->channels_count <= GFX_MAX_NUM_CHANNELS); + for (cgltf_size c = 0; c < animation->channels_count; ++c) { + const cgltf_animation_channel* channel = &animation->channels[c]; + ChannelDesc* channel_desc = &animation_desc->channels[c]; + const cgltf_animation_sampler* sampler = channel->sampler; + + const size_t target_index = channel->target_node - data->nodes; + assert(target_index < data->nodes_count); + + assert(target_index >= base_joint_index); + const size_t tight_target_index = target_index - base_joint_index; + assert(tight_target_index < anima_desc->num_joints); + + *channel_desc = (ChannelDesc){ + .target = tight_target_index, + .type = from_gltf_animation_path_type(channel->target_path), + .interpolation = from_gltf_interpolation_type(sampler->interpolation), + .num_keyframes = 0}; + + // Read time inputs. + AccessorIter iter = make_accessor_iter(sampler->input); + AccessorData input = {0}; + for (cgltf_size i = 0; accessor_iter_next(&iter, &input); ++i) { + channel_desc->keyframes[i].time = input.x; + channel_desc->num_keyframes++; + } + + // Read transform outputs. + AccessorData output = {0}; + switch (channel->target_path) { + case cgltf_animation_path_type_translation: { + iter = make_accessor_iter(sampler->output); + for (cgltf_size i = 0; accessor_iter_next(&iter, &output); ++i) { + channel_desc->keyframes[i].translation = + vec3_make(output.x, output.y, output.z); + } + break; + } + case cgltf_animation_path_type_rotation: { + iter = make_accessor_iter(sampler->output); + for (cgltf_size i = 0; accessor_iter_next(&iter, &output); ++i) { + channel_desc->keyframes[i].rotation = + qmake(output.x, output.y, output.z, output.w); + } + break; + } + default: + // TODO: Handle other channel transformations. + break; + } + } + } +} + +/// Load all nodes from the glTF scene. +/// +/// This function ignores the many scenes and default scene of the glTF spec +/// and instead just loads all nodes into a single gfx Scene. +static void load_nodes( + const cgltf_data* data, SceneNode* root_node, SceneObject** objects, + SceneCamera** cameras, const Anima* anima, SceneNode** nodes) { + // Note that with glTF 2.0, nodes do not form a DAG / scene graph but a + // disjount union of strict trees: + // + // "For Version 2.0 conformance, the glTF node hierarchy is not a directed + // acyclic graph (DAG) or scene graph, but a disjoint union of strict trees. + // That is, no node may be a direct descendant of more than one node. This + // restriction is meant to simplify implementation and facilitate + // conformance." + // + // This matches the gfx library implementation, where every node can have at + // most one parent. + assert(data); + assert(root_node); + assert(objects); + assert(cameras); + assert(nodes); + + cgltf_size next_camera = 0; + + for (cgltf_size n = 0; n < data->nodes_count; ++n) { + const cgltf_node* node = &data->nodes[n]; + + // Add SceneObject, SceneCamera or Lights. + // TODO: Handle lights once they are implemented in the gfx library. + if (node->mesh) { + const cgltf_size mesh_index = node->mesh - data->meshes; + assert(mesh_index < data->meshes_count); + SceneObject* object = objects[mesh_index]; + gfx_construct_object_node(nodes[n], object); + + if (node->skin) { + assert(anima); + + const cgltf_size skin_index = node->skin - data->skins; + assert(skin_index < data->skins_count); + const Skeleton* skeleton = gfx_get_anima_skeleton(anima, skin_index); + gfx_set_object_skeleton(object, skeleton); + } + } else if (node->camera) { + assert(next_camera < data->cameras_count); + + Camera camera; + const cgltf_camera* cam = node->camera; + + // TODO: We could define a function load_cameras() the same way we load + // every mesh and then remove this ad-hoc loading of cameras here, as well + // as remove 'next_camera'. + switch (cam->type) { + case cgltf_camera_type_orthographic: + camera = camera_orthographic( + 0, cam->data.orthographic.xmag, 0, cam->data.orthographic.ymag, + cam->data.orthographic.znear, cam->data.orthographic.zfar); + break; + case cgltf_camera_type_perspective: + camera = camera_perspective( + cam->data.perspective.yfov, cam->data.perspective.aspect_ratio, + cam->data.perspective.znear, cam->data.perspective.zfar); + break; + case cgltf_camera_type_invalid: + break; + } + + gfx_set_camera_camera(cameras[next_camera], &camera); + gfx_construct_camera_node(nodes[n], cameras[next_camera]); + ++next_camera; + } else { + // TODO: implementation for missing node types. + // These nodes currently default to logical nodes. + } + assert(nodes[n]); + + // Set transform. + mat4 transform; + if (node->has_matrix) { + transform = mat4_from_array(node->matrix); + } else { + transform = mat4_id(); + if (node->has_scale) { + const mat4 scale = mat4_scale(vec3_from_array(node->scale)); + transform = mat4_mul(transform, scale); + } + if (node->has_rotation) { + const quat q = quat_from_array(node->rotation); + const mat4 rotate = mat4_from_quat(q); + transform = mat4_mul(transform, rotate); + } + if (node->has_translation) { + const mat4 translate = + mat4_translate(vec3_from_array(node->translation)); + transform = mat4_mul(translate, transform); + } + } + gfx_set_node_transform(nodes[n], &transform); + + // If this is a top-level node in the glTF scene, set its parent to the + // given root node. + if (!node->parent) { + gfx_set_node_parent(nodes[n], root_node); + } else { + const cgltf_size parent_index = node->parent - data->nodes; + assert(parent_index < data->nodes_count); + SceneNode* parent = nodes[parent_index]; + assert(parent); + gfx_set_node_parent(nodes[n], parent); + } + } // SceneNode. +} + +/// Remove joint nodes from the Gfx Scene. +/// +/// Joint nodes are not needed because joints are packed into the Anima. +static void remove_joint_nodes( + const cgltf_data* data, SceneNode** scene_nodes) { + assert(data); + assert(scene_nodes); + + // This works assuming the joint nodes are contiguous. Contiguity is checked + // when loading skins. See load_skins(). + size_t min_joint_index = (size_t)-1; + size_t max_joint_index = 0; + + // First get the minimum and maximum indices of all joint nodes. + for (cgltf_size s = 0; s < data->skins_count; ++s) { + const cgltf_skin* skin = &data->skins[s]; + + for (cgltf_size j = 0; j < skin->joints_count; ++j) { + // Joint is an index/pointer into the nodes array. + const cgltf_size joint_index = skin->joints[j] - data->nodes; + assert(joint_index < data->nodes_count); + + if (joint_index < min_joint_index) { + min_joint_index = joint_index; + } + if (joint_index > max_joint_index) { + max_joint_index = joint_index; + } + } + } + + assert(min_joint_index < data->nodes_count); + assert(max_joint_index < data->nodes_count); + + // Now walk over the joint nodes. If a joint's parent is itself not a joint + // node, then that joint is a root of a joint hierarchy (skins in glTF may + // have multiple roots). In such case, delete the root joint recursively. + for (cgltf_size s = 0; s < data->skins_count; ++s) { + const cgltf_skin* skin = &data->skins[s]; + + for (cgltf_size j = 0; j < skin->joints_count; ++j) { + // Joint is an index/pointer into the nodes array. + const cgltf_size joint_index = skin->joints[j] - data->nodes; + assert(joint_index < data->nodes_count); + + const cgltf_node* joint = &data->nodes[joint_index]; + + // Parent node index. + const cgltf_size parent_index = joint->parent - data->nodes; + assert(parent_index < data->nodes_count); + + // If the parent is not a joint node, recursively delete this joint node. + if ((parent_index < min_joint_index) || + (parent_index > max_joint_index)) { + gfx_destroy_node(&scene_nodes[joint_index]); + } + } + } +} + +/// Load all scenes from the glTF file. +/// +/// If the scene is loaded from memory, set filepath = null. +/// +/// This function ignores the many scenes and default scene of the glTF spec +/// and instead just loads all scenes into a single Gfx Scene. +static Model* load_scene( + cgltf_data* data, Gfx* gfx, const mstring* filepath, ShaderProgram* shader, + const cgltfTangentBuffer* cgltf_tangent_buffers, + cgltf_size num_tangent_buffers) { + // In a GLTF scene, buffers can be shared among meshes, meshes among nodes, + // etc. Each object is referenced by its index in the relevant array. Here we + // do a button-up construction, first allocating our own graphics objects in + // the same quantities and then re-using the GLTF indices to index these + // arrays. + // + // For simplicity, this function also handles all of the cleanup. Arrays are + // allocated up front, and the helper functions construct their elements. If + // an error is encountered, the helper functions can simply return and this + // function cleans up any intermediate objects that had been created up until + // the point of failure. + // + // Loading animation data: + // - Buffers with animation sampler data need to stay on the CPU, not + // uploaded to the GPU. We could try to implement GPU animation at a later + // stage. + assert(data); + assert(gfx); + assert(filepath); + assert((num_tangent_buffers == 0) || (cgltf_tangent_buffers != 0)); + + bool success = false; + + GfxCore* gfxcore = gfx_get_core(gfx); + const size_t primitive_count = get_total_primitives(data); + + const mstring directory = mstring_dirname(*filepath); + LOGD("Filepath: %s", mstring_cstr(filepath)); + LOGD("Directory: %s", mstring_cstr(&directory)); + + Buffer** tangent_buffers = 0; + Buffer** buffers = 0; + LoadTextureCmd* load_texture_cmds = 0; + const Texture** textures = 0; // Textures are owned by asset cache. + Material** materials = 0; + Geometry** geometries = 0; + Mesh** meshes = 0; + AnimaDesc* anima_desc = 0; + SceneObject** scene_objects = 0; + SceneCamera** scene_cameras = 0; + SceneNode** scene_nodes = 0; + Anima* anima = 0; + SceneNode* root_node = 0; + Model* model = 0; + + tangent_buffers = calloc(num_tangent_buffers, sizeof(Buffer*)); + buffers = calloc(data->buffers_count, sizeof(Buffer*)); + textures = calloc(data->textures_count, sizeof(Texture*)); + materials = calloc(data->materials_count, sizeof(Material*)); + geometries = calloc(primitive_count, sizeof(Geometry*)); + meshes = calloc(primitive_count, sizeof(Mesh*)); + scene_objects = calloc(data->meshes_count, sizeof(SceneObject*)); + scene_cameras = calloc(data->cameras_count, sizeof(SceneCamera**)); + scene_nodes = calloc(data->nodes_count, sizeof(SceneNode**)); + // A glTF scene does not necessarily have textures. Materials can be given + // as constants, for example. + if (data->textures_count > 0) { + load_texture_cmds = calloc(data->textures_count, sizeof(LoadTextureCmd)); + } + + if (!buffers || !tangent_buffers || + ((data->textures_count > 0) && !load_texture_cmds) || !textures || + !materials || !geometries || !meshes || !scene_objects || + !scene_cameras || !scene_nodes) { + goto cleanup; + } + + if ((num_tangent_buffers > 0) && + !load_tangent_buffers( + cgltf_tangent_buffers, num_tangent_buffers, gfxcore, + tangent_buffers)) { + goto cleanup; + } + + if (!load_buffers(data, gfxcore, buffers)) { + goto cleanup; + } + + if (data->textures_count > 0) { + load_textures_lazy( + data, gfxcore, mstring_cstr(&directory), load_texture_cmds); + } + + if (!load_materials(data, gfx, load_texture_cmds, textures, materials)) { + goto cleanup; + } + + if (!load_meshes( + data, gfxcore, buffers, tangent_buffers, cgltf_tangent_buffers, + num_tangent_buffers, materials, shader, primitive_count, geometries, + meshes, scene_objects)) { + goto cleanup; + } + + // Skins refer to nodes, and nodes may refer to skins. To break this circular + // dependency, glTF defines skins in terms of node indices. We could do the + // same if Gfx allowed allocating nodes contiguously in memory. For now, + // create the nodes up front and use the indices of the array to map to the + // node_idx. + for (cgltf_size i = 0; i < data->nodes_count; ++i) { + scene_nodes[i] = gfx_make_node(); + } + + // Create the scene's root node. + // This is an anima node if the scene has skins; otherwise it is a logical + // node. + root_node = gfx_make_node(); + if (data->skins_count > 0) { + anima_desc = calloc(1, sizeof(AnimaDesc)); + if (!anima_desc) { + goto cleanup; + } + + const cgltf_size base = find_base_joint_index(data); + + anima_desc->num_skeletons = data->skins_count; + anima_desc->num_animations = data->animations_count; + anima_desc->num_joints = load_skins(data, buffers, base, anima_desc); + load_animations(data, base, anima_desc); + + compute_joint_bounding_boxes( + data, anima_desc->num_joints, anima_desc->joints); + + anima = gfx_make_anima(anima_desc); + gfx_construct_anima_node(root_node, anima); + } + + // The root node becomes the root of all scene nodes. + load_nodes(data, root_node, scene_objects, scene_cameras, anima, scene_nodes); + + // Clean up scene nodes that correspond to joints in the glTF. These are + // not needed anymore. + if (data->skins_count > 0) { + remove_joint_nodes(data, scene_nodes); + } + + model = gfx_make_model(root_node); + + success = true; + +cleanup: + // The arrays of resources are no longer needed. The resources themselves are + // destroyed only if this function fails. + if (tangent_buffers) { + if (!success) { + for (cgltf_size i = 0; i < num_tangent_buffers; ++i) { + if (tangent_buffers[i]) { + gfx_destroy_buffer(gfxcore, &tangent_buffers[i]); + } + } + } + free(tangent_buffers); + } + if (buffers) { + if (!success) { + for (cgltf_size i = 0; i < data->buffers_count; ++i) { + if (buffers[i]) { + gfx_destroy_buffer(gfxcore, &buffers[i]); + } + } + } + free(buffers); + } + if (load_texture_cmds) { + free(load_texture_cmds); + } + if (textures) { + free(textures); + } + if (materials) { + if (!success) { + for (cgltf_size i = 0; i < data->materials_count; ++i) { + if (materials[i]) { + gfx_destroy_material(&materials[i]); + } + } + } + free(materials); + } + if (geometries) { + if (!success) { + for (size_t i = 0; i < primitive_count; ++i) { + if (geometries[i]) { + gfx_destroy_geometry(gfxcore, &geometries[i]); + } + } + } + free(geometries); + } + if (meshes) { + if (!success) { + for (size_t i = 0; i < primitive_count; ++i) { + if (meshes[i]) { + gfx_destroy_mesh(&meshes[i]); + } + } + } + free(meshes); + } + if (anima_desc) { + free(anima_desc); + } + if (scene_objects) { + if (!success) { + for (cgltf_size i = 0; i < data->meshes_count; ++i) { + if (scene_objects[i]) { + gfx_destroy_object(&scene_objects[i]); + } + } + } + free(scene_objects); + } + if (scene_cameras) { + if (!success) { + for (cgltf_size i = 0; i < data->cameras_count; ++i) { + if (scene_cameras[i]) { + gfx_destroy_camera(&scene_cameras[i]); + } + } + } + free(scene_cameras); + } + if (scene_nodes) { + if (!success) { + for (cgltf_size i = 0; i < data->nodes_count; ++i) { + if (scene_nodes[i]) { + gfx_destroy_node(&scene_nodes[i]); + } + } + } + free(scene_nodes); + } + if (!success) { + if (root_node) { + gfx_destroy_node(&root_node); // Node owns the anima. + } else if (anima) { + gfx_destroy_anima(&anima); + } + } + return model; +} + +Model* gfx_model_load(Gfx* gfx, const LoadModelCmd* cmd) { + assert(gfx); + assert(cmd); + + Model* model = 0; + + cgltf_options options = {0}; + cgltf_data* data = NULL; + cgltfTangentBuffer* tangent_buffers = 0; + + cgltf_result result; + switch (cmd->origin) { + case AssetFromFile: + result = cgltf_parse_file(&options, mstring_cstr(&cmd->filepath), &data); + break; + case AssetFromMemory: + result = cgltf_parse(&options, cmd->data, cmd->size_bytes, &data); + break; + } + if (result != cgltf_result_success) { + goto cleanup; + } + + if (cmd->origin == AssetFromFile) { + // Must call cgltf_load_buffers() to load buffer data. + result = cgltf_load_buffers(&options, data, mstring_cstr(&cmd->filepath)); + if (result != cgltf_result_success) { + goto cleanup; + } + } + + // Compute tangents for normal-mapped models that are missing them. + cgltf_size num_tangent_buffers = 0; + cgltf_compute_tangents( + &options, data, &tangent_buffers, &num_tangent_buffers); + + model = load_scene( + data, gfx, &cmd->filepath, cmd->shader, tangent_buffers, + num_tangent_buffers); + +cleanup: + if (data) { + cgltf_free(data); + } + if (tangent_buffers) { + free(tangent_buffers); + } + return model; +} diff --git a/src/asset/model.h b/src/asset/model.h new file mode 100644 index 0000000..d6399b1 --- /dev/null +++ b/src/asset/model.h @@ -0,0 +1,12 @@ +/// Load scene files. +#pragma once + +#include + +typedef struct Gfx Gfx; +typedef struct Model Model; + +/// Load a model. +/// +/// Currently only supports the GLTF format. +Model* gfx_model_load(Gfx*, const LoadModelCmd*); diff --git a/src/asset/texture.c b/src/asset/texture.c new file mode 100644 index 0000000..c790394 --- /dev/null +++ b/src/asset/texture.c @@ -0,0 +1,177 @@ +#include "texture.h" + +#include "gfx/core.h" + +#include "error.h" + +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" + +#include + +static void flip_horizontally( + unsigned char* pixels, int width, int height, int components) { + assert(pixels); + + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width / 2; ++x) { + unsigned char* p1 = &pixels[(y * width + x) * components]; + unsigned char* p2 = &pixels[(y * width + (width - x - 1)) * components]; + + for (int c = 0; c < components; ++c) { + unsigned char tmp = *p1; + *p1 = *p2; + *p2 = tmp; + p1++; + p2++; + } + } + } +} + +// Note that the cubemap coordinate system uses the one in RenderMan: +// +// https://www.khronos.org/opengl/wiki/Cubemap_Texture +// +// This is what happens: +// +// - Cubemaps follow a left-handed coordinate system. Say, +X is right, +Y is +// up, and +Z is forward. +// - The texture coordinate system follow's DirectX's, so +V goes down, not up +// like it does in OpenGL. +// +// For this reason, we do X and Y flips when doing cubemap textures so that we +// can sample cubemaps as if they were given in the usual OpenGL coordinate +// system. +Texture* gfx_texture_load(GfxCore* gfxcore, const LoadTextureCmd* cmd) { + assert(gfxcore); + assert(cmd); + assert(cmd->origin == AssetFromFile || cmd->origin == AssetFromMemory); + assert(cmd->type == LoadTexture || cmd->type == LoadCubemap); + + int width, height, components, old_components; + unsigned char* pixels[6] = {0}; + + switch (cmd->origin) { + case AssetFromFile: + switch (cmd->type) { + case LoadTexture: { + const char* filepath = mstring_cstr(&cmd->data.texture.filepath); + stbi_set_flip_vertically_on_load(0); + pixels[0] = stbi_load(filepath, &width, &height, &components, 0); + if (!pixels[0]) { + log_error("Failed to load texture file: %s", filepath); + } + break; + } + case LoadCubemap: + for (int i = 0; i < 6; ++i) { + // Flip +Y and -Y textures vertically. + stbi_set_flip_vertically_on_load(((i == 2) || (i == 3)) ? 1 : 0); + const char* filepath = + mstring_cstr(&cmd->data.cubemap.filepaths.filepath_pos_x + i); + stbi_uc* image_pixels = + stbi_load(filepath, &width, &height, &components, 0); + if (!image_pixels) { + log_error("Failed to load texture file: %s", filepath); + break; + } + if (i > 0 && components != old_components) { + log_error("All textures in a cubemap must have the same number of " + "components"); + break; + } + if ((i != 2) && (i != 3)) { + flip_horizontally(image_pixels, width, height, components); + } + pixels[i] = image_pixels; + old_components = components; + } + break; + } + break; + case AssetFromMemory: + // TODO: Load textures from memory. + log_error("Loading textures from memory is not yet implemented"); + return 0; + } + + // Error out if we failed to load a texture. + if (!pixels[0] || + (cmd->type == LoadCubemap && + (!pixels[1] || !pixels[2] || !pixels[3] || !pixels[4] || !pixels[5]))) { + for (int i = 0; i < 6; ++i) { + if (pixels[i]) { + stbi_image_free(pixels[i]); + } + } + return 0; + } + + TextureDesc desc = (TextureDesc){0}; + desc.width = width; + desc.height = height; + + switch (cmd->type) { + case LoadTexture: + desc.dimension = Texture2D; + break; + case LoadCubemap: + desc.dimension = TextureCubeMap; + break; + } + + switch (components) { + case 3: + switch (cmd->colour_space) { + case LinearColourSpace: + desc.format = TextureRGB8; + break; + case sRGB: + desc.format = TextureSRGB8; + break; + default: + log_error("Unsupported texture colour space: %d", cmd->colour_space); + return 0; + } + break; + case 4: + switch (cmd->colour_space) { + case LinearColourSpace: + desc.format = TextureRGBA8; + break; + case sRGB: + desc.format = TextureSRGBA8; + break; + default: + log_error("Unsupported texture colour space: %d", cmd->colour_space); + return 0; + } + break; + default: + log_error("Unsupported number of texture components: %d", components); + return 0; + } + + desc.filtering = cmd->filtering; + desc.mipmaps = cmd->mipmaps; + + switch (cmd->type) { + case LoadTexture: + desc.data.pixels = pixels[0]; + break; + case LoadCubemap: + for (int i = 0; i < 6; ++i) { + *(&desc.data.cubemap.pixels_pos_x + i) = pixels[i]; + } + break; + } + + Texture* texture = gfx_make_texture(gfxcore, &desc); + for (int i = 0; i < 6; ++i) { + if (pixels[i]) { + stbi_image_free(pixels[i]); + } + } + return texture; +} diff --git a/src/asset/texture.h b/src/asset/texture.h new file mode 100644 index 0000000..0d38bd9 --- /dev/null +++ b/src/asset/texture.h @@ -0,0 +1,7 @@ +/// Load textures from images. +#pragma once + +#include + +/// Load a texture. +Texture* gfx_texture_load(GfxCore*, const LoadTextureCmd*); diff --git a/src/core/buffer.c b/src/core/buffer.c new file mode 100644 index 0000000..3b7e4bc --- /dev/null +++ b/src/core/buffer.c @@ -0,0 +1,85 @@ +#include "buffer.h" + +#include +#include + +#include +#include +#include + +static size_t get_buffer_size_bytes( + BufferType type, const BufferDataDesc* desc) { + return desc->count * gfx_get_buffer_type_size_bytes(type); +} + +static GLenum get_buffer_usage(BufferUsage usage) { + switch (usage) { + case BufferStatic: + return GL_STATIC_DRAW; + case BufferDynamic: + return GL_DYNAMIC_DRAW; + } + FAIL("Unhandled buffer usage"); + return GL_STATIC_DRAW; +} + +size_t gfx_get_buffer_type_size_bytes(BufferType type) { + switch (type) { + case BufferUntyped: + return 1; + case Buffer2d: + return sizeof(vec2); + case Buffer3d: + return sizeof(vec3); + case Buffer4d: + return sizeof(vec4); + case BufferFloat: + return sizeof(float); + case BufferU8: + return sizeof(uint8_t); + case BufferU16: + return sizeof(uint16_t); + } + FAIL("Unhandled buffer type"); + return 0; +} + +bool gfx_init_buffer(Buffer* buffer, const BufferDesc* desc) { + assert(buffer); + + buffer->type = desc->type; + buffer->usage = desc->usage; + buffer->size_bytes = get_buffer_size_bytes(desc->type, &desc->data); + const GLenum usage = get_buffer_usage(desc->usage); + + glGenBuffers(1, &buffer->vbo); + glBindBuffer(GL_ARRAY_BUFFER, buffer->vbo); + glBufferData(GL_ARRAY_BUFFER, buffer->size_bytes, desc->data.data, usage); + glBindBuffer(GL_ARRAY_BUFFER, 0); + ASSERT_GL; + + return true; +} + +void gfx_del_buffer(Buffer* buffer) { + assert(buffer); + if (buffer->vbo) { + glDeleteBuffers(1, &buffer->vbo); + buffer->vbo = 0; + } +} + +void gfx_update_buffer(Buffer* buffer, const BufferDataDesc* desc) { + assert(buffer); + assert(desc); + // OpenGL allows updating static buffers, but it is not optimal for + // performance, so we enforce data in static buffers remain static. + assert(buffer->usage == BufferDynamic); + + const size_t update_size_bytes = get_buffer_size_bytes(buffer->type, desc); + assert(update_size_bytes <= buffer->size_bytes); + + glBindBuffer(GL_ARRAY_BUFFER, buffer->vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, update_size_bytes, desc->data); + glBindBuffer(GL_ARRAY_BUFFER, 0); +} diff --git a/src/core/buffer.h b/src/core/buffer.h new file mode 100644 index 0000000..b9080f0 --- /dev/null +++ b/src/core/buffer.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "gl_util.h" + +#include + +#include +#include + +typedef struct Buffer { + GLuint vbo; + BufferType type; + BufferUsage usage; + size_t size_bytes; +} Buffer; + +/// Return the buffer type size in bytes. +size_t gfx_get_buffer_type_size_bytes(BufferType); + +/// Create a buffer from raw data. +bool gfx_init_buffer(Buffer*, const BufferDesc*); + +/// Destroy the buffer. +void gfx_del_buffer(Buffer*); diff --git a/src/core/constants.h b/src/core/constants.h new file mode 100644 index 0000000..a6a3b94 --- /dev/null +++ b/src/core/constants.h @@ -0,0 +1,9 @@ +#pragma once + +// Shaders vertex attribute locations must match the channels here. +#define GFX_POSITION_CHANNEL 0 +#define GFX_NORMAL_CHANNEL 1 +#define GFX_TANGENT_CHANNEL 2 +#define GFX_TEXCOORDS_CHANNEL 3 +#define GFX_JOINTS_CHANNEL 4 +#define GFX_WEIGHTS_CHANNEL 5 diff --git a/src/core/core.c b/src/core/core.c new file mode 100644 index 0000000..90038c6 --- /dev/null +++ b/src/core/core.c @@ -0,0 +1,429 @@ +#include "core_impl.h" + +#include "gl_util.h" + +// #include + +#include + +void gfx_init_gfxcore(GfxCore* gfxcore) { + assert(gfxcore); + + mempool_make(&gfxcore->buffers); + mempool_make(&gfxcore->framebuffers); + mempool_make(&gfxcore->geometries); + mempool_make(&gfxcore->renderbuffers); + mempool_make(&gfxcore->shaders); + mempool_make(&gfxcore->shader_programs); + mempool_make(&gfxcore->textures); + + mempool_make(&gfxcore->shader_cache); + mempool_make(&gfxcore->program_cache); + + glEnable(GL_CULL_FACE); + glFrontFace(GL_CCW); + glCullFace(GL_BACK); + + glEnable(GL_DEPTH_TEST); + + // Filter cubemaps across their faces to avoid seams. + // https://www.khronos.org/opengl/wiki/Cubemap_Texture#Seamless_cubemap + glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); +} + +// Conveniently destroy any objects that have not been destroyed by the +// application. +void gfx_del_gfxcore(GfxCore* gfxcore) { + assert(gfxcore); + + mempool_foreach(&gfxcore->buffers, buffer, { gfx_del_buffer(buffer); }); + + mempool_foreach(&gfxcore->framebuffers, framebuffer, { + gfx_del_framebuffer(framebuffer); + }); + + mempool_foreach( + &gfxcore->geometries, geometry, { gfx_del_geometry(geometry); }); + + mempool_foreach(&gfxcore->renderbuffers, renderbuffer, { + gfx_del_renderbuffer(renderbuffer); + }); + + mempool_foreach( + &gfxcore->shader_programs, prog, { gfx_del_shader_program(prog); }); + + mempool_foreach(&gfxcore->shaders, shader, { gfx_del_shader(shader); }); + + mempool_foreach(&gfxcore->textures, texture, { gfx_del_texture(texture); }); +} + +// ----------------------------------------------------------------------------- +// Render commands. +// ----------------------------------------------------------------------------- + +void gfx_start_frame(GfxCore* gfxcore) { + assert(gfxcore); + + glViewport( + gfxcore->viewport.x, gfxcore->viewport.y, gfxcore->viewport.width, + gfxcore->viewport.height); + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + ASSERT_GL; +} + +void gfx_end_frame(GfxCore* gfxcore) { + assert(gfxcore); + ASSERT_GL; +} + +void gfx_set_viewport(GfxCore* gfxcore, int x, int y, int width, int height) { + assert(gfxcore); + gfxcore->viewport = + (Viewport){.x = x, .y = y, .width = width, .height = height}; +} + +void gfx_get_viewport( + GfxCore* gfxcore, int* x, int* y, int* width, int* height) { + assert(gfxcore); + assert(x); + assert(y); + assert(width); + assert(height); + + *x = gfxcore->viewport.x; + *y = gfxcore->viewport.y; + *width = gfxcore->viewport.width; + *height = gfxcore->viewport.height; +} + +void gfx_clear(GfxCore* gfxcore, vec4 colour) { + assert(gfxcore); + + glClearColor(colour.x, colour.y, colour.z, colour.w); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +void gfx_set_blending(GfxCore* gfxcore, bool enable) { + assert(gfxcore); + if (enable) { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } else { + glDisable(GL_BLEND); + } +} + +void gfx_set_depth_mask(GfxCore* gfxcore, bool enable) { + assert(gfxcore); + glDepthMask(enable ? GL_TRUE : GL_FALSE); +} + +void gfx_set_culling(GfxCore* gfxcore, bool enable) { + assert(gfxcore); + if (enable) { + glEnable(GL_CULL_FACE); + } else { + glDisable(GL_CULL_FACE); + } +} + +void gfx_set_polygon_offset(GfxCore* gfxcore, float scale, float bias) { + assert(gfxcore); + if ((scale != 0.0f) || (bias != 0.0f)) { + glEnable(GL_POLYGON_OFFSET_FILL); + } else { + glDisable(GL_POLYGON_OFFSET_FILL); + } + glPolygonOffset(scale, bias); +} + +void gfx_reset_polygon_offset(GfxCore* gfxcore) { + assert(gfxcore); + glPolygonOffset(0, 0); + glDisable(GL_POLYGON_OFFSET_FILL); +} + +// ----------------------------------------------------------------------------- +// Buffers. +// ----------------------------------------------------------------------------- + +Buffer* gfx_make_buffer(GfxCore* gfxcore, const BufferDesc* desc) { + assert(gfxcore); + assert(desc); + + Buffer* buffer = mempool_alloc(&gfxcore->buffers); + if (!gfx_init_buffer(buffer, desc)) { + mempool_free(&gfxcore->buffers, &buffer); + return 0; + } + return buffer; +} + +void gfx_destroy_buffer(GfxCore* gfxcore, Buffer** buffer) { + assert(gfxcore); + assert(buffer); + if (*buffer) { + gfx_del_buffer(*buffer); + mempool_free(&gfxcore->buffers, buffer); + } +} + +// ----------------------------------------------------------------------------- +// Geometry. +// ----------------------------------------------------------------------------- + +Geometry* gfx_make_geometry(GfxCore* gfxcore, const GeometryDesc* desc) { + assert(gfxcore); + assert(desc); + + Geometry* geometry = mempool_alloc(&gfxcore->geometries); + if (!gfx_init_geometry(geometry, gfxcore, desc)) { + mempool_free(&gfxcore->geometries, &geometry); + return 0; + } + return geometry; +} + +void gfx_destroy_geometry(GfxCore* gfxcore, Geometry** geometry) { + assert(gfxcore); + assert(geometry); + + if (*geometry) { + gfx_del_geometry(*geometry); + mempool_free(&gfxcore->geometries, geometry); + } +} + +// ----------------------------------------------------------------------------- +// Textures. +// ----------------------------------------------------------------------------- + +Texture* gfx_make_texture(GfxCore* gfxcore, const TextureDesc* desc) { + assert(gfxcore); + assert(desc); + + Texture* texture = mempool_alloc(&gfxcore->textures); + if (!gfx_init_texture(texture, desc)) { + mempool_free(&gfxcore->textures, &texture); + return 0; + } + return texture; +} + +void gfx_destroy_texture(GfxCore* gfxcore, Texture** texture) { + assert(gfxcore); + assert(texture); + assert(*texture); + + if (*texture) { + gfx_del_texture(*texture); + mempool_free(&gfxcore->textures, texture); + } +} + +// ----------------------------------------------------------------------------- +// Renderbuffers. +// ----------------------------------------------------------------------------- + +RenderBuffer* gfx_make_renderbuffer( + GfxCore* gfxcore, const RenderBufferDesc* desc) { + assert(gfxcore); + assert(desc); + + RenderBuffer* renderbuffer = mempool_alloc(&gfxcore->renderbuffers); + if (!gfx_init_renderbuffer(renderbuffer, desc)) { + mempool_free(&gfxcore->renderbuffers, &renderbuffer); + } + return renderbuffer; +} + +void gfx_destroy_renderbuffer(GfxCore* gfxcore, RenderBuffer** renderbuffer) { + assert(gfxcore); + assert(renderbuffer); + assert(*renderbuffer); + + if (*renderbuffer) { + gfx_del_renderbuffer(*renderbuffer); + mempool_free(&gfxcore->renderbuffers, renderbuffer); + } +} + +// ----------------------------------------------------------------------------- +// Framebuffers. +// ----------------------------------------------------------------------------- + +FrameBuffer* gfx_make_framebuffer( + GfxCore* gfxcore, const FrameBufferDesc* desc) { + assert(gfxcore); + assert(desc); + + FrameBuffer* framebuffer = mempool_alloc(&gfxcore->framebuffers); + if (!gfx_init_framebuffer(framebuffer, desc)) { + mempool_free(&gfxcore->framebuffers, &framebuffer); + return 0; + } + return framebuffer; +} + +void gfx_destroy_framebuffer(GfxCore* gfxcore, FrameBuffer** framebuffer) { + assert(gfxcore); + assert(framebuffer); + assert(*framebuffer); + + if (*framebuffer) { + gfx_del_framebuffer(*framebuffer); + mempool_free(&gfxcore->framebuffers, framebuffer); + } +} + +// ----------------------------------------------------------------------------- +// Shaders. +// ----------------------------------------------------------------------------- + +static uint64_t hash_shader_desc(const ShaderDesc* desc) { + assert(desc); + // Note that defines may affect shader permutations, so we need to hash those + // as well. + uint64_t hash = 0; + for (size_t i = 0; i < desc->num_defines; ++i) { + const ShaderCompilerDefine* define = &desc->defines[i]; + hash = (((hash << 13) + sstring_hash(define->name)) << 7) + + sstring_hash(define->value); + } + return (hash << 17) + cstring_hash(desc->code); +} + +static uint64_t hash_program_desc(const ShaderProgramDesc* desc) { + assert(desc); + return ((uint64_t)desc->vertex_shader->id << 32) | + (uint64_t)desc->fragment_shader->id; +} + +static Shader* find_cached_shader(ShaderCache* cache, uint64_t hash) { + assert(cache); + mempool_foreach(cache, entry, { + if (entry->hash == hash) { + return entry->shader; + } + }); + return 0; +} + +static ShaderProgram* find_cached_program(ProgramCache* cache, uint64_t hash) { + assert(cache); + mempool_foreach(cache, entry, { + if (entry->hash == hash) { + return entry->program; + } + }); + return 0; +} + +static ShaderCacheEntry* find_shader_cache_entry( + ShaderCache* cache, const Shader* shader) { + assert(cache); + assert(shader); + mempool_foreach(cache, entry, { + if (entry->shader == shader) { + return entry; + } + }); + return 0; +} + +static ShaderProgramCacheEntry* find_program_cache_entry( + ProgramCache* cache, const ShaderProgram* prog) { + assert(cache); + assert(prog); + mempool_foreach(cache, entry, { + if (entry->program == prog) { + return entry; + } + }); + return 0; +} + +Shader* gfx_make_shader(GfxCore* gfxcore, const ShaderDesc* desc) { + assert(gfxcore); + assert(desc); + + // Check the shader cache first. + ShaderCache* cache = &gfxcore->shader_cache; + const uint64_t hash = hash_shader_desc(desc); + Shader* shader = find_cached_shader(cache, hash); + if (shader) { + // LOGD("Found cached shader with hash [%lx]", hash); + return shader; + } + + shader = mempool_alloc(&gfxcore->shaders); + if (!shader) { + return 0; + } + if (!gfx_compile_shader(shader, desc)) { + mempool_free(&gfxcore->shaders, &shader); + return 0; + } + ShaderCacheEntry* entry = mempool_alloc(cache); + *entry = (ShaderCacheEntry){.hash = hash, .shader = shader}; + // LOGD("Added shader with hash [%lx] to cache", hash); + return shader; +} + +void gfx_destroy_shader(GfxCore* gfxcore, Shader** shader) { + assert(gfxcore); + assert(shader); + + if (*shader) { + // Remove the shader from the cache. + ShaderCache* cache = &gfxcore->shader_cache; + ShaderCacheEntry* entry = find_shader_cache_entry(cache, *shader); + assert(entry); // Must be there, shaders can't go untracked. + mempool_free(cache, &entry); + + gfx_del_shader(*shader); + mempool_free(&gfxcore->shaders, shader); + } +} + +ShaderProgram* gfx_make_shader_program( + GfxCore* gfxcore, const ShaderProgramDesc* desc) { + assert(gfxcore); + assert(desc); + + // Check the shader program cache first. + ProgramCache* cache = &gfxcore->program_cache; + const uint64_t hash = hash_program_desc(desc); + ShaderProgram* prog = find_cached_program(cache, hash); + if (prog) { + // LOGD("Found cached shader program with hash [%lx]", hash); + return prog; + } + + prog = mempool_alloc(&gfxcore->shader_programs); + if (!gfx_build_shader_program(prog, desc)) { + mempool_free(&gfxcore->shader_programs, &prog); + return 0; + } + ShaderProgramCacheEntry* entry = mempool_alloc(cache); + *entry = (ShaderProgramCacheEntry){.hash = hash, .program = prog}; + // LOGD("Added shader program with hash [%lx] to cache", hash); + return prog; +} + +void gfx_destroy_shader_program(GfxCore* gfxcore, ShaderProgram** prog) { + assert(gfxcore); + assert(prog); + if (*prog) { + // Remove the shader program from the cache. + ProgramCache* cache = &gfxcore->program_cache; + ShaderProgramCacheEntry* entry = find_program_cache_entry(cache, *prog); + assert(entry); // Must be there, shaders can't go untracked. + mempool_free(cache, &entry); + + gfx_del_shader_program(*prog); + mempool_free(&gfxcore->shader_programs, prog); + } +} diff --git a/src/core/core_impl.h b/src/core/core_impl.h new file mode 100644 index 0000000..eefdfbe --- /dev/null +++ b/src/core/core_impl.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + +#include "buffer.h" +#include "framebuffer.h" +#include "geometry.h" +#include "renderbuffer.h" +#include "shader.h" +#include "shader_program.h" +#include "texture.h" + +#include + +#include + +// TODO: Make a generic (hash, void*) structure and define functions over it. +// Then define a macro that defines type-safe macros given the type of the +// entry. +typedef struct ShaderCacheEntry { + uint64_t hash; + Shader* shader; +} ShaderCacheEntry; + +typedef struct ShaderProgramCacheEntry { + uint64_t hash; + ShaderProgram* program; +} ShaderProgramCacheEntry; + +DEF_MEMPOOL(buffer_pool, Buffer, GFX_MAX_NUM_BUFFERS) +DEF_MEMPOOL(framebuffer_pool, FrameBuffer, GFX_MAX_NUM_FRAMEBUFFERS) +DEF_MEMPOOL(geometry_pool, Geometry, GFX_MAX_NUM_GEOMETRIES) +DEF_MEMPOOL(renderbuffer_pool, RenderBuffer, GFX_MAX_NUM_RENDERBUFFERS) +DEF_MEMPOOL(shader_pool, Shader, GFX_MAX_NUM_SHADERS) +DEF_MEMPOOL(shader_program_pool, ShaderProgram, GFX_MAX_NUM_SHADER_PROGRAMS) +DEF_MEMPOOL(texture_pool, Texture, GFX_MAX_NUM_TEXTURES) + +DEF_MEMPOOL(ShaderCache, ShaderCacheEntry, GFX_MAX_NUM_SHADERS) +DEF_MEMPOOL(ProgramCache, ShaderProgramCacheEntry, GFX_MAX_NUM_SHADER_PROGRAMS) + +typedef struct { + int x; + int y; + int width; + int height; +} Viewport; + +typedef struct GfxCore { + Viewport viewport; + // mempools for render-specific objects: textures, geometry, etc. + buffer_pool buffers; + framebuffer_pool framebuffers; + geometry_pool geometries; + renderbuffer_pool renderbuffers; + shader_pool shaders; + shader_program_pool shader_programs; + texture_pool textures; + // Caches. + ShaderCache shader_cache; + ProgramCache program_cache; +} GfxCore; + +/// Create a new render backend. +void gfx_init_gfxcore(GfxCore*); + +/// Destroy the render backend. +void gfx_del_gfxcore(GfxCore*); diff --git a/src/core/framebuffer.c b/src/core/framebuffer.c new file mode 100644 index 0000000..76d9002 --- /dev/null +++ b/src/core/framebuffer.c @@ -0,0 +1,151 @@ +#include "framebuffer.h" + +#include "renderbuffer.h" +#include "texture.h" + +#include + +#include + +static void framebuffer_attach_colour( + FrameBuffer* framebuffer, const FrameBufferAttachment* attachment) { + assert(framebuffer); + assert(attachment); + + switch (attachment->type) { + case FrameBufferNoAttachment: + break; + case FrameBufferTexture: + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, + attachment->texture.texture->id, attachment->texture.mip_level); + break; + case FrameBufferCubemapTexture: + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + to_GL_cubemap_face(attachment->cubemap.face), + attachment->cubemap.texture->id, attachment->cubemap.mip_level); + break; + case FrameBufferRenderBuffer: + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, + attachment->renderbuffer->id); + break; + } + + ASSERT_GL; +} + +static void framebuffer_attach_depth( + FrameBuffer* framebuffer, const FrameBufferAttachment* attachment) { + assert(framebuffer); + assert(attachment); + + switch (attachment->type) { + case FrameBufferNoAttachment: + break; + case FrameBufferTexture: + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_DEPTH_COMPONENT, + attachment->texture.texture->id, attachment->texture.mip_level); + break; + // TODO: Could distinguish between colour and depth attachment types to make + // this a compile-time error. + case FrameBufferCubemapTexture: + log_error("Cannot use a cubemap texture as a depth framebuffer attachment"); + break; + case FrameBufferRenderBuffer: + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, + attachment->renderbuffer->id); + break; + } + + ASSERT_GL; +} + +bool gfx_init_framebuffer( + FrameBuffer* framebuffer, const FrameBufferDesc* desc) { + assert(framebuffer); + assert(desc); + + glGenFramebuffers(1, &framebuffer->id); + if (!framebuffer->id) { + log_error("glGenFramebuffers() failed"); + return false; + } + + // Allow incomplete framebuffers for flexibility. + // Attach buffers and check the framebuffer status only if buffers are given + // up front. + if (desc->colour.type != FrameBufferNoAttachment || + desc->depth.type != FrameBufferNoAttachment) { + // TODO: Could use the "named" API to avoid having to bind the framebuffer. + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->id); + framebuffer_attach_colour(framebuffer, &desc->colour); + framebuffer_attach_depth(framebuffer, &desc->depth); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + log_error("glCheckFramebufferStatus() failed"); + gfx_del_framebuffer(framebuffer); + return false; + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + + ASSERT_GL; + return true; +} + +bool gfx_framebuffer_attach_colour( + FrameBuffer* framebuffer, const FrameBufferAttachment* attachment) { + assert(framebuffer); + assert(attachment); + + // TODO: Could use the "named" API to avoid having to bind the framebuffer. + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->id); + framebuffer_attach_colour(framebuffer, attachment); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + log_error("glCheckFramebufferStatus() failed"); + return false; + } + return true; +} + +bool gfx_framebuffer_attach_depth( + FrameBuffer* framebuffer, const FrameBufferAttachment* attachment) { + assert(framebuffer); + assert(attachment); + + // TODO: Could use the "named" API to avoid having to bind the framebuffer. + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->id); + framebuffer_attach_depth(framebuffer, attachment); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + log_error("glCheckFramebufferStatus() failed"); + return false; + } + return true; +} + +void gfx_del_framebuffer(FrameBuffer* framebuffer) { + assert(framebuffer); + if (framebuffer->id) { + glDeleteFramebuffers(1, &framebuffer->id); + framebuffer->id = 0; + } +} + +void gfx_activate_framebuffer(const FrameBuffer* framebuffer) { + assert(framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, framebuffer->id); +} + +void gfx_deactivate_framebuffer(const FrameBuffer* framebuffer) { + assert(framebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void gfx_framebuffer_set_viewport( + FrameBuffer* framebuffer, int x, int y, int width, int height) { + assert(framebuffer); + glViewport(x, y, width, height); +} diff --git a/src/core/framebuffer.h b/src/core/framebuffer.h new file mode 100644 index 0000000..1a3439c --- /dev/null +++ b/src/core/framebuffer.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "gl_util.h" + +typedef struct FrameBuffer { + GLuint id; +} FrameBuffer; + +/// Create a new framebuffer. +bool gfx_init_framebuffer(FrameBuffer*, const FrameBufferDesc*); + +/// Destroy the framebuffer. +void gfx_del_framebuffer(FrameBuffer*); diff --git a/src/core/geometry.c b/src/core/geometry.c new file mode 100644 index 0000000..cfc749f --- /dev/null +++ b/src/core/geometry.c @@ -0,0 +1,326 @@ +#include "geometry.h" + +#include "buffer.h" +#include "constants.h" + +#include + +#include +#include + +/// Determines whether a view is populated. +/// +/// Note that views are allowed to have no data, in which case a buffer of the +/// specified size is created. +#define view_is_populated(BUFFER_VIEW) (BUFFER_VIEW.size_bytes > 0) + +static GLenum primitive_type_to_gl(PrimitiveType type) { + switch (type) { + case Triangles: + return GL_TRIANGLES; + case TriangleFan: + return GL_TRIANGLE_FAN; + case TriangleStrip: + return GL_TRIANGLE_STRIP; + } + FAIL("primitive_type_to_gl(): missing case"); + return GL_INVALID_ENUM; +} + +/// Create a typed buffer for the buffer view if the view does not already point +/// to a buffer. +void init_view_buffer( + GfxCore* gfxcore, BufferView* view, BufferType buffer_type, + BufferUsage buffer_usage) { + if (!view->buffer) { + view->buffer = gfx_make_buffer( + gfxcore, + &(BufferDesc){ + .usage = buffer_usage, + .type = buffer_type, + .data.data = view->data, + .data.count = view->size_bytes / + gfx_get_buffer_type_size_bytes(buffer_type)}); + } + assert(view->size_bytes <= view->buffer->size_bytes); +} + +/// Configure the buffer in teh VAO. +static void configure_buffer( + GfxCore* gfxcore, const GeometryDesc* desc, BufferView* view, + size_t num_components, size_t component_size_bytes, GLenum component_type, + GLboolean normalized, GLuint channel) { + assert(gfxcore); + assert(desc); + assert(view); + assert(view->buffer); + assert( + desc->num_verts <= + view->size_bytes / (num_components * component_size_bytes)); + assert(view->size_bytes <= view->buffer->size_bytes); + + glBindBuffer(GL_ARRAY_BUFFER, view->buffer->vbo); + glEnableVertexAttribArray(channel); + if ((component_type == GL_FLOAT) || normalized) { + glVertexAttribPointer( + channel, num_components, component_type, normalized, view->stride_bytes, + (const void*)view->offset_bytes); + } else { + assert(!normalized); + assert( + (component_type == GL_BYTE) || (component_type == GL_UNSIGNED_BYTE) || + (component_type == GL_SHORT) || (component_type == GL_UNSIGNED_SHORT) || + (component_type == GL_INT) || component_type == GL_UNSIGNED_INT); + glVertexAttribIPointer( + channel, num_components, component_type, view->stride_bytes, + (const void*)view->offset_bytes); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); +} + +static bool configure_vertex_attributes(GfxCore* gfxcore, GeometryDesc* desc) { + assert(gfxcore); + assert(desc); + + if (view_is_populated(desc->positions3d)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->positions3d, Buffer3d, desc->buffer_usage); + if (!desc->positions3d.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->positions3d, 3, sizeof(float), + GL_FLOAT, GL_FALSE, GFX_POSITION_CHANNEL); + } else if (view_is_populated(desc->positions2d)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->positions2d, Buffer2d, desc->buffer_usage); + if (!desc->positions2d.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->positions2d, 2, sizeof(float), + GL_FLOAT, GL_FALSE, GFX_POSITION_CHANNEL); + } + if (view_is_populated(desc->normals)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->normals, Buffer3d, desc->buffer_usage); + if (!desc->normals.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->normals, 3, sizeof(float), GL_FLOAT, + GL_FALSE, GFX_NORMAL_CHANNEL); + } + if (view_is_populated(desc->tangents)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->tangents, Buffer4d, desc->buffer_usage); + if (!desc->tangents.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->tangents, 4, sizeof(float), GL_FLOAT, + GL_FALSE, GFX_TANGENT_CHANNEL); + } + if (view_is_populated(desc->texcoords)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->texcoords, Buffer2d, desc->buffer_usage); + if (!desc->texcoords.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->texcoords, 2, sizeof(float), + GL_FLOAT, GL_FALSE, GFX_TEXCOORDS_CHANNEL); + } + if (view_is_populated(desc->joints.u8)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->joints.u8, BufferU8, desc->buffer_usage); + if (!desc->joints.u8.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->joints.u8, 4, sizeof(uint8_t), + GL_UNSIGNED_BYTE, GL_FALSE, GFX_JOINTS_CHANNEL); + } else if (view_is_populated(desc->joints.u16)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->joints.u16, BufferU16, desc->buffer_usage); + if (!desc->joints.u16.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->joints.u16, 4, sizeof(uint16_t), + GL_UNSIGNED_SHORT, GL_FALSE, GFX_JOINTS_CHANNEL); + } + + // If weights are given as unsigned integers, then they are normalized + // when read by the shader. + if (view_is_populated(desc->weights.u8)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->weights.u8, BufferU8, desc->buffer_usage); + if (!desc->weights.u8.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->weights.u8, 4, sizeof(uint8_t), + GL_UNSIGNED_BYTE, GL_TRUE, GFX_WEIGHTS_CHANNEL); + } else if (view_is_populated(desc->weights.u16)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->weights.u16, BufferU16, + desc->buffer_usage); + if (!desc->weights.u16.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->weights.u16, 4, sizeof(uint16_t), + GL_UNSIGNED_SHORT, GL_TRUE, GFX_WEIGHTS_CHANNEL); + } else if (view_is_populated(desc->weights.floats)) { + init_view_buffer( + gfxcore, (BufferView*)&desc->weights.floats, BufferFloat, + desc->buffer_usage); + if (!desc->weights.floats.buffer) { + return false; + } + configure_buffer( + gfxcore, desc, (BufferView*)&desc->weights.floats, 4, sizeof(float), + GL_FLOAT, GL_FALSE, GFX_WEIGHTS_CHANNEL); + } + + return true; +} + +static bool configure_indices(GfxCore* gfxcore, GeometryDesc* desc) { + assert(gfxcore); + assert(desc); + + if (view_is_populated(desc->indices8)) { + assert(desc->num_indices > 0); + assert( + desc->num_indices <= desc->indices8.size_bytes / sizeof(VertexIndex8)); + init_view_buffer( + gfxcore, (BufferView*)&desc->indices8, BufferU8, desc->buffer_usage); + if (!desc->indices8.buffer) { + return false; + } + } else if (view_is_populated(desc->indices16)) { + assert(desc->num_indices > 0); + assert( + desc->num_indices <= + desc->indices16.size_bytes / sizeof(VertexIndex16)); + init_view_buffer( + gfxcore, (BufferView*)&desc->indices16, BufferU16, desc->buffer_usage); + if (!desc->indices16.buffer) { + return false; + } + } + + return true; +} + +bool gfx_init_geometry( + Geometry* geometry, GfxCore* gfxcore, const GeometryDesc* input_desc) { + assert(geometry); + assert(gfxcore); + assert(input_desc); + assert( + view_is_populated(input_desc->positions3d) || + view_is_populated(input_desc->positions2d)); + assert(input_desc->num_verts > 0); + + geometry->mode = primitive_type_to_gl(input_desc->type); + geometry->desc = *input_desc; + geometry->num_verts = input_desc->num_verts; + geometry->gfxcore = gfxcore; + + // The geometry's copy of the descriptor is manipulated below. Create a + // shorter name for it. + GeometryDesc* desc = &geometry->desc; + + glGenVertexArrays(1, &geometry->vao); + glBindVertexArray(geometry->vao); + if (!configure_vertex_attributes(gfxcore, desc)) { + goto cleanup; + } + if (!configure_indices(gfxcore, desc)) { + goto cleanup; + } + glBindVertexArray(0); + ASSERT_GL; + + return true; + +cleanup: + gfx_del_geometry(geometry); + return 0; +} + +void gfx_del_geometry(Geometry* geometry) { + assert(geometry); + if (geometry->vao) { + glDeleteVertexArrays(1, &geometry->vao); + geometry->vao = 0; + } +} + +void gfx_update_geometry(Geometry* geometry, const GeometryDesc* desc) { + assert(geometry); + assert(desc); + // New geometry size cannot exceed original size. + assert(desc->positions3d.size_bytes <= geometry->desc.positions3d.size_bytes); + assert(desc->positions2d.size_bytes <= geometry->desc.positions2d.size_bytes); + assert(desc->normals.size_bytes <= geometry->desc.normals.size_bytes); + assert(desc->tangents.size_bytes <= geometry->desc.tangents.size_bytes); + assert(desc->texcoords.size_bytes <= geometry->desc.texcoords.size_bytes); + assert(desc->joints.u8.size_bytes <= geometry->desc.joints.u8.size_bytes); + assert(desc->joints.u16.size_bytes <= geometry->desc.joints.u16.size_bytes); + assert(desc->weights.u8.size_bytes <= geometry->desc.weights.u8.size_bytes); + assert(desc->weights.u16.size_bytes <= geometry->desc.weights.u16.size_bytes); + assert( + desc->weights.floats.size_bytes <= + geometry->desc.weights.floats.size_bytes); + + if (desc->positions3d.data) { + // The geometry must already have an underlying GPU buffer. + assert(geometry->desc.positions3d.buffer); + gfx_update_buffer( + geometry->desc.positions3d.buffer, + &(BufferDataDesc){ + .vec3s = desc->positions3d.data, + .count = desc->positions3d.size_bytes / sizeof(vec3)}); + } + // TODO: more + else { + FAIL("TODO: gfx_update_geometry() - handle other buffer types"); + } + + if (desc->num_verts != 0) { + geometry->num_verts = desc->num_verts; + } +} + +void gfx_render_geometry(const Geometry* geometry) { + assert(geometry); + assert(geometry->vao); + + const GeometryDesc* desc = &geometry->desc; + glBindVertexArray(geometry->vao); + + if (desc->indices8.buffer) { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, desc->indices8.buffer->vbo); + glDrawElements( + geometry->mode, desc->num_indices, GL_UNSIGNED_BYTE, + (const void*)desc->indices8.offset_bytes); + } else if (desc->indices16.buffer) { + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, desc->indices16.buffer->vbo); + glDrawElements( + geometry->mode, desc->num_indices, GL_UNSIGNED_SHORT, + (const void*)desc->indices16.offset_bytes); + } else { + glDrawArrays(geometry->mode, 0, geometry->num_verts); + } + + glBindVertexArray(0); +} + +aabb3 gfx_get_geometry_aabb(const Geometry* geometry) { + assert(geometry); + return geometry->desc.aabb; +} diff --git a/src/core/geometry.h b/src/core/geometry.h new file mode 100644 index 0000000..c37a76f --- /dev/null +++ b/src/core/geometry.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "gl_util.h" + +#include + +/// A piece of renderable geometry. +/// +/// The Geometry does not own its buffers, since buffers are typically shared +/// to reduce the memory footprint and the number of draw calls. More generally, +/// the renderer assumes ownership of all rendering resources, which simplifies +/// their management. +typedef struct Geometry { + GLuint vao; + GLenum mode; + GeometryDesc desc; + size_t num_verts; // May differ from the initial value in the descriptor if + // the geometry is updated. + GfxCore* gfxcore; +} Geometry; + +/// Create new geometry. +bool gfx_init_geometry(Geometry*, GfxCore*, const GeometryDesc*); + +/// Destroy the geometry. +void gfx_del_geometry(Geometry*); diff --git a/src/core/gl_util.h b/src/core/gl_util.h new file mode 100644 index 0000000..d2d6e22 --- /dev/null +++ b/src/core/gl_util.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#define GFX_GL_CONTEXT_PC 1 +#define GFX_GL_CONTEXT_ES 2 + +#ifndef GFX_GL_CONTEXT +#define GFX_GL_CONTEXT GFX_GL_CONTEXT_PC +#endif // GFX_GL_CONTEXT + +/// Log an error if an OpenGL has occurred. +#ifndef NDEBUG +#define ASSERT_GL \ + { \ + GLenum e = glGetError(); \ + switch (e) { \ + case GL_NO_ERROR: \ + break; \ + case GL_INVALID_ENUM: \ + LOGE("GL_INVALID_ENUM"); \ + break; \ + case GL_INVALID_VALUE: \ + LOGE("GL_INVALID_VALUE"); \ + break; \ + case GL_INVALID_OPERATION: \ + LOGE("GL_INVALID_OPERATION"); \ + break; \ + case GL_INVALID_FRAMEBUFFER_OPERATION: \ + LOGE("GL_INVALID_FRAMEBUFFER_OPERATION"); \ + break; \ + case GL_OUT_OF_MEMORY: \ + LOGE("GL_OUT_OF_MEMORY"); \ + break; \ + /*case GL_STACK_UNDERFLOW: LOGE("GL_STACK_UNDERFLOW");*/ \ + /*case GL_STACK_OVERFLOW: LOGE("GL_STACK_OVERFLOW");*/ \ + default: \ + LOGE("Unknown OpenGL error"); \ + break; \ + } \ + } +#else // Not NDEBUG. +#define ASSERT_GL +#endif diff --git a/src/core/renderbuffer.c b/src/core/renderbuffer.c new file mode 100644 index 0000000..2753f3b --- /dev/null +++ b/src/core/renderbuffer.c @@ -0,0 +1,35 @@ +#include "renderbuffer.h" + +#include "texture.h" + +#include + +bool gfx_init_renderbuffer( + RenderBuffer* renderbuffer, const RenderBufferDesc* desc) { + assert(renderbuffer); + assert(desc); + + glGenRenderbuffers(1, &renderbuffer->id); + if (!renderbuffer->id) { + log_error("glGenRenderbuffers failed"); + return false; + } + + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer->id); + glRenderbufferStorage( + GL_RENDERBUFFER, to_GL_internal_format(desc->texture_format), desc->width, + desc->height); + glBindRenderbuffer(GL_RENDERBUFFER, 0); + + ASSERT_GL; + return true; +} + +void gfx_del_renderbuffer(RenderBuffer* renderbuffer) { + assert(renderbuffer); + + if (renderbuffer->id) { + glDeleteRenderbuffers(1, &renderbuffer->id); + renderbuffer->id = 0; + } +} diff --git a/src/core/renderbuffer.h b/src/core/renderbuffer.h new file mode 100644 index 0000000..ea11610 --- /dev/null +++ b/src/core/renderbuffer.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "gl_util.h" + +typedef struct RenderBuffer { + GLuint id; +} RenderBuffer; + +/// Create a new renderbuffer. +bool gfx_init_renderbuffer(RenderBuffer*, const RenderBufferDesc*); + +/// Destroy the renderbuffer. +void gfx_del_renderbuffer(RenderBuffer*); diff --git a/src/core/shader.c b/src/core/shader.c new file mode 100644 index 0000000..dded084 --- /dev/null +++ b/src/core/shader.c @@ -0,0 +1,92 @@ +#include "shader.h" + +#include "gl_util.h" +#include + +#include +#include + +#include +#include + +static GLenum shader_type_to_gl(ShaderType type) { + switch (type) { + case VertexShader: + return GL_VERTEX_SHADER; + case FragmentShader: + return GL_FRAGMENT_SHADER; + } + FAIL("shader_type_to_gl(): missing case"); + return GL_INVALID_ENUM; +} + +static lstring make_defines_string(const ShaderDesc* desc) { + lstring defines = {0}; + for (size_t i = 0; i < desc->num_defines; ++i) { + const ShaderCompilerDefine* define = &desc->defines[i]; + lstring_append_cstr(&defines, "#define "); + lstring_append_cstr(&defines, sstring_cstr(&define->name)); + lstring_append_cstr(&defines, " "); + lstring_append_cstr(&defines, sstring_cstr(&define->value)); + lstring_append_cstr(&defines, "\n"); + } + return defines; +} + +/// Creates an OpenGL shader. +/// Returns non-zero on success, 0 on failure. +static GLuint create_shader(const ShaderDesc* desc) { + const GLuint shader = glCreateShader(shader_type_to_gl(desc->type)); + if (!shader) { + return 0; + } + +#if GFX_GL_CONTEXT == GFX_GL_CONTEXT_ES + const char* header = "#version 300 es\n\nprecision highp float;"; +#else + const char* header = "#version 400 core\n\n"; +#endif + + lstring defines = make_defines_string(desc); + + const char* source_bits[] = {header, lstring_cstr(&defines), desc->code}; + const GLint source_lengths[] = { + strlen(header), lstring_length(&defines), strlen(desc->code)}; + + glShaderSource(shader, 3, source_bits, source_lengths); + glCompileShader(shader); + GLint result; + glGetShaderiv(shader, GL_COMPILE_STATUS, &result); + if (result == GL_FALSE) { + GLint log_len; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_len); + if (log_len > 0) { + char* log = calloc(log_len, sizeof(char)); + glGetShaderInfoLog(shader, log_len, NULL, log); + static const char* sep = "----------"; + LOGE("Failed loading shader: %s\n%s\n%s\n%s", log, sep, desc->code, sep); + free(log); + } else { + LOGE("Failed loading shader:\n%s", desc->code); + } + glDeleteShader(shader); + return 0; + } + ASSERT_GL; + return shader; +} + +bool gfx_compile_shader(Shader* shader, const ShaderDesc* desc) { + shader->id = create_shader(desc); + return shader->id != 0; +} + +void gfx_del_shader(Shader* shader) { + assert(shader); + + if (shader->id) { + glDeleteShader(shader->id); + shader->id = 0; + } + ASSERT_GL; +} diff --git a/src/core/shader.h b/src/core/shader.h new file mode 100644 index 0000000..b9f5679 --- /dev/null +++ b/src/core/shader.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "gl_util.h" + +#include + +typedef struct Shader { + GLuint id; +} Shader; + +/// Compile a new shader. +bool gfx_compile_shader(Shader*, const ShaderDesc*); + +/// Destroy the shader. +void gfx_del_shader(Shader*); diff --git a/src/core/shader_program.c b/src/core/shader_program.c new file mode 100644 index 0000000..3cbe48d --- /dev/null +++ b/src/core/shader_program.c @@ -0,0 +1,291 @@ +#include "shader_program.h" + +#include "gl_util.h" +#include "shader.h" +#include "texture.h" +#include + +#include + +#include +#include + +/// Creates an OpenGL shader program. +/// Returns non-zero on success, 0 on failure. +static GLuint create_program(GLuint vertex_shader, GLuint fragment_shader) { + const GLuint prog = glCreateProgram(); + if (prog == 0) { + LOGE("Failed creating shader program"); + return 0; + } + glAttachShader(prog, vertex_shader); + glAttachShader(prog, fragment_shader); + glLinkProgram(prog); + GLint result; + glGetProgramiv(prog, GL_LINK_STATUS, &result); + if (result == GL_FALSE) { + GLint log_len; + glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &log_len); + if (log_len > 0) { + char* log = calloc(log_len, sizeof(char)); + glGetProgramInfoLog(prog, log_len, NULL, log); + LOGE("Failed creating shader program: %s", log); + free(log); + } else { + LOGE("Failed creating shader program"); + } + glDeleteProgram(prog); + return 0; + } + ASSERT_GL; + return prog; +} + +bool gfx_build_shader_program( + ShaderProgram* prog, const ShaderProgramDesc* desc) { + assert(prog); + assert(desc); + + prog->id = create_program(desc->vertex_shader->id, desc->fragment_shader->id); + return prog->id != 0; +} + +void gfx_del_shader_program(ShaderProgram* prog) { + assert(prog); + + if (prog->id) { + glDeleteProgram(prog->id); + prog->id = 0; + } + ASSERT_GL; +} + +void gfx_activate_shader_program(const ShaderProgram* prog) { + assert(prog); + glUseProgram(prog->id); + ASSERT_GL; +} + +void gfx_deactivate_shader_program(const ShaderProgram* prog) { + assert(prog); + glUseProgram(0); + ASSERT_GL; +} + +static void set_texture_uniform( + GLuint prog, const char* name, int texture_unit, const Texture* texture) { + assert(prog != 0); + assert(name); + assert(texture); + + const GLint location = glGetUniformLocation(prog, name); + if (location >= 0) { + glActiveTexture(GL_TEXTURE0 + texture_unit); + glBindTexture(texture->target, texture->id); + glUniform1i(location, texture_unit); + } +} + +static void set_mat4_uniform( + GLuint prog, const char* name, const mat4* mats, size_t count) { + assert(prog != 0); + assert(name); + assert(mats); + + const GLint location = glGetUniformLocation(prog, name); + if (location >= 0) { + glUniformMatrix4fv(location, count, GL_FALSE, (const float*)mats); + } +} + +static void set_vec3_uniform(GLuint prog, const char* name, vec3 value) { + assert(prog != 0); + assert(name); + + const GLint location = glGetUniformLocation(prog, name); + if (location >= 0) { + glUniform3f(location, value.x, value.y, value.z); + } +} + +static void set_vec4_uniform(GLuint prog, const char* name, vec4 value) { + assert(prog != 0); + assert(name); + + const GLint location = glGetUniformLocation(prog, name); + if (location >= 0) { + glUniform4f(location, value.x, value.y, value.z, value.w); + } +} + +static void set_float_uniform(GLuint prog, const char* name, float value) { + assert(prog != 0); + assert(name); + + const GLint location = glGetUniformLocation(prog, name); + if (location >= 0) { + glUniform1f(location, value); + } +} + +void gfx_apply_uniforms(const ShaderProgram* prog) { + assert(prog); + + int next_texture_unit = 0; + for (int i = 0; i < prog->num_uniforms; ++i) { + const ShaderUniform* uniform = &prog->uniforms[i]; + switch (uniform->type) { + case UniformTexture: + set_texture_uniform( + prog->id, uniform->name.str, next_texture_unit, + uniform->value.texture); + next_texture_unit++; + break; + case UniformMat4: + set_mat4_uniform(prog->id, uniform->name.str, &uniform->value.mat4, 1); + break; + case UniformVec3: + set_vec3_uniform(prog->id, uniform->name.str, uniform->value.vec3); + break; + case UniformVec4: + set_vec4_uniform(prog->id, uniform->name.str, uniform->value.vec4); + break; + case UniformFloat: + set_float_uniform(prog->id, uniform->name.str, uniform->value.scalar); + break; + case UniformMat4Array: + set_mat4_uniform( + prog->id, uniform->name.str, uniform->value.array.values, + uniform->value.array.count); + break; + } + } +} + +// Get the ShaderUniform object by name from the shader program if it already +// exists, or allocate a new one otherwise. +static ShaderUniform* get_or_allocate_uniform( + ShaderProgram* prog, const char* name) { + assert(prog); + assert(name); + + // First search for the uniform in the list. + for (int i = 0; i < prog->num_uniforms; ++i) { + ShaderUniform* uniform = &prog->uniforms[i]; + if (sstring_eq_cstr(uniform->name, name)) { + return uniform; + } + } + + // Create the uniform if it does not exist. + if (prog->num_uniforms == GFX_MAX_UNIFORMS_PER_SHADER) { + FAIL("Exceeded the maximum number of uniforms per shader. Please increase " + "this value."); + return 0; + } + ShaderUniform* uniform = &prog->uniforms[prog->num_uniforms]; + prog->num_uniforms++; + return uniform; +} + +// The functions below save the value of a uniform in the shader program. If the +// uniform does not even exist, then there is no need to store the value. + +void gfx_set_texture_uniform( + ShaderProgram* prog, const char* name, const Texture* texture) { + assert(prog); + assert(name); + assert(texture); + + const GLint location = glGetUniformLocation(prog->id, name); + if (location < 0) { + return; + } + ShaderUniform* uniform = get_or_allocate_uniform(prog, name); + assert(uniform); + uniform->name = sstring_make(name); + uniform->type = UniformTexture; + uniform->value.texture = texture; +} + +void gfx_set_mat4_uniform( + ShaderProgram* prog, const char* name, const mat4* mat) { + assert(prog); + assert(name); + assert(mat); + + const GLint location = glGetUniformLocation(prog->id, name); + if (location < 0) { + return; + } + ShaderUniform* uniform = get_or_allocate_uniform(prog, name); + assert(uniform); + uniform->name = sstring_make(name); + uniform->type = UniformMat4; + uniform->value.mat4 = *mat; +} + +void gfx_set_vec3_uniform(ShaderProgram* prog, const char* name, vec3 value) { + assert(prog); + assert(name); + + const GLint location = glGetUniformLocation(prog->id, name); + if (location < 0) { + return; + } + ShaderUniform* uniform = get_or_allocate_uniform(prog, name); + assert(uniform); + uniform->name = sstring_make(name); + uniform->type = UniformVec3; + uniform->value.vec3 = value; +} + +void gfx_set_vec4_uniform(ShaderProgram* prog, const char* name, vec4 value) { + assert(prog); + assert(name); + + const GLint location = glGetUniformLocation(prog->id, name); + if (location < 0) { + return; + } + ShaderUniform* uniform = get_or_allocate_uniform(prog, name); + assert(uniform); + uniform->name = sstring_make(name); + uniform->type = UniformVec4; + uniform->value.vec4 = value; +} + +void gfx_set_float_uniform(ShaderProgram* prog, const char* name, float value) { + assert(prog); + assert(name); + + // No need to store the uniform on our side if it does not exist in the + // program. + const GLint location = glGetUniformLocation(prog->id, name); + if (location < 0) { + return; + } + ShaderUniform* uniform = get_or_allocate_uniform(prog, name); + assert(uniform); + uniform->name = sstring_make(name); + uniform->type = UniformFloat; + uniform->value.scalar = value; +} + +void gfx_set_mat4_array_uniform( + ShaderProgram* prog, const char* name, const mat4* mats, size_t count) { + assert(prog); + assert(name); + assert(mats); + + const GLint location = glGetUniformLocation(prog->id, name); + if (location < 0) { + return; + } + ShaderUniform* uniform = get_or_allocate_uniform(prog, name); + assert(uniform); + uniform->name = sstring_make(name); + uniform->type = UniformMat4Array; + uniform->value.array.count = count; + uniform->value.array.values = mats; +} diff --git a/src/core/shader_program.h b/src/core/shader_program.h new file mode 100644 index 0000000..1443663 --- /dev/null +++ b/src/core/shader_program.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include "gl_util.h" + +#include + +#include + +typedef struct Texture Texture; + +typedef struct ShaderProgram { + GLuint id; + ShaderUniform uniforms[GFX_MAX_UNIFORMS_PER_SHADER]; + int num_uniforms; +} ShaderProgram; + +/// Create a new shader program. +bool gfx_build_shader_program(ShaderProgram*, const ShaderProgramDesc*); + +/// Destroy the shader program. +void gfx_del_shader_program(ShaderProgram*); diff --git a/src/core/texture.c b/src/core/texture.c new file mode 100644 index 0000000..89f7ec0 --- /dev/null +++ b/src/core/texture.c @@ -0,0 +1,218 @@ +#include "texture.h" + +#include + +#include +#include + +#include + +bool gfx_init_texture(Texture* texture, const TextureDesc* desc) { + assert(texture); + assert(desc); + + glGenTextures(1, &texture->id); + if (!texture->id) { + log_error("glGenTextures() failed"); + return false; + } + texture->target = to_GL_dimension(desc->dimension); + glBindTexture(texture->target, texture->id); + + // glTexStorageXD + const int levels = + desc->mipmaps + ? max(max(log2(desc->width), log2(desc->height)), log2(desc->depth)) + + 1 + : 1; + const GLenum internal_format = to_GL_internal_format(desc->format); + switch (texture->target) { + case GL_TEXTURE_2D: + case GL_TEXTURE_CUBE_MAP: + glTexStorage2D( + texture->target, levels, internal_format, desc->width, desc->height); + break; + default: + FAIL("Unhandled texture dimension"); + gfx_del_texture(texture); + return false; + } + + texture->format = to_GL_format(desc->format); + texture->type = to_GL_type(desc->format); + texture->width = desc->width; + texture->height = desc->height; + gfx_update_texture(texture, &desc->data); + + // gfx_update_texture() unbinds the texture at the end, so re-bind it here. + glBindTexture(texture->target, texture->id); + + // Mipmaps. + if (desc->mipmaps) { + glGenerateMipmap(texture->target); + } + + // Texture filtering. + const bool linear = desc->filtering == LinearFiltering; + GLenum min = desc->mipmaps ? (linear ? GL_LINEAR_MIPMAP_LINEAR + : GL_NEAREST_MIPMAP_NEAREST) + : (linear ? GL_LINEAR : GL_NEAREST); + GLenum mag = linear ? GL_LINEAR : GL_NEAREST; + glTexParameteri(texture->target, GL_TEXTURE_MIN_FILTER, min); + glTexParameteri(texture->target, GL_TEXTURE_MAG_FILTER, mag); + + // Texture wrapping. + GLenum wrap = GL_INVALID_ENUM; + switch (desc->wrap) { + case Repeat: + wrap = GL_REPEAT; + break; + case ClampToEdge: + wrap = GL_CLAMP_TO_EDGE; + break; + } + glTexParameteri(texture->target, GL_TEXTURE_WRAP_R, wrap); + glTexParameteri(texture->target, GL_TEXTURE_WRAP_S, wrap); + glTexParameteri(texture->target, GL_TEXTURE_WRAP_T, wrap); + + glBindTexture(texture->target, 0); + return true; +} + +void gfx_del_texture(Texture* texture) { + assert(texture); + + if (texture->id) { + glDeleteTextures(1, &texture->id); + texture->id = 0; + } +} + +void gfx_update_texture(Texture* texture, const TextureDataDesc* desc) { + assert(texture); + assert(desc); + + glBindTexture(texture->target, texture->id); + + // glTexSubImageXD + switch (texture->target) { + case GL_TEXTURE_2D: + if (desc->pixels) { + glTexSubImage2D( + GL_TEXTURE_2D, /*level=*/0, /*xoffset=*/0, + /*yoffset=*/0, texture->width, texture->height, texture->format, + texture->type, desc->pixels); + } + break; + case GL_TEXTURE_CUBE_MAP: + for (int i = 0; i < 6; ++i) { + const void* pixels = *(&desc->cubemap.pixels_pos_x + i); + if (pixels) { + glTexSubImage2D( + GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, /*level=*/0, /*xoffset=*/0, + /*yoffset=*/0, texture->width, texture->height, texture->format, + texture->type, pixels); + } + } + break; + default: + FAIL("Unhandled texture dimension"); + break; + } + + glBindTexture(texture->target, 0); +} + +GLenum to_GL_dimension(TextureDimension dim) { + switch (dim) { + case Texture2D: + return GL_TEXTURE_2D; + case TextureCubeMap: + return GL_TEXTURE_CUBE_MAP; + default: + FAIL("Unhandled TextureDimension"); + return GL_INVALID_ENUM; + } +} + +GLenum to_GL_internal_format(TextureFormat format) { + switch (format) { + case TextureDepth: + return GL_DEPTH_COMPONENT; + case TextureRG16: + return GL_RG16; + case TextureRG16F: + return GL_RG16F; + case TextureRGB8: + return GL_RGB8; + case TextureR11G11B10F: + return GL_R11F_G11F_B10F; + case TextureRGBA8: + return GL_RGBA8; + case TextureSRGB8: + return GL_SRGB8; + case TextureSRGBA8: + return GL_SRGB8_ALPHA8; + default: + FAIL("Unhandled TextureFormat"); + return GL_INVALID_ENUM; + } +} + +GLenum to_GL_format(TextureFormat format) { + switch (format) { + case TextureDepth: + return GL_DEPTH_COMPONENT; + case TextureRG16: + case TextureRG16F: + return GL_RG; + case TextureRGB8: + case TextureR11G11B10F: + case TextureSRGB8: + return GL_RGB; + case TextureRGBA8: + case TextureSRGBA8: + return GL_RGBA; + default: + FAIL("Unhandled TextureFormat"); + return GL_INVALID_ENUM; + } +} + +GLenum to_GL_type(TextureFormat format) { + switch (format) { + case TextureDepth: + case TextureRG16F: + case TextureR11G11B10F: + return GL_FLOAT; + case TextureRG16: + case TextureRGB8: + case TextureRGBA8: + case TextureSRGB8: + case TextureSRGBA8: + return GL_UNSIGNED_BYTE; + default: + FAIL("Unhandled TextureFormat"); + return GL_INVALID_ENUM; + } +} + +GLenum to_GL_cubemap_face(CubemapFace face) { + switch (face) { + case CubemapFacePosX: + return GL_TEXTURE_CUBE_MAP_POSITIVE_X; + case CubemapFaceNegX: + return GL_TEXTURE_CUBE_MAP_NEGATIVE_X; + case CubemapFacePosY: + return GL_TEXTURE_CUBE_MAP_POSITIVE_Y; + case CubemapFaceNegY: + return GL_TEXTURE_CUBE_MAP_NEGATIVE_Y; + case CubemapFacePosZ: + return GL_TEXTURE_CUBE_MAP_POSITIVE_Z; + case CubemapFaceNegZ: + return GL_TEXTURE_CUBE_MAP_NEGATIVE_Z; + default: + FAIL("Unhandled CubemapFace"); + return GL_INVALID_ENUM; + } +} diff --git a/src/core/texture.h b/src/core/texture.h new file mode 100644 index 0000000..4af41e9 --- /dev/null +++ b/src/core/texture.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "gl_util.h" + +typedef struct Texture { + GLuint id; + GLenum target; + GLenum format; + GLenum type; + int width; + int height; +} Texture; + +/// Create a new texture. +bool gfx_init_texture(Texture*, const TextureDesc*); + +/// Destroy the texture. +void gfx_del_texture(Texture*); + +/// Converts a TextureDimension into the OpenGL enum equivalent. +GLenum to_GL_dimension(TextureDimension dim); + +/// Converts a texture format into an OpenGL internal format. +GLenum to_GL_internal_format(TextureFormat format); + +/// Converts a texture format into an OpenGL format. +GLenum to_GL_format(TextureFormat format); + +/// Converts a texture format into an OpenGL type. +GLenum to_GL_type(TextureFormat format); + +/// Converts a cubemap face into the OpenGL enum equivalent. +GLenum to_GL_cubemap_face(CubemapFace face); diff --git a/src/gfx.c b/src/gfx.c new file mode 100644 index 0000000..cd2ac90 --- /dev/null +++ b/src/gfx.c @@ -0,0 +1,73 @@ +#include + +#include "asset/asset_cache.h" +#include "core/core_impl.h" +#include "renderer/imm_renderer_impl.h" +#include "renderer/renderer_impl.h" +#include "scene/scene_memory.h" + +#include + +#include +#include + +typedef struct Gfx { + AssetCache asset_cache; + GfxCore gfxcore; + Renderer renderer; + ImmRenderer imm_renderer; +} Gfx; + +Gfx* gfx_init(void) { + Gfx* gfx = calloc(1, sizeof(Gfx)); + if (!gfx) { + return 0; + } + gfx_init_gfxcore(&gfx->gfxcore); + if (!renderer_make(&gfx->renderer, &gfx->gfxcore)) { + gfx_destroy(&gfx); + return 0; + } + if (!imm_renderer_make(&gfx->imm_renderer, &gfx->gfxcore)) { + // TODO: Add error logs to the initialization failure cases here and inside + // the renderers. + gfx_destroy(&gfx); + return 0; + } + gfx_init_asset_cache(&gfx->asset_cache); + scene_mem_init(); + return gfx; +} + +void gfx_destroy(Gfx** gfx) { + if (!gfx) { + return; + } + scene_mem_destroy(); + gfx_destroy_asset_cache(&(*gfx)->asset_cache); + renderer_destroy(&(*gfx)->renderer); + imm_renderer_destroy(&(*gfx)->imm_renderer); + gfx_del_gfxcore(&(*gfx)->gfxcore); + free(*gfx); + *gfx = 0; +} + +GfxCore* gfx_get_core(Gfx* gfx) { + assert(gfx); + return &gfx->gfxcore; +} + +Renderer* gfx_get_renderer(Gfx* gfx) { + assert(gfx); + return &gfx->renderer; +} + +ImmRenderer* gfx_get_imm_renderer(Gfx* gfx) { + assert(gfx); + return &gfx->imm_renderer; +} + +AssetCache* gfx_get_asset_cache(Gfx* gfx) { + assert(gfx); + return &gfx->asset_cache; +} diff --git a/src/gfx_assert.h b/src/gfx_assert.h new file mode 100644 index 0000000..f4b3aa5 --- /dev/null +++ b/src/gfx_assert.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +#include // Include after log to use log's LOGE(). diff --git a/src/renderer/imm_renderer.c b/src/renderer/imm_renderer.c new file mode 100644 index 0000000..8cf3a10 --- /dev/null +++ b/src/renderer/imm_renderer.c @@ -0,0 +1,260 @@ +#include "imm_renderer_impl.h" + +#include +#include + +#include + +#include +#include // memcpy + +bool imm_renderer_make(ImmRenderer* renderer, GfxCore* gfxcore) { + assert(renderer); + assert(gfxcore); + + const size_t num_triangle_verts = IMM_MAX_NUM_TRIANGLES * 3; + + renderer->gfxcore = gfxcore; + + renderer->triangles = gfx_make_geometry( + gfxcore, &(GeometryDesc){ + .type = Triangles, + .buffer_usage = BufferDynamic, + .num_verts = num_triangle_verts, + .positions3d = (BufferView3d){ + .size_bytes = num_triangle_verts * sizeof(vec3)}}); + if (!renderer->triangles) { + goto cleanup; + } + + renderer->shader = gfx_make_immediate_mode_shader(gfxcore); + if (!renderer->shader) { + goto cleanup; + } + + renderer->matrix_stack[0] = mat4_id(); + renderer->stack_pointer = 0; + + gfx_imm_set_colour(renderer, vec4_make(0.0, 0.0, 0.0, 1.0)); + + return true; + +cleanup: + imm_renderer_destroy(renderer); + return false; +} + +void imm_renderer_destroy(ImmRenderer* renderer) { + assert(renderer); + assert(renderer->gfxcore); + + if (renderer->triangles) { + gfx_destroy_geometry(renderer->gfxcore, &renderer->triangles); + // TODO: Could also destroy the geometry's buffers here. + } + + if (renderer->shader) { + gfx_destroy_shader_program(renderer->gfxcore, &renderer->shader); + } +} + +void imm_renderer_flush(ImmRenderer* renderer) { + assert(renderer); + + if (renderer->num_triangle_verts > 0) { + gfx_update_geometry( + renderer->triangles, + &(GeometryDesc){ + .num_verts = renderer->num_triangle_verts, + .positions3d = (BufferView3d){ + .data = renderer->triangle_verts, + .size_bytes = renderer->num_triangle_verts * sizeof(vec3)} + }); + + gfx_apply_uniforms(renderer->shader); + gfx_render_geometry(renderer->triangles); + + renderer->num_triangle_verts = 0; + } +} + +void gfx_imm_start(ImmRenderer* renderer) { + assert(renderer); + // Shader uniforms are applied lazily. + // TODO: In the event that gfx_activate_shader_program() activates uniforms + // automatically for convenience, call an overload here that doesn't do so. + ShaderProgram* shader = renderer->shader; + gfx_activate_shader_program(shader); +} + +void gfx_imm_end(ImmRenderer* renderer) { + assert(renderer); + imm_renderer_flush(renderer); + gfx_deactivate_shader_program(renderer->shader); +} + +void gfx_imm_draw_triangles( + ImmRenderer* renderer, const vec3 verts[], size_t num_triangles) { + assert(renderer); + assert(verts); + const size_t new_verts = num_triangles * 3; + assert( + renderer->num_triangle_verts + new_verts < (IMM_MAX_NUM_TRIANGLES * 3)); + + memcpy( + renderer->triangle_verts + renderer->num_triangle_verts, verts, + new_verts * sizeof(vec3)); + + renderer->num_triangle_verts += new_verts; +} + +void gfx_imm_draw_triangle(ImmRenderer* renderer, const vec3 verts[3]) { + gfx_imm_draw_triangles(renderer, verts, 1); +} + +void gfx_imm_draw_aabb2(ImmRenderer* renderer, aabb2 box) { + assert(renderer); + + // clang-format off + const vec3 verts[4] = { + vec3_make(box.min.x, box.min.y, 0), // 3 ---- 2 + vec3_make(box.max.x, box.min.y, 0), // | | + vec3_make(box.max.x, box.max.y, 0), // | | + vec3_make(box.min.x, box.max.y, 0)}; // 0 ---- 1 + // clang-format on + +#define tri(i0, i1, i2) verts[i0], verts[i1], verts[i2] + const vec3 tris[6] = {tri(0, 1, 2), tri(0, 2, 3)}; +#undef tri + + gfx_imm_draw_triangles(renderer, tris, 2); +} + +void gfx_imm_draw_aabb3(ImmRenderer* renderer, aabb3 box) { + assert(renderer); + + // clang-format off + const vec3 vertices[8] = { + vec3_make(box.min.x, box.min.y, box.max.z), // 7 ----- 6 + vec3_make(box.max.x, box.min.y, box.max.z), // / /| + vec3_make(box.max.x, box.max.y, box.max.z), // 3 ----- 2 | + vec3_make(box.min.x, box.max.y, box.max.z), // | | | + vec3_make(box.min.x, box.min.y, box.min.z), // | 4 ----- 5 + vec3_make(box.max.x, box.min.y, box.min.z), // |/ |/ + vec3_make(box.max.x, box.max.y, box.min.z), // 0 ----- 1 + vec3_make(box.min.x, box.max.y, box.min.z)}; + // clang-format on + + gfx_imm_draw_box3(renderer, vertices); +} + +void gfx_imm_draw_box3(ImmRenderer* renderer, const vec3 vertices[8]) { + assert(renderer); + assert(vertices); + + // 7 ----- 6 + // / /| + // 3 ----- 2 | + // | | | + // | 4 ----- 5 + // |/ |/ + // 0 ----- 1 + +#define tri(i0, i1, i2) vertices[i0], vertices[i1], vertices[i2] + const vec3 tris[36] = {// Front. + tri(0, 1, 2), tri(0, 2, 3), + // Right. + tri(1, 5, 6), tri(1, 6, 2), + // Back. + tri(5, 4, 7), tri(5, 7, 6), + // Left. + tri(4, 0, 03), tri(4, 3, 7), + // Top. + tri(3, 2, 6), tri(3, 6, 7), + // Bottom. + tri(0, 4, 5), tri(0, 5, 1)}; + + gfx_imm_draw_triangles(renderer, tris, 12); +} + +// Load the top of the matrix stack into the shader. +static void update_shader_model_matrix(ImmRenderer* renderer) { + assert(renderer); + imm_renderer_flush(renderer); + gfx_set_mat4_uniform( + renderer->shader, "Model", + &renderer->matrix_stack[renderer->stack_pointer]); +} + +void gfx_imm_load_identity(ImmRenderer* renderer) { + assert(renderer); + renderer->matrix_stack[0] = mat4_id(); + renderer->stack_pointer = 0; + update_shader_model_matrix(renderer); +} + +void gfx_imm_push_matrix(ImmRenderer* renderer, const mat4* matrix) { + assert(renderer); + assert(matrix); + assert(renderer->stack_pointer >= 0); + assert(renderer->stack_pointer < IMM_MAX_NUM_MATRICES); // TODO: hard assert. + + renderer->matrix_stack[renderer->stack_pointer + 1] = + mat4_mul(*matrix, renderer->matrix_stack[renderer->stack_pointer]); + renderer->stack_pointer += 1; + + update_shader_model_matrix(renderer); +} + +void gfx_imm_pop_matrix(ImmRenderer* renderer) { + assert(renderer); + assert(renderer->stack_pointer > 0); // TODO: hard assert. + + // For debugging, zero out the matrix stack as matrices are popped out. + memset( + &renderer->matrix_stack[renderer->stack_pointer], 0, + sizeof(renderer->matrix_stack[0])); + + renderer->stack_pointer -= 1; + + update_shader_model_matrix(renderer); +} + +void gfx_imm_translate(ImmRenderer* renderer, vec3 offset) { + assert(renderer); + const mat4 mat = mat4_translate(offset); + gfx_imm_push_matrix(renderer, &mat); +} + +void gfx_imm_set_camera(ImmRenderer* renderer, const Camera* camera) { + assert(renderer); + assert(renderer->shader); + imm_renderer_flush(renderer); + const mat4 view = spatial3_inverse_transform(&camera->spatial); + const mat4 view_proj = mat4_mul(camera->projection, view); + gfx_imm_set_view_projection_matrix(renderer, &view_proj); +} + +void gfx_imm_set_model_matrix(ImmRenderer* renderer, const mat4* model) { + assert(renderer); + assert(model); + imm_renderer_flush(renderer); + renderer->matrix_stack[0] = *model; + renderer->stack_pointer = 0; + update_shader_model_matrix(renderer); +} + +void gfx_imm_set_view_projection_matrix( + ImmRenderer* renderer, const mat4* view_proj) { + assert(renderer); + assert(renderer->shader); + imm_renderer_flush(renderer); + gfx_set_mat4_uniform(renderer->shader, "ViewProjection", view_proj); +} + +void gfx_imm_set_colour(ImmRenderer* renderer, vec4 colour) { + assert(renderer); + assert(renderer->shader); + imm_renderer_flush(renderer); + gfx_set_vec4_uniform(renderer->shader, "Colour", colour); +} diff --git a/src/renderer/imm_renderer_impl.h b/src/renderer/imm_renderer_impl.h new file mode 100644 index 0000000..5ece354 --- /dev/null +++ b/src/renderer/imm_renderer_impl.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include + +typedef struct Geometry Geometry; +typedef struct ShaderProgram ShaderProgram; + +/// Immediate mode renderer. +/// +/// Currently, the immediate mode renderer can only draw up to a maximum number +/// of primitives per frame. It does not adjust this number dynamically. Keeps +/// things simple while the extra complexity is not needed. +typedef struct ImmRenderer { + GfxCore* gfxcore; + ShaderProgram* shader; + Geometry* triangles; + size_t num_triangle_verts; // Number of triangle verts this frame. + // TODO: wireframe rendering. + struct { + bool wireframe : 1; + } flags; + vec3 triangle_verts[IMM_MAX_NUM_TRIANGLES * 3]; + // Matrix stack contains pre-multiplied matrices. + // It is also never empty. The top of the stack is an identity matrix when the + // stack is "empty" from the user's perspective. + mat4 matrix_stack[IMM_MAX_NUM_MATRICES]; + int stack_pointer; +} ImmRenderer; + +/// Create a new immediate mode renderer. +bool imm_renderer_make(ImmRenderer*, GfxCore*); + +/// Destroy the immediate mode renderer. +void imm_renderer_destroy(ImmRenderer*); + +/// Flush draw commands. +void imm_renderer_flush(ImmRenderer*); diff --git a/src/renderer/renderer.c b/src/renderer/renderer.c new file mode 100644 index 0000000..c2a7dda --- /dev/null +++ b/src/renderer/renderer.c @@ -0,0 +1,396 @@ +#include "renderer_impl.h" + +#include "scene/animation_impl.h" +#include "scene/camera_impl.h" +#include "scene/light_impl.h" +#include "scene/material_impl.h" +#include "scene/mesh_impl.h" +#include "scene/model_impl.h" +#include "scene/node_impl.h" +#include "scene/object_impl.h" +#include "scene/scene_impl.h" +#include "scene/scene_memory.h" + +#include +#include +#include + +#include +#include +#include + +#include + +// TODO: Move to a header like "constants.h". +static const int IRRADIANCE_MAP_WIDTH = 1024; +static const int IRRADIANCE_MAP_HEIGHT = 1024; +static const int PREFILTERED_ENVIRONMENT_MAP_WIDTH = 128; +static const int PREFILTERED_ENVIRONMENT_MAP_HEIGHT = 128; +static const int BRDF_INTEGRATION_MAP_WIDTH = 512; +static const int BRDF_INTEGRATION_MAP_HEIGHT = 512; + +bool renderer_make(Renderer* renderer, GfxCore* gfxcore) { + assert(renderer); + assert(gfxcore); + + renderer->gfxcore = gfxcore; + + return true; +} + +void renderer_destroy(Renderer* renderer) { + if (!renderer) { + return; + } + assert(renderer->gfxcore); + GfxCore* gfxcore = renderer->gfxcore; + if (renderer->ibl) { + gfx_destroy_ibl(gfxcore, &renderer->ibl); + } + if (renderer->shaders.debug) { + gfx_destroy_shader_program(gfxcore, &renderer->shaders.debug); + } + if (renderer->shaders.normals) { + gfx_destroy_shader_program(gfxcore, &renderer->shaders.normals); + } + if (renderer->shaders.normal_mapped_normals) { + gfx_destroy_shader_program( + gfxcore, &renderer->shaders.normal_mapped_normals); + } + if (renderer->shaders.tangents) { + gfx_destroy_shader_program(gfxcore, &renderer->shaders.tangents); + } +} + +/// Initialize renderer state for IBL if not already initialized. +static bool init_ibl(Renderer* renderer) { + assert(renderer); + + if (!renderer->ibl && !(renderer->ibl = gfx_make_ibl(renderer->gfxcore))) { + return false; + } + + if (!renderer->brdf_integration_map && + !(renderer->brdf_integration_map = gfx_make_brdf_integration_map( + renderer->ibl, renderer->gfxcore, BRDF_INTEGRATION_MAP_WIDTH, + BRDF_INTEGRATION_MAP_HEIGHT))) { + return false; + } + + return true; +} + +static ShaderProgram* load_shader(Renderer* renderer, RenderSceneMode mode) { + assert(renderer); + +#define LOAD_AND_RETURN(pShader, constructor) \ + { \ + if (!pShader) { \ + pShader = constructor(renderer->gfxcore); \ + } \ + assert(pShader); \ + return pShader; \ + } + + switch (mode) { + case RenderDefault: + return 0; + case RenderDebug: + LOAD_AND_RETURN(renderer->shaders.debug, gfx_make_debug3d_shader); + case RenderNormals: + LOAD_AND_RETURN(renderer->shaders.normals, gfx_make_view_normals_shader); + case RenderNormalMappedNormals: + LOAD_AND_RETURN( + renderer->shaders.normal_mapped_normals, + gfx_make_view_normal_mapped_normals_shader); + case RenderTangents: + LOAD_AND_RETURN(renderer->shaders.tangents, gfx_make_view_tangents_shader); + } + assert(false); + return 0; +} + +// static void log_matrix(const mat4* m) { +// for (int row = 0; row < 4; ++row) { +// LOGI("[ %5.2f, %5.2f, %5.2f, %5.2f ]", m->val[0][row], m->val[1][row], +// m->val[2][row], m->val[3][row]); +// } +// } + +/// Computes irradiance and prefiltered environment maps for the light if they +/// have not been already computed. +static bool setup_environment_light( + Renderer* renderer, GfxCore* gfxcore, EnvironmentLight* light) { + assert(renderer); + assert(light); + + if (!init_ibl(renderer)) { + return false; + } + + if (light->irradiance_map) { + assert(light->prefiltered_environment_map); + return true; + } + + Texture* irradiance_map = 0; + Texture* prefiltered_environment_map = 0; + + if (!(irradiance_map = gfx_make_irradiance_map( + renderer->ibl, gfxcore, light->environment_map, + IRRADIANCE_MAP_WIDTH, IRRADIANCE_MAP_HEIGHT))) { + goto cleanup; + } + + int max_mip_level = 0; + if (!(prefiltered_environment_map = gfx_make_prefiltered_environment_map( + renderer->ibl, gfxcore, light->environment_map, + PREFILTERED_ENVIRONMENT_MAP_WIDTH, + PREFILTERED_ENVIRONMENT_MAP_HEIGHT, &max_mip_level))) { + goto cleanup; + } + + light->irradiance_map = irradiance_map; + light->prefiltered_environment_map = prefiltered_environment_map; + light->max_reflection_lod = max_mip_level; + + return true; + +cleanup: + if (irradiance_map) { + gfx_destroy_texture(gfxcore, &irradiance_map); + } + if (prefiltered_environment_map) { + gfx_destroy_texture(gfxcore, &prefiltered_environment_map); + } + return false; +} + +typedef struct RenderState { + GfxCore* gfxcore; + Renderer* renderer; + ShaderProgram* shader; // Null to use scene shaders. + const Scene* scene; + const Camera* camera; + const mat4* camera_rotation; // From camera to world space, rotation only. + const mat4* view_matrix; + const mat4* projection; + const float fovy; + const float aspect; + Light* environment_light; + const Anima* anima; + size_t num_joints; + mat4 joint_matrices[GFX_MAX_NUM_JOINTS]; +} RenderState; + +/// Load joint matrices into the render state. +static void load_skeleton(RenderState* state, skeleton_idx skeleton_index) { + assert(state); + assert(skeleton_index.val != 0); + + const Skeleton* skeleton = mem_get_skeleton(skeleton_index); + assert(skeleton); + assert(skeleton->num_joints <= GFX_MAX_NUM_JOINTS); + + state->num_joints = skeleton->num_joints; + + for (size_t i = 0; i < skeleton->num_joints; ++i) { + const joint_idx joint_index = skeleton->joints[i]; + const Joint* joint = &state->anima->joints[joint_index]; + state->joint_matrices[i] = joint->joint_matrix; + } +} + +/// Draw the scene recursively. +static void draw_recursively( + RenderState* state, mat4 parent_transform, const SceneNode* node) { + assert(state); + const mat4 node_transform = mat4_mul(parent_transform, node->transform); + + // Anima. + if (node->type == AnimaNode) { + state->anima = gfx_get_node_anima(node); + } + // Activate light. + else if (node->type == LightNode) { + Light* light = mem_get_light(node->light); + assert(light); + + if (light->type == EnvironmentLightType) { + bool result = setup_environment_light( + state->renderer, state->gfxcore, &light->environment); + // TODO: Handle the result in a better way. + assert(result); + state->environment_light = light; + } + } + // Model. + else if (node->type == ModelNode) { + const Model* model = gfx_get_node_model(node); + const SceneNode* root = mem_get_node(model->root); + draw_recursively(state, parent_transform, root); + } + // Render object. + else if (node->type == ObjectNode) { + const SceneObject* object = mem_get_object(node->object); + assert(object); + + // TODO: Here we would frustum-cull the object. + + // TODO: Avoid computing matrices like Modelview or MVP if the shader does + // not use them. + const mat4 model_matrix = node_transform; + const mat4 modelview = mat4_mul(*state->view_matrix, model_matrix); + const mat4 mvp = mat4_mul(*state->projection, modelview); + + if (object->skeleton.val) { + load_skeleton(state, object->skeleton); + } + + for (mesh_link_idx mesh_link_index = object->mesh_link; + mesh_link_index.val;) { + const MeshLink* mesh_link = mem_get_mesh_link(mesh_link_index); + mesh_link_index = mesh_link->next; + + const Mesh* mesh = mem_get_mesh(mesh_link->mesh); + if (!mesh) { + continue; + } + assert(mesh->geometry); + assert(mesh->material); + + // TODO: Here we would frustum-cull the mesh. The AABB would have to be + // transformed by the model matrix. Rotation would make the AABB + // relatively large, but still, the culling would be conservative. + + // Apply common shader uniforms not captured by materials. + ShaderProgram* shader = state->shader ? state->shader : mesh->shader; + gfx_set_mat4_uniform(shader, "ModelMatrix", &model_matrix); + gfx_set_mat4_uniform(shader, "Modelview", &modelview); + gfx_set_mat4_uniform(shader, "View", state->view_matrix); + gfx_set_mat4_uniform(shader, "Projection", state->projection); + gfx_set_mat4_uniform(shader, "MVP", &mvp); + gfx_set_mat4_uniform(shader, "CameraRotation", state->camera_rotation); + gfx_set_float_uniform(shader, "Fovy", state->fovy); + gfx_set_float_uniform(shader, "Aspect", state->aspect); + if (state->camera) { + gfx_set_vec3_uniform( + shader, "CameraPosition", state->camera->spatial.p); + } + if (state->num_joints > 0) { + gfx_set_mat4_array_uniform( + shader, "JointMatrices", state->joint_matrices, state->num_joints); + } + // Apply lights. + if (state->environment_light) { + const EnvironmentLight* light = &state->environment_light->environment; + assert(light->environment_map); + assert(light->irradiance_map); + assert(light->prefiltered_environment_map); + assert(state->renderer->brdf_integration_map); + gfx_set_texture_uniform( + shader, "BRDFIntegrationMap", + state->renderer->brdf_integration_map); + gfx_set_texture_uniform(shader, "Sky", light->environment_map); + gfx_set_texture_uniform(shader, "IrradianceMap", light->irradiance_map); + gfx_set_texture_uniform( + shader, "PrefilteredEnvironmentMap", + light->prefiltered_environment_map); + gfx_set_float_uniform( + shader, "MaxReflectionLOD", light->max_reflection_lod); + } + material_activate(shader, mesh->material); + gfx_activate_shader_program(shader); + gfx_apply_uniforms(shader); + gfx_render_geometry(mesh->geometry); + } + + // Reset state for next object. + state->num_joints = 0; + } + + // Render children recursively. + for (node_idx child_index = node->child; child_index.val;) { + const SceneNode* child = mem_get_node(child_index); + draw_recursively(state, node_transform, child); + child_index = child->next; + } +} + +void gfx_render_scene(Renderer* renderer, const RenderSceneParams* params) { + assert(renderer); + assert(params); + assert(params->scene); + + ShaderProgram* const shader = load_shader(renderer, params->mode); + + const Scene* scene = params->scene; + const SceneCamera* camera = params->camera; + + GfxCore* gfxcore = renderer->gfxcore; + + mat4 projection, camera_rotation, view_matrix; + if (camera) { + projection = camera->camera.projection; + camera_rotation = + mat4_rotation(spatial3_transform(&camera->camera.spatial)); + view_matrix = spatial3_inverse_transform(&camera->camera.spatial); + } else { + projection = mat4_id(); + camera_rotation = mat4_id(); + view_matrix = mat4_id(); + } + + int x, y, width, height; + gfx_get_viewport(gfxcore, &x, &y, &width, &height); + const float aspect = (float)width / (float)height; + + RenderState state = { + .gfxcore = gfxcore, + .renderer = renderer, + .shader = shader, + .scene = scene, + .camera = &camera->camera, + .camera_rotation = &camera_rotation, + .view_matrix = &view_matrix, + .projection = &projection, + .environment_light = 0, + // Assuming a perspective matrix. + .fovy = atan(1.0 / (mat4_at(projection, 1, 1))) * 2, + .aspect = aspect}; + + draw_recursively(&state, mat4_id(), scene->root); +} + +static void update_rec(SceneNode* node, const SceneCamera* camera, R t) { + assert(node); + assert(camera); + + const NodeType node_type = gfx_get_node_type(node); + + // TODO: Models do not need to be animated if they are not visible to the + // camera. + if (node_type == AnimaNode) { + Anima* anima = gfx_get_node_anima_mut(node); + gfx_update_animation(anima, (R)t); + } else if (node_type == ModelNode) { + Model* model = gfx_get_node_model_mut(node); + SceneNode* root = gfx_get_model_root_mut(model); + update_rec(root, camera, t); + } + + // Children. + SceneNode* child = gfx_get_node_child_mut(node); + while (child) { + update_rec(child, camera, t); + child = gfx_get_node_sibling_mut(child); + } +} + +void gfx_update(Scene* scene, const SceneCamera* camera, R t) { + assert(scene); + assert(camera); + + SceneNode* node = gfx_get_scene_root(scene); + update_rec(node, camera, t); +} diff --git a/src/renderer/renderer_impl.h b/src/renderer/renderer_impl.h new file mode 100644 index 0000000..fc14dcb --- /dev/null +++ b/src/renderer/renderer_impl.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +typedef struct IBL IBL; +typedef struct ShaderProgram ShaderProgram; +typedef struct Texture Texture; + +typedef struct Renderer { + GfxCore* gfxcore; + IBL* ibl; + Texture* brdf_integration_map; + struct { + ShaderProgram* debug; + ShaderProgram* normals; + ShaderProgram* normal_mapped_normals; + ShaderProgram* tangents; + } shaders; +} Renderer; + +/// Create a new renderer. +bool renderer_make(Renderer*, GfxCore*); + +/// Destroy the renderer. +void renderer_destroy(Renderer*); diff --git a/src/scene/animation.c b/src/scene/animation.c new file mode 100644 index 0000000..08d02ce --- /dev/null +++ b/src/scene/animation.c @@ -0,0 +1,524 @@ +#include "animation_impl.h" + +#include "node_impl.h" +#include "scene_memory.h" + +#include + +// #include // Debugging. + +static const R PLAYBACK_UNINITIALIZED = -1; + +static joint_idx get_anima_root_joint_index(Anima* anima) { + assert(anima); + assert(anima->num_joints > 0); + assert(anima->num_joints < GFX_MAX_NUM_JOINTS); + return anima->num_joints - 1; +} + +static Joint* get_anima_root_joint(Anima* anima) { + assert(anima); + return &anima->joints[get_anima_root_joint_index(anima)]; +} + +static const Joint* get_anima_joint(const Anima* anima, joint_idx index) { + assert(anima); + assert(index < GFX_MAX_NUM_JOINTS); + assert(index != INDEX_NONE); + assert(index < anima->num_joints); + return &anima->joints[index]; +} + +static Joint* get_anima_joint_mut(Anima* anima, joint_idx index) { + return (Joint*)get_anima_joint(anima, index); +} + +static const Joint* get_skeleton_joint( + const Anima* anima, const Skeleton* skeleton, joint_idx index) { + assert(anima); + assert(skeleton); + return get_anima_joint(anima, skeleton->joints[index]); +} + +static void set_joint_parent( + Anima* anima, joint_idx joint_index, joint_idx parent_index) { + assert(anima); + assert(joint_index != INDEX_NONE); + assert(joint_index != get_anima_root_joint_index(anima)); + assert(parent_index != INDEX_NONE); + + Joint* parent = get_anima_joint_mut(anima, parent_index); + + if (parent->child == INDEX_NONE) { + parent->child = joint_index; + } else { + // Find the last child in the chain of children. + Joint* child = get_anima_joint_mut(anima, parent->child); + while (child->next != INDEX_NONE) { + child = get_anima_joint_mut(anima, child->next); + } + // Wire up this joint as the last child's sibling. + child->next = joint_index; + } +} + +static void make_joint(Anima* anima, const JointDesc* desc, Joint* joint) { + assert(anima); + assert(desc); + assert(joint); + + // The joint matrix needs to be initialized so that meshes look right even if + // no animation is played. Initializing joint matrices to the identity makes + // meshes appear in their bind pose. + joint->child = INDEX_NONE; + joint->next = INDEX_NONE; + joint->transform = mat4_id(); + joint->inv_bind_matrix = desc->inv_bind_matrix; + joint->joint_matrix = mat4_id(); + joint->box = desc->box; +} + +static Skeleton* make_skeleton(const SkeletonDesc* desc) { + assert(desc); + assert(desc->num_joints <= GFX_MAX_NUM_JOINTS); + + Skeleton* skeleton = mem_alloc_skeleton(); + skeleton->num_joints = desc->num_joints; + memcpy( + skeleton->joints, desc->joints, + desc->num_joints * sizeof(skeleton->joints[0])); + return skeleton; +} + +static Animation* make_animation(const AnimationDesc* desc) { + assert(desc); + assert(desc->num_channels < GFX_MAX_NUM_CHANNELS); + + Animation* animation = mem_alloc_animation(); + animation->name = desc->name; + animation->duration = 0; + animation->num_channels = desc->num_channels; + R start_time = 0; + R end_time = 0; + + for (size_t c = 0; c < desc->num_channels; ++c) { + const ChannelDesc* channel_desc = &desc->channels[c]; + Channel* channel = &animation->channels[c]; + + channel->target = channel_desc->target; + channel->type = channel_desc->type; + channel->interpolation = channel_desc->interpolation; + channel->num_keyframes = channel_desc->num_keyframes; + assert(channel_desc->num_keyframes < GFX_MAX_NUM_KEYFRAMES); + + for (size_t k = 0; k < channel_desc->num_keyframes; ++k) { + const KeyframeDesc* keyframe_desc = &channel_desc->keyframes[k]; + Keyframe* keyframe = &channel->keyframes[k]; + + keyframe->time = keyframe_desc->time; + keyframe->translation = keyframe_desc->translation; + keyframe->rotation = keyframe_desc->rotation; + + start_time = keyframe->time < start_time ? keyframe->time : start_time; + end_time = keyframe->time > end_time ? keyframe->time : end_time; + } + } + + // LOGD("Animation start/end: %f / %f", start_time, end_time); + animation->duration = end_time - start_time; + assert(animation->duration >= 0); + return animation; +} + +Anima* gfx_make_anima(const AnimaDesc* desc) { + assert(desc); + assert(desc->num_joints > 0); + assert(desc->num_joints <= GFX_MAX_NUM_JOINTS); + // All joints should have a parent except for the root. + for (size_t i = 0; i < desc->num_joints - 1; ++i) { + const joint_idx parent = desc->joints[i].parent; + assert(parent != INDEX_NONE); + assert(parent < desc->num_joints); + } + // The root should have no parent. + assert(desc->joints[desc->num_joints - 1].parent == INDEX_NONE); + + Anima* anima = mem_alloc_anima(); + + // Wire the skeletons in the same order they are given in the descriptor. + Skeleton* last_skeleton = 0; + for (size_t i = 0; i < desc->num_skeletons; ++i) { + Skeleton* skeleton = make_skeleton(&desc->skeletons[i]); + const skeleton_idx skeleton_index = mem_get_skeleton_index(skeleton); + if (last_skeleton == 0) { + anima->skeleton = skeleton_index; + } else { + last_skeleton->next = skeleton_index; + } + last_skeleton = skeleton; + } + + // Wire the animations in the same order they are given in the descriptor. + Animation* last_animation = 0; + for (size_t i = 0; i < desc->num_animations; ++i) { + Animation* animation = make_animation(&desc->animations[i]); + const animation_idx animation_index = mem_get_animation_index(animation); + if (last_animation == 0) { + anima->animation = animation_index; + } else { + last_animation->next = animation_index; + } + last_animation = animation; + } + + // Create joints. + anima->num_joints = desc->num_joints; + // Initialize all joints. + // Child and sibling pointers must be initialized before wiring up the + // hierarchy. + for (size_t i = 0; i < desc->num_joints; ++i) { + Joint* joint = get_anima_joint_mut(anima, i); + make_joint(anima, &desc->joints[i], joint); + } + // Wire up joints to their parents. -1 to skip the root. + for (size_t i = 0; i < desc->num_joints - 1; ++i) { + set_joint_parent(anima, i, desc->joints[i].parent); + } + + return anima; +} + +void gfx_destroy_anima(Anima** anima) { + assert(anima); + + if (*anima) { + for (skeleton_idx i = (*anima)->skeleton; i.val != 0;) { + Skeleton* skeleton = mem_get_skeleton(i); + i = skeleton->next; + mem_free_skeleton(&skeleton); + } + + for (animation_idx i = (*anima)->animation; i.val != 0;) { + Animation* animation = mem_get_animation(i); + i = animation->next; + mem_free_animation(&animation); + } + + if ((*anima)->parent.val) { + gfx_del_node((*anima)->parent); + } + + mem_free_anima(anima); + } +} + +static Animation* find_animation(animation_idx index, const char* name) { + assert(name); + + while (index.val != 0) { + Animation* animation = mem_get_animation(index); + if (sstring_eq_cstr(animation->name, name)) { + // LOGD( + // "Found animation at index %u, %s - %s", index.val, + // sstring_cstr(&animation->name), name); + // LOGD("Animation has duration %f", animation->duration); + return animation; + } + index = animation->next; + } + + return 0; +} + +bool gfx_play_animation(Anima* anima, const AnimationPlaySettings* settings) { + assert(anima); + assert(settings); + + // TODO: Should we animate at t=0 here to kickstart the animation? Otherwise + // the client is forced to call gfx_update_animation() to do this. + Animation* animation = find_animation(anima->animation, settings->name); + if (!animation) { + return false; + } + // Playback initialized on first call to update(). + AnimationState* state = &anima->state; + state->start_time = PLAYBACK_UNINITIALIZED; + state->animation = mem_get_animation_index(animation); + state->loop = settings->loop; + return true; +} + +static void gfx_set_joint_position(Joint* joint, vec3 position) { + assert(joint); + mat4_set_v3(&joint->transform, position); +} + +static void gfx_set_joint_rotation(Joint* joint, quat rotation) { + assert(joint); + mat4_set_3x3(&joint->transform, mat4_from_quat(rotation)); +} + +static void find_keyframes(const Channel* channel, R t, int* prev, int* next) { + assert(channel); + assert(prev); + assert(next); + + *prev = -1; + *next = 0; + while (((*next + 1) < (int)channel->num_keyframes) && + (t >= channel->keyframes[*next + 1].time)) { + (*prev)++; + (*next)++; + } +} + +static R normalize_time(R a, R b, R t) { + assert(a <= t); + assert(t <= b); + return (t - a) / (b - a); +} + +static quat interpolate_rotation( + const Channel* channel, int prev, int next, R t) { + assert(channel); + + if (next == 0) { + // Animation has not started at this point in time yet. + return channel->keyframes[next].rotation; + } else { + switch (channel->interpolation) { + case StepInterpolation: + return channel->keyframes[prev].rotation; + case LinearInterpolation: { + const R normalized_t = normalize_time( + channel->keyframes[prev].time, channel->keyframes[next].time, t); + return qnormalize(qslerp( + channel->keyframes[prev].rotation, channel->keyframes[next].rotation, + normalized_t)); + break; + } + case CubicSplineInterpolation: + assert(false); // TODO + return qmake(0, 0, 0, 0); + default: + assert(false); + return qmake(0, 0, 0, 0); + } + } +} + +static vec3 interpolate_translation( + const Channel* channel, int prev, int next, R t) { + assert(channel); + + if (next == 0) { + // Animation has not started at this point in time yet. + return channel->keyframes[next].translation; + } else { + switch (channel->interpolation) { + case StepInterpolation: + return channel->keyframes[prev].translation; + case LinearInterpolation: { + const R normalized_t = normalize_time( + channel->keyframes[prev].time, channel->keyframes[next].time, t); + return vec3_lerp( + channel->keyframes[prev].translation, + channel->keyframes[next].translation, normalized_t); + break; + } + case CubicSplineInterpolation: + assert(false); // TODO + return vec3_make(0, 0, 0); + default: + assert(false); + return vec3_make(0, 0, 0); + } + } +} + +static void animate_channel(Anima* anima, const Channel* channel, R t) { + assert(anima); + assert(channel); + assert(channel->target < anima->num_joints); + + int prev, next; + find_keyframes(channel, t, &prev, &next); + + // Note that not all channels extend to the duration of an animation; some + // channels may stop animating their targets earlier. Clamp the animation time + // to the channel's end keyframe to make the rest of the math (normalize_time) + // work. + t = t > channel->keyframes[next].time ? channel->keyframes[next].time : t; + + Joint* target = get_anima_joint_mut(anima, channel->target); + + switch (channel->type) { + case RotationChannel: { + const quat rotation = interpolate_rotation(channel, prev, next, t); + gfx_set_joint_rotation(target, rotation); + break; + } + case TranslationChannel: { + const vec3 translation = interpolate_translation(channel, prev, next, t); + gfx_set_joint_position(target, translation); + break; + } + // Not yet supported. + case ScaleChannel: + case WeightsChannel: + default: + // TODO: Add back the assertion or add support for scaling. + // assert(false); + break; + } +} + +static void compute_joint_matrices_rec( + Anima* anima, Joint* joint, const mat4* parent_global_joint_transform, + const mat4* root_inv_global_transform) { + assert(anima); + assert(joint); + assert(parent_global_joint_transform); + assert(root_inv_global_transform); + + const mat4 global_joint_transform = + mat4_mul(*parent_global_joint_transform, joint->transform); + + // Compute this joint's matrix. + joint->joint_matrix = mat4_mul( + *root_inv_global_transform, + mat4_mul(global_joint_transform, joint->inv_bind_matrix)); + + // Recursively compute the joint matrices for this joint's siblings. + if (joint->next != INDEX_NONE) { + Joint* sibling = get_anima_joint_mut(anima, joint->next); + + compute_joint_matrices_rec( + anima, sibling, parent_global_joint_transform, + root_inv_global_transform); + } + + // Recursively compute the joint matrices for this joint's children. + if (joint->child != INDEX_NONE) { + Joint* child = get_anima_joint_mut(anima, joint->child); + + compute_joint_matrices_rec( + anima, child, &global_joint_transform, root_inv_global_transform); + } +} + +void gfx_update_animation(Anima* anima, R t) { + assert(anima); + + AnimationState* state = &anima->state; + if (state->animation.val == 0) { + return; // No active animation. + } + const Animation* animation = mem_get_animation(state->animation); + assert(animation); + + // On a call to play(), the start time is set to -1 to signal that the + // animation playback has not yet been initialized. + if (state->start_time == PLAYBACK_UNINITIALIZED) { + state->start_time = t; + } + // Locate the current time point inside the animation's timeline. + assert(t >= state->start_time); + assert(animation->duration >= 0.0); + const R local_time = t - state->start_time; + const R animation_time = state->loop + ? rmod(local_time, animation->duration) + : clamp(local_time, 0.0, animation->duration); + + // LOGD( + // "animation_time = %f, animation duration: %f", animation_time, + // animation->duration); + + // Play through the animation to transform skeleton nodes. + for (size_t i = 0; i < animation->num_channels; ++i) { + const Channel* channel = &animation->channels[i]; + animate_channel(anima, channel, animation_time); + } + + // Compute joint matrices after having transformed the skeletons. + // + // The anima's parent node is the common ancestor of all skeletons, and its + // transform maps the skeletons from object space to world space. This is the + // transform used as the "global transform" in the joint matrix equations. + // + // Joint matrix calculation begins by descending from the anima's root joint, + // which we have constructed to be the common root of all skeletons. + // + // This procedure touches every joint exactly once. + SceneNode* root_node = mem_get_node(anima->parent); + // LOGD("Root: %u, child: %u", anima->parent.val, root->child.val); + const mat4 root_global_transform = gfx_get_node_global_transform(root_node); + const mat4 root_inv_global_transform = mat4_inverse(root_global_transform); + + Joint* root_joint = get_anima_root_joint(anima); + compute_joint_matrices_rec( + anima, root_joint, &root_global_transform, &root_inv_global_transform); +} + +const Skeleton* gfx_get_anima_skeleton(const Anima* anima, size_t i) { + assert(anima); + + skeleton_idx skeleton_index = anima->skeleton; + const Skeleton* skeleton = mem_get_skeleton(skeleton_index); + + for (size_t j = 1; j < i; ++j) { + skeleton_index = skeleton->next; + mem_get_skeleton(skeleton_index); + } + + return skeleton; +} + +size_t gfx_get_skeleton_num_joints(const Skeleton* skeleton) { + assert(skeleton); + return skeleton->num_joints; +} + +bool gfx_joint_has_box( + const Anima* anima, const Skeleton* skeleton, size_t joint_index) { + assert(anima); + assert(skeleton); + assert(joint_index < skeleton->num_joints); + + const Joint* joint = get_skeleton_joint(anima, skeleton, joint_index); + return !aabb3_is_empty(joint->box); +} + +Box gfx_get_joint_box( + const Anima* anima, const Skeleton* skeleton, size_t joint_index) { + assert(anima); + assert(skeleton); + + const Joint* joint = get_skeleton_joint(anima, skeleton, joint_index); + + // Transform the box to anima space. + // Note that joint matrices do not usually have a translation since joints + // mostly just rotate with respect to their parent. + const vec3 pmin = joint->box.min; + const vec3 pmax = joint->box.max; + return (Box){ + .vertices = { + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmin.x, pmin.y, pmax.z), 1), + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmax.x, pmin.y, pmax.z), 1), + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmax.x, pmax.y, pmax.z), 1), + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmin.x, pmax.y, pmax.z), 1), + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmin.x, pmin.y, pmin.z), 1), + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmax.x, pmin.y, pmin.z), 1), + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmax.x, pmax.y, pmin.z), 1), + mat4_mul_vec3( + joint->joint_matrix, vec3_make(pmin.x, pmax.y, pmin.z), 1), + } + }; +} diff --git a/src/scene/animation_impl.h b/src/scene/animation_impl.h new file mode 100644 index 0000000..4408158 --- /dev/null +++ b/src/scene/animation_impl.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include + +#include "types.h" + +#include +#include +#include +#include +#include + +#include +#include + +typedef struct Buffer Buffer; + +// Currently ignoring scale in skinning and animation. +// +// TODO: Simultaneous animation of disjoint animations. + +/// Skeleton joint. +/// Joints are mutable and store the transform and joint matrices that result +/// from animation, aside from the inverse bind matrix. +typedef struct Joint { + joint_idx child; /// First child Joint; index into Anima's joints. + joint_idx next; /// Next sibling Joint; index into Anima's joints. + mat4 transform; /// Local transform relative to parent. + mat4 inv_bind_matrix; /// Transforms the mesh into the joint's local space. + mat4 joint_matrix; /// inv(global) * global joint transform * inv(bind). + aabb3 box; /// Bounding box of vertices affected by joint. +} Joint; + +/// Animation skeleton. +typedef struct Skeleton { + skeleton_idx next; + size_t num_joints; + joint_idx joints[GFX_MAX_NUM_JOINTS]; /// Indices into Anima's joints array. +} Skeleton; + +/// A keyframe of animation. +typedef struct Keyframe { + R time; /// Start time in [0, end animation time] + union { + vec3 translation; + quat rotation; + }; +} Keyframe; + +/// Animation channel. +typedef struct Channel { + joint_idx target; /// Index into Anima's joints array. + ChannelType type; + AnimationInterpolation interpolation; + size_t num_keyframes; + Keyframe keyframes[GFX_MAX_NUM_KEYFRAMES]; +} Channel; + +/// A skeletal animation. +typedef struct Animation { + animation_idx next; + sstring name; + R duration; + size_t num_channels; + Channel channels[GFX_MAX_NUM_CHANNELS]; +} Animation; + +/// Animation state. +/// +/// This represents the current state of an animation. +typedef struct AnimationState { + R start_time; // Time when the current animation started playing. -1 means the + // animation playback has not yet been initialized. + animation_idx animation; // Current animation. 0 = no animation. + bool loop; +} AnimationState; + +/// Animation object. +/// +/// This is the top-level animation object that encapsulates everything +/// necessary for animation. +/// +/// For lack of a better name, this is called an Anima. It is short and the +/// Latin root of animation. +/// +/// The last joint of the joints array at index 'num_joints - 1' is the root of +/// all skeletons; specifically, the root of all joints that otherwise would +/// have no parent (a skeleton need not have its own root and can be a set of +/// disjoint node hierarchies). +typedef struct Anima { + node_idx parent; /// Parent SceneNode. + skeleton_idx skeleton; /// Index of first skeleton. + animation_idx animation; /// Index of first animation. + AnimationState state; /// Current animation state. + size_t num_joints; /// Number of actual joints in the array. + Joint joints[GFX_MAX_NUM_JOINTS]; /// Shared by all skeletons. +} Anima; diff --git a/src/scene/camera.c b/src/scene/camera.c new file mode 100644 index 0000000..be7d806 --- /dev/null +++ b/src/scene/camera.c @@ -0,0 +1,37 @@ +#include "camera_impl.h" + +#include "node_impl.h" +#include "scene_memory.h" + +#include + +SceneCamera* gfx_make_camera() { + SceneCamera* camera = mem_alloc_camera(); + + camera->camera = camera_perspective( + /*fovy=*/90.0 * TO_RAD, /*aspect=*/16.0 / 9.0, + /*near=*/0.1, /*far=*/1000); + + return camera; +} + +void gfx_destroy_camera(SceneCamera** camera) { + assert(camera); + if (*camera) { + if ((*camera)->parent.val) { + gfx_del_node((*camera)->parent); + } + mem_free_camera(camera); + } +} + +void gfx_set_camera_camera(SceneCamera* scene_camera, Camera* camera) { + assert(scene_camera); + assert(camera); + scene_camera->camera = *camera; +} + +Camera* gfx_get_camera_camera(SceneCamera* camera) { + assert(camera); + return &camera->camera; +} diff --git a/src/scene/camera_impl.h b/src/scene/camera_impl.h new file mode 100644 index 0000000..20c3890 --- /dev/null +++ b/src/scene/camera_impl.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "types.h" + +#include + +typedef struct SceneCamera { + Camera camera; + node_idx parent; // Parent SceneNode. +} SceneCamera; diff --git a/src/scene/light.c b/src/scene/light.c new file mode 100644 index 0000000..adbec8d --- /dev/null +++ b/src/scene/light.c @@ -0,0 +1,42 @@ +#include "light_impl.h" + +#include "node_impl.h" +#include "scene_memory.h" + +#include + +static void make_environment_light( + Light* light, const EnvironmentLightDesc* desc) { + assert(light); + assert(desc); + light->type = EnvironmentLightType; + light->environment.environment_map = desc->environment_map; +} + +Light* gfx_make_light(const LightDesc* desc) { + assert(desc); + + Light* light = mem_alloc_light(); + + switch (desc->type) { + case EnvironmentLightType: + make_environment_light(light, &desc->light.environment); + break; + default: + log_error("Unhandled light type"); + gfx_destroy_light(&light); + return 0; + } + + return light; +} + +void gfx_destroy_light(Light** light) { + assert(light); + if (*light) { + if ((*light)->parent.val) { + gfx_del_node((*light)->parent); + } + mem_free_light(light); + } +} diff --git a/src/scene/light_impl.h b/src/scene/light_impl.h new file mode 100644 index 0000000..1aa0bb4 --- /dev/null +++ b/src/scene/light_impl.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "types.h" + +typedef struct Texture Texture; + +/// An environment light. +typedef struct EnvironmentLight { + const Texture* environment_map; + const Texture* irradiance_map; // Renderer implementation. + const Texture* prefiltered_environment_map; // Renderer implementation. + int max_reflection_lod; // Mandatory when prefiltered_environment_map is + // given. +} EnvironmentLight; + +/// A scene light. +typedef struct Light { + LightType type; + union { + EnvironmentLight environment; + }; + node_idx parent; // Parent SceneNode. +} Light; diff --git a/src/scene/material.c b/src/scene/material.c new file mode 100644 index 0000000..3248243 --- /dev/null +++ b/src/scene/material.c @@ -0,0 +1,57 @@ +#include "material_impl.h" + +#include "scene_memory.h" + +#include + +static void material_make(Material* material, const MaterialDesc* desc) { + assert(material); + assert(desc); + assert(desc->num_uniforms < GFX_MAX_UNIFORMS_PER_MATERIAL); + material->num_uniforms = desc->num_uniforms; + for (int i = 0; i < desc->num_uniforms; ++i) { + material->uniforms[i] = desc->uniforms[i]; + } +} + +Material* gfx_make_material(const MaterialDesc* desc) { + assert(desc); + Material* material = mem_alloc_material(); + material_make(material, desc); + return material; +} + +void gfx_destroy_material(Material** material) { mem_free_material(material); } + +static void set_uniform(ShaderProgram* prog, const ShaderUniform* uniform) { + switch (uniform->type) { + case UniformTexture: + gfx_set_texture_uniform(prog, uniform->name.str, uniform->value.texture); + break; + case UniformMat4: + gfx_set_mat4_uniform(prog, uniform->name.str, &uniform->value.mat4); + break; + case UniformVec3: + gfx_set_vec3_uniform(prog, uniform->name.str, uniform->value.vec3); + break; + case UniformVec4: + gfx_set_vec4_uniform(prog, uniform->name.str, uniform->value.vec4); + break; + case UniformFloat: + gfx_set_float_uniform(prog, uniform->name.str, uniform->value.scalar); + break; + case UniformMat4Array: + gfx_set_mat4_array_uniform( + prog, uniform->name.str, uniform->value.array.values, + uniform->value.array.count); + break; + } +} + +void material_activate(ShaderProgram* shader, const Material* material) { + assert(material); + for (int i = 0; i < material->num_uniforms; ++i) { + const ShaderUniform* uniform = &material->uniforms[i]; + set_uniform(shader, uniform); + } +} diff --git a/src/scene/material_impl.h b/src/scene/material_impl.h new file mode 100644 index 0000000..a6aa95b --- /dev/null +++ b/src/scene/material_impl.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +typedef struct ShaderProgram ShaderProgram; + +typedef struct Material { + ShaderUniform uniforms[GFX_MAX_UNIFORMS_PER_MATERIAL]; + int num_uniforms; +} Material; + +/// Activate the material. +/// +/// This activates the material's shader and configures the shader uniforms that +/// are specific to the material. +void material_activate(ShaderProgram* shader, const Material* material); diff --git a/src/scene/mesh.c b/src/scene/mesh.c new file mode 100644 index 0000000..1a93bed --- /dev/null +++ b/src/scene/mesh.c @@ -0,0 +1,24 @@ +#include "mesh_impl.h" + +#include "scene_memory.h" + +#include + +static void mesh_make(Mesh* mesh, const MeshDesc* desc) { + assert(mesh); + assert(desc); + assert(desc->geometry); + assert(desc->material); + assert(desc->shader); + mesh->geometry = desc->geometry; + mesh->material = desc->material; + mesh->shader = desc->shader; +} + +Mesh* gfx_make_mesh(const MeshDesc* desc) { + Mesh* mesh = mem_alloc_mesh(); + mesh_make(mesh, desc); + return mesh; +} + +void gfx_destroy_mesh(Mesh** mesh) { mem_free_mesh(mesh); } diff --git a/src/scene/mesh_impl.h b/src/scene/mesh_impl.h new file mode 100644 index 0000000..560b77e --- /dev/null +++ b/src/scene/mesh_impl.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +typedef struct Mesh { + const Geometry* geometry; + const Material* material; + ShaderProgram* shader; +} Mesh; + +// TODO: a mesh_render() that takes a transform, applies the material and the +// transform, and then renders the geometry. diff --git a/src/scene/model.c b/src/scene/model.c new file mode 100644 index 0000000..cc41a9a --- /dev/null +++ b/src/scene/model.c @@ -0,0 +1,45 @@ +#include "model_impl.h" + +#include + +#include "scene_memory.h" + +#include + +Model* gfx_make_model(const SceneNode* root) { + assert(root); + + Model* model = mem_alloc_model(); + model->root = mem_get_node_index(root); + return model; +} + +void gfx_del_model(Model** model) { + assert(model); + + if (*model) { + SceneNode* root = mem_get_node((*model)->root); + gfx_destroy_node(&root); + *model = 0; + } +} + +Anima* gfx_get_model_anima(Model* model) { + assert(model); + + SceneNode* root = mem_get_node(model->root); + if (gfx_get_node_type(root) == AnimaNode) { + return gfx_get_node_anima_mut(root); + } else { + return 0; + } +} + +const SceneNode* gfx_get_model_root(const Model* model) { + assert(model); + return mem_get_node(model->root); +} + +SceneNode* gfx_get_model_root_mut(Model* model) { + return (SceneNode*)gfx_get_model_root(model); +} diff --git a/src/scene/model_impl.h b/src/scene/model_impl.h new file mode 100644 index 0000000..a99d32c --- /dev/null +++ b/src/scene/model_impl.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "scene_memory.h" + +/// Model. +typedef struct Model { + node_idx root; + node_idx parent; // Parent SceneNode. +} Model; + +/// Create a new model. +Model* gfx_make_model(const SceneNode* root); + +/// Destroy the model. +void gfx_del_model(Model**); diff --git a/src/scene/node.c b/src/scene/node.c new file mode 100644 index 0000000..67ce93c --- /dev/null +++ b/src/scene/node.c @@ -0,0 +1,409 @@ +#include "node_impl.h" + +#include "animation_impl.h" +#include "camera_impl.h" +#include "light_impl.h" +#include "model_impl.h" +#include "object_impl.h" +#include "scene_graph.h" +#include "scene_memory.h" + +#include "gfx_assert.h" + +#include +#include + +static void scene_node_make(SceneNode* node) { + assert(node); + node->type = LogicalNode; + node->transform = mat4_id(); +} + +SceneNode* gfx_make_node() { + SceneNode* node = mem_alloc_node(); + scene_node_make(node); + return node; +} + +SceneNode* gfx_make_anima_node(Anima* anima) { + assert(anima); + SceneNode* node = gfx_make_node(); + node->type = AnimaNode; + node->anima = mem_get_anima_index(anima); + anima->parent = mem_get_node_index(node); + return node; +} + +SceneNode* gfx_make_camera_node(SceneCamera* camera) { + assert(camera); + SceneNode* node = gfx_make_node(); + node->type = CameraNode; + node->camera = mem_get_camera_index(camera); + camera->parent = mem_get_node_index(node); + return node; +} + +SceneNode* gfx_make_light_node(Light* light) { + assert(light); + SceneNode* node = gfx_make_node(); + node->type = LightNode; + node->light = mem_get_light_index(light); + light->parent = mem_get_node_index(node); + return node; +} + +SceneNode* gfx_make_model_node(Model* model) { + assert(model); + SceneNode* node = gfx_make_node(); + node->type = ModelNode; + node->model = mem_get_model_index(model); + model->parent = mem_get_node_index(node); + return node; +} + +SceneNode* gfx_make_object_node(SceneObject* object) { + assert(object); + SceneNode* node = gfx_make_node(); + node->type = ObjectNode; + node->object = mem_get_object_index(object); + object->parent = mem_get_node_index(node); + return node; +} + +/// Frees the node's resource. +static void free_node_resource(SceneNode* node) { + assert(node); + + // Set the resource's parent node back to 0 to avoid a recursive call into + // gfx_del_node(). + switch (node->type) { + case AnimaNode: { + Anima* anima = mem_get_anima(node->anima); + anima->parent.val = 0; + gfx_destroy_anima(&anima); + return; + } + case CameraNode: { + SceneCamera* camera = mem_get_camera(node->camera); + camera->parent.val = 0; + gfx_destroy_camera(&camera); + return; + } + case LightNode: { + Light* light = mem_get_light(node->light); + light->parent.val = 0; + gfx_destroy_light(&light); + return; + } + case ModelNode: { + return; // Model data is owned by the asset cache. + } + case ObjectNode: { + SceneObject* object = mem_get_object(node->object); + object->parent.val = 0; + gfx_destroy_object(&object); + return; + } + case LogicalNode: + return; // Logical nodes have no resource. + } + FAIL("unhandled node type"); +} + +void gfx_construct_anima_node(SceneNode* node, Anima* anima) { + assert(node); + assert(anima); + free_node_resource(node); + node->type = AnimaNode; + node->anima = mem_get_anima_index(anima); + anima->parent = mem_get_node_index(node); +} + +void gfx_construct_camera_node(SceneNode* node, SceneCamera* camera) { + assert(node); + assert(camera); + free_node_resource(node); + node->type = CameraNode; + node->camera = mem_get_camera_index(camera); + camera->parent = mem_get_node_index(node); +} + +// TODO: Add a common helper function between each gfx_make_xyz_node() and +// gfx_construct_xyz_node() pair. +void gfx_construct_light_node(SceneNode* node, Light* light) { + assert(node); + assert(light); + free_node_resource(node); + node->type = LightNode; + node->light = mem_get_light_index(light); + light->parent = mem_get_node_index(node); +} + +void gfx_construct_model_node(SceneNode* node, Model* model) { + assert(node); + assert(model); + free_node_resource(node); + node->type = ModelNode; + node->model = mem_get_model_index(model); + model->parent = mem_get_node_index(node); +} + +void gfx_construct_object_node(SceneNode* node, SceneObject* object) { + assert(node); + assert(object); + free_node_resource(node); + node->type = ObjectNode; + node->object = mem_get_object_index(object); + object->parent = mem_get_node_index(node); +} + +static void destroy_node_rec(SceneNode* node) { + assert(node); + + // First child. + if (node->child.val) { + destroy_node_rec(mem_get_node(node->child)); + } + + // Right sibling. + if (node->next.val) { + destroy_node_rec(mem_get_node(node->next)); + } + + free_node_resource(node); + mem_free_node(&node); +} + +void gfx_destroy_node(SceneNode** node) { + assert(node); + if (*node) { + // Since the node and the whole hierarchy under it gets destroyed, there is + // no need to individually detach every node from its hierarchy. We can + // simply detach the given node and then destroy it and its sub-hierarchy. + TREE_REMOVE(*node); + destroy_node_rec(*node); + *node = 0; + } +} + +// TODO: Think more about ownership of nodes and resources. Should this function +// even exist? +void gfx_del_node(node_idx index) { + assert(index.val); + SceneNode* node = mem_get_node(index); + assert(node); + // TODO: Should destroy children recursively? + TREE_REMOVE(node); + mem_free_node(&node); +} + +NodeType gfx_get_node_type(const SceneNode* node) { + assert(node); + return node->type; +} + +#define NODE_GET(node, field, expected_type) \ + { \ + assert(node); \ + assert(node->type == expected_type); \ + return mem_get_##field(node->field); \ + } + +const Anima* gfx_get_node_anima(const SceneNode* node) { + NODE_GET(node, anima, AnimaNode); +} + +Anima* gfx_get_node_anima_mut(SceneNode* node) { + NODE_GET(node, anima, AnimaNode); +} + +const SceneCamera* gfx_get_node_camera(const SceneNode* node) { + NODE_GET(node, camera, CameraNode); +} + +SceneCamera* gfx_get_node_camera_mut(SceneNode* node) { + NODE_GET(node, camera, CameraNode); +} + +const Light* gfx_get_node_light(const SceneNode* node) { + NODE_GET(node, light, LightNode); +} + +Light* gfx_get_node_light_mut(SceneNode* node) { + NODE_GET(node, light, LightNode); +} + +const Model* gfx_get_node_model(const SceneNode* node) { + NODE_GET(node, model, ModelNode); +} + +Model* gfx_get_node_model_mut(SceneNode* node) { + NODE_GET(node, model, ModelNode); +} + +const SceneObject* gfx_get_node_object(const SceneNode* node) { + NODE_GET(node, object, ObjectNode); +} + +SceneObject* gfx_get_node_object_mut(SceneNode* node) { + NODE_GET(node, object, ObjectNode); +} + +const SceneNode* gfx_get_node_parent(const SceneNode* node) { + assert(node); + return mem_get_node(node->parent); +} + +SceneNode* gfx_get_node_parent_mut(SceneNode* node) { + assert(node); + return mem_get_node(node->parent); +} + +const SceneNode* gfx_get_node_child(const SceneNode* node) { + assert(node); + if (node->child.val) { + return mem_get_node(node->child); + } else { + return 0; + } +} + +SceneNode* gfx_get_node_child_mut(SceneNode* node) { + return (SceneNode*)gfx_get_node_child(node); +} + +const SceneNode* gfx_get_node_sibling(const SceneNode* node) { + assert(node); + if (node->next.val) { + return mem_get_node(node->next); + } else { + return 0; + } +} + +SceneNode* gfx_get_node_sibling_mut(SceneNode* node) { + return (SceneNode*)gfx_get_node_sibling(node); +} + +mat4 gfx_get_node_transform(const SceneNode* node) { + assert(node); + return node->transform; +} + +mat4 gfx_get_node_global_transform(const SceneNode* node) { + assert(node); + mat4 transform = node->transform; + node_idx parent_index = node->parent; + while (parent_index.val != 0) { + const SceneNode* parent = mem_get_node(parent_index); + transform = mat4_mul(parent->transform, transform); + parent_index = parent->parent; + } + return transform; +} + +void gfx_set_node_parent(SceneNode* child, SceneNode* parent_node) { + assert(child); + // Parent can be null. + SET_PARENT(child, parent_node); +} + +void gfx_set_node_transform(SceneNode* node, const mat4* transform) { + assert(node); + assert(transform); + node->transform = *transform; +} + +void gfx_set_node_position(SceneNode* node, const vec3* position) { + assert(node); + assert(position); + mat4_set_v3(&node->transform, *position); +} + +void gfx_set_node_rotation(SceneNode* node, const quat* rotation) { + assert(node); + assert(rotation); + mat4_set_3x3(&node->transform, mat4_from_quat(*rotation)); +} + +void gfx_set_node_rotation_mat(SceneNode* node, const mat4* rotation) { + assert(node); + assert(rotation); + mat4_set_3x3(&node->transform, *rotation); +} + +static const char* get_node_type_str(NodeType type) { + switch (type) { + case LogicalNode: + return "LogicalNode"; + case AnimaNode: + return "AnimaNode"; + case CameraNode: + return "CameraNode"; + case LightNode: + return "LightNode"; + case ModelNode: + return "ModelNode"; + case ObjectNode: + return "ObjectNode"; + } + FAIL("Unhandled node type"); + return ""; +} + +static void log_node_hierarchy_rec(const SceneNode* node, const sstring* pad) { + assert(node); + assert(pad); + + LOGI( + "%s%s (%u)", sstring_cstr(pad), get_node_type_str(node->type), + mem_get_node_index(node).val); + + // Log the children. + if (node->child.val) { + const sstring new_pad = sstring_concat_cstr(*pad, " "); + log_node_hierarchy_rec(mem_get_node(node->child), &new_pad); + } + + // Then log the siblings. + if (node->next.val) { + log_node_hierarchy_rec(mem_get_node(node->next), pad); + } +} + +void gfx_log_node_hierarchy(const SceneNode* node) { + const sstring pad = sstring_make(""); + log_node_hierarchy_rec(node, &pad); +} + +static SceneNode* clone_scene_rec(const SceneNode* node) { + assert(node); + + SceneNode* copy = mem_alloc_node(); + *copy = *node; // Shallow clone of the node's resource. + + if (node->child.val) { + SceneNode* child = mem_get_node(node->child); + SceneNode* child_copy = clone_scene_rec(child); + copy->child = mem_get_node_index(child_copy); + child_copy->parent = mem_get_node_index(copy); + } + + if (node->next.val) { + SceneNode* next = mem_get_node(node->next); + SceneNode* next_copy = clone_scene_rec(next); + copy->next = mem_get_node_index(next_copy); + next_copy->prev = mem_get_node_index(copy); + } + + return copy; +} + +SceneNode* gfx_clone_scene_shallow(const SceneNode* node) { + assert(node); + // Must be a root node; not allowed to have siblings. + assert(!node->prev.val); + assert(!node->next.val); + + return clone_scene_rec(node); +} diff --git a/src/scene/node_impl.h b/src/scene/node_impl.h new file mode 100644 index 0000000..c79f252 --- /dev/null +++ b/src/scene/node_impl.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "types.h" + +#include +#include + +/// Scene node. +/// +/// The SceneNode owns its cameras, objects, lights and child nodes. These +/// together form a strict tree hierarchy and not a more general DAG. +typedef struct SceneNode { + NodeType type; + union { + anima_idx anima; + camera_idx camera; + light_idx light; + model_idx model; + object_idx object; + }; + mat4 transform; // Transformation for this node and its children. + node_idx parent; // Parent SceneNode. + node_idx child; // First child SceneNode. + node_idx next; // Next sibling SceneNode. + node_idx prev; // Previous sibling SceneNode. +} SceneNode; + +/// Recursively destroy a node given its index but without destroying the node +/// resources. +/// +/// The node and its children are removed from the scene graph. +/// +/// This function is for the library's internal use only. +void gfx_del_node(node_idx); + +/// Return a shallow clone of the scene rooted at the given node. +/// The given node must have no siblings (must be a root node). +SceneNode* gfx_clone_scene_shallow(const SceneNode*); diff --git a/src/scene/object.c b/src/scene/object.c new file mode 100644 index 0000000..e8e3ee6 --- /dev/null +++ b/src/scene/object.c @@ -0,0 +1,83 @@ +#include "object_impl.h" + +#include + +#include "mesh_impl.h" +#include "node_impl.h" +#include "scene_memory.h" + +#include + +static aabb3 calc_object_aabb(const SceneObject* object) { + assert(object); + + bool first = true; + aabb3 box; + + mesh_link_idx ml = object->mesh_link; + while (ml.val) { + const MeshLink* mesh_link = mem_get_mesh_link(ml); + const mesh_idx mi = mesh_link->mesh; + if (mi.val) { + const Mesh* mesh = mem_get_mesh(mi); + const aabb3 mesh_box = gfx_get_geometry_aabb(mesh->geometry); + if (first) { + box = mesh_box; + first = false; + } else { + box = aabb3_sum(box, mesh_box); + } + } + ml = mesh_link->next; + } + + return box; +} + +static void add_object_mesh(SceneObject* object, Mesh* mesh) { + assert(object); + assert(mesh); + + MeshLink* link = mem_alloc_mesh_link(); + link->mesh = mem_get_mesh_index(mesh); + link->next = object->mesh_link; + object->mesh_link = mem_get_mesh_link_index(link); +} + +SceneObject* gfx_make_object(const ObjectDesc* desc) { + assert(desc); + + SceneObject* object = mem_alloc_object(); + for (size_t i = 0; i < desc->num_meshes; ++i) { + add_object_mesh(object, desc->meshes[i]); + } + object->box = calc_object_aabb(object); + return object; +} + +void gfx_destroy_object(SceneObject** object) { + assert(object); + + if (*object) { + if ((*object)->parent.val) { + gfx_del_node((*object)->parent); + } + mem_free_object(object); + } +} + +void gfx_set_object_skeleton(SceneObject* object, const Skeleton* skeleton) { + assert(object); + assert(skeleton); + object->skeleton = mem_get_skeleton_index(skeleton); +} + +const Skeleton* gfx_get_object_skeleton(const SceneObject* object) { + assert(object); + return (object->skeleton.val == 0) ? 0 : mem_get_skeleton(object->skeleton); +} + +aabb3 gfx_get_object_aabb(const SceneObject* object) { + assert(object); + return object->box; +} diff --git a/src/scene/object_impl.h b/src/scene/object_impl.h new file mode 100644 index 0000000..88f8e31 --- /dev/null +++ b/src/scene/object_impl.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "types.h" + +#include + +typedef struct MeshLink { + mesh_idx mesh; + mesh_link_idx next; // Next MeshLink in the list. +} MeshLink; + +/// Scene object. +/// +/// A SceneObject does not own its Meshes, and they are instead shared for +/// re-use. The SceneObject consequently embeds a list of MeshLinks as opposed +/// to a list of Meshes. The MeshLinks define a list of Meshes, which can be +/// different for each SceneObject. Each SceneObject may then have a unique list +/// of Meshes, and the Meshes are re-used. +typedef struct SceneObject { + mesh_link_idx mesh_link; /// First MeshLink in the list. + skeleton_idx skeleton; /// 0 for static objects. + node_idx parent; /// Parent SceneNode. + aabb3 box; +} SceneObject; diff --git a/src/scene/scene.c b/src/scene/scene.c new file mode 100644 index 0000000..54452dd --- /dev/null +++ b/src/scene/scene.c @@ -0,0 +1,25 @@ +#include "scene_impl.h" + +#include "node_impl.h" +#include "scene_memory.h" + +#include + +Scene* gfx_make_scene(void) { + Scene* scene = mem_alloc_scene(); + scene->root = gfx_make_node(); + return scene; +} + +void gfx_destroy_scene(Scene** scene) { + assert(scene); + if (*scene) { + gfx_destroy_node(&(*scene)->root); + mem_free_scene(scene); + } +} + +SceneNode* gfx_get_scene_root(Scene* scene) { + assert(scene); + return scene->root; +} diff --git a/src/scene/scene_graph.h b/src/scene/scene_graph.h new file mode 100644 index 0000000..a26f828 --- /dev/null +++ b/src/scene/scene_graph.h @@ -0,0 +1,138 @@ +/// Functions for list manipulation. +#pragma once + +#include "scene_memory.h" + +// NOTE: SceneMemory guarantees that index 0 can be regarded as an invalid +// index. + +#define MEM_GET(INDEX) \ + _Generic((INDEX), camera_idx \ + : mem_get_camera, material_idx \ + : mem_get_material, mesh_idx \ + : mem_get_mesh, mesh_link_idx \ + : mem_get_mesh_link, node_idx \ + : mem_get_node, object_idx \ + : mem_get_object, scene_idx \ + : mem_get_scene)(INDEX) + +#define MEM_GET_INDEX(ITEM) \ + _Generic((ITEM), SceneCamera * \ + : mem_get_camera_index, Material * \ + : mem_get_material_index, Mesh * \ + : mem_get_mesh_index, MeshLink * \ + : mem_get_mesh_link_index, SceneNode * \ + : mem_get_node_index, SceneObject * \ + : mem_get_object_index, Scene * \ + : mem_get_scene_index)(ITEM) + +/// Assert the list node invariant. +/// +/// - A node does not point to itself. +#define ASSERT_LIST_NODE_INVARIANT(ITEM) \ + { \ + const gfx_idx item_idx = MEM_GET_INDEX(ITEM).val; \ + assert((ITEM)->prev.val != item_idx); \ + assert((ITEM)->next.val != item_idx); \ + } + +/// Assert the tree node invariant. +/// +/// - A node does not point to itself. +/// - The node's left and right siblings cannot be equal, unless both are 0. +/// - The node's left/right sibling cannot be its child, unless both are 0. +/// - The node's parent cannot be the node's child or sibling, unless it's 0. +/// - If the node has a parent and the node is the leftmost sibling, then the +/// parent's child is the node. +#define ASSERT_TREE_NODE_INVARIANT(ITEM) \ + { \ + const gfx_idx item_idx = MEM_GET_INDEX(ITEM).val; \ + assert((ITEM)->prev.val != item_idx); \ + assert((ITEM)->next.val != item_idx); \ + if ((ITEM)->prev.val) { \ + assert((ITEM)->prev.val != (ITEM)->next.val); \ + } \ + if ((ITEM)->child.val) { \ + assert((ITEM)->child.val != (ITEM)->prev.val); \ + assert((ITEM)->child.val != (ITEM)->next.val); \ + } \ + assert((ITEM)->parent.val != item_idx); \ + if ((ITEM)->parent.val && !(ITEM)->prev.val) { \ + assert((ITEM)->parent.val != (ITEM)->prev.val); \ + assert((ITEM)->parent.val != (ITEM)->next.val); \ + const __typeof__(ITEM) item_parent = MEM_GET((ITEM)->parent); \ + assert(item_parent->child.val == item_idx); \ + } \ + } + +/// Prepend an item to a list. +/// Modify HEAD_INDEX to equal the index of the new head. +#define LIST_PREPEND(HEAD_INDEX, ITEM) \ + (ITEM)->next = HEAD_INDEX; \ + if (HEAD_INDEX.val) { \ + __typeof__(ITEM) old_head = MEM_GET(HEAD_INDEX); \ + old_head->prev = MEM_GET_INDEX(ITEM); \ + } \ + HEAD_INDEX = MEM_GET_INDEX(ITEM); \ + ASSERT_LIST_NODE_INVARIANT(ITEM); + +/// Disconnect an item from its siblings. +#define LIST_REMOVE(ITEM) \ + if ((ITEM)->prev.val) { \ + __typeof__(ITEM) prev_sibling = MEM_GET((ITEM)->prev); \ + prev_sibling->next = (ITEM)->next; \ + } \ + if ((ITEM)->next.val) { \ + __typeof__(ITEM) next_sibling = MEM_GET((ITEM)->next); \ + next_sibling->prev = (ITEM)->prev; \ + } \ + (ITEM)->prev.val = 0; \ + (ITEM)->next.val = 0; \ + ASSERT_LIST_NODE_INVARIANT(ITEM); + +/// Set the child's parent. +/// +/// The hierarchy is a strict tree hierarchy and a parent node points to its +/// first/leftmost child only. To add a new child, the new child becomes the +/// leftmost node in the list of siblings, the one that the parent then points +/// to. +/// +/// The child is also completely disconnected from its previous hierarchy. This +/// is because siblings in a hierarchy must all point to the same parent. +#define SET_PARENT(CHILD, PARENT) \ + assert(CHILD); \ + assert(CHILD != PARENT); \ + ASSERT_TREE_NODE_INVARIANT(CHILD); \ + ASSERT_TREE_NODE_INVARIANT(PARENT); \ + TREE_REMOVE(CHILD); /* Disconnect CHILD from its previous hierarchy. */ \ + if (PARENT) { \ + LIST_PREPEND((PARENT)->child, CHILD); \ + (CHILD)->parent = MEM_GET_INDEX(PARENT); \ + } else { \ + (CHILD)->parent.val = 0; \ + } \ + ASSERT_TREE_NODE_INVARIANT(CHILD); \ + if (PARENT) { \ + ASSERT_TREE_NODE_INVARIANT(PARENT); \ + } + +/// Remove an item from its hierarchy. +/// +/// The item is disconnected from its parents and siblings. The hierarchy rooted +/// under the item remains intact. +#define TREE_REMOVE(ITEM) \ + assert(ITEM); \ + if ((ITEM)->parent.val) { \ + /* The parent points only to its first/leftmost child. If this item is */ \ + /* the leftmost sibling, then we need to rewire the parent to point to */ \ + /* the next sibling to keep the parent connected to its children. */ \ + __typeof__(ITEM) parent = MEM_GET((ITEM)->parent); \ + const __typeof__(ITEM) parent_child = MEM_GET(parent->child); \ + if (parent_child == ITEM) { \ + assert((ITEM)->prev.val == 0); \ + parent->child = (ITEM)->next; \ + } \ + } \ + (ITEM)->parent.val = 0; \ + LIST_REMOVE(ITEM); /* Disconnect ITEM from its siblings. */ \ + ASSERT_TREE_NODE_INVARIANT(ITEM); diff --git a/src/scene/scene_impl.h b/src/scene/scene_impl.h new file mode 100644 index 0000000..992f620 --- /dev/null +++ b/src/scene/scene_impl.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include "types.h" + +typedef struct SceneNode SceneNode; + +typedef struct Scene { + SceneNode* root; + scene_idx next; + scene_idx prev; +} Scene; diff --git a/src/scene/scene_memory.c b/src/scene/scene_memory.c new file mode 100644 index 0000000..85c27e7 --- /dev/null +++ b/src/scene/scene_memory.c @@ -0,0 +1,149 @@ +#include "scene_memory.h" + +#include + +#include "animation_impl.h" +#include "camera_impl.h" +#include "light_impl.h" +#include "material_impl.h" +#include "mesh_impl.h" +#include "model_impl.h" +#include "node_impl.h" +#include "object_impl.h" +#include "scene_impl.h" + +#include + +DEF_MEMPOOL(anima_pool, Anima, GFX_MAX_NUM_ANIMAS) +DEF_MEMPOOL(animation_pool, Animation, GFX_MAX_NUM_ANIMATIONS) +DEF_MEMPOOL(camera_pool, SceneCamera, GFX_MAX_NUM_CAMERAS) +DEF_MEMPOOL(light_pool, Light, GFX_MAX_NUM_LIGHTS) +DEF_MEMPOOL(material_pool, Material, GFX_MAX_NUM_MATERIALS) +DEF_MEMPOOL(mesh_pool, Mesh, GFX_MAX_NUM_MESHES) +DEF_MEMPOOL(mesh_link_pool, MeshLink, GFX_MAX_NUM_MESH_LINKS) +DEF_MEMPOOL(model_pool, Model, GFX_MAX_NUM_MODELS) +DEF_MEMPOOL(node_pool, SceneNode, GFX_MAX_NUM_NODES) +DEF_MEMPOOL(object_pool, SceneObject, GFX_MAX_NUM_OBJECTS) +DEF_MEMPOOL(scene_pool, Scene, GFX_MAX_NUM_SCENES) +DEF_MEMPOOL(skeleton_pool, Skeleton, GFX_MAX_NUM_SKELETONS) + +/// Scene memory. +/// +/// Holds memory pools for every type of scene object. +typedef struct SceneMemory { + anima_pool animas; + animation_pool animations; + camera_pool cameras; + light_pool lights; + material_pool materials; + mesh_pool meshs; // Purposeful typo to make the PLURAL() macro work. + mesh_link_pool mesh_links; + model_pool models; + node_pool nodes; + object_pool objects; + scene_pool scenes; + skeleton_pool skeletons; +} SceneMemory; + +static SceneMemory mem; + +#define ALLOC_DUMMY(POOL) \ + { \ + const void* object = mempool_alloc(POOL); \ + assert(mempool_get_block_index(POOL, object) == 0); \ + } + +#define PLURAL(name) name##s +#define MEM_FIELD(name) mem.PLURAL(name) + +void scene_mem_init() { + mempool_make(&mem.animas); + mempool_make(&mem.animations); + mempool_make(&mem.cameras); + mempool_make(&mem.lights); + mempool_make(&mem.materials); + mempool_make(&mem.meshs); + mempool_make(&mem.mesh_links); + mempool_make(&mem.models); + mempool_make(&mem.nodes); + mempool_make(&mem.objects); + mempool_make(&mem.scenes); + mempool_make(&mem.skeletons); + + // Allocate dummy objects at index 0 to guarantee that no objects allocated by + // the caller map to index 0. + ALLOC_DUMMY(&mem.animas); + ALLOC_DUMMY(&mem.animations); + ALLOC_DUMMY(&mem.cameras); + ALLOC_DUMMY(&mem.lights); + ALLOC_DUMMY(&mem.materials); + ALLOC_DUMMY(&mem.meshs); + ALLOC_DUMMY(&mem.mesh_links); + ALLOC_DUMMY(&mem.models); + ALLOC_DUMMY(&mem.nodes); + ALLOC_DUMMY(&mem.objects); + ALLOC_DUMMY(&mem.scenes); + ALLOC_DUMMY(&mem.skeletons); +} + +void scene_mem_destroy() { + // NOTE: the dummy objects are not constructed, so the destruction code below + // always skips index 0. (I don't really like the conditional inside the loop, + // but this gets the job done without having to specialize the loop macro.) +#define DESTROY(name) \ + mempool_foreach(&MEM_FIELD(name), obj, { \ + if (i > 0) { \ + gfx_destroy_##name(&obj); \ + } \ + }) + + // Models contain scene elements. Destruction is handled by the remainder of + // scene destructionb elow. + // + // First destroy the scenes. This will recursively destroy the scene's nodes + // and their objects and avoid a double-free when we then destroy any stray + // scene elements. + DESTROY(scene); + // Then delete stray nodes. This will delete their children nodes and + // resource. + DESTROY(node); + // Destroy remaining scene elements. + DESTROY(anima); + // Animations are owned by animas and do not have a destructor. + DESTROY(camera); + DESTROY(light); + DESTROY(material); + DESTROY(mesh); + // Mesh links don't have a destructor. + DESTROY(object); + // Skeletons are owned by animas and do not have a destructor. +} + +#define DEF_MEMORY(name, type) \ + /* xyz* mem_alloc_xyz(); */ \ + type* mem_alloc_##name() { return mempool_alloc(&MEM_FIELD(name)); } \ + /* void mem_free_xyz(xyz**); */ \ + void mem_free_##name(type** obj) { mempool_free(&MEM_FIELD(name), obj); } \ + /* xyz* mem_get_xyz(xyz_idx); */ \ + type* mem_get_##name(NAMED_INDEX(name) index) { \ + assert(index.val != 0); /* 0 is the dummy allocation. */ \ + return mempool_get_block(&MEM_FIELD(name), index.val); \ + } \ + /* xyz_idx mem_get_xyz_index(const xyz*); */ \ + NAMED_INDEX(name) mem_get_##name##_index(const type* obj) { \ + return (NAMED_INDEX(name)){ \ + .val = mempool_get_block_index(&MEM_FIELD(name), obj)}; \ + } + +DEF_MEMORY(anima, Anima) +DEF_MEMORY(animation, Animation) +DEF_MEMORY(camera, SceneCamera) +DEF_MEMORY(light, Light) +DEF_MEMORY(material, Material) +DEF_MEMORY(mesh, Mesh) +DEF_MEMORY(mesh_link, MeshLink) +DEF_MEMORY(model, Model) +DEF_MEMORY(node, SceneNode) +DEF_MEMORY(object, SceneObject) +DEF_MEMORY(scene, Scene) +DEF_MEMORY(skeleton, Skeleton) diff --git a/src/scene/scene_memory.h b/src/scene/scene_memory.h new file mode 100644 index 0000000..d175cba --- /dev/null +++ b/src/scene/scene_memory.h @@ -0,0 +1,39 @@ +/// Memory management of scene objects. +#pragma once + +#include "types.h" + +/// Initialize scene memory. +/// +/// The scene memory guarantees that every object maps to an index different +/// than 0. This way, 0 can be used as a special index to denote "no value". +void scene_mem_init(); + +/// Destroy the scene memory and all allocated objects. +void scene_mem_destroy(); + +#define NAMED_INDEX(name) name##_idx + +#define DECL_MEMORY(name, type) \ + typedef struct type type; \ + /* xyz* mem_alloc_xyz() */ \ + type* mem_alloc_##name(); \ + /* mem_free_xyz(xyz**) */ \ + void mem_free_##name(type**); \ + /* xyz* mem_get_xyz(xyz_idx); */ \ + type* mem_get_##name(NAMED_INDEX(name)); \ + /* xyz_idx mem_get_xyz_index(const xyz*); */ \ + NAMED_INDEX(name) mem_get_##name##_index(const type*); + +DECL_MEMORY(anima, Anima) +DECL_MEMORY(animation, Animation) +DECL_MEMORY(camera, SceneCamera) +DECL_MEMORY(light, Light) +DECL_MEMORY(material, Material) +DECL_MEMORY(mesh, Mesh) +DECL_MEMORY(mesh_link, MeshLink) +DECL_MEMORY(model, Model) +DECL_MEMORY(node, SceneNode) +DECL_MEMORY(object, SceneObject) +DECL_MEMORY(scene, Scene) +DECL_MEMORY(skeleton, Skeleton) diff --git a/src/scene/types.h b/src/scene/types.h new file mode 100644 index 0000000..d0ffc41 --- /dev/null +++ b/src/scene/types.h @@ -0,0 +1,24 @@ +/// Strongly-typed indices for every kind of scene node resource. +#pragma once + +#include + +typedef uint16_t gfx_idx; + +#define DEF_STRONG_INDEX(TYPE_NAME, IDX_TYPE) \ + typedef struct TYPE_NAME##_idx { \ + IDX_TYPE val; \ + } TYPE_NAME##_idx; + +DEF_STRONG_INDEX(anima, gfx_idx) +DEF_STRONG_INDEX(animation, gfx_idx) +DEF_STRONG_INDEX(camera, gfx_idx) +DEF_STRONG_INDEX(light, gfx_idx) +DEF_STRONG_INDEX(material, gfx_idx) +DEF_STRONG_INDEX(mesh, gfx_idx) +DEF_STRONG_INDEX(mesh_link, gfx_idx) +DEF_STRONG_INDEX(model, gfx_idx) +DEF_STRONG_INDEX(node, gfx_idx) +DEF_STRONG_INDEX(object, gfx_idx) +DEF_STRONG_INDEX(scene, gfx_idx) +DEF_STRONG_INDEX(skeleton, gfx_idx) diff --git a/src/util/geometry.c b/src/util/geometry.c new file mode 100644 index 0000000..afe0109 --- /dev/null +++ b/src/util/geometry.c @@ -0,0 +1,44 @@ +#include + +#include + +static void make_quad_11_positions(vec2 positions[4]) { + positions[0] = vec2_make(-1, +1); + positions[1] = vec2_make(-1, -1); + positions[2] = vec2_make(+1, +1); + positions[3] = vec2_make(+1, -1); +} + +static void make_quad_01_positions(vec2 positions[4]) { + positions[0] = vec2_make(0, 0); + positions[1] = vec2_make(1, 0); + positions[2] = vec2_make(1, 1); + positions[3] = vec2_make(0, 1); +} + +static GeometryDesc make_quad_desc(vec2 positions[4]) { + GeometryDesc desc = (GeometryDesc){0}; + desc.positions2d.data = positions; + desc.positions2d.size_bytes = 4 * sizeof(vec2); + desc.num_verts = 4; + desc.type = TriangleStrip; + return desc; +} + +Geometry* gfx_make_quad_11(GfxCore* gfxcore) { + assert(gfxcore); + + vec2 positions[4]; + make_quad_11_positions(positions); + const GeometryDesc geometry_desc = make_quad_desc(positions); + return gfx_make_geometry(gfxcore, &geometry_desc); +} + +Geometry* gfx_make_quad_01(GfxCore* gfxcore) { + assert(gfxcore); + + vec2 positions[4]; + make_quad_01_positions(positions); + const GeometryDesc geometry_desc = make_quad_desc(positions); + return gfx_make_geometry(gfxcore, &geometry_desc); +} diff --git a/src/util/ibl.c b/src/util/ibl.c new file mode 100644 index 0000000..5a79990 --- /dev/null +++ b/src/util/ibl.c @@ -0,0 +1,328 @@ +#include + +#include +#include +#include +#include + +#include +#include + +typedef struct IBL { + Geometry* quad; + ShaderProgram* brdf_integration_map_shader; + ShaderProgram* irradiance_map_shader; + ShaderProgram* prefiltered_environment_map_shader; + Texture* brdf_integration_map; + FrameBuffer* framebuffer; + mat4 rotations[6]; +} IBL; + +static const CubemapFace faces[6] = { + CubemapFacePosX, // Right. + CubemapFaceNegX, // Left. + CubemapFacePosY, // Up. + CubemapFaceNegY, // Down. + CubemapFacePosZ, // Back. + CubemapFaceNegZ, // Front. +}; + +static const float flips[6] = { + -1.0f, // Right. + -1.0f, // Left. + +1.0f, // Up. + +1.0f, // Down. + -1.0f, // Back. + -1.0f, // Front. +}; + +IBL* gfx_make_ibl(GfxCore* gfxcore) { + assert(gfxcore); + + IBL* ibl = calloc(1, sizeof(IBL)); + if (!ibl) { + return 0; + } + + if (!(ibl->quad = gfx_make_quad_11(gfxcore))) { + goto cleanup; + } + + // We only need the BRDF integration once since we are caching the map, but + // compiling the shader up front may lead to fewer surprises. Not that the + // shader is fully compiled up front anyway, since the driver will typically + // defer full compilation to the first draw call. + if (!(ibl->brdf_integration_map_shader = + gfx_make_brdf_integration_map_shader(gfxcore))) { + goto cleanup; + } + + if (!(ibl->irradiance_map_shader = gfx_make_irradiance_map_shader(gfxcore))) { + goto cleanup; + } + + if (!(ibl->prefiltered_environment_map_shader = + gfx_make_prefiltered_environment_map_shader(gfxcore))) { + goto cleanup; + } + + // Create an empty framebuffer for now. Will attach the colour buffer later + // as we render the faces of the cube. + if (!(ibl->framebuffer = gfx_make_framebuffer( + gfxcore, + &(FrameBufferDesc){ + .colour = + (FrameBufferAttachment){.type = FrameBufferNoAttachment}, + .depth = (FrameBufferAttachment){ + .type = FrameBufferNoAttachment}}))) { + goto cleanup; + } + + // TODO: Debug the camera rotations. Irradiance debug output should appear + // just like the input cubemap. + + // Right. + ibl->rotations[0] = mat4_lookat( + /*position=*/vec3_make(0, 0, 0), + /*target=*/vec3_make(1, 0, 0), + /*up=*/vec3_make(0, 1, 0)); + // Left. + ibl->rotations[1] = mat4_lookat( + /*position=*/vec3_make(0, 0, 0), + /*target=*/vec3_make(-1, 0, 0), + /*up=*/vec3_make(0, 1, 0)); + // Up. + ibl->rotations[2] = mat4_lookat( + /*position=*/vec3_make(0, 0, 0), + /*target=*/vec3_make(0, 1, 0), + /*up=*/vec3_make(0, 0, 1)); + // Down. + ibl->rotations[3] = mat4_lookat( + /*position=*/vec3_make(0, 0, 0), + /*target=*/vec3_make(0, -1, 0), + /*up=*/vec3_make(0, 0, -1)); + // Back. + ibl->rotations[4] = mat4_lookat( + /*position=*/vec3_make(0, 0, 0), + /*target=*/vec3_make(0, 0, 1), + /*up=*/vec3_make(0, 1, 0)); + // Front. + ibl->rotations[5] = mat4_lookat( + /*position=*/vec3_make(0, 0, 0), + /*target=*/vec3_make(0, 0, -1), + /*up=*/vec3_make(0, 1, 0)); + + return ibl; + +cleanup: + gfx_destroy_ibl(gfxcore, &ibl); + return 0; +} + +void gfx_destroy_ibl(GfxCore* gfxcore, IBL** ibl) { + if (!ibl) { + return; + } + if ((*ibl)->quad) { + gfx_destroy_geometry(gfxcore, &(*ibl)->quad); + } + if ((*ibl)->brdf_integration_map_shader) { + gfx_destroy_shader_program(gfxcore, &(*ibl)->brdf_integration_map_shader); + } + if ((*ibl)->irradiance_map_shader) { + gfx_destroy_shader_program(gfxcore, &(*ibl)->irradiance_map_shader); + } + if ((*ibl)->prefiltered_environment_map_shader) { + gfx_destroy_shader_program( + gfxcore, &(*ibl)->prefiltered_environment_map_shader); + } + if ((*ibl)->brdf_integration_map) { + gfx_destroy_texture(gfxcore, &(*ibl)->brdf_integration_map); + } + if ((*ibl)->framebuffer) { + gfx_destroy_framebuffer(gfxcore, &(*ibl)->framebuffer); + } + free(*ibl); + *ibl = 0; +} + +Texture* gfx_make_brdf_integration_map( + IBL* ibl, GfxCore* gfxcore, int width, int height) { + assert(ibl); + assert(gfxcore); + + if (ibl->brdf_integration_map) { + return ibl->brdf_integration_map; + } + + bool success = false; + + if (!(ibl->brdf_integration_map = gfx_make_texture( + gfxcore, &(TextureDesc){ + .width = width, + .height = height, + .depth = 1, + .dimension = Texture2D, + .format = TextureRG16F, + .filtering = LinearFiltering, + .wrap = ClampToEdge, + .mipmaps = false}))) { + goto cleanup; + } + + gfx_activate_framebuffer(ibl->framebuffer); + gfx_framebuffer_set_viewport(ibl->framebuffer, 0, 0, width, height); + gfx_activate_shader_program(ibl->brdf_integration_map_shader); + if (!gfx_framebuffer_attach_colour( + ibl->framebuffer, &(FrameBufferAttachment){ + .type = FrameBufferTexture, + .texture.texture = ibl->brdf_integration_map, + .texture.mip_level = 0})) { + goto cleanup; + } + gfx_render_geometry(ibl->quad); + + success = true; + +cleanup: + gfx_deactivate_shader_program(ibl->brdf_integration_map_shader); + gfx_deactivate_framebuffer(ibl->framebuffer); + if (!success && ibl->brdf_integration_map) { + gfx_destroy_texture(gfxcore, &ibl->brdf_integration_map); + return 0; + } else { + return ibl->brdf_integration_map; + } +} + +Texture* gfx_make_irradiance_map( + IBL* ibl, GfxCore* gfxcore, const Texture* environment_map, int width, + int height) { + assert(ibl); + assert(gfxcore); + assert(environment_map); + + bool success = false; + + Texture* irradiance_map = 0; + + // TODO: Could define colour-renderable texture formats separately to make + // framebuffer creation less error-prone. Or, at the very least, validate the + // choice at runtime. + // + // Make sure to use a float colour format to avoid [0,1] clamping when the + // irradiance values are computed! + if (!(irradiance_map = gfx_make_texture( + gfxcore, &(TextureDesc){ + .width = width, + .height = height, + .depth = 1, + .dimension = TextureCubeMap, + .format = TextureR11G11B10F, + .filtering = LinearFiltering, + .mipmaps = false}))) { + goto cleanup; + } + + gfx_activate_framebuffer(ibl->framebuffer); + gfx_framebuffer_set_viewport(ibl->framebuffer, 0, 0, width, height); + gfx_activate_shader_program(ibl->irradiance_map_shader); + gfx_set_texture_uniform(ibl->irradiance_map_shader, "Sky", environment_map); + for (int i = 0; i < 6; ++i) { + if (!gfx_framebuffer_attach_colour( + ibl->framebuffer, &(FrameBufferAttachment){ + .type = FrameBufferCubemapTexture, + .cubemap.face = faces[i], + .cubemap.texture = irradiance_map})) { + goto cleanup; + } + gfx_set_float_uniform(ibl->irradiance_map_shader, "Flip", flips[i]); + gfx_set_mat4_uniform( + ibl->irradiance_map_shader, "CameraRotation", &ibl->rotations[i]); + gfx_apply_uniforms(ibl->irradiance_map_shader); + gfx_render_geometry(ibl->quad); + } + + success = true; + +cleanup: + gfx_deactivate_shader_program(ibl->irradiance_map_shader); + gfx_deactivate_framebuffer(ibl->framebuffer); + if (!success && irradiance_map) { + gfx_destroy_texture(gfxcore, &irradiance_map); + return 0; + } else { + return irradiance_map; + } +} + +Texture* gfx_make_prefiltered_environment_map( + IBL* ibl, GfxCore* gfxcore, const Texture* environment_map, int width, + int height, int* max_mip_level) { + assert(ibl); + assert(gfxcore); + assert(environment_map); + assert(max_mip_level); + + bool success = false; + + Texture* prefiltered_env_map = 0; + + if (!(prefiltered_env_map = gfx_make_texture( + gfxcore, &(TextureDesc){ + .width = width, + .height = height, + .depth = 1, + .dimension = TextureCubeMap, + .format = TextureR11G11B10F, + .filtering = LinearFiltering, + .mipmaps = true}))) { + goto cleanup; + } + + gfx_activate_framebuffer(ibl->framebuffer); + gfx_activate_shader_program(ibl->prefiltered_environment_map_shader); + gfx_set_texture_uniform( + ibl->prefiltered_environment_map_shader, "Sky", environment_map); + const int max_mip = (int)(rlog2(min(width, height))); + for (int mip = 0; mip <= max_mip; ++mip) { + const int mip_width = width >> mip; + const int mip_height = height >> mip; + const float roughness = (float)mip / (float)(max_mip); + gfx_framebuffer_set_viewport(ibl->framebuffer, 0, 0, mip_width, mip_height); + gfx_set_float_uniform( + ibl->prefiltered_environment_map_shader, "Roughness", roughness); + + for (int i = 0; i < 6; ++i) { + if (!gfx_framebuffer_attach_colour( + ibl->framebuffer, &(FrameBufferAttachment){ + .type = FrameBufferCubemapTexture, + .cubemap.face = faces[i], + .cubemap.mip_level = mip, + .cubemap.texture = prefiltered_env_map})) { + goto cleanup; + } + gfx_set_float_uniform( + ibl->prefiltered_environment_map_shader, "Flip", flips[i]); + gfx_set_mat4_uniform( + ibl->prefiltered_environment_map_shader, "CameraRotation", + &ibl->rotations[i]); + gfx_apply_uniforms(ibl->prefiltered_environment_map_shader); + gfx_render_geometry(ibl->quad); + } + } + + *max_mip_level = max_mip; + + success = true; + +cleanup: + gfx_deactivate_shader_program(ibl->prefiltered_environment_map_shader); + gfx_deactivate_framebuffer(ibl->framebuffer); + if (!success && prefiltered_env_map) { + gfx_destroy_texture(gfxcore, &prefiltered_env_map); + return 0; + } else { + return prefiltered_env_map; + } +} diff --git a/src/util/shader.c b/src/util/shader.c new file mode 100644 index 0000000..f5c22cc --- /dev/null +++ b/src/util/shader.c @@ -0,0 +1,136 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static ShaderProgram* make_shader_program( + GfxCore* gfxcore, const char* vert_source, const char* frag_source, + const ShaderCompilerDefine* defines, size_t num_defines) { + assert(gfxcore); + assert(vert_source); + assert(frag_source); + + Shader* vert = 0; + Shader* frag = 0; + + ShaderDesc vertex_shader_desc = { + .code = vert_source, .type = VertexShader, .num_defines = num_defines}; + ShaderDesc fragment_shader_desc = { + .code = frag_source, .type = FragmentShader, .num_defines = num_defines}; + if (num_defines > 0) { + memcpy( + vertex_shader_desc.defines, defines, + num_defines * sizeof(ShaderCompilerDefine)); + memcpy( + fragment_shader_desc.defines, defines, + num_defines * sizeof(ShaderCompilerDefine)); + } + vert = gfx_make_shader(gfxcore, &vertex_shader_desc); + if (!vert) { + goto cleanup; + } + frag = gfx_make_shader(gfxcore, &fragment_shader_desc); + if (!frag) { + goto cleanup; + } + + ShaderProgramDesc shader_program_desc = { + .vertex_shader = vert, .fragment_shader = frag}; + ShaderProgram* prog = gfx_make_shader_program(gfxcore, &shader_program_desc); + if (!prog) { + goto cleanup; + } + return prog; + +cleanup: + if (vert) { + gfx_destroy_shader(gfxcore, &vert); + } + if (frag) { + gfx_destroy_shader(gfxcore, &frag); + } + return 0; +} + +ShaderProgram* gfx_make_brdf_integration_map_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, quad_vert, brdf_integration_map_frag, 0, 0); +} + +ShaderProgram* gfx_make_cook_torrance_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, cook_torrance_vert, cook_torrance_frag, 0, 0); +} + +ShaderProgram* gfx_make_cook_torrance_shader_perm( + GfxCore* gfxcore, const ShaderCompilerDefine* defines, size_t num_defines) { + return make_shader_program( + gfxcore, cook_torrance_vert, cook_torrance_frag, defines, num_defines); +} + +ShaderProgram* gfx_make_immediate_mode_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, immediate_mode_vert, immediate_mode_frag, 0, 0); +} + +ShaderProgram* gfx_make_irradiance_map_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, cubemap_filtering_vert, irradiance_map_frag, 0, 0); +} + +ShaderProgram* gfx_make_prefiltered_environment_map_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, cubemap_filtering_vert, prefiltered_environment_map_frag, 0, 0); +} + +ShaderProgram* gfx_make_debug3d_shader(GfxCore* gfxcore) { + return make_shader_program(gfxcore, debug3d_vert, debug3d_frag, 0, 0); +} + +ShaderProgram* gfx_make_skyquad_shader(GfxCore* gfxcore) { + return make_shader_program(gfxcore, skyquad_vert, skyquad_frag, 0, 0); +} + +ShaderProgram* gfx_make_view_normal_mapped_normals_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, view_normal_mapped_normals_vert, view_normal_mapped_normals_frag, + 0, 0); +} + +ShaderProgram* gfx_make_view_normals_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, view_normals_vert, view_normals_frag, 0, 0); +} + +ShaderProgram* gfx_make_view_tangents_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, view_tangents_vert, view_tangents_frag, 0, 0); +} + +ShaderProgram* gfx_make_view_texture_shader(GfxCore* gfxcore) { + return make_shader_program( + gfxcore, view_texture_vert, view_texture_frag, 0, 0); +} diff --git a/src/util/skyquad.c b/src/util/skyquad.c new file mode 100644 index 0000000..08fa044 --- /dev/null +++ b/src/util/skyquad.c @@ -0,0 +1,161 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +SceneObject* gfx_make_skyquad(GfxCore* gfxcore, const Texture* texture) { + assert(gfxcore); + assert(texture); + + ShaderProgram* shader = 0; + Geometry* geometry = 0; + Material* material = 0; + Mesh* mesh = 0; + SceneObject* object = 0; + + shader = gfx_make_skyquad_shader(gfxcore); + if (!shader) { + goto cleanup; + } + + geometry = gfx_make_quad_11(gfxcore); + if (!geometry) { + goto cleanup; + } + + MaterialDesc material_desc = (MaterialDesc){0}; + material_desc.uniforms[0] = (ShaderUniform){ + .type = UniformTexture, + .value.texture = texture, + .name = sstring_make("Skyquad")}; + material_desc.num_uniforms = 1; + material = gfx_make_material(&material_desc); + if (!material) { + goto cleanup; + } + + MeshDesc mesh_desc = (MeshDesc){0}; + mesh_desc.geometry = geometry; + mesh_desc.material = material; + mesh_desc.shader = shader; + mesh = gfx_make_mesh(&mesh_desc); + if (!mesh) { + goto cleanup; + } + + object = gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}}); + if (!object) { + goto cleanup; + } + + return object; + +cleanup: + if (shader) { + gfx_destroy_shader_program(gfxcore, &shader); + } + if (geometry) { + gfx_destroy_geometry(gfxcore, &geometry); + } + if (material) { + gfx_destroy_material(&material); + } + if (mesh) { + gfx_destroy_mesh(&mesh); + } + if (object) { + gfx_destroy_object(&object); + } + return false; +} + +/// Create an environment light node. +static SceneNode* make_environment_light( + SceneNode* root, const Texture* environment_map) { + assert(root); + assert(environment_map); + + Light* light = 0; + SceneNode* light_node = 0; + + light = gfx_make_light(&(LightDesc){ + .type = EnvironmentLightType, + .light = {(EnvironmentLightDesc){.environment_map = environment_map}}}); + if (!light) { + goto cleanup; + } + + light_node = gfx_make_light_node(light); + if (!light_node) { + goto cleanup; + } + gfx_set_node_parent(light_node, root); + + return light_node; + +cleanup: + if (light) { + gfx_destroy_light(&light); + } + if (light_node) { + gfx_destroy_node(&light_node); + } + return 0; +} + +SceneNode* gfx_setup_skyquad( + GfxCore* gfxcore, SceneNode* root, const Texture* environment_map) { + assert(gfxcore); + assert(root); + assert(environment_map); + + SceneObject* skyquad_object = 0; + SceneNode* object_node = 0; + SceneNode* light_node = 0; + + // Create the skyquad object. + skyquad_object = gfx_make_skyquad(gfxcore, environment_map); + if (!skyquad_object) { + goto cleanup; + } + + // Create an object node to render the skyquad in the background. + object_node = gfx_make_object_node(skyquad_object); + if (!object_node) { + goto cleanup; + } + gfx_set_node_parent(object_node, root); + + // Create an environment light node under which to root objects affected by + // the skyquad. + light_node = make_environment_light(root, environment_map); + if (!light_node) { + goto cleanup; + } + + return light_node; + +cleanup: + if (skyquad_object) { + gfx_destroy_object(&skyquad_object); + } + if (object_node) { + gfx_destroy_node(&object_node); + } + if (light_node) { + gfx_destroy_node(&light_node); + } + return 0; +} -- cgit v1.2.3