Skip to content
Open
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
154 changes: 154 additions & 0 deletions common/math/Frustum.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//// Frustum.hpp //////////////////////////////////////////////////////////////
//
// zzzzz zzz zzzzzzzzzzzzz zzzz zzzz zzzzzz zzzzz
// zzzzzzz zzz zzzz zzzz zzzz zzzz
// zzz zzz zzz zzzzzzzzzzzzz zzzz zzzz zzz
// zzz zzz zzz z zzzz zzzz zzzz zzzz
// zzz zzz zzzzzzzzzzzzz zzzz zzz zzzzzzz zzzzz
//
// Author: Mehdy MORVAN
// Date: 15/02/2026
// Description: Header file for frustum culling utilities
//
///////////////////////////////////////////////////////////////////////////////
#pragma once

#include <glm/glm.hpp>
#include <array>
#include <algorithm>

namespace nexo::math {

struct Plane {
glm::vec3 normal{};
float distance = 0.0f;

void normalize()
{
const float len = glm::length(normal);
normal /= len;
distance /= len;
}
Comment on lines +26 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against zero-length normal in Plane::normalize().

If a degenerate or zero view-projection matrix is passed, glm::length(normal) can be 0, causing division-by-zero and NaN propagation through all subsequent distanceTo calls. A simple guard avoids silent corruption.

🛡️ Proposed fix
 void normalize()
 {
     const float len = glm::length(normal);
+    if (len < 1e-8f)
+        return;
     normal /= len;
     distance /= len;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void normalize()
{
const float len = glm::length(normal);
normal /= len;
distance /= len;
}
void normalize()
{
const float len = glm::length(normal);
if (len < 1e-8f)
return;
normal /= len;
distance /= len;
}
🤖 Prompt for AI Agents
In `@common/math/Frustum.hpp` around lines 26 - 31, Plane::normalize currently
divides normal and distance by glm::length(normal) without guarding zero length;
change normalize to compute len = glm::length(normal) and if len > epsilon (e.g.
1e-6f or std::numeric_limits<float>::epsilon()) perform the division, otherwise
set normal to glm::vec3(0.0f) and distance to 0.0f (or simply return) to avoid
division-by-zero and NaN propagation that will break distanceTo and other
callers.


[[nodiscard]] float distanceTo(const glm::vec3 &point) const
{
return glm::dot(normal, point) + distance;
}
};

class Frustum {
public:
explicit Frustum(const glm::mat4 &vp)
{
extractPlanes(vp);
}

/**
* @brief Tests whether an axis-aligned bounding box intersects the frustum.
*
* Uses the p-vertex optimization: for each frustum plane, finds the AABB vertex
* most in the direction of the plane normal. If that vertex is behind the plane,
* the entire AABB is outside the frustum.
*
* @param aabbMin The minimum corner of the AABB in world space.
* @param aabbMax The maximum corner of the AABB in world space.
* @return true if the AABB is at least partially inside the frustum.
*/
[[nodiscard]] bool intersectsAABB(const glm::vec3 &aabbMin, const glm::vec3 &aabbMax) const
{
for (const auto &plane : m_planes)
{
// P-vertex: the corner most in the direction of the plane normal
const glm::vec3 pVertex(
plane.normal.x >= 0.0f ? aabbMax.x : aabbMin.x,
plane.normal.y >= 0.0f ? aabbMax.y : aabbMin.y,
plane.normal.z >= 0.0f ? aabbMax.z : aabbMin.z
);

if (plane.distanceTo(pVertex) < 0.0f)
return false;
}
return true;
}

private:
std::array<Plane, 6> m_planes;

/**
* @brief Extracts frustum planes from a view-projection matrix.
*
* Uses the Gribb/Hartmann method to extract and normalize the six frustum planes
* directly from the combined view-projection matrix.
*/
void extractPlanes(const glm::mat4 &vp)
{
// Left: Row3 + Row0
m_planes[0].normal.x = vp[0][3] + vp[0][0];
m_planes[0].normal.y = vp[1][3] + vp[1][0];
m_planes[0].normal.z = vp[2][3] + vp[2][0];
m_planes[0].distance = vp[3][3] + vp[3][0];

// Right: Row3 - Row0
m_planes[1].normal.x = vp[0][3] - vp[0][0];
m_planes[1].normal.y = vp[1][3] - vp[1][0];
m_planes[1].normal.z = vp[2][3] - vp[2][0];
m_planes[1].distance = vp[3][3] - vp[3][0];

// Bottom: Row3 + Row1
m_planes[2].normal.x = vp[0][3] + vp[0][1];
m_planes[2].normal.y = vp[1][3] + vp[1][1];
m_planes[2].normal.z = vp[2][3] + vp[2][1];
m_planes[2].distance = vp[3][3] + vp[3][1];

// Top: Row3 - Row1
m_planes[3].normal.x = vp[0][3] - vp[0][1];
m_planes[3].normal.y = vp[1][3] - vp[1][1];
m_planes[3].normal.z = vp[2][3] - vp[2][1];
m_planes[3].distance = vp[3][3] - vp[3][1];

// Near: Row3 + Row2
m_planes[4].normal.x = vp[0][3] + vp[0][2];
m_planes[4].normal.y = vp[1][3] + vp[1][2];
m_planes[4].normal.z = vp[2][3] + vp[2][2];
m_planes[4].distance = vp[3][3] + vp[3][2];

// Far: Row3 - Row2
m_planes[5].normal.x = vp[0][3] - vp[0][2];
m_planes[5].normal.y = vp[1][3] - vp[1][2];
m_planes[5].normal.z = vp[2][3] - vp[2][2];
m_planes[5].distance = vp[3][3] - vp[3][2];

for (auto &plane : m_planes)
plane.normalize();
}
};

/**
* @brief Transforms a local-space AABB by a 4x4 matrix to produce a world-space AABB.
*
* Uses Arvo's method for efficient AABB-matrix transformation, producing the tightest
* axis-aligned bounding box that contains the transformed original box.
*/
inline void transformAABB(const glm::vec3 &localMin, const glm::vec3 &localMax,
const glm::mat4 &transform,
glm::vec3 &worldMin, glm::vec3 &worldMax)
{
// Start with the translation component
const glm::vec3 translation(transform[3]);
worldMin = translation;
worldMax = translation;

// Apply rotation/scale contribution using Arvo's method
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
const float a = transform[j][i] * localMin[j];
const float b = transform[j][i] * localMax[j];
worldMin[i] += std::min(a, b);
worldMax[i] += std::max(a, b);
}
}
}

} // namespace nexo::math
33 changes: 33 additions & 0 deletions engine/src/EntityFactory3D.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getCubeVAO();
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-0.5f);
mesh.localMax = glm::vec3(0.5f);

auto material = std::make_unique<components::Material>();
material->albedoColor = color;
Expand Down Expand Up @@ -80,6 +83,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getCubeVAO();
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-0.5f);
mesh.localMax = glm::vec3(0.5f);

const auto materialRef = assets::AssetCatalog::getInstance().createAsset<assets::Material>(
assets::AssetLocation("_internal::CubeMat@_internal"),
Expand Down Expand Up @@ -174,6 +180,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getTetrahedronVAO();
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);
Comment on lines +183 to +185
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting default primitive bounds into named constants.

All non-cube primitives use glm::vec3(-1.0f) / glm::vec3(1.0f). A pair of constants (or a small helper) would remove the repetition across 8 overloads and make it easier to adjust if the actual geometry doesn't exactly match [-1, 1]³.

Also applies to: 217-219, 247-249, 280-282, 311-313, 344-346, 375-377, 408-410

🧰 Tools
🪛 Cppcheck (2.19.0)

[style] 183-183: The function 'removeEventDebugFlags' is never used.

(unusedFunction)


[style] 184-184: The function 'addEventDebugFlag' is never used.

(unusedFunction)


[style] 185-185: The function 'resetEventDebugFlags' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
In `@engine/src/EntityFactory3D.cpp` around lines 183 - 185, Extract the repeated
glm::vec3(-1.0f)/glm::vec3(1.0f) into named constants (e.g. const glm::vec3
kDefaultPrimitiveMin = glm::vec3(-1.0f) and const glm::vec3 kDefaultPrimitiveMax
= glm::vec3(1.0f)) or a small helper that returns the pair, and replace
occurrences where mesh.hasBounds is set and mesh.localMin / mesh.localMax are
assigned (the repeated blocks setting mesh.hasBounds = true; mesh.localMin =
glm::vec3(-1.0f); mesh.localMax = glm::vec3(1.0f)) across the various primitive
creation overloads so all use the new constants/helper.


auto material = std::make_unique<components::Material>();
material->albedoColor = color;
Expand Down Expand Up @@ -205,6 +214,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getTetrahedronVAO();
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);

const auto materialRef = assets::AssetCatalog::getInstance().createAsset<assets::Material>(
assets::AssetLocation("_internal::TetrahedronMat@_internal"),
Expand Down Expand Up @@ -232,6 +244,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getPyramidVAO();
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);

auto material = std::make_unique<components::Material>();
material->albedoColor = color;
Expand Down Expand Up @@ -262,6 +277,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getPyramidVAO();
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);

const auto materialRef = assets::AssetCatalog::getInstance().createAsset<assets::Material>(
assets::AssetLocation("_internal::PyramidMat@_internal"),
Expand Down Expand Up @@ -290,6 +308,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getCylinderVAO(nbSegment);
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);

auto material = std::make_unique<components::Material>();
material->albedoColor = color;
Expand Down Expand Up @@ -320,6 +341,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getCylinderVAO(nbSegment);
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);

const auto materialRef = assets::AssetCatalog::getInstance().createAsset<assets::Material>(
assets::AssetLocation("_internal::CylinderMat@_internal"),
Expand Down Expand Up @@ -348,6 +372,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getSphereVAO(nbSubdivision);
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);

auto material = std::make_unique<components::Material>();
material->albedoColor = color;
Expand Down Expand Up @@ -378,6 +405,9 @@ namespace nexo

components::StaticMeshComponent mesh;
mesh.vao = renderer::NxRenderer3D::getSphereVAO(nbSubdivision);
mesh.hasBounds = true;
mesh.localMin = glm::vec3(-1.0f);
mesh.localMax = glm::vec3(1.0f);

const auto materialRef = assets::AssetCatalog::getInstance().createAsset<assets::Material>(
assets::AssetLocation("_internal::SphereMat@_internal"),
Expand Down Expand Up @@ -489,6 +519,9 @@ namespace nexo

components::StaticMeshComponent staticMesh;
staticMesh.vao = mesh.vao;
staticMesh.hasBounds = true;
staticMesh.localMin = mesh.localMin;
staticMesh.localMax = mesh.localMax;

components::RenderComponent renderComponent;
renderComponent.isRendered = true;
Expand Down
2 changes: 2 additions & 0 deletions engine/src/assets/Assets/Model/Model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ namespace nexo::assets {
AssetRef<Material> material;

glm::vec3 localCenter = {0.0f, 0.0f, 0.0f};
glm::vec3 localMin = {0.0f, 0.0f, 0.0f};
glm::vec3 localMax = {0.0f, 0.0f, 0.0f};
};

struct MeshNode {
Expand Down
2 changes: 1 addition & 1 deletion engine/src/assets/Assets/Model/ModelImporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ namespace nexo::assets {
}

LOG(NEXO_INFO, "Loaded mesh {}", mesh->mName.C_Str());
return {mesh->mName.C_Str(), vao, materialComponent, centerLocal};
return {mesh->mName.C_Str(), vao, materialComponent, centerLocal, minBB, maxBB};
}

glm::mat4 ModelImporter::convertAssimpMatrixToGLM(const aiMatrix4x4& matrix)
Expand Down
14 changes: 13 additions & 1 deletion engine/src/components/StaticMesh.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,37 @@
#include "renderer/Attributes.hpp"
#include "renderer/VertexArray.hpp"

#include <glm/glm.hpp>

namespace nexo::components {

struct StaticMeshComponent {
std::shared_ptr<renderer::NxVertexArray> vao;

renderer::RequiredAttributes meshAttributes;

bool hasBounds = false;
glm::vec3 localMin = {0.0f, 0.0f, 0.0f};
glm::vec3 localMax = {0.0f, 0.0f, 0.0f};

struct Memento {
std::shared_ptr<renderer::NxVertexArray> vao;
bool hasBounds;
glm::vec3 localMin;
glm::vec3 localMax;
};

void restore(const Memento &memento)
{
vao = memento.vao;
hasBounds = memento.hasBounds;
localMin = memento.localMin;
localMax = memento.localMax;
}

[[nodiscard]] Memento save() const
{
return {vao};
return {vao, hasBounds, localMin, localMax};
}
};

Expand Down
55 changes: 31 additions & 24 deletions engine/src/systems/RenderCommandSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "components/StaticMesh.hpp"
#include "components/Transform.hpp"
#include "core/event/Input.hpp"
#include "math/Frustum.hpp"
#include "math/Projection.hpp"
#include "math/Vector.hpp"
#include "renderPasses/Masks.hpp"
Expand Down Expand Up @@ -286,36 +287,42 @@ namespace nexo::system {
const auto materialSpan = get<components::MaterialComponent>();
const std::span<const ecs::Entity> entitySpan = m_group->entities();

std::vector<renderer::DrawCommand> drawCommands;
for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) {
const ecs::Entity entity = entitySpan[i];
if (coord->entityHasComponent<components::CameraComponent>(entity) && sceneType != SceneType::EDITOR)
continue;
const auto &transform = transformSpan[i];
const auto &materialAsset = materialSpan[i].material.lock();
std::string shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : "";
const auto &mesh = meshSpan[i];
auto shader = renderer::ShaderLibrary::getInstance().get(shaderStr);
if (!shader)
continue;
drawCommands.push_back(createDrawCommand(
entity,
shader,
mesh,
materialAsset,
transform)
);
for (auto &camera : renderContext.cameras) {
const math::Frustum frustum(camera.viewProjectionMatrix);
std::vector<renderer::DrawCommand> drawCommands;

Comment on lines +290 to +293
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider reserving the drawCommands vector.

partition->count is known upfront. A reserve avoids repeated reallocations in the inner loop.

♻️ Suggested change
         const math::Frustum frustum(camera.viewProjectionMatrix);
         std::vector<renderer::DrawCommand> drawCommands;
+        drawCommands.reserve(partition->count);
🤖 Prompt for AI Agents
In `@engine/src/systems/RenderCommandSystem.cpp` around lines 290 - 293, The
drawCommands vector inside the cameras loop is constructed without reserving
capacity even though partition->count is known; before pushing commands in the
inner loop (inside the for (auto &camera : renderContext.cameras) block where
const math::Frustum frustum(camera.viewProjectionMatrix) is created), call
drawCommands.reserve(partition->count) (or compute an appropriate capacity) so
the vector in RenderCommandSystem::[current function] avoids repeated
reallocations when filling renderer::DrawCommand entries.

for (size_t i = partition->startIndex; i < partition->startIndex + partition->count; ++i) {
const ecs::Entity entity = entitySpan[i];
if (coord->entityHasComponent<components::CameraComponent>(entity) && sceneType != SceneType::EDITOR)
continue;
const auto &transform = transformSpan[i];
const auto &mesh = meshSpan[i];

// Frustum culling: skip entities whose AABB is entirely outside the camera frustum
if (mesh.hasBounds)
{
glm::vec3 worldMin, worldMax;
math::transformAABB(mesh.localMin, mesh.localMax, transform.worldMatrix, worldMin, worldMax);
if (!frustum.intersectsAABB(worldMin, worldMax))
continue;
}

if (coord->entityHasComponent<components::SelectedTag>(entity))
drawCommands.push_back(createSelectedDrawCommand(mesh, materialAsset, transform));
}
const auto &materialAsset = materialSpan[i].material.lock();
std::string shaderStr = materialAsset && materialAsset->isLoaded() ? materialAsset->getData()->shader : "";
auto shader = renderer::ShaderLibrary::getInstance().get(shaderStr);
if (!shader)
continue;
Comment on lines +310 to +314
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Shader lookup inside per-entity loop: silently skipping entities with no shader.

When shaderStr is empty (no material or no shader field), ShaderLibrary::get("") returns null and the entity is silently skipped. This is defensive but could mask configuration issues. Consider a debug-level log or metric for skipped entities to aid troubleshooting missing meshes.

🤖 Prompt for AI Agents
In `@engine/src/systems/RenderCommandSystem.cpp` around lines 310 - 314, The
per-entity loop currently retrieves shaderStr from materialSpan[i].material (via
materialAsset) and calls renderer::ShaderLibrary::getInstance().get(shaderStr),
silently continuing when shader is null; add a debug-level log (or increment a
diagnostic metric/counter) when shaderStr is empty or get(...) returns null so
missing shaders are visible during debugging. Locate the code around
materialSpan, materialAsset, shaderStr and the call to
ShaderLibrary::getInstance().get and emit something like a process/engine debug
log with context (entity id or index and shaderStr) or increment a
"skipped_entities_no_shader" counter before the continue to surface
configuration issues while preserving current control flow.


for (auto &camera : renderContext.cameras) {
for (auto &cmd : drawCommands) {
auto cmd = createDrawCommand(entity, shader, mesh, materialAsset, transform);
cmd.uniforms["uViewProjection"] = camera.viewProjectionMatrix;
cmd.uniforms["uCamPos"] = camera.cameraPosition;
setupLights(cmd, renderContext.sceneLights);
drawCommands.push_back(std::move(cmd));

if (coord->entityHasComponent<components::SelectedTag>(entity))
drawCommands.push_back(createSelectedDrawCommand(mesh, materialAsset, transform));
Comment on lines +322 to +323
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the createSelectedDrawCommand function definition
rg -n 'createSelectedDrawCommand' --type cpp

Repository: NexoEngine/game-engine

Length of output: 606


🏁 Script executed:

# Search for shader files and check for uViewProjection usage
fd -e glsl -e vert -e frag -e vs -e fs -e shader | head -20

Repository: NexoEngine/game-engine

Length of output: 408


🏁 Script executed:

# Also check for shader content in the codebase
rg -n 'uViewProjection' -t cpp

Repository: NexoEngine/game-engine

Length of output: 924


🏁 Script executed:

# Search for "Flat color" and "Albedo unshaded transparent" shader references
rg -n '"Flat color"|"Albedo unshaded transparent"' -t cpp -A3 -B3

Repository: NexoEngine/game-engine

Length of output: 6339


🏁 Script executed:

# Check if the shaders use uViewProjection
cat engine/src/systems/RenderCommandSystem.cpp | sed -n '203,230p'

Repository: NexoEngine/game-engine

Length of output: 1705


🏁 Script executed:

# Check the shader files
cat resources/shaders/flat_color.glsl
cat resources/shaders/albedo_unshaded_transparent.glsl

Repository: NexoEngine/game-engine

Length of output: 1461


🏁 Script executed:

# Compare with RenderBillboardSystem implementation
cat engine/src/systems/RenderBillboardSystem.cpp | sed -n '110,135p'

Repository: NexoEngine/game-engine

Length of output: 1785


🏁 Script executed:

# Check the context around line 322-323
cat engine/src/systems/RenderCommandSystem.cpp | sed -n '315,330p'

Repository: NexoEngine/game-engine

Length of output: 987


Set uViewProjection and uCamPos uniforms on the selected draw command.

Both "Flat color" and "Albedo unshaded transparent" shaders require uViewProjection in their vertex shaders to project the mesh correctly. At line 322-323, the selected draw command is pushed without these uniforms, while regular draw commands (line 317) properly set them. With the per-camera loop structure, this omission causes incorrect rendering of selection masks, especially in multi-camera setups.

Fix: set VP uniforms on the selected draw command
             if (coord->entityHasComponent<components::SelectedTag>(entity))
-                drawCommands.push_back(createSelectedDrawCommand(mesh, materialAsset, transform));
+            {
+                auto selectedCmd = createSelectedDrawCommand(mesh, materialAsset, transform);
+                selectedCmd.uniforms["uViewProjection"] = camera.viewProjectionMatrix;
+                selectedCmd.uniforms["uCamPos"] = camera.cameraPosition;
+                drawCommands.push_back(std::move(selectedCmd));
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (coord->entityHasComponent<components::SelectedTag>(entity))
drawCommands.push_back(createSelectedDrawCommand(mesh, materialAsset, transform));
if (coord->entityHasComponent<components::SelectedTag>(entity))
{
auto selectedCmd = createSelectedDrawCommand(mesh, materialAsset, transform);
selectedCmd.uniforms["uViewProjection"] = camera.viewProjectionMatrix;
selectedCmd.uniforms["uCamPos"] = camera.cameraPosition;
drawCommands.push_back(std::move(selectedCmd));
}
🤖 Prompt for AI Agents
In `@engine/src/systems/RenderCommandSystem.cpp` around lines 322 - 323, The
selected draw command created by createSelectedDrawCommand(mesh, materialAsset,
transform) is missing the camera uniforms so selection shaders don't get
projected; after creating/pushing the selected draw command (the code that calls
coord->entityHasComponent<components::SelectedTag> and
drawCommands.push_back(createSelectedDrawCommand(...))) set the same per-camera
uniforms used for regular draw commands—specifically set "uViewProjection" to
the current camera viewProjection matrix and "uCamPos" to the current camera
position (use the same variables or helper used for the regular draw command
path) so the selected draw command uses the correct VP and camera position for
each camera.

}

camera.pipeline.addDrawCommands(drawCommands);
if (sceneType == SceneType::EDITOR && renderContext.gridParams.enabled)
camera.pipeline.addDrawCommand(createGridDrawCommand(camera, renderContext));
Expand Down
Loading