VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
shadowRendererDirectional.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Created by Arnis Lektauers on 18.10.2025.
5//
7
8#include <algorithm>
9#include <cmath>
10#include <cstring>
11#include <numbers>
12
14#include "renderer.h"
15#include "shadowRenderer.h"
16#include "scene/graphNode.h"
17
18namespace visutwin::canvas
19{
20 ShadowRendererDirectional::ShadowRendererDirectional(const std::shared_ptr<GraphicsDevice>& device,
21 Renderer* renderer, ShadowRenderer* shadowRenderer)
22 : _renderer(renderer), _shadowRenderer(shadowRenderer), _device(device)
23 {
24 }
25
26 // lines 204-216.
27 void ShadowRendererDirectional::generateSplitDistances(Light* light, const float nearDist, const float farDist)
28 {
29 float* distances = light->shadowCascadeDistancesData();
30 const int numCascades = light->numCascades();
31 const float distribution = light->cascadeDistribution();
32
33 // Fill all 4 with farDist as default
34 for (int i = 0; i < 4; ++i) {
35 distances[i] = farDist;
36 }
37
38 for (int i = 1; i < numCascades; ++i) {
39 const float fraction = static_cast<float>(i) / static_cast<float>(numCascades);
40 const float linearDist = nearDist + (farDist - nearDist) * fraction;
41 const float logDist = nearDist * std::pow(farDist / std::max(nearDist, 0.001f), fraction);
42 distances[i - 1] = linearDist + (logDist - linearDist) * distribution;
43 }
44 distances[numCascades - 1] = farDist;
45 }
46
48 {
49 if (!light || !camera || !_shadowRenderer || light->type() != LightType::LIGHTTYPE_DIRECTIONAL) {
50 return;
51 }
52
53 // lines 72-201.
54 // Compute split distances for all cascades.
55 const float nearDist = camera->nearClip();
56 const float farDist = std::min(camera->farClip(), light->shadowDistance());
57 generateSplitDistances(light, nearDist, farDist);
58
59 // Get light direction from the light's node. Directional lights emit along -Y.
60 GraphNode* lightNode = light->node();
61 Vector3 lightDir(0.0f, -1.0f, 0.0f);
62 Quaternion lightRotation;
63 if (lightNode) {
64 const auto& lightWorld = lightNode->worldTransform();
65 lightDir = Vector3(lightWorld.getColumn(1)) * -1.0f;
66 if (lightDir.lengthSquared() < 1e-8f) {
67 lightDir = Vector3(0.0f, -1.0f, 0.0f);
68 } else {
69 lightDir = lightDir.normalized();
70 }
71 lightRotation = lightNode->rotation();
72 }
73
74 // Shadow camera rotation: align -Z with lightDir.
75 // upstream applies the light rotation then rotates -90° on X to map -Y → -Z.
76 const Quaternion pitchDown = Quaternion::fromEulerAngles(-90.0f, 0.0f, 0.0f);
77 const Quaternion shadowRotation = lightRotation * pitchDown;
78
79 // Build shadow-camera rotation matrix for pixel alignment calculations.
80 const Matrix4 shadowRotMat = Matrix4::trs(Vector3(0.0f), shadowRotation, Vector3(1.0f));
81
82 // Camera world transform for transforming frustum corners to world space.
83 const Matrix4 cameraWorldMat = camera->node()
84 ? camera->node()->worldTransform() : Matrix4::identity();
85
86 const int numCascades = light->numCascades();
87 const float* distances = light->shadowCascadeDistances().data();
88 const int resolution = light->shadowResolution();
89
90 for (int cascade = 0; cascade < numCascades; ++cascade) {
91 LightRenderData* lightRenderData = _shadowRenderer->getLightRenderData(light, camera, cascade);
92 if (!lightRenderData || !lightRenderData->shadowCamera) {
93 continue;
94 }
95
96 Camera* shadowCam = lightRenderData->shadowCamera;
97 auto& shadowCamNode = shadowCam->node();
98 if (!shadowCamNode) {
99 continue;
100 }
101
102 // Set cascade viewport/scissor from the light's cascade layout.
103 lightRenderData->shadowViewport = light->cascadeViewports()[cascade];
104 lightRenderData->shadowScissor = light->cascadeViewports()[cascade];
105
106 // Get frustum corners for this cascade's depth slice.
107 const float frustumNear = (cascade == 0) ? nearDist : distances[cascade - 1];
108 const float frustumFar = distances[cascade];
109 auto frustumPoints = camera->getFrustumCorners(frustumNear, frustumFar);
110
111 // Transform corners to world space and compute bounding sphere center.
112 Vector3 center(0.0f);
113 for (int i = 0; i < 8; ++i) {
114 frustumPoints[i] = cameraWorldMat.transformPoint(frustumPoints[i]);
115 center = center + frustumPoints[i];
116 }
117 center = center * (1.0f / 8.0f);
118
119 // Compute bounding sphere radius (max distance from center to any corner).
120 float radius = 0.0f;
121 for (int i = 0; i < 8; ++i) {
122 const float dist = (frustumPoints[i] - center).length();
123 if (dist > radius) {
124 radius = dist;
125 }
126 }
127
128 // Pixel-align shadow camera position to avoid shadow swimming.
129 //lines 136-151.
130 if (resolution > 0 && radius > 0.0f) {
131 // Use per-cascade pixel count for the snap grid. With multi-cascade,
132 // each cascade viewport is a fraction of the full texture (e.g. 0.5×0.5
133 // for 4 cascades in a 2×2 grid). Snapping to the full resolution would
134 // produce a grid 2× too coarse.
135 const float vpSize = light->cascadeViewports()[cascade].getZ(); // normalized width
136 const float cascadeRes = static_cast<float>(resolution) * vpSize;
137 const float sizeRatio = 0.25f * cascadeRes / radius;
138
139 // Extract shadow camera axes from rotation matrix.
140 const Vector3 right = Vector3(shadowRotMat.getColumn(0));
141 const Vector3 up = Vector3(shadowRotMat.getColumn(1));
142 const Vector3 forward = Vector3(shadowRotMat.getColumn(2));
143
144 // Project center onto shadow camera axes, snap, reconstruct.
145 const float x = std::ceil(center.dot(up) * sizeRatio) / sizeRatio;
146 const float y = std::ceil(center.dot(right) * sizeRatio) / sizeRatio;
147 const float z = center.dot(forward);
148
149 center = up * x + right * y + forward * z;
150 }
151
152 // Position shadow camera far behind the center, looking along lightDir.
153 // upstream positions at center + forward * 1,000,000 initially for culling,
154 // then tightens near/far to the actual caster depth range (lines 190-197).
155 shadowCamNode->setRotation(shadowRotation);
156 shadowCamNode->setPosition(center);
157 shadowCamNode->translateLocal(0.0f, 0.0f, 1000000.0f);
158
159 // Set orthographic projection to encompass the cascade's bounding sphere.
161 shadowCam->setOrthoHeight(radius);
162 shadowCam->setNearClip(0.01f);
163 shadowCam->setFarClip(2000000.0f);
164 shadowCam->setAspectRatio(1.0f);
165
166 // tighten shadow camera near/far to the depth range of
167 // the frustum slice for maximum depth precision in the shadow map.
168 //lines 190-197.
169 // Without per-cascade caster culling, we approximate using the frustum points'
170 // depth range (which bounds the receivers). A generous margin is added for
171 // shadow casters that may be outside the camera frustum.
172 {
173 const Matrix4 shadowCamView = shadowCamNode->worldTransform().inverse();
174 float depthMin = 1e30f;
175 float depthMax = -1e30f;
176 for (int i = 0; i < 8; ++i) {
177 const float z = shadowCamView.transformPoint(frustumPoints[i]).getZ();
178 if (z < depthMin) depthMin = z;
179 if (z > depthMax) depthMax = z;
180 }
181
182 // Reposition: translate forward so near plane is just behind closest point.
183 // Upstream: shadowCamNode.translateLocal(0, 0, depthRange.max + 0.1)
184 shadowCamNode->translateLocal(0.0f, 0.0f, depthMax + 0.1f);
185
186 // Set tight far clip covering the frustum depth range.
187 // Upstream: shadowCam.farClip = depthRange.max - depthRange.min + 0.2
188 shadowCam->setFarClip(depthMax - depthMin + 0.2f);
189 }
190
191 // Build the viewport-scaled shadow matrix for this cascade:
192 // shadowMatrix = viewportMatrix × shadowCamProj × shadowCamView
193 // The viewport matrix maps NDC to the cascade's sub-region of the atlas.
194 const Matrix4 shadowView = shadowCamNode->worldTransform().inverse();
195 const Matrix4 shadowVP = shadowCam->projectionMatrix() * shadowView;
196
197 const Vector4& vp = light->cascadeViewports()[cascade];
198 // upstream Mat4.setViewport: maps clip coords to viewport sub-region.
199 // Metal texture origin is top-left (vs OpenGL bottom-left), which flips the
200 // Y axis. This is handled by negating the Y scale; the Y translate stays the
201 // same as upstream because the viewport coordinates already correspond to
202 // Metal's top-left-origin coordinate system. Remaps NDC [-1,1] → [0,1] for Z.
203 Matrix4 viewportMatrix = Matrix4::identity();
204 viewportMatrix.setElement(0, 0, vp.getZ() * 0.5f); // X: scale by width/2
205 viewportMatrix.setElement(3, 0, vp.getX() + vp.getZ() * 0.5f); // X: translate to region center
206 // Metal: negative Y scale maps NDC Y to top-down texture V; the translate
207 // is the same as upstream (no extra 1-y flip needed) because Metal viewport
208 // and texture coordinates share the same top-left origin.
209 viewportMatrix.setElement(1, 1, -vp.getW() * 0.5f); // Y: scale by -height/2 (Metal Y flip)
210 viewportMatrix.setElement(3, 1, vp.getY() + vp.getW() * 0.5f); // Y: translate to region center
211 // Z: map from OpenGL NDC [-1,1] to Metal [0,1]
212 // Note: shadow-vertex.metal applies clip.z = 0.5*(clip.z+clip.w), so depth
213 // is already in [0,1] after vertex shader. The projection matrix produces
214 // OpenGL NDC z in [-1,1], but we bake the [0,1] mapping here since the
215 // shader reads the final shadow depth directly.
216 viewportMatrix.setElement(2, 2, 0.5f); // Z: scale by 0.5
217 viewportMatrix.setElement(3, 2, 0.5f); // Z: bias by 0.5
218
219 const Matrix4 shadowMatrix = viewportMatrix * shadowVP;
220
221 // Store in the light's matrix palette (column-major, 16 floats per cascade).
222 //_shadowMatrixPalette.set(data, face * 16).
223 // Matrix4 is 64 bytes on all SIMD backends — memcpy directly (same H1 fix
224 // as SkinBatchInstance::updateMatrices).
225 float* palette = light->shadowMatrixPaletteData();
226 std::memcpy(&palette[cascade * 16], &shadowMatrix, 64);
227 }
228 }
229
230 std::shared_ptr<RenderPass> ShadowRendererDirectional::getLightRenderPass(Light* light, Camera* camera,
231 const int face, const bool clearRenderTarget, const bool allCascadesRendering)
232 {
233 if (!_shadowRenderer || !_device || !light || !camera || light->type() != LightType::LIGHTTYPE_DIRECTIONAL) {
234 return nullptr;
235 }
236
237 // Prepare all cascade faces (each gets its render target assigned).
238 const int faceCount = light->numShadowFaces();
239 Camera* shadowCamera = nullptr;
240 for (int f = 0; f < faceCount; ++f) {
241 shadowCamera = _shadowRenderer->prepareFace(light, camera, f);
242 }
243 if (!shadowCamera) {
244 return nullptr;
245 }
246
247 auto renderPass = std::make_shared<RenderPassShadowDirectional>(_device, _shadowRenderer, light, camera, shadowCamera, face,
248 allCascadesRendering);
249 _shadowRenderer->setupRenderPass(renderPass.get(), shadowCamera, clearRenderTarget);
250 return renderPass;
251 }
252
254 const std::unordered_map<Camera*, std::vector<Light*>>& cameraDirShadowLights)
255 {
256 if (!frameGraph || !_shadowRenderer || !_device) {
257 return;
258 }
259
260 for (const auto& [camera, lights] : cameraDirShadowLights) {
261 if (!camera) {
262 continue;
263 }
264 for (auto* light : lights) {
265 if (!light || light->type() != LightType::LIGHTTYPE_DIRECTIONAL) {
266 continue;
267 }
268 if (!_shadowRenderer->needsShadowRendering(light)) {
269 continue;
270 }
271
272 // Single render pass per light — the pass internally loops over all cascades
273 // with per-cascade viewport/scissor.
274 auto renderPass = getLightRenderPass(light, camera, 0, true, true);
275 if (renderPass) {
276 frameGraph->addRenderPass(renderPass);
277 }
278 }
279 }
280 }
281}
Perspective or orthographic camera with projection matrix, jitter (TAA), and render target binding.
Definition camera.h:40
void setProjection(ProjectionType value)
Definition camera.cpp:17
float farClip() const
Definition camera.h:57
std::array< Vector3, 8 > getFrustumCorners(const float nearDist, const float farDist) const
Definition camera.h:126
void setOrthoHeight(const float value)
Definition camera.h:61
float nearClip() const
Definition camera.h:54
void setAspectRatio(float value)
Definition camera.cpp:25
const Matrix4 & projectionMatrix()
Definition camera.h:65
const std::unique_ptr< GraphNode > & node() const
Definition camera.h:102
void setFarClip(const float value)
Definition camera.h:58
void setNearClip(const float value)
Definition camera.h:55
void addRenderPass(const std::shared_ptr< RenderPass > &renderPass)
Hierarchical scene graph node with local/world transforms and parent-child relationships.
Definition graphNode.h:28
const Matrix4 & worldTransform()
Definition graphNode.cpp:86
Directional, point, spot, or area light with shadow mapping and cookie projection.
Definition light.h:54
int shadowResolution() const
Definition light.h:113
float * shadowCascadeDistancesData()
Definition light.h:97
float shadowDistance() const
Definition light.h:110
float * shadowMatrixPaletteData()
Definition light.h:95
const std::array< float, 4 > & shadowCascadeDistances() const
Definition light.h:96
int numCascades() const
Definition light.cpp:42
float cascadeDistribution() const
Definition light.h:87
int numShadowFaces() const
Definition light.cpp:31
const std::array< Vector4, 4 > & cascadeViewports() const
Definition light.h:93
LightType type() const
Definition light.h:70
GraphNode * node() const
Definition light.h:107
Per-face shadow rendering data: shadow camera, viewport, and scissor.
Definition light.h:25
void buildNonClusteredRenderPasses(FrameGraph *frameGraph, const std::unordered_map< Camera *, std::vector< Light * > > &cameraDirShadowLights)
ShadowRendererDirectional(const std::shared_ptr< GraphicsDevice > &device, Renderer *renderer, ShadowRenderer *shadowRenderer)
static void generateSplitDistances(Light *light, float nearDist, float farDist)
std::shared_ptr< RenderPass > getLightRenderPass(Light *light, Camera *camera, int face, bool clearRenderTarget, bool allCascadesRendering)
4x4 column-major transformation matrix with SIMD acceleration.
Definition matrix4.h:31
void setElement(const int col, int row, const float value)
Definition matrix4.h:376
static Matrix4 identity()
Definition matrix4.h:108
Vector3 transformPoint(const Vector3 &v) const
Matrix4 inverse() const
Vector4 getColumn(int col) const
static Matrix4 trs(const Vector3 &t, const Quaternion &r, const Vector3 &s)
Unit quaternion for rotation representation with SIMD-accelerated slerp and multiply.
Definition quaternion.h:20
static Quaternion fromEulerAngles(float ax, float ay, float az)
3D vector for positions, directions, and normals with multi-backend SIMD acceleration.
Definition vector3.h:29
Vector3 normalized() const
float dot(const Vector3 &other) const
float lengthSquared() const
Definition vector3.h:233
4D vector for homogeneous coordinates, color values, and SIMD operations.
Definition vector4.h:20
float getX() const
Definition vector4.h:85
float getY() const
Definition vector4.h:98