diff --git a/Assets/Shaders/glsl/pbr.fs b/Assets/Shaders/glsl/pbr.fs index 6bbaeb73..24f43ad6 100644 --- a/Assets/Shaders/glsl/pbr.fs +++ b/Assets/Shaders/glsl/pbr.fs @@ -96,7 +96,7 @@ void main() { vec3 albedo = pow(material.hasBaseColorMap ? texture(material.baseColorMap, ftex_coords).rgb : material.baseColor, vec3(2.2)); float metallic = material.hasMetallicMap ? texture(material.metallicMap, ftex_coords).b : material.metallic; float roughness = material.hasRoughnessMap ? texture(material.roughnessMap, ftex_coords).g : material.roughness; - float ao = material.hasAoMap ? texture(material.aoMap, ftex_coords).length() : material.ao; + float ao = material.hasAoMap ? texture(material.aoMap, ftex_coords).r : material.ao; vec3 N = getNormalFromMap(); vec3 V = normalize(fview - fposition); @@ -109,11 +109,19 @@ void main() { // reflectance equation vec3 Lo = vec3(0.0); for(int i = 0; i < light_count; ++i) { - // calculate per-light radiance - vec3 L = normalize(lights[i].position - fposition); + // calculate per-light radiance. Directional lights (type 1) come from a fixed + // direction with no distance attenuation; point/spot use position + falloff. + vec3 L; + float attenuation; + if(lights[i].type == 1) { + L = normalize(-lights[i].rotation); + attenuation = 1.0; + } else { + L = normalize(lights[i].position - fposition); + float distance = length(lights[i].position - fposition); + attenuation = 1.0 / (1 + lights[i].distance_dropoff * distance * distance); + } vec3 H = normalize(V + L); - float distance = length(lights[i].position - fposition); - float attenuation = 1.0 / (1 + lights[i].distance_dropoff * distance * distance); vec3 radiance = lights[i].color * attenuation; // Cook-Torrance BRDF diff --git a/Assets/Shaders/glsl/phong.fs b/Assets/Shaders/glsl/phong.fs index 9dab910c..162e7b64 100644 --- a/Assets/Shaders/glsl/phong.fs +++ b/Assets/Shaders/glsl/phong.fs @@ -22,6 +22,8 @@ struct Material { uniform Material material; in vec3 fnormal; +in vec3 ftangent; +in vec3 fbitangent; in vec3 fposition; in vec3 fview; in vec2 ftex_coords; @@ -64,16 +66,27 @@ vec4 applyLight(Light light) { } else if(light.type == 2) { //TODO: Spot lights } + // Non-void function must return on every path (spot/unknown types fell off the end -> UB). + return vec4(0.0); } -vec3 colorToNormal(vec3 color) { - return normalize(vec3(color.x * 2 - 1, color.y * 2 - 1, color.z * 2 - 1)); +// Transform the tangent-space normal-map sample into world space using a proper TBN basis +// (Gram-Schmidt re-orthogonalized), matching pbr.fs. The previous math was not a TBN +// transform at all. +vec3 getNormalFromMap() { + vec3 tangentNormal = texture(material.normal_map, ftex_coords).xyz * 2.0 - 1.0; + vec3 N = normalize(fnormal); + vec3 T = normalize(ftangent); + T = normalize(T - dot(T, N) * N); + vec3 B = cross(N, T); + mat3 TBN = mat3(T, B, N); + return normalize(TBN * tangentNormal); } void main() { if(material.use_normal_map) { - normal = normalize(fnormal + (fnormal * colorToNormal(texture(material.normal_map, ftex_coords).xyz))); + normal = getNormalFromMap(); } else { normal = fnormal; } diff --git a/Assets/Shaders/glsl/picking.fs b/Assets/Shaders/glsl/picking.fs index 74642cb8..950e88f2 100644 --- a/Assets/Shaders/glsl/picking.fs +++ b/Assets/Shaders/glsl/picking.fs @@ -5,5 +5,11 @@ uniform int objectID; out vec4 frag_color; void main() { - frag_color = vec4((objectID & 0xFF) / 255.0, ((objectID & 0xFF00) >> 8) / 255, ((objectID & 0xFF0000) >> 16 / 255), 1.0); + // Encode the 24-bit object id across R,G,B (decoded as R + G<<8 + B<<16). The old + // code used integer division (/255) and had an operator-precedence bug (>> 16 / 255), + // so ids >= 256 could not round-trip. + frag_color = vec4(float(objectID & 0xFF) / 255.0, + float((objectID >> 8) & 0xFF) / 255.0, + float((objectID >> 16) & 0xFF) / 255.0, + 1.0); } \ No newline at end of file diff --git a/Assets/Shaders/glsl/picking.vs b/Assets/Shaders/glsl/picking.vs new file mode 100644 index 00000000..d934db04 --- /dev/null +++ b/Assets/Shaders/glsl/picking.vs @@ -0,0 +1,32 @@ +#version 420 core + +#include "vert_uniforms.glsl" +#define MAX_BONES 100 +#define MAX_BONE_INFLUENCE 4 + +layout (location = 0) in vec3 vertex; +layout (location = 5) in ivec4 bone_ids; +layout (location = 6) in vec4 bone_weights; + +// Picking is not instanced, so the model matrix is a uniform (skinning.vs takes it as a +// per-instance vertex attribute). Skinning is applied with the same logic as skinning.vs +// so an animated model is picked at its current pose, matching what is rendered on screen. +uniform mat4 model; +uniform mat4 bonesTransformMatrices[MAX_BONES]; + +void main() { + vec4 totalPosition = vec4(0.0); + if (bone_ids == ivec4(-1)) { + totalPosition = vec4(vertex, 1.0); + } else { + for (int i = 0; i < MAX_BONE_INFLUENCE; i++) { + if (bone_ids[i] == -1) continue; + if (bone_ids[i] >= MAX_BONES) { + totalPosition = vec4(vertex, 1.0); + break; + } + totalPosition += bonesTransformMatrices[bone_ids[i]] * vec4(vertex, 1.0) * bone_weights[i]; + } + } + gl_Position = uProjection * uView * model * totalPosition; +} diff --git a/Assets/Shaders/glsl/skybox.vs b/Assets/Shaders/glsl/skybox.vs index 0817ad8f..34b22f77 100644 --- a/Assets/Shaders/glsl/skybox.vs +++ b/Assets/Shaders/glsl/skybox.vs @@ -11,5 +11,9 @@ out vec3 TexCoords; void main() { TexCoords = aPos; - gl_Position = uProjection * uView * vec4(aPos, 1.0); + // Strip translation from the view so the skybox stays centered on the camera, and use + // the z=w trick so its depth is always 1.0 -- drawn behind everything with GL_LEQUAL. + mat4 rotView = mat4(mat3(uView)); + vec4 clipPos = uProjection * rotView * vec4(aPos, 1.0); + gl_Position = clipPos.xyww; } \ No newline at end of file diff --git a/Assets/Shaders/picking.shader.json b/Assets/Shaders/picking.shader.json index 606a78aa..51cd7488 100644 --- a/Assets/Shaders/picking.shader.json +++ b/Assets/Shaders/picking.shader.json @@ -1,7 +1,7 @@ [ { "stage": "vertex", - "source": "glsl/skinning.vs" + "source": "glsl/picking.vs" }, { "stage": "fragment", diff --git a/ICE/Assets/include/AssetBank.h b/ICE/Assets/include/AssetBank.h index 4fed8d15..e6ff08a4 100644 --- a/ICE/Assets/include/AssetBank.h +++ b/ICE/Assets/include/AssetBank.h @@ -73,6 +73,11 @@ class AssetBank { } bool addAsset(const AssetPath& path, const std::shared_ptr& asset) { + // Reject a failed load (loaders now return nullptr on error) instead of inserting a + // null asset that would later be dereferenced. + if (asset == nullptr) { + return false; + } if (!nameMapping.contains(path) && !resources.contains(nextUID)) { resources.try_emplace(nextUID, AssetBankEntry{path, asset}); nameMapping.try_emplace(path, nextUID); diff --git a/ICE/Assets/include/AssetPath.h b/ICE/Assets/include/AssetPath.h index 66c122cd..db9c15d7 100644 --- a/ICE/Assets/include/AssetPath.h +++ b/ICE/Assets/include/AssetPath.h @@ -21,7 +21,9 @@ class AssetPath { std::string prefix() const; template static AssetPath WithTypePrefix(std::string path) { - return AssetPath(typenames[typeid(T)] + ASSET_PATH_SEPARATOR + path); + // .at() is read-only: operator[] inserted an empty prefix for unregistered types + // (silently producing "/name" paths) and mutated the shared static map (data race). + return AssetPath(typenames.at(typeid(T)) + ASSET_PATH_SEPARATOR + path); } bool operator==(AssetPath other) const { return (other.toString() == this->toString()); } diff --git a/ICE/Assets/src/Model.cpp b/ICE/Assets/src/Model.cpp index 6938398b..087906b2 100644 --- a/ICE/Assets/src/Model.cpp +++ b/ICE/Assets/src/Model.cpp @@ -29,8 +29,12 @@ void Model::traverse(std::vector &meshes, std::vector &mater transforms.push_back(node_transform); } + // Accumulate this node's local transform into the children's base. This only + // affects Model::traverse, which is used solely by the editor thumbnail renderer + // (a flat, scene-graph-less render) -- the live scene/animation path uses + // spawnTree + the scene graph and must NOT accumulate here. for (const auto &child_idx : node.children) { - recursive_traversal(child_idx, transform); + recursive_traversal(child_idx, transform * node.localTransform); } }; diff --git a/ICE/Assets/src/Texture.cpp b/ICE/Assets/src/Texture.cpp index d478954d..4b205972 100644 --- a/ICE/Assets/src/Texture.cpp +++ b/ICE/Assets/src/Texture.cpp @@ -25,6 +25,13 @@ Texture2D::Texture2D(void* data, int width, int height, TextureFormat fmt, bool } TextureCube::TextureCube(const std::string& path) { + // Load the equirectangular source image as RGB; OpenGLTextureCube converts it into the + // six cube faces. This used to be an empty stub, so cubemaps loaded from a path had null + // data and zero size. + int channels = 0; + data_ = getDataFromFile(path, &m_width, &m_height, &channels, STBI_rgb); + m_owns_data = true; // stb-allocated; freed in ~Texture + m_format = TextureFormat::RGB8; } TextureCube::TextureCube(void* data, int width, int height, TextureFormat fmt) { data_ = data; diff --git a/ICE/Components/include/TransformComponent.h b/ICE/Components/include/TransformComponent.h index 9c1af307..04bb5149 100644 --- a/ICE/Components/include/TransformComponent.h +++ b/ICE/Components/include/TransformComponent.h @@ -54,7 +54,7 @@ struct TransformComponent : public Component { Eigen::Vector3f getRotationEulerDeg() const { return m_rotation.toRotationMatrix().eulerAngles(0, 1, 2) * 180.0 / M_PI; } Eigen::Matrix4f getModelMatrix() const { - if (m_dirty) { + if (m_model_dirty) { m_model_matrix = Eigen::Matrix4f::Identity(); // Translation @@ -66,14 +66,18 @@ struct TransformComponent : public Component { // Scale m_model_matrix.block<3, 3>(0, 0) *= m_scale.asDiagonal(); - m_dirty = false; + m_model_dirty = false; } return m_model_matrix; } Eigen::Matrix4f getWorldMatrix() const { - if (m_dirty) { + // Separate dirty flag from the model matrix: getModelMatrix() used to clear the + // single shared flag, so calling it first left getWorldMatrix() returning a stale + // value. markDirty() sets both, and getModelMatrix() only clears the model flag. + if (m_world_dirty) { m_world_matrix = m_parent_matrix * getModelMatrix(); + m_world_dirty = false; } return m_world_matrix; } @@ -81,6 +85,7 @@ struct TransformComponent : public Component { void updateParentMatrix(const Eigen::Matrix4f& parent_matrix) { m_parent_matrix = parent_matrix; m_world_matrix = parent_matrix * getModelMatrix(); + m_world_dirty = false; m_version++; } @@ -125,7 +130,8 @@ struct TransformComponent : public Component { private: void markDirty() { - m_dirty = true; + m_model_dirty = true; + m_world_dirty = true; m_version++; } @@ -136,7 +142,8 @@ struct TransformComponent : public Component { mutable Eigen::Matrix4f m_model_matrix = Eigen::Matrix4f::Identity(); mutable Eigen::Matrix4f m_parent_matrix = Eigen::Matrix4f::Identity(); mutable Eigen::Matrix4f m_world_matrix = Eigen::Matrix4f::Identity(); - mutable bool m_dirty = true; + mutable bool m_model_dirty = true; + mutable bool m_world_dirty = true; mutable uint32_t m_version = 0; }; diff --git a/ICE/Core/include/ICEEngine.h b/ICE/Core/include/ICEEngine.h index c7482fad..7c32d14a 100644 --- a/ICE/Core/include/ICEEngine.h +++ b/ICE/Core/include/ICEEngine.h @@ -24,6 +24,9 @@ class ICEEngine { void step(); + // Duration of the last step() in seconds. + double getDeltaTime() const { return m_delta_time; } + void setupScene(const std::shared_ptr& camera_); std::shared_ptr getCamera(); @@ -64,6 +67,7 @@ class ICEEngine { std::shared_ptr project = nullptr; std::chrono::steady_clock::time_point lastFrameTime; + double m_delta_time = 0.0; EngineConfig config; }; diff --git a/ICE/Core/src/ICEEngine.cpp b/ICE/Core/src/ICEEngine.cpp index cd84d6c9..fad6e831 100644 --- a/ICE/Core/src/ICEEngine.cpp +++ b/ICE/Core/src/ICEEngine.cpp @@ -30,15 +30,23 @@ void ICEEngine::initialize(const std::shared_ptr &graphics_fact api = graphics_factory->createRendererAPI(); api->initialize(); internalFB = graphics_factory->createFramebuffer({720, 720, 1}); + // Seed the frame clock so the first step() doesn't report a huge delta (time since + // the steady_clock epoch). + lastFrameTime = std::chrono::steady_clock::now(); } void ICEEngine::step() { + // Delta time in seconds as a double: the old integer-millisecond cast truncated to 0 + // above ~1000 fps (freezing animation) and lost ~13% at 144 Hz. auto now = std::chrono::steady_clock::now(); - auto dt = std::chrono::duration_cast(now - lastFrameTime).count(); + m_delta_time = std::chrono::duration(now - lastFrameTime).count(); lastFrameTime = now; + if (!project) { + return; + } auto render_system = project->getCurrentScene()->getRegistry()->getSystem(); render_system->setTarget(m_target_fb); - project->getCurrentScene()->getRegistry()->updateSystems(dt); + project->getCurrentScene()->getRegistry()->updateSystems(m_delta_time); } void ICEEngine::setupScene(const std::shared_ptr &camera_) { diff --git a/ICE/Graphics/include/ForwardRenderer.h b/ICE/Graphics/include/ForwardRenderer.h index f0c4d41a..8fb84081 100644 --- a/ICE/Graphics/include/ForwardRenderer.h +++ b/ICE/Graphics/include/ForwardRenderer.h @@ -9,7 +9,9 @@ #include #include +#include #include +#include #include #include "Camera.h" @@ -54,8 +56,10 @@ class ForwardRenderer : public Renderer { std::vector m_drawables; std::vector m_lights; - // Instance batching storage - std::unordered_map> m_instance_batches; + // Instance batching storage, keyed by the exact (mesh, material, shader) triple so + // distinct batches can never collide into one (the old XOR-hashed uint64 key could). + using BatchKey = std::tuple; + std::map> m_instance_batches; RendererConfig config; }; diff --git a/ICE/Graphics/include/GraphicsAPI.h b/ICE/Graphics/include/GraphicsAPI.h index 258bd3eb..c76866c3 100644 --- a/ICE/Graphics/include/GraphicsAPI.h +++ b/ICE/Graphics/include/GraphicsAPI.h @@ -9,6 +9,8 @@ #include #include +#include "RenderState.h" + namespace ICE { enum GraphicsAPI { None = 0x0, @@ -29,6 +31,7 @@ class RendererAPI { virtual void finish() const = 0; virtual void setDepthTest(bool enable) const = 0; virtual void setDepthMask(bool enable) const = 0; + virtual void setDepthFunc(DepthFunc func) const = 0; virtual void setBackfaceCulling(bool enable) const = 0; virtual void checkAndLogErrors() const = 0; diff --git a/ICE/Graphics/include/InstanceData.h b/ICE/Graphics/include/InstanceData.h index b7662f9d..467200f8 100644 --- a/ICE/Graphics/include/InstanceData.h +++ b/ICE/Graphics/include/InstanceData.h @@ -5,43 +5,13 @@ #pragma once #include -#include -#include -#include - -#include "GPUMesh.h" -#include "Material.h" -#include "ShaderProgram.h" -#include "Buffers.h" namespace ICE { -// Per-instance data for instanced rendering +// Per-instance data for instanced rendering. struct InstanceData { Eigen::Matrix4f model_matrix; // Future: Add per-instance color, material index, etc. }; -// Batch of instances sharing the same mesh/material/shader -struct InstanceBatch { - std::shared_ptr mesh; - std::shared_ptr material; - std::shared_ptr shader; - std::unordered_map> textures; - std::vector instances; - - // GPU buffer for instance data (created on demand) - std::shared_ptr instance_buffer; - bool buffer_dirty = true; - - // Generate hash key for batching - uint64_t getBatchKey() const { - uint64_t hash = 0; - hash ^= reinterpret_cast(mesh.get()) + 0x9e3779b9 + (hash << 6) + (hash >> 2); - hash ^= reinterpret_cast(material.get()) + 0x9e3779b9 + (hash << 6) + (hash >> 2); - hash ^= reinterpret_cast(shader.get()) + 0x9e3779b9 + (hash << 6) + (hash >> 2); - return hash; - } -}; - } // namespace ICE diff --git a/ICE/Graphics/include/RenderCommand.h b/ICE/Graphics/include/RenderCommand.h index ecf7cfc3..39d3b7a1 100644 --- a/ICE/Graphics/include/RenderCommand.h +++ b/ICE/Graphics/include/RenderCommand.h @@ -10,6 +10,7 @@ #include "GPUMesh.h" #include "Material.h" #include "Model.h" +#include "RenderState.h" #include "ShaderProgram.h" namespace ICE { @@ -35,7 +36,10 @@ struct RenderCommand { // (e.g. the skybox) that don't set them don't read indeterminate values. bool faceCulling : 1 = true; bool depthTest : 1 = true; + bool depthWrite : 1 = true; bool is_instanced : 1 = false; + + DepthFunc depth_func = DepthFunc::Less; // Instancing support const std::vector* instance_data = nullptr; @@ -50,13 +54,18 @@ struct RenderCommand { uint64_t transparent_bit = is_transparent ? 1ULL : 0ULL; uint64_t shader_bits = (reinterpret_cast(shader) >> 3) & 0x1FFFFF; uint64_t material_bits = (reinterpret_cast(material) >> 3) & 0x1FFFFF; - uint64_t depth_bits = static_cast(depth_sq * 1000.0f) & 0x1FFFFF; - + // Clamp instead of masking: depth_sq * 1000 overflowed 21 bits past ~46 units and + // wrapped, scrambling the order. Clamped, far objects just pin at the max bucket. + uint64_t depth_raw = static_cast(depth_sq * 1000.0f); + uint64_t depth_bits = depth_raw > 0x1FFFFF ? 0x1FFFFF : depth_raw; + if (is_transparent) { - // Transparent: sort by depth (back-to-front) - sort_key = (transparent_bit << 63) | (depth_bits << 42) | (shader_bits << 21) | material_bits; + // Alpha blending needs back-to-front: invert depth so farther fragments (larger + // depth) get a smaller key and are drawn first. The old code sorted front-to-back. + uint64_t depth_far_first = 0x1FFFFF - depth_bits; + sort_key = (transparent_bit << 63) | (depth_far_first << 42) | (shader_bits << 21) | material_bits; } else { - // Opaque: sort by shader/material, then depth (front-to-back) + // Opaque: sort by shader/material to minimize state changes, then front-to-back. sort_key = (transparent_bit << 63) | (shader_bits << 42) | (material_bits << 21) | depth_bits; } } diff --git a/ICE/Graphics/include/RenderData.h b/ICE/Graphics/include/RenderData.h index 093b3a3b..8c677b8e 100644 --- a/ICE/Graphics/include/RenderData.h +++ b/ICE/Graphics/include/RenderData.h @@ -2,23 +2,24 @@ #include -static std::vector full_quad_v = { - -1.0f, -1.0f, 0.0f, // TOP LEFT - 1.0, -1.0f, 0.0f, // TOP RIGHT - -1.0f, 1.0, 0.0f, // BOTTOM LEFT - 1.0, 1.0, 0.0f, // BOTTOM RIGHT +// Fullscreen-quad geometry in NDC. `inline` gives a single shared definition across +// translation units instead of one static copy per TU. +inline const std::vector full_quad_v = { + -1.0f, -1.0f, 0.0f, // bottom-left + 1.0f, -1.0f, 0.0f, // bottom-right + -1.0f, 1.0f, 0.0f, // top-left + 1.0f, 1.0f, 0.0f, // top-right }; -static std::vector full_quad_idx = { +inline const std::vector full_quad_idx = { 0, 1, 2, 2, 1, 3 }; - -static std::vector full_quad_tx = { - 0, 0, // TOP LEFT - 1, 0, // TOP RIGHT - 0, 1, // BOTTOM LEFT - 1, 1, // BOTTOM RIGHT - 1, 0, // TOP RIGHT - 0, 1 // BOTTOM LEFT +// One UV per vertex (the quad has 4 vertices); the previous array had 6 pairs, so the +// last two were dead. +inline const std::vector full_quad_tx = { + 0, 0, // bottom-left + 1, 0, // bottom-right + 0, 1, // top-left + 1, 1, // top-right }; diff --git a/ICE/Graphics/include/RenderState.h b/ICE/Graphics/include/RenderState.h new file mode 100644 index 00000000..123c0f24 --- /dev/null +++ b/ICE/Graphics/include/RenderState.h @@ -0,0 +1,7 @@ +#pragma once + +namespace ICE { +// Depth comparison function for a draw. Less is the default; LEqual is needed by the +// skybox (its fragments sit exactly at the far plane via the z=w trick). +enum class DepthFunc { Less, LEqual }; +} // namespace ICE diff --git a/ICE/Graphics/src/ForwardRenderer.cpp b/ICE/Graphics/src/ForwardRenderer.cpp index f0cbc09e..8031e933 100644 --- a/ICE/Graphics/src/ForwardRenderer.cpp +++ b/ICE/Graphics/src/ForwardRenderer.cpp @@ -67,27 +67,29 @@ void ForwardRenderer::prepareFrame(Camera& camera) { skybox_cmd.textures = &m_skybox->textures; skybox_cmd.model_matrix = Eigen::Matrix4f::Identity(); skybox_cmd.is_instanced = false; + // Draw after opaque geometry (so it only fills background pixels) but before + // transparent. Its fragments sit at the far plane (z=w in skybox.vs), so it needs + // GL_LEQUAL and must not write depth. + skybox_cmd.depthTest = true; + skybox_cmd.depthWrite = false; + skybox_cmd.depth_func = DepthFunc::LEqual; + skybox_cmd.sort_key = 0x7FFFFFFFFFFFFFFFULL; // last among opaque (transparent bit 63 = 0) m_render_commands.push_back(skybox_cmd); } - // Instance batching: Group drawables by mesh/material/shader - std::unordered_map> instance_batches; + // Instance batching: group drawables by the exact (mesh, material, shader) triple. + std::map> instance_batches; std::vector non_instanced_drawables; // Skinned meshes, etc. - + for (const auto& drawable : m_drawables) { // Skip instancing for skinned meshes (has bones) if (!drawable.bone_matrices.empty()) { non_instanced_drawables.push_back(&drawable); continue; } - - // Create batch key (mesh + material + shader) - uint64_t batch_key = 0; - batch_key ^= reinterpret_cast(drawable.mesh.get()) + 0x9e3779b9 + (batch_key << 6) + (batch_key >> 2); - batch_key ^= reinterpret_cast(drawable.material.get()) + 0x9e3779b9 + (batch_key << 6) + (batch_key >> 2); - batch_key ^= reinterpret_cast(drawable.shader.get()) + 0x9e3779b9 + (batch_key << 6) + (batch_key >> 2); - - instance_batches[batch_key].push_back(&drawable); + + BatchKey key{drawable.mesh.get(), drawable.material.get(), drawable.shader.get()}; + instance_batches[key].push_back(&drawable); } // Convert batches to render commands @@ -124,6 +126,7 @@ void ForwardRenderer::prepareFrame(Camera& camera) { } const auto* first = batch[0]; + auto dist = (first->model_matrix.block<3, 1>(0, 3) - camera_pos).squaredNorm(); RenderCommand cmd; cmd.mesh = first->mesh.get(); cmd.material = first->material.get(); @@ -134,6 +137,7 @@ void ForwardRenderer::prepareFrame(Camera& camera) { cmd.is_instanced = true; cmd.instance_count = batch.size(); cmd.instance_data = &instance_data_vec; // Link to stored data + cmd.computeSortKey(cmd.material->isTransparent(), dist); m_render_commands.push_back(cmd); } } @@ -170,7 +174,10 @@ void ForwardRenderer::endFrame() { m_skybox.reset(); m_drawables.clear(); m_lights.clear(); +#ifndef NDEBUG + // Only drain GL errors in debug builds; skip the per-frame glGetError round-trip in release. m_api->checkAndLogErrors(); +#endif m_render_commands.clear(); } diff --git a/ICE/Graphics/src/GeometryPass.cpp b/ICE/Graphics/src/GeometryPass.cpp index 68f644d4..a7785a2a 100644 --- a/ICE/Graphics/src/GeometryPass.cpp +++ b/ICE/Graphics/src/GeometryPass.cpp @@ -25,6 +25,8 @@ void GeometryPass::execute() { m_api->setBackfaceCulling(command.faceCulling); m_api->setDepthTest(command.depthTest); + m_api->setDepthMask(command.depthWrite); + m_api->setDepthFunc(command.depth_func); if (shader != current_shader) { shader->bind(); diff --git a/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h b/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h index e46f273a..24600532 100644 --- a/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h +++ b/ICE/GraphicsAPI/OpenGL/include/OpenGLRendererAPI.h @@ -33,6 +33,8 @@ namespace ICE { void setDepthMask(bool enable) const override; + void setDepthFunc(DepthFunc func) const override; + void setBackfaceCulling(bool enable) const override; void checkAndLogErrors() const override; diff --git a/ICE/GraphicsAPI/OpenGL/src/OpenGLFramebuffer.cpp b/ICE/GraphicsAPI/OpenGL/src/OpenGLFramebuffer.cpp index ce406ece..1e32c87b 100644 --- a/ICE/GraphicsAPI/OpenGL/src/OpenGLFramebuffer.cpp +++ b/ICE/GraphicsAPI/OpenGL/src/OpenGLFramebuffer.cpp @@ -28,7 +28,9 @@ void OpenGLFramebuffer::resize(int width, int height) { glBindFramebuffer(GL_FRAMEBUFFER, uid); glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL); + // Keep the RGBA8 format from construction: the resize path used to recreate the color + // attachment as unsized GL_RGB, silently dropping the alpha channel after the first resize. + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -38,6 +40,12 @@ void OpenGLFramebuffer::resize(int width, int height) { glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depth); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + Logger::Log(Logger::FATAL, "Graphics", "Framebuffer incomplete after resize (%dx%d)", width, height); + } + // Leave this framebuffer bound: callers (e.g. the editor picking pass) bind() then + // resize() and expect to keep rendering into it. } int OpenGLFramebuffer::getTexture() { @@ -50,7 +58,7 @@ OpenGLFramebuffer::OpenGLFramebuffer(FrameBufferFormat fmt) : Framebuffer(fmt) { glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fmt.width, fmt.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, fmt.width, fmt.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -81,9 +89,11 @@ void OpenGLFramebuffer::bindAttachment(int slot) const { } Eigen::Vector4i OpenGLFramebuffer::readPixel(int x, int y) { - glFlush(); + // Bind this framebuffer's read target explicitly rather than assuming it is current. + glBindFramebuffer(GL_READ_FRAMEBUFFER, uid); + glReadBuffer(GL_COLOR_ATTACHMENT0); glFinish(); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glPixelStorei(GL_PACK_ALIGNMENT, 1); unsigned char data[4]; auto pixels = Eigen::Vector4i(); glReadPixels(x, format.height - y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, data); @@ -91,6 +101,7 @@ Eigen::Vector4i OpenGLFramebuffer::readPixel(int x, int y) { pixels.y() = data[1]; pixels.z() = data[2]; pixels.w() = data[3]; + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); return pixels; } } // namespace ICE \ No newline at end of file diff --git a/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp b/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp index 27b1df70..e26a0c54 100644 --- a/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp +++ b/ICE/GraphicsAPI/OpenGL/src/OpenGLRendererAPI.cpp @@ -58,6 +58,10 @@ void OpenGLRendererAPI::setDepthMask(bool enable) const { glDepthMask(enable ? GL_TRUE : GL_FALSE); } +void OpenGLRendererAPI::setDepthFunc(DepthFunc func) const { + glDepthFunc(func == DepthFunc::LEqual ? GL_LEQUAL : GL_LESS); +} + void OpenGLRendererAPI::setBackfaceCulling(bool enable) const { if (enable) { glEnable(GL_CULL_FACE); @@ -67,9 +71,21 @@ void OpenGLRendererAPI::setBackfaceCulling(bool enable) const { } void OpenGLRendererAPI::checkAndLogErrors() const { - unsigned int err; + // glDebugMessageCallback would be nicer but it is not core until GL 4.3; the engine + // targets 4.1 (macOS caps there), so decode the enum to a readable name instead of + // logging a bare number. Callers should only drain this in debug builds. + GLenum err; while ((err = glGetError()) != GL_NO_ERROR) { - Logger::Log(Logger::ERROR, "Graphics", "OpenGL Error %d", err); + const char *name; + switch (err) { + case GL_INVALID_ENUM: name = "GL_INVALID_ENUM"; break; + case GL_INVALID_VALUE: name = "GL_INVALID_VALUE"; break; + case GL_INVALID_OPERATION: name = "GL_INVALID_OPERATION"; break; + case GL_INVALID_FRAMEBUFFER_OPERATION: name = "GL_INVALID_FRAMEBUFFER_OPERATION"; break; + case GL_OUT_OF_MEMORY: name = "GL_OUT_OF_MEMORY"; break; + default: name = "GL_UNKNOWN"; break; + } + Logger::Log(Logger::ERROR, "Graphics", "OpenGL error: %s (0x%x)", name, err); } } } // namespace ICE \ No newline at end of file diff --git a/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp b/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp index 24b6c876..d911d80b 100644 --- a/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp +++ b/ICE/GraphicsAPI/OpenGL/src/OpenGLShader.cpp @@ -33,10 +33,13 @@ OpenGLShader::OpenGLShader(const Shader &shader_asset) { Logger::Log(Logger::FATAL, "Graphics", "Shader linking error: %s", errorLog.data()); } - // Stage objects are no longer needed once linked into the program. + // Stage objects are no longer needed once linked into the program. Skip 0, which + // marks a stage that failed to compile (and so was never attached). for (GLuint shader : stage_shaders) { - glDetachShader(m_programID, shader); - glDeleteShader(shader); + if (shader != 0) { + glDetachShader(m_programID, shader); + glDeleteShader(shader); + } } } @@ -115,7 +118,11 @@ GLuint OpenGLShader::compileAndAttachStage(ShaderStage stage, const std::string GLint shader; Logger::Log(Logger::VERBOSE, "Graphics", "\t + Compiling shader stage..."); if (!compileShader(stageToGLStage(stage), source, &shader)) { + // Don't attach a stage that failed to compile: attaching it only guarantees the + // link fails too, producing a broken-but-alive program. Return 0 to signal failure. Logger::Log(Logger::FATAL, "Graphics", "Error while compiling shader stage"); + glDeleteShader(shader); + return 0; } glAttachShader(m_programID, shader); return static_cast(shader); diff --git a/ICE/GraphicsAPI/OpenGL/src/OpenGLTextureCube.cpp b/ICE/GraphicsAPI/OpenGL/src/OpenGLTextureCube.cpp index a2c7271c..eb804378 100644 --- a/ICE/GraphicsAPI/OpenGL/src/OpenGLTextureCube.cpp +++ b/ICE/GraphicsAPI/OpenGL/src/OpenGLTextureCube.cpp @@ -16,8 +16,15 @@ OpenGLTextureCube::OpenGLTextureCube(const TextureCube &texture_asset) { auto width = texture_asset.getWidth(); auto faces = equirectangularToCubemap((uint8_t *) texture_asset.data(), width, texture_asset.getHeight()); + // RGB (3-byte) rows: without alignment 1 the default of 4 shears any face whose width + // is not a multiple of 4. + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); for (int i = 0; i < 6; i++) { - glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width / 4, width / 4, 0, GL_RGB, GL_UNSIGNED_BYTE, faces[i]); + glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB8, width / 4, width / 4, 0, GL_RGB, GL_UNSIGNED_BYTE, faces[i]); + } + // equirectangularToCubemap allocates the six faces with new[]; free them after upload. + for (int i = 0; i < 6; i++) { + delete[] faces[i]; } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); diff --git a/ICE/IO/src/EngineConfig.cpp b/ICE/IO/src/EngineConfig.cpp index 960370d3..e57086c8 100644 --- a/ICE/IO/src/EngineConfig.cpp +++ b/ICE/IO/src/EngineConfig.cpp @@ -26,18 +26,21 @@ EngineConfig EngineConfig::LoadFromFile() { configFile.open(ICE_CONFIG_FILE); if (!configFile.is_open()) { - Logger::Log(Logger::FATAL, "IO", "Couldn't open config file"); - exit(EXIT_FAILURE); + // Library code must not exit() the host process; log and return an empty config. + Logger::Log(Logger::ERROR, "IO", "Couldn't open config file"); + return config; } - json j; - configFile >> j; - configFile.close(); - - for (const auto &project : j["projects"]) { - std::filesystem::path path = std::string(project["path"]); - std::string name = project["name"]; - config.localProjects.push_back(Project(path, name)); + try { + json j; + configFile >> j; + for (const auto &project : j["projects"]) { + std::filesystem::path path = std::string(project["path"]); + std::string name = project["name"]; + config.localProjects.push_back(Project(path, name)); + } + } catch (const std::exception &e) { + Logger::Log(Logger::ERROR, "IO", "Failed to parse config file: %s", e.what()); } } return config; @@ -51,8 +54,8 @@ void EngineConfig::save() { std::ofstream configFile; configFile.open(ICE_CONFIG_FILE); if (!configFile.is_open()) { - Logger::Log(Logger::FATAL, "IO", "Couldn't open config file"); - exit(EXIT_FAILURE); + Logger::Log(Logger::ERROR, "IO", "Couldn't open config file for writing"); + return; } json j; diff --git a/ICE/IO/src/MaterialLoader.cpp b/ICE/IO/src/MaterialLoader.cpp index fc73fd2f..4b827121 100644 --- a/ICE/IO/src/MaterialLoader.cpp +++ b/ICE/IO/src/MaterialLoader.cpp @@ -5,18 +5,30 @@ #include "MaterialLoader.h" #include +#include #include #include using json = nlohmann::json; - +#undef ERROR namespace ICE { std::shared_ptr MaterialLoader::load(const std::vector &files) { + if (files.empty()) { + return nullptr; + } + std::ifstream infile(files[0]); + if (!infile.is_open()) { + Logger::Log(Logger::ERROR, "IO", "Could not open material file '%s'", files[0].string().c_str()); + return nullptr; + } json j; - std::ifstream infile = std::ifstream(files[0]); - infile >> j; - infile.close(); + try { + infile >> j; + } catch (const std::exception &e) { + Logger::Log(Logger::ERROR, "IO", "Failed to parse material '%s': %s", files[0].string().c_str(), e.what()); + return nullptr; + } bool transparent = false; if (j.contains("transparent")) { diff --git a/ICE/IO/src/ModelLoader.cpp b/ICE/IO/src/ModelLoader.cpp index ac2005ff..05aac8be 100644 --- a/ICE/IO/src/ModelLoader.cpp +++ b/ICE/IO/src/ModelLoader.cpp @@ -63,7 +63,13 @@ int ModelLoader::processNode(const aiNode *ainode, std::vector &nod std::unordered_set &used_names, const Eigen::Matrix4f &parent_transform) { std::string name = ainode->mName.C_Str(); if (used_names.contains(name)) { - name = name + "_" + std::to_string(used_names.size()); + // Suffix with an incrementing counter checked against existing names. The old + // "_" suffix could still collide with a node that already had that name. + const std::string base = name; + int counter = 1; + do { + name = base + "_" + std::to_string(counter++); + } while (used_names.contains(name)); } used_names.insert(name); diff --git a/ICE/IO/src/Project.cpp b/ICE/IO/src/Project.cpp index 17453714..1c49c8ac 100644 --- a/ICE/IO/src/Project.cpp +++ b/ICE/IO/src/Project.cpp @@ -229,8 +229,17 @@ json Project::dumpAsset(AssetUID uid, const std::shared_ptr &asset) { void Project::loadFromFile() { std::ifstream infile = std::ifstream(m_base_directory / (m_name + ".ice")); + if (!infile.is_open()) { + Logger::Log(Logger::ERROR, "IO", "Could not open project file '%s'", (m_base_directory / (m_name + ".ice")).string().c_str()); + return; + } json j; - infile >> j; + try { + infile >> j; + } catch (const std::exception &e) { + Logger::Log(Logger::ERROR, "IO", "Failed to parse project file: %s", e.what()); + return; + } infile.close(); std::vector sceneNames = j["scenes"]; @@ -253,8 +262,17 @@ void Project::loadFromFile() { for (const auto &s : sceneNames) { infile = std::ifstream(m_scenes_directory / (s + ".ics")); + if (!infile.is_open()) { + Logger::Log(Logger::ERROR, "IO", "Could not open scene file '%s'", s.c_str()); + continue; + } json scenejson; - infile >> scenejson; + try { + infile >> scenejson; + } catch (const std::exception &e) { + Logger::Log(Logger::ERROR, "IO", "Failed to parse scene '%s': %s", s.c_str(), e.what()); + continue; + } infile.close(); Scene scene = Scene(scenejson["m_name"]); @@ -328,7 +346,15 @@ void Project::copyAssetFile(const fs::path &folder, const std::string &assetName auto dst = subfolder / (assetName + src.extension().string()); std::ifstream srcStream(src, std::ios::binary); + if (!srcStream.is_open()) { + Logger::Log(Logger::ERROR, "IO", "Could not open source asset '%s'", src.string().c_str()); + return; + } std::ofstream dstStream(dst, std::ios::binary); + if (!dstStream.is_open()) { + Logger::Log(Logger::ERROR, "IO", "Could not open destination '%s' for asset copy", dst.string().c_str()); + return; + } dstStream << srcStream.rdbuf(); dstStream.flush(); @@ -343,9 +369,12 @@ bool Project::renameAsset(const AssetPath &oldName, const AssetPath &newName) { if (m_asset_bank->renameAsset(oldName, newName)) { auto path = m_base_directory / "Assets"; for (const auto &file : getFilesInDir(path / oldName.prefix())) { - if (file.substr(0, file.find_last_of(".")) == oldName.getName()) { + // stem()/extension() handle files with no extension (the old substr(find_last_of(".")) + // threw std::out_of_range on those). + fs::path fp(file); + if (fp.stem().string() == oldName.getName()) { if (rename((path / oldName.prefix() / file).string().c_str(), - (path / oldName.prefix() / (newName.getName() + file.substr(file.find_last_of(".")))).string().c_str()) + (path / oldName.prefix() / (newName.getName() + fp.extension().string())).string().c_str()) == 0) { return true; } @@ -359,8 +388,9 @@ bool Project::renameAsset(const AssetPath &oldName, const AssetPath &newName) { std::vector Project::getFilesInDir(const fs::path &folder) { std::vector files; for (const auto &entry : fs::directory_iterator(folder)) { - std::string sp = entry.path().string(); - files.push_back(sp.substr(sp.find_last_of("/") + 1)); + // Use std::filesystem to extract the filename: splitting on '/' returned the whole + // path on Windows, where the separator is '\'. + files.push_back(entry.path().filename().string()); } return files; } diff --git a/ICE/IO/src/ShaderLoader.cpp b/ICE/IO/src/ShaderLoader.cpp index cdc2a740..01744ebe 100644 --- a/ICE/IO/src/ShaderLoader.cpp +++ b/ICE/IO/src/ShaderLoader.cpp @@ -17,10 +17,16 @@ std::shared_ptr ShaderLoader::load(const std::vector> json; - infile.close(); + if (!infile.is_open()) { + throw ICEException("Could not open shader file: " + files[0].string()); + } + nlohmann::json json; + try { + infile >> json; + } catch (const std::exception &e) { + throw ICEException("Failed to parse shader '" + files[0].string() + "': " + e.what()); + } ShaderSource shader_sources; for (const auto &stage_source : json) { diff --git a/ICE/IO/src/TextureLoader.cpp b/ICE/IO/src/TextureLoader.cpp index e24a0b76..6fa90e46 100644 --- a/ICE/IO/src/TextureLoader.cpp +++ b/ICE/IO/src/TextureLoader.cpp @@ -20,6 +20,9 @@ std::shared_ptr Texture2DLoader::load(const std::vector TextureCubeLoader::load(const std::vector &file) { Logger::Log(Logger::VERBOSE, "IO", "Loading cubemap..."); + if (file.empty()) { + return nullptr; + } auto texture = std::make_shared(file[0].string()); texture->setSources(file); return texture; diff --git a/ICE/Math/src/ICEMath.cpp b/ICE/Math/src/ICEMath.cpp index 00586af3..2d6cbb74 100644 --- a/ICE/Math/src/ICEMath.cpp +++ b/ICE/Math/src/ICEMath.cpp @@ -158,7 +158,12 @@ std::array equirectangularToCubemap(uint8_t *inputPixels, int widt Eigen::Vector3f cube = orientation(i, (2 * (x + 0.5) / faceWidth - 1), (2 * (y + 0.5) / faceHeight - 1)); auto r = cube.norm(); - auto lon = fmod(atan2(cube.y(), cube.x()) + rotation, 2 * M_PI); + // rotation is in degrees; it was being added straight to a radian longitude. + // Also wrap negative longitudes into [0, 2pi) so they don't clamp to column 0. + auto lon = fmod(atan2(cube.y(), cube.x()) + DEG_TO_RAD(rotation), 2 * M_PI); + if (lon < 0) { + lon += 2 * M_PI; + } auto lat = acos(cube.z() / r); int fx = width * lon / M_PI / 2 - 0.5; diff --git a/ICE/System/include/AnimationSystem.h b/ICE/System/include/AnimationSystem.h index 87c4f7d9..29962890 100644 --- a/ICE/System/include/AnimationSystem.h +++ b/ICE/System/include/AnimationSystem.h @@ -19,6 +19,8 @@ class AnimationSystem : public System { AnimationSystem(const std::shared_ptr& reg, const std::shared_ptr& bank); void update(double delta) override; + int updateOrder() const override { return AnimationSystemOrder; } + std::vector getSignatures(const ComponentManager& comp_manager) const override { Signature signature; signature.set(comp_manager.getComponentType()); diff --git a/ICE/System/include/RenderSystem.h b/ICE/System/include/RenderSystem.h index 73950dbf..d9d076e8 100644 --- a/ICE/System/include/RenderSystem.h +++ b/ICE/System/include/RenderSystem.h @@ -34,6 +34,8 @@ class RenderSystem : public System { void onEntityRemoved(Entity e) override; void update(double delta) override; + int updateOrder() const override { return RenderSystemOrder; } + void submitModel(const std::shared_ptr &model, const Eigen::Matrix4f &transform); std::shared_ptr getRenderer() const; diff --git a/ICE/System/include/SceneGraphSystem.h b/ICE/System/include/SceneGraphSystem.h index de31aa33..c0a59ab9 100644 --- a/ICE/System/include/SceneGraphSystem.h +++ b/ICE/System/include/SceneGraphSystem.h @@ -14,6 +14,8 @@ class SceneGraphSystem : public System { void onEntityRemoved(Entity e) override; void update(double delta) override; + int updateOrder() const override { return SceneGraphSystemOrder; } + std::vector getSignatures(const ComponentManager &comp_manager) const override { Signature signature0; signature0.set(comp_manager.getComponentType()); diff --git a/ICE/System/include/System.h b/ICE/System/include/System.h index 7001b006..7eee4407 100644 --- a/ICE/System/include/System.h +++ b/ICE/System/include/System.h @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -16,12 +17,26 @@ namespace ICE { class Scene; class ComponentManager; +// Canonical per-frame update order (lower runs first): input/scripts advance state, +// animation updates local transforms, the scene graph propagates them to world space, +// then rendering consumes the final transforms. +enum SystemUpdateOrder : int { + ScriptSystemOrder = 100, + AnimationSystemOrder = 200, + SceneGraphSystemOrder = 300, + RenderSystemOrder = 400, +}; + class System { public: virtual void update(double delta) = 0; virtual void onEntityAdded(Entity e) {}; virtual void onEntityRemoved(Entity e) {}; + // Lower values update first. Determines the deterministic per-frame ordering; the + // default puts unclassified systems before rendering. + virtual int updateOrder() const { return 0; } + virtual std::vector getSignatures(const ComponentManager& comp_manager) const = 0; virtual ~System() = default; @@ -36,9 +51,7 @@ class SystemManager { void entityDestroyed(Entity entity) { // Erase a destroyed entity from all system lists // mEntities is a set so no check needed - for (auto const& pair : systems) { - auto const& system = pair.second; - + for (auto const& system : orderedSystems) { system->entities.erase(entity); system->onEntityRemoved(entity); } @@ -46,8 +59,7 @@ class SystemManager { void entitySignatureChanged(Entity entity, Signature entitySignature, const ComponentManager& comp_manager) { // Notify each system that an entity's signature changed - for (auto const& pair : systems) { - auto const& system = pair.second; + for (auto const& system : orderedSystems) { auto const& systemSignature = system->getSignatures(comp_manager); // Entity signature matches system signature - insert into set @@ -69,14 +81,22 @@ class SystemManager { } void updateSystems(double delta) { - for (auto const& [signature, system] : systems) { + // Iterate in deterministic updateOrder() order (not hash order of the type map). + for (auto const& system : orderedSystems) { system->update(delta); } } template void addSystem(const std::shared_ptr& system) { - systems.try_emplace(typeid(T), system); + if (!systems.try_emplace(typeid(T), system).second) { + return; // already registered + } + // Keep orderedSystems sorted by updateOrder(); stable for equal orders (later + // registrations of the same order run after earlier ones). + auto pos = std::upper_bound(orderedSystems.begin(), orderedSystems.end(), std::static_pointer_cast(system), + [](const std::shared_ptr& a, const std::shared_ptr& b) { return a->updateOrder() < b->updateOrder(); }); + orderedSystems.insert(pos, system); } template @@ -85,7 +105,9 @@ class SystemManager { } private: - // Map from system type string pointer to a system pointer + // Map from system type index to a system pointer (fast getSystem() lookup). std::unordered_map> systems; + // Same systems, kept sorted by updateOrder() for deterministic iteration. + std::vector> orderedSystems; }; } // namespace ICE diff --git a/ICE/System/src/AnimationSystem.cpp b/ICE/System/src/AnimationSystem.cpp index 24d69ebb..79c0b442 100644 --- a/ICE/System/src/AnimationSystem.cpp +++ b/ICE/System/src/AnimationSystem.cpp @@ -16,14 +16,15 @@ void AnimationSystem::update(double dt) { auto model = m_asset_bank->getAsset(pose->skeletonModel); const auto& animations = model->getAnimations(); - // Advance current animation time - anim->currentTime += dt * anim->speed; - if (!animations.contains(anim->currentAnimation)) { continue; } const auto& currentAnim = animations.at(anim->currentAnimation); + // Advance current animation time. dt is in seconds; animation keyframes are in + // ticks, so convert with ticksPerSecond (previously ignored -> wrong playback rate). + anim->currentTime += dt * currentAnim.ticksPerSecond * anim->speed; + if (anim->currentTime > currentAnim.duration) { if (anim->loop) { anim->currentTime = std::fmod(anim->currentTime, currentAnim.duration); @@ -44,7 +45,7 @@ void AnimationSystem::update(double dt) { // Advance previous animation time as well if (animations.contains(anim->previousAnimation)) { const auto& prevAnim = animations.at(anim->previousAnimation); - anim->previousTime += dt * anim->speed; + anim->previousTime += dt * prevAnim.ticksPerSecond * anim->speed; if (anim->previousTime > prevAnim.duration) { anim->previousTime = std::fmod(anim->previousTime, prevAnim.duration); } diff --git a/ICE/System/src/RenderSystem.cpp b/ICE/System/src/RenderSystem.cpp index 2bc83e28..dd21a281 100644 --- a/ICE/System/src/RenderSystem.cpp +++ b/ICE/System/src/RenderSystem.cpp @@ -90,7 +90,11 @@ void RenderSystem::update(double delta) { auto skel_transform = m_registry->tryGetComponent(skeleton_entity); if (pose && skel_transform) { for (const auto &[id, ibm] : skinning.inverseBindMatrices) { - bone_matrices.try_emplace(id, pose->bone_transform[id] * ibm); + // bone_transform is a vector indexed by bone id; guard against a bone id + // outside the current pose (out-of-bounds vector access is UB). + if (id >= 0 && static_cast(id) < pose->bone_transform.size()) { + bone_matrices.try_emplace(id, pose->bone_transform[id] * ibm); + } } model_mat = skel_transform->getWorldMatrix(); } diff --git a/ICEBERG/Components/UniformInputs.h b/ICEBERG/Components/UniformInputs.h index 15b5a8e7..467d0949 100644 --- a/ICEBERG/Components/UniformInputs.h +++ b/ICEBERG/Components/UniformInputs.h @@ -40,14 +40,15 @@ class UniformInputs { m_asset_combo.setValues(path_with_none); m_assets_ids = {0}; m_assets_ids.insert(m_assets_ids.end(), ids.begin(), ids.end()); + m_asset_combo.onSelectionChanged( + [cb = this->m_callback, id_list = this->m_assets_ids](const std::string &, int index) { cb(id_list[index]); }); if (std::holds_alternative(m_value)) { auto it = std::find(m_assets_ids.begin(), m_assets_ids.end(), std::get(m_value)); if (it != m_assets_ids.end()) { m_asset_combo.setSelected(std::distance(m_assets_ids.begin(), it)); } } - m_asset_combo.onSelectionChanged( - [cb = this->m_callback, id_list = this->m_assets_ids](const std::string &, int index) { cb(id_list[index]); }); + } void setForceVectorNumeric(bool force_vector_numeric) { m_force_vector_numeric = force_vector_numeric; } diff --git a/ICEBERG/src/Viewport.cpp b/ICEBERG/src/Viewport.cpp index 16d104be..6f7d299b 100644 --- a/ICEBERG/src/Viewport.cpp +++ b/ICEBERG/src/Viewport.cpp @@ -44,7 +44,26 @@ Viewport::Viewport(const std::shared_ptr &engine, const std::fun auto tc = registry->getComponent(e); auto rc = registry->getComponent(e); - shader->loadMat4("model", tc->getWorldMatrix()); + auto model_mat = tc->getWorldMatrix(); + + // Skin the picking geometry exactly like the render pass does, so an + // animated model is picked at its current pose instead of its bind pose. + if (registry->entityHasComponent(e)) { + const auto &skinning = m_engine->getGPURegistry()->getMeshSkinningData(rc->mesh); + auto skeleton_entity = registry->getComponent(e)->skeleton_entity; + auto pose = registry->tryGetComponent(skeleton_entity); + auto skel_transform = registry->tryGetComponent(skeleton_entity); + if (pose && skel_transform) { + for (const auto &[id, ibm] : skinning.inverseBindMatrices) { + if (id >= 0 && static_cast(id) < pose->bone_transform.size()) { + shader->loadMat4("bonesTransformMatrices[" + std::to_string(id) + "]", pose->bone_transform[id] * ibm); + } + } + model_mat = skel_transform->getWorldMatrix(); + } + } + + shader->loadMat4("model", model_mat); shader->loadInt("objectID", e); auto mesh = m_engine->getGPURegistry()->getMesh(rc->mesh); if (mesh) { diff --git a/ICEFIELD/icefield.cpp b/ICEFIELD/icefield.cpp index 774788c1..5f3933aa 100644 --- a/ICEFIELD/icefield.cpp +++ b/ICEFIELD/icefield.cpp @@ -43,7 +43,7 @@ int main(void) { engine.getAssetBank()->getUID(AssetPath::WithTypePrefix("base_mat")))); } - /* + auto model_id = engine.getAssetBank()->getUID(AssetPath::WithTypePrefix("Adventurer")); auto entity2 = scene->spawnTree(model_id, engine.getAssetBank()); scene->getRegistry()->addComponent(entity2, AnimationComponent{.currentAnimation = "Walk", .loop = true}); @@ -51,7 +51,7 @@ int main(void) { auto entity3 = scene->spawnTree(model_id, engine.getAssetBank()); scene->getRegistry()->getComponent(entity3)->setPosition({1, 0, 0}); scene->getRegistry()->addComponent(entity3, AnimationComponent{.currentAnimation = "Run", .loop = true}); - */ + auto camera = std::make_shared(60.0, 16.0 / 9.0, 0.01, 10000.0); camera->backward(5); camera->up(5);