Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions Assets/Shaders/glsl/pbr.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down
19 changes: 16 additions & 3 deletions Assets/Shaders/glsl/phong.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 7 additions & 1 deletion Assets/Shaders/glsl/picking.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
32 changes: 32 additions & 0 deletions Assets/Shaders/glsl/picking.vs
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 5 additions & 1 deletion Assets/Shaders/glsl/skybox.vs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion Assets/Shaders/picking.shader.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"stage": "vertex",
"source": "glsl/skinning.vs"
"source": "glsl/picking.vs"
},
{
"stage": "fragment",
Expand Down
5 changes: 5 additions & 0 deletions ICE/Assets/include/AssetBank.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ class AssetBank {
}

bool addAsset(const AssetPath& path, const std::shared_ptr<Asset>& 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);
Expand Down
4 changes: 3 additions & 1 deletion ICE/Assets/include/AssetPath.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ class AssetPath {
std::string prefix() const;
template<typename T>
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()); }
Expand Down
6 changes: 5 additions & 1 deletion ICE/Assets/src/Model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ void Model::traverse(std::vector<AssetUID> &meshes, std::vector<AssetUID> &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);
}
};

Expand Down
7 changes: 7 additions & 0 deletions ICE/Assets/src/Texture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 12 additions & 5 deletions ICE/Components/include/TransformComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -66,21 +66,26 @@ 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;
}

void updateParentMatrix(const Eigen::Matrix4f& parent_matrix) {
m_parent_matrix = parent_matrix;
m_world_matrix = parent_matrix * getModelMatrix();
m_world_dirty = false;
m_version++;
}

Expand Down Expand Up @@ -125,7 +130,8 @@ struct TransformComponent : public Component {

private:
void markDirty() {
m_dirty = true;
m_model_dirty = true;
m_world_dirty = true;
m_version++;
}

Expand All @@ -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;
};

Expand Down
4 changes: 4 additions & 0 deletions ICE/Core/include/ICEEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>& camera_);

std::shared_ptr<Camera> getCamera();
Expand Down Expand Up @@ -64,6 +67,7 @@ class ICEEngine {
std::shared_ptr<Project> project = nullptr;

std::chrono::steady_clock::time_point lastFrameTime;
double m_delta_time = 0.0;

EngineConfig config;
};
Expand Down
12 changes: 10 additions & 2 deletions ICE/Core/src/ICEEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,23 @@ void ICEEngine::initialize(const std::shared_ptr<GraphicsFactory> &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<std::chrono::milliseconds>(now - lastFrameTime).count();
m_delta_time = std::chrono::duration<double>(now - lastFrameTime).count();
lastFrameTime = now;
if (!project) {
return;
}
auto render_system = project->getCurrentScene()->getRegistry()->getSystem<RenderSystem>();
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> &camera_) {
Expand Down
8 changes: 6 additions & 2 deletions ICE/Graphics/include/ForwardRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include <GraphicsAPI.h>
#include <Registry.h>

#include <map>
#include <optional>
#include <tuple>
#include <vector>

#include "Camera.h"
Expand Down Expand Up @@ -54,8 +56,10 @@ class ForwardRenderer : public Renderer {
std::vector<Drawable> m_drawables;
std::vector<Light> m_lights;

// Instance batching storage
std::unordered_map<uint64_t, std::vector<InstanceData>> 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<GPUMesh*, Material*, ShaderProgram*>;
std::map<BatchKey, std::vector<InstanceData>> m_instance_batches;

RendererConfig config;
};
Expand Down
3 changes: 3 additions & 0 deletions ICE/Graphics/include/GraphicsAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include <GLFW/glfw3.h>
#include <VertexArray.h>

#include "RenderState.h"

namespace ICE {
enum GraphicsAPI {
None = 0x0,
Expand All @@ -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;

Expand Down
32 changes: 1 addition & 31 deletions ICE/Graphics/include/InstanceData.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,13 @@
#pragma once

#include <Eigen/Dense>
#include <memory>
#include <vector>
#include <unordered_map>

#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<GPUMesh> mesh;
std::shared_ptr<Material> material;
std::shared_ptr<ShaderProgram> shader;
std::unordered_map<AssetUID, std::shared_ptr<GPUTexture>> textures;
std::vector<InstanceData> instances;

// GPU buffer for instance data (created on demand)
std::shared_ptr<VertexBuffer> instance_buffer;
bool buffer_dirty = true;

// Generate hash key for batching
uint64_t getBatchKey() const {
uint64_t hash = 0;
hash ^= reinterpret_cast<uint64_t>(mesh.get()) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
hash ^= reinterpret_cast<uint64_t>(material.get()) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
hash ^= reinterpret_cast<uint64_t>(shader.get()) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
return hash;
}
};

} // namespace ICE
Loading
Loading