Cogs.Core
LightSystem.cpp
1#ifdef _WIN32
2// TODO: Remove when we update past GLM 0.9.9.7
3#pragma warning(push)
4#pragma warning(disable: 4127) // conditional expression is constant
5#include <glm/ext/matrix_clip_space.hpp>
6#pragma warning(pop)
7#endif
8
9#include "LightSystem.h"
10
11#include "Rendering/ICapabilities.h"
12
13#include "Context.h"
14#include "Services/Time.h"
15
16#include "Scene/GetBounds.h"
17
18#include "Systems/Core/TransformSystem.h"
19#include "Systems/Core/CameraSystem.h"
20
21#include "Renderer/CullingManager.h"
22#include "Renderer/IRenderer.h"
23
24#include "Resources/TextureManager.h"
25#include "Resources/MaterialManager.h"
26
27#include "Utilities/Parsing.h"
28#include "Utilities/Math.h"
29
30#include <glm/glm.hpp>
31#include <glm/gtx/compatibility.hpp>
32#include <glm/gtc/color_space.hpp>
33
34namespace
35{
36 using namespace Cogs::Core;
37
38 struct CamState
39 {
40 const CameraData * cameraData = nullptr;
41 glm::mat4 rawInverseProjection;
42 glm::mat4 rawInverseViewProjection;
43 };
44
45 static const glm::vec3 corners[] =
46 {
47 { -1, -1, -1 },
48 { 1, -1, -1 },
49 { 1, 1, -1 },
50 { -1, 1, -1 },
51 { -1, -1, 1 },
52 { 1, -1, 1 },
53 { 1, 1, 1 },
54 { -1, 1, 1 },
55 };
56
57 static const uint32_t edgeIndicesData[][2] = {
58 {0, 1},
59 {0, 2},
60 {0, 4},
61 {1, 3},
62 {1, 5},
63 {2, 3},
64 {2, 6},
65 {3, 7},
66 {4, 5},
67 {4, 6},
68 {5, 7},
69 {6, 7}
70 };
71
72 static void clipLineSegments(std::vector<glm::vec3>& E, const glm::vec4 plane)
73 {
74 size_t o = 0;
75 for (size_t i = 0; i < E.size(); i += 2) {
76 const auto a = E[i + 0];
77 const auto b = E[i + 1];
78 const float d0 = glm::dot(glm::vec4(a, 1.f), plane);
79 const float d1 = glm::dot(glm::vec4(b, 1.f), plane);
80
81 bool b0 = 0.f <= d0;
82 bool b1 = 0.f <= d1;
83
84 auto d = b - a;
85 auto num = glm::dot(d, glm::vec3(plane));
86 if (glm::abs(num) < 0.01f) { // close to parallel, discard and let other edges find intersection.
87 b0 = b1 = false;
88 }
89 if (b0) E[o++] = a;
90 if (b1) E[o++] = b;
91 if (b0 == b1) continue;
92
93 // <a + t(b-a), m> = 0 <=> <a,m> + t<b-a,m> = 0 <=> t = -<a,m> / <b-a,m>
94
95 auto t = -glm::dot(glm::vec4(a, 1.f), plane) / num;
96
97 E[o++] = a + t * d;
98 }
99 E.resize(o);
100 assert((E.size() & 1) == 0);
101
102 }
103
104 static glm::mat4 rotateVecToY(const glm::vec2& d)
105 {
106 auto l2 = glm::dot(d, d);
107 float c = 1.f;
108 float s = 0.f;
109 if (0.001f <= l2) {
110 const auto r = 1.f / glm::sqrt(l2);
111 c = r * d.y; // Cosine of rotation
112 s = r * d.x; // Sine of rotation
113 }
114 return glm::mat4( c, s, 0, 0, // Rotation about Z matrix.
115 -s, c, 0, 0,
116 0, 0, 1, 0,
117 0, 0, 0, 1);
118 }
119
120 static glm::mat4 createLightViewMatrix(const glm::vec4 & /*cascadeLine*/, const glm::quat& rotation)
121 {
122 const glm::mat4 lightView = glm::mat4_cast(glm::conjugate(rotation));
123
124 auto lz = glm::abs(euclidean(lightView * glm::vec4(0, 0, 1, 1)));
125
126 glm::vec3 align_axis = glm::vec3(0, 1, 0);
127 if (lz.y > lz.x && lz.y > lz.z) {
128 // light mainly from y-dir, align light y with z.
129 align_axis = glm::vec3(0, 0, 1);
130 }
131
132 // Try to rotate such that the light view Y-axis aligns with the cascade-line,
133 // so the cascade frustum enclose the cascade as tight as possible.
134 auto cascadeAxisLW = glm::vec3(glm::vec2(glm::mat3(lightView) * glm::vec3(align_axis)), 0.f);
135 return rotateVecToY(cascadeAxisLW) * lightView;
136 }
137
138 static glm::mat4 createRotaryAlignmentMatrix(const glm::mat4& M)
139 {
140 auto ro = euclidean(M * glm::vec4(0, 0, 0, 1));
141 auto rx = euclidean(M * glm::vec4(1, 0, 0, 1)) - ro;
142 auto ry = euclidean(M * glm::vec4(0, 1, 0, 1)) - ro;
143 auto rz = euclidean(M * glm::vec4(0, 0, 1, 1)) - ro;
144
145 auto xz = glm::abs(rx.z);
146 auto yz = glm::abs(ry.z);
147 auto zz = glm::abs(rz.z);
148
149 glm::vec2 d;
150 if (xz < yz && xz < zz) {
151 d = glm::vec2(rx);
152 }
153 else if (yz < zz) {
154 d = glm::vec2(ry);
155 }
156 else {
157 d = glm::vec2(rz);
158 }
159 return rotateVecToY(d);
160 }
161
162 static void createFrustumFitMatrix(Context * context,
163 glm::mat4& rawCascadeProjectionMatrix,
164 glm::mat4& rawCascadeCullMatrix,
165 LightCascadeFrustaPoints* frustaPoints,
166 const CameraData* refCamData,
167 const std::vector<CamState>& camStates,
168 const glm::mat4& lightView,
169 const glm::vec4& cascadeLine,
170 const float n, const float f,
171 float frustumSlack,
172 uint32_t resolution,
173 glm::uvec2 &blueNoiseOffset)
174 {
175
176 auto toCull = createRotaryAlignmentMatrix(lightView * refCamData->inverseViewMatrix);
177
178 glm::vec3 min_lv(std::numeric_limits<float>::max());
179 glm::vec3 min_cv = min_lv;
180 glm::vec3 max_lv(-std::numeric_limits<float>::max());
181 glm::vec3 max_cv = max_lv;
182
183 for (const auto & camState : camStates) {
184 const auto M = lightView * camState.rawInverseViewProjection;
185 if (camState.cameraData == refCamData) {
186 // cascadeLine is per definition aligned with the reference camera,
187 // so we can avoid the more elaborate and numerically challenging
188 // approach of arbitrary clipping.
189
190 // First, we find the z and w clip-space values of points on the near
191 // and far-planes.
192 const auto row2 = glm::transpose(refCamData->rawProjectionMatrix)[2];
193 const auto row3 = glm::transpose(refCamData->rawProjectionMatrix)[3];
194 const auto zn = glm::dot(row2, glm::vec4(0, 0, -n, 1));
195 const auto wn = glm::dot(row3, glm::vec4(0, 0, -n, 1));
196 const auto zf = glm::dot(row2, glm::vec4(0, 0, -f, 1));
197 const auto wf = glm::dot(row3, glm::vec4(0, 0, -f, 1));
198
199 // Then we transform the corners of the truncated frustum into light
200 // space, and use this to form a bounding box.
201 for (unsigned i = 0; i < 4; i++) {
202 const auto pn = euclidean(M * glm::vec4(wn*glm::vec2(corners[i]), zn, wn));
203 const auto pf = euclidean(M * glm::vec4(wf*glm::vec2(corners[i]), zf, wf));
204 if (frustaPoints) {
205 frustaPoints->points.push_back(pn);
206 frustaPoints->points.push_back(pf);
207 }
208 min_lv = glm::min(min_lv, glm::min(pn, pf));
209 max_lv = glm::max(max_lv, glm::max(pn, pf));
210
211 min_cv = glm::min(min_cv, glm::min(glm::vec3(toCull * glm::vec4(pn, 1)), // toCull is just a rotation, so it is safe to skip 1/w.
212 glm::vec3(toCull * glm::vec4(pf, 1))));
213 max_cv = glm::max(max_cv, glm::max(glm::vec3(toCull * glm::vec4(pn, 1)),
214 glm::vec3(toCull * glm::vec4(pf, 1))));
215 }
216 }
217 else {
218 // Cascade-line is not aligned, so we use a more elaborate approach:
219 // First, we transform the (un-truncated) view frustum to light space.
220 glm::vec3 p[8];
221 for (unsigned i = 0; i < 8; i++) {
222 p[i] = euclidean(M * glm::vec4(corners[i], 1.f));
223 }
224
225 // And we form all edges of the frustum. We will clip these edges against
226 // planes orthogonal to the cascade line at near and far values. In the
227 // end, the clipped edges will span the convex hull of the clipped frustum.
228 // We use the end-points of the clipped edges to form a bounding box.
229 std::vector<glm::vec3> E;
230 for (const auto & e : edgeIndicesData) {
231 E.push_back(p[e[0]]);
232 E.push_back(p[e[1]]);
233 }
234 // Assuming ligthView is just rotations (i.e., no need for inverse-transpose).
235 clipLineSegments(E, lightView * glm::vec4(glm::vec3(cascadeLine), cascadeLine.w - n));
236 clipLineSegments(E, -lightView * glm::vec4(glm::vec3(cascadeLine), cascadeLine.w - f));
237 for (const auto & pp : E) {
238 if (frustaPoints) {
239 frustaPoints->points.push_back(pp);
240 }
241 min_lv = glm::min(min_lv, pp);
242 max_lv = glm::max(max_lv, pp);
243
244 min_cv = glm::min(min_cv, glm::vec3(toCull * glm::vec4(pp, 1)));
245 max_cv = glm::max(max_cv, glm::vec3(toCull * glm::vec4(pp, 1)));
246 }
247 }
248
249 if (frustaPoints) {
250 frustaPoints->offsets.push_back(unsigned(frustaPoints->points.size()));
251 }
252 }
253
254 // Quadratic viewport that exactly fits the frustum. This viewport will be
255 // slightly expanded with a margin later.
256 const float size = std::max((max_lv.x - min_lv.x),
257 (max_lv.y - min_lv.y));
258 glm::vec2 center_lv = 0.5f * glm::vec2(min_lv + max_lv);
259 const float zNear = -max_lv.z - std::max(0.1f, frustumSlack) * (max_lv.z - min_lv.z);
260 const float zFar = -min_lv.z + frustumSlack * (max_lv.z - min_lv.z);
261
262 // Check if viewport fits in the frustum used in the previous frame, and is
263 // not way smaller. If it is an OK fit, we recylce the frustum to minimize
264 // jittering of the shadows when the view changes slightly.
265 bool needNewFrustum = frustumSlack == 0.f;
266 const float factor = 0.5f * (1.f + frustumSlack);
267 const float greaterThanValue = glm::max(0.f, 1.f - 2.f * frustumSlack);
268 for (unsigned i = 0; i < 8u; i++) {
269 glm::vec4 p = rawCascadeProjectionMatrix * glm::vec4(center_lv.x + 0.5 * (((i >> 0) & 1) ? -size : size),
270 center_lv.y + 0.5 * (((i >> 1) & 1) ? -size : size),
271 ((i >> 2) & 1) ? min_lv.z : max_lv.z,
272 1.f);
273
274 bool xOk = ((-p.w <= p.x) && (p.x <= -greaterThanValue * p.w)) || ((greaterThanValue * p.w <= p.x) && (p.x <= p.w));
275 bool yOk = ((-p.w <= p.y) && (p.y <= -greaterThanValue * p.w)) || ((greaterThanValue * p.y <= p.y) && (p.y <= p.w));
276 bool zOk = ((-p.w <= p.z) && (p.z <= -0.5f * p.w)) || ((0.5f * p.z <= p.z) && (p.z <= p.w));
277 needNewFrustum = needNewFrustum || !(xOk && yOk && zOk);
278 }
279
280 if (needNewFrustum) {
281 // Last frame's frustum didn't match, so we create a new one.
282
283 {
284 // Adjust for origin offset.
285 const float steppyness = 5.f;
286 const float texelSize = std::exp2(std::ceil(log2(size / resolution) * steppyness) / steppyness);
287 const glm::dvec3 origin = context->transformSystem->getOrigin();
288 const glm::dvec3 offset = glm::dmat3(lightView) * origin;
289 const double snappX = std::floor((center_lv.x + offset.x) / texelSize);
290 const double snappY = std::floor((center_lv.y + offset.y) / texelSize);
291 double snapX_ = std::fmod(snappX, 64.0);
292 if (snapX_ < 0.0) snapX_ += 64;
293 double snapY_ = std::fmod(snappY, 64.0);
294 if (snapY_ < 0.0) snapY_ += 64;
295
296 blueNoiseOffset = glm::uvec2(static_cast<uint32_t>(snapX_), static_cast<uint32_t>(snapY_));
297 blueNoiseOffset = glm::uvec2(blueNoiseOffset.x, (64u - blueNoiseOffset.y) % 64u);
298 }
299
300 // Grow viewport slightly
301 const glm::vec2 viewportMin = center_lv - glm::vec2(factor * size);
302 const glm::vec2 viewportMax = center_lv + glm::vec2(factor * size);
303 if (frustaPoints) {
304 frustaPoints->viewportMin = viewportMin;
305 frustaPoints->viewportMax = viewportMax;
306 }
307 rawCascadeProjectionMatrix = glm::ortho(viewportMin.x, viewportMax.x,
308 viewportMin.y, viewportMax.y,
309 zNear, zFar);
310 }
311 rawCascadeCullMatrix = glm::ortho(min_cv.x, max_cv.x,
312 min_cv.y, max_cv.y,
313 zNear, zFar) * toCull;
314 }
315
316 static void getCameras(Context* context, const CameraData* & refCamData, std::vector<CamState>& camStates, const LightComponent& light)
317 {
318 if (auto e = light.lodReference.lock(); e) {
319 if (auto * c = e->getComponent<CameraComponent>(); c && ((c->lightingMask & light.lightingLayer) != LightingLayers::None)) {
320 refCamData = &context->cameraSystem->getData(c);
321 }
322 }
323 if (!refCamData) {
324 refCamData = &context->cameraSystem->getMainCameraData();
325 }
326
327 for (auto & we : light.cameras) {
328 if (auto e = we.lock(); e) {
329 if (const auto * c = e->getComponent<CameraComponent>(); c && ((c->lightingMask & light.lightingLayer) != LightingLayers::None)) {
330 camStates.emplace_back();
331 camStates.back().cameraData = &context->cameraSystem->getData(c);
332 }
333 }
334 }
335 if (camStates.empty() && refCamData) {
336 camStates.emplace_back();
337 camStates.back().cameraData = refCamData;
338 }
339 for (auto & camState : camStates) {
340 camState.rawInverseViewProjection = glm::inverse(camState.cameraData->rawViewProjection);
341 }
342 }
343
344 static void calculateCascadeCount(LightData & lightData, float zNear, float zFar, float FOV)
345 {
346 if(!lightData.dynamicCascadeCount) return;
347 float a = FOV*0.5f;
348 float tana = tanf(a);
349 float ns = tana*zNear;
350 float fs = tana*zFar;
351 float r = fs/ns-1.0f;
352 lightData.numViewports = (uint16_t)ceil(r);
353 lightData.numViewports = std::max(lightData.numViewports, (uint16_t)1);
354 lightData.numViewports = std::min(lightData.numViewports, (uint16_t)lightData.maxViewports);
355 }
356
357 static void calculateSplits(Context* context, LightData & lightData, float zNear, float zFar)
358 {
359 const auto expFactor = context->variables->get("shadows.cascades.expFactor")->getFloat();
360 const auto overlapFactor = 0.5f * context->variables->get("shadows.cascades.overlapFactor")->getFloat();
361
362
363 const float cascadeWidth = 1.f / static_cast<float>(lightData.numViewports);
364 if(lightData.numViewports == 1){
365 lightData.nearDepths[0] = zNear;
366 lightData.farDepths[0] = zFar;
367 }
368 else{
369 for (int i = 0; i < lightData.numViewports; ++i) {
370 const float iF = std::min(1.f, cascadeWidth * (static_cast<float>(i) + 1.f + overlapFactor));
371 const float iN = std::max(0.f, cascadeWidth * (static_cast<float>(i) - overlapFactor));
372
373 const float zExpNear = zNear * glm::pow(zFar / zNear, iN);
374 const float zLinearNear = zNear + iN * (zFar - zNear);
375 lightData.nearDepths[i] = glm::max(glm::lerp(zLinearNear, zExpNear, expFactor), zNear);
376 assert(std::isfinite(lightData.nearDepths[i]));
377
378 const float zExp = zNear * glm::pow(zFar / zNear, iF);
379 const float zLinear = zNear + iF * (zFar - zNear);
380
381 lightData.farDepths[i] = glm::lerp(zLinear, zExp, expFactor);
382 assert(std::isfinite(lightData.farDepths[i]));
383 }
384 }
385 }
386
387 static float calculateSplits(Context * context, LightData & lightData, const glm::mat4& rotation, const CameraData* refCamData, const std::vector<CamState>& camStates, const float maxShadowDistance)
388 {
389 // Determine axis along which we will calculate cascades.
390 const glm::mat4 & M = refCamData->inverseViewMatrix;
391 const glm::vec3 o = glm::vec3(M * glm::vec4(0, 0, 0, 1));
392 const glm::vec3 a = glm::normalize(glm::vec3(M * glm::vec4(0, 0, -1, 0)));
393 const float d = -glm::dot(o, a);
394 assert(std::isfinite(d));
395 lightData.cascadeLine = glm::vec4(a, d);
396
397 // Determine range along cascade axis
398 auto zNear = refCamData->nearDistance;
399 auto zFar = refCamData->farDistance;
400 for (auto & camState : camStates) {
401 for (auto c : corners) {
402 auto q = camState.rawInverseViewProjection * glm::vec4(c, 1.f);
403 if (std::numeric_limits<float>::epsilon() < q.w) {
404 auto t = glm::dot(a, (1.f / q.w)*glm::vec3(q)) + d;
405 assert(std::isfinite(t));
406
407 zNear = glm::min(zNear, t);
408 zFar = glm::max(zFar, t);
409 }
410 }
411 }
412
413 if(!lightData.tightShadowBounds){
414 calculateCascadeCount(lightData, zNear, zFar, refCamData->fieldOfView);
415 calculateSplits(context, lightData, refCamData->nearDistance, std::min(zFar, refCamData->nearDistance + maxShadowDistance));
416 return std::max(zNear, refCamData->nearDistance - maxShadowDistance);
417 }
418 else{
419 const Cogs::Geometry::BoundingBox bbox = context->bounds->getShadowBounds(context);
420 const glm::vec3 bbox_corners[] = {
421 glm::vec3(bbox.min.x, bbox.min.y, bbox.min.z),
422 glm::vec3(bbox.max.x, bbox.min.y, bbox.min.z),
423 glm::vec3(bbox.max.x, bbox.max.y, bbox.min.z),
424 glm::vec3(bbox.min.x, bbox.max.y, bbox.min.z),
425 glm::vec3(bbox.min.x, bbox.min.y, bbox.max.z),
426 glm::vec3(bbox.max.x, bbox.min.y, bbox.max.z),
427 glm::vec3(bbox.max.x, bbox.max.y, bbox.max.z),
428 glm::vec3(bbox.min.x, bbox.max.y, bbox.max.z),
429 };
430 const glm::vec2 viewport_corners[] = { {-1, -1}, {1, -1}, {1, 1}, {-1, 1} };
431
432 // zmin, zmax: bounding box z-extent (cascade line extent)
433 float zmin = std::numeric_limits<float>::max();
434 float zmax = 0.0f;
435
436 //glm::vec3 lightDir = glm::vec3(lightData.lightDirection);
437 glm::vec3 lightDir = glm::mat3(rotation) * glm::vec3(0, 0, -1);
438
439 // Construct a shadow frustrum slice (A plane containing the light vector):
440 glm::vec3 planeTangent = glm::cross(a, lightDir);
441 glm::vec3 n = glm::normalize(glm::cross(lightDir, planeTangent)); // Plane Normal
442
443 for (auto &camState : camStates) {
444 const CameraData &cameraData = *camState.cameraData;
445 const glm::vec3 ndcLightDir = euclidean(cameraData.rawProjectionMatrix*glm::vec4(glm::mat3(cameraData.viewMatrix)*lightDir, 1.0f));
446 const float eps = 0.0001f;
447 const bool along = ndcLightDir.z <= 0.0f;
448
449 // Find z-range for bounding volume
450 float azmin = std::numeric_limits<float>::max();
451 float azmax = -std::numeric_limits<float>::max();
452 for (const glm::vec3 &p0 : bbox_corners){
453 float t = glm::dot(a, p0) + d;
454 azmin = std::min(azmin, t);
455 azmax = std::max(azmax, t);
456 }
457
458 // Check that shadows cannot travel infinitely far into the view frustrum.
459 // Is camPos+lightDir inside the view frustrum? Then don't cull the with this camera.
460 if(std::abs(ndcLightDir.x)-eps <= 1.0f && std::abs(ndcLightDir.y)-eps <= 1.0f){// && ndcLightDir.z <= 0.0f){
461 zmin = zNear;
462 zmax = zFar;
463 if(along) zmin = std::max(zmin, azmin);
464 else zmax = std::min(zmax, azmax);
465 break;
466 }
467
468 // Generate a slice for all points on the bbox
469 for (const glm::vec3 &p0 : bbox_corners){
470 // Intersect with frustrum
471 for (const glm::vec2 &corn : viewport_corners){
472 // Frustrum corner line:
473 glm::vec3 l0 = glm::vec3(cameraData.inverseViewMatrix * glm::vec4(0, 0, 0, 1));
474 glm::vec3 l = glm::normalize(euclidean(camState.rawInverseViewProjection * glm::vec4(corn, 1.0f, 1.f))-l0);
475 // Intersect plane with frustrum lines:
476 float LdotN = glm::dot(l, n);
477 float ld = glm::dot(p0-l0, n)/LdotN;
478 glm::vec3 p = l0+ld*l;
479 float t = glm::dot(a, p) + d;
480
481 if(along) t = std::max(t, azmin);
482 else t = std::min(t, azmax);
483
484 t = std::max(0.0f, t);
485 zmin = std::min(zmin, t);
486 zmax = std::max(zmax, t);
487 }
488 }
489 }
490
491 zNear = glm::max(zNear, zmin);
492 zFar = glm::min(zFar, zmax);
493
494 zFar = glm::max(zNear, zFar);
495
496 zNear = std::max(zNear, refCamData->nearDistance);
497 zFar = std::min(zFar, refCamData->nearDistance + maxShadowDistance);
498 calculateCascadeCount(lightData, zNear, zFar, refCamData->fieldOfView);
499 calculateSplits(context, lightData, zNear, zFar);
500 return zNear;
501 }
502 }
503} // namespace ...
504
506{
508 context->variables->set("shadows.cascades.expFactor", 0.9f);
509 context->variables->set("shadows.cascades.overlapFactor", 0.2f);
510 context->variables->set("renderer.maxShadowDistance", 30000.0f);
511}
512
514{
515}
516
518{
519 const bool originOnTop = context->device->getCapabilities()->getDeviceCapabilities().OriginOnTop;
520
521 if (!cascadeArray) {
522 cascadeArray = context->textureManager->create();
523 cascadeArray->setName("Light.ShadowCascades");
524 }
525
526 bool useTextureCubeArrays = context->device->getCapabilities()->getDeviceCapabilities().TextureCubeArrays;
527 if(context->device->getType() == GraphicsDeviceType::WebGPU){
528 useTextureCubeArrays = false; // TODO: Add support for texture cube arrays for WebGPU
529 }
530
531 if (!cubeArray) {
532 cubeArray = context->textureManager->create();
533 if(useTextureCubeArrays)
534 cubeArray->setName("Light.ShadowCubeArray");
535 else
536 cubeArray->setName("Light.ShadowCube");
537 }
538
539 auto transformSystem = context->transformSystem;
540 auto variables = context->variables.get();
541
542 softShadows = parseEnum<SoftShadows>(variables->get("shadows.softShadows", "Default"));
543 auto shadowUpdate = parseEnum<ShadowUpdate>(variables->get("shadows.update", "Default"));
544 const auto pointShadowResolution = static_cast<unsigned>(std::max(1, variables->get("shadows.pointShadowResolution", 256)));
545
546 unsigned cascadeShadowResolution = (unsigned)std::max(0, variables->get("shadows.cascadeShadowResolution", 1024));
547
548 // How much larger the light frustum will be to avoid jittery shadows when camera moves
549 const float frustumSlack = glm::clamp(variables->get("shadows.frustumSlack", 0.1f), 0.f, 1.f);
550
551 const auto pointShadowFormat = parseTextureFormat(variables->get("shadows.pointShadowFormat", "R32_TYPELESS"));
552 const auto cascadeShadowFormat = parseTextureFormat(variables->get("shadows.cascadeShadowFormat", "R32_TYPELESS"));
553 const bool shadowsEnabled = variables->get("renderer.shadowsEnabled", false);
554
555 const bool lightSystemRun = variables->getOrAdd("lightSystem.run", true);
556 if (!lightSystemRun) {
557 return;
558 }
559
560 bool anyChanged = false;
561 for (const auto & light : pool) {
562 anyChanged |= light.hasChanged();
563 }
564
565 lightsChanged |= anyChanged;
566
567 uint32_t cascadeInstances = 0;
568 uint32_t layerCount = 0;
569
570 uint32_t cubeInstances = 0;
571 for (const auto & light : pool) {
572 const auto transformComponent = light.getComponent<TransformComponent>();
573
574 auto & lightData = getData(&light);
575 lightData.enabled = light.enabled;
576 lightData.castShadows = light.enabled && light.castShadows && shadowsEnabled;
577 lightData.tightShadowBounds = light.tightShadowBounds;
578 lightData.dynamicCascadeCount = light.dynamicCascadeCount;
579 lightData.lightColor = glm::vec4(glm::convertSRGBToLinear(glm::vec3(light.lightColor)),
580 light.lightColor.a);
581
582 if (!light.enabled) continue;
583
584 lightData.shadowIntensityOffset = light.shadowIntensityOffset;
585
586 if (light.lightType == LightType::Directional) {
587
588 lightData.lightDirection = transformSystem->getLocalToWorld(transformComponent) * glm::vec4(0, 0, -1, 0);
589 lightData.lightDirection = glm::normalize(lightData.lightDirection);
590
591 lightData.lightPosition = glm::vec4(0, 0, 0, 0);
592
593 if (lightData.castShadows) {
594
595 uint32_t framesSinceDirty = context->time->getFrame() - context->engine->getLastDirtyFrame();
596 if (framesSinceDirty <= lightData.maxViewports) {
597 context->engine->triggerUpdate();
598 }
599
600 auto passOptions = &lightData.passOptions;
601 passOptions->setFlag(RenderPassOptions::Flags::NoDepthClip);
602 passOptions->depthBias = light.shadowBias;
603 passOptions->depthSlopedBias = light.shadowSlopedBias;
604 passOptions->depthBiasClamp = light.shadowBiasClamp;
605
606 lightData.textureSize = cascadeShadowResolution;
607 lightData.shadowUpdate = shadowUpdate;
608
609 lightData.maxViewports = 4;
610 lightData.numViewports = lightData.maxViewports;
611 lightData.shadowTexture = cascadeArray;
612 lightData.arrayOffset = layerCount;
613
614 layerCount += lightData.maxViewports;
615 ++cascadeInstances;
616
617 const CameraData * refCamData = nullptr;
618 std::vector<CamState> cameras;
619 getCameras(context, refCamData, cameras, light);
620 auto nearest = calculateSplits(context, lightData, transformSystem->getLocalToWorld(transformComponent), refCamData, cameras, context->variables->get("renderer.maxShadowDistance", 30000.0f));
621
622 for (size_t i = 0; i < lightData.numViewports; ++i) {
623 auto frame = context->time->getFrame();
624 if (lightData.shadowUpdate == ShadowUpdate::Partial) {
625 lightData.frameMod[i] = (uint16_t)lightData.numViewports;
626 lightData.frameOffset[i] = (uint16_t)i;
627 }
628 else if (lightData.shadowUpdate == ShadowUpdate::Static) {
629 lightData.frameMod[i] = 0;
630 lightData.frameOffset[i] = 0;
631 }
632 else if (lightData.shadowUpdate == ShadowUpdate::StaticPartial) {
633 lightData.frameMod[i] = (uint16_t)lightData.numViewports;
634 lightData.frameOffset[i] = (uint16_t)i;
635 }
636 else if (lightData.shadowUpdate == ShadowUpdate::None) {
637 lightData.frameMod[i] = 1;
638 lightData.frameOffset[i] = static_cast<uint16_t>(-1);
639 }
640 else {
641 lightData.frameMod[i] = 0;
642 lightData.frameOffset[i] = 0;
643 }
644 if (lightData.frameMod[i] != 0) {
645 if ((frame % lightData.frameMod[i]) != lightData.frameOffset[i]) {
646 continue;
647 }
648 }
649
650 // Note nearDepth[i] is for cascade i-1, while farDepth[i] is for cascade i.
651 auto n = 0 < i ? lightData.nearDepths[i - 1] : nearest;
652 auto f = lightData.farDepths[i];
653
654 auto frustaPoints = lightData.frustaPointsCapture ? &lightData.frustaPoints[i] : nullptr;
655 if (frustaPoints) {
656 frustaPoints->points.clear();
657 frustaPoints->offsets.clear();
658 }
659
660 glm::mat4 lightView = createLightViewMatrix(lightData.cascadeLine, transformComponent->rotation);
661 glm::uvec2 blueNoiseOffset;
662 glm::mat4 rawCascadeCullMatrix;
663 createFrustumFitMatrix(context, lightData.lightRawProjection[i], rawCascadeCullMatrix, frustaPoints, refCamData, cameras,
664 lightView, lightData.cascadeLine, n, f, frustumSlack, lightData.textureSize, blueNoiseOffset);
665
666 auto cascadeProjectionMatrix = context->renderer->getProjectionMatrix(lightData.lightRawProjection[i]);
667
668 // Set of cascade render view
669 // --------------------------
670 //
671 // Handling of y-flip for GL backends is done when updating engine buffers
672 //
673 CameraData& lightCameraData = lightData.lightCameraData[i];
674
675 lightCameraData.layerMask = RenderLayers::Default;
676
677 lightCameraData.viewMatrix = lightView;
678 lightCameraData.projectionMatrix = cascadeProjectionMatrix;
679
680 lightCameraData.viewProjection = cascadeProjectionMatrix * lightView;
681 lightCameraData.inverseViewMatrix = glm::inverse(lightView);
682 lightCameraData.inverseViewProjectionMatrix = glm::inverse(lightCameraData.viewProjection);
683 lightCameraData.inverseProjectionMatrix = glm::inverse(cascadeProjectionMatrix);
684
685 lightCameraData.rawProjectionMatrix = lightData.lightRawProjection[i];
686 lightCameraData.rawViewProjection = lightData.lightRawProjection[i] * lightView;
687 lightCameraData.rawViewCullMatrix = rawCascadeCullMatrix * lightView;
688
689 lightCameraData.passOptions = passOptions;
690
691 lightCameraData.viewportOrigin = { 0, 0 };
692
693 lightCameraData.viewportSize = { lightData.textureSize , lightData.textureSize };
694 lightCameraData.blueNoiseOffset = blueNoiseOffset;
695
696 // Enable shadow pancaking for cascades.
697 lightCameraData.depthClamp = 0;
698
699 lightCameraData.frustum = Geometry::calculateFrustum<Geometry::Frustum, glm::mat4>(lightCameraData.viewProjection);
700 }
701 }
702 }
703 else if (light.lightType == LightType::Point) {
704
705 if (!useTextureCubeArrays && lightData.castShadows) {
706
707 if (cubeInstances) { // we only support one cube
708 lightData.castShadows = false;
709 }
710 }
711
712 glm::vec3 lightPosition = transformSystem->getLocalToWorld(transformComponent) * glm::vec4(0, 0, 0, 1);
713 lightData.lightDirection = glm::vec4(0, 0, -1, 1);
714 lightData.lightPosition = glm::vec4(lightPosition, 1);
715
716 if (lightData.castShadows) {
717
718 auto passOptions = &lightData.passOptions;
719 passOptions->unsetFlag(RenderPassOptions::Flags::NoDepthClip);
720 passOptions->depthBias = light.shadowBias;
721 passOptions->depthSlopedBias = light.shadowSlopedBias;
722 passOptions->depthBiasClamp = light.shadowBiasClamp;
723
724 lightData.shadowUpdate = shadowUpdate;
725 lightData.shadowTexture = cubeArray;
726 lightData.maxViewports = 6;
727 lightData.numViewports = lightData.maxViewports;
728 lightData.arrayOffset = cubeInstances * lightData.numViewports;
729 ++cubeInstances;
730
731 if (light.shadowNearPlane != 0) {
732 lightData.nearDepths[0] = light.shadowNearPlane;
733 } else {
734 lightData.nearDepths[0] = glm::max(0.1f, light.range / 1000.0f);
735 }
736
737 lightData.farDepths[0] = light.range;
738
739 for (size_t i = 0; i < lightData.numViewports; ++i) {
740 auto frame = context->time->getFrame();
741 if (lightData.shadowUpdate == ShadowUpdate::Partial) {
742 lightData.frameMod[i] = (uint16_t)lightData.numViewports;
743 lightData.frameOffset[i] = (uint16_t)i;
744 }
745 else if (lightData.shadowUpdate == ShadowUpdate::Static) {
746 lightData.frameMod[i] = 0;
747 lightData.frameOffset[i] = 0;
748 }
749 else if (lightData.shadowUpdate == ShadowUpdate::StaticPartial) {
750 lightData.frameMod[i] = (uint16_t)lightData.numViewports;
751 lightData.frameOffset[i] = (uint16_t)i;
752 }
753 else if (lightData.shadowUpdate == ShadowUpdate::None) {
754 lightData.frameMod[i] = 1;
755 lightData.frameOffset[i] = static_cast<uint16_t>(-1);
756 }
757 else {
758 lightData.frameMod[i] = 0;
759 lightData.frameOffset[i] = 0;
760 }
761 if (lightData.frameMod[i] != 0) {
762 if ((frame % lightData.frameMod[i]) != lightData.frameOffset[i]) {
763 continue;
764 }
765 }
766 CameraData& lightCameraData = lightData.lightCameraData[i];
767
768
769 // Calculate point light projection
770 // --------------------------------
771 //
772 // If origin is on botton (GL backends), we flip the Y axis out of the
773 // projection so that we match engine conventions. Note that this flips
774 // orientation so we must flip the winding order used for culling.
775 //
776 glm::mat4 lightProjection = glm::perspective(glm::pi<float>() / 2.0f,
777 1.0f,
778 lightData.nearDepths[0],
779 lightData.farDepths[0]);
780 if (!originOnTop) {
781 lightProjection = glm::mat4(1.f, 0.f, 0.f, 0.f,
782 0.f, -1.f, 0.f, 0.f,
783 0.f, 0.f, 1.f, 0.f,
784 0.f, 0.f, 0.f, 1.f) * lightProjection;
785 lightCameraData.flipWindingOrder = true;
786 }
787 lightProjection = context->renderer->getProjectionMatrix(lightProjection);
788
789 glm::vec3 directions[6] = {
790 glm::vec3(1, 0, 0),
791 glm::vec3(-1, 0, 0),
792 glm::vec3(0, 0, 1),
793 glm::vec3(0, 0, -1),
794 glm::vec3(0, 1, 0),
795 glm::vec3(0, -1, 0),
796 };
797
798 glm::vec3 ups[6] = {
799 glm::vec3(0, 0, 1),
800 glm::vec3(0, 0, 1),
801 glm::vec3(0, -1, 0),
802 glm::vec3(0, 1, 0),
803 glm::vec3(0, 0, 1),
804 glm::vec3(0, 0, 1),
805 };
806
807 const glm::mat4 lightView = glm::lookAt(lightPosition, lightPosition + directions[i], ups[i]);
808
809 lightCameraData.layerMask = RenderLayers::Default;
810 lightCameraData.viewMatrix = lightView;
811 lightCameraData.projectionMatrix = lightProjection;
812 lightCameraData.inverseViewMatrix = glm::inverse(lightView);
813 lightCameraData.viewProjection = lightProjection * lightView;
814 lightCameraData.inverseViewProjectionMatrix = glm::inverse(lightCameraData.viewProjection);
815 lightCameraData.inverseProjectionMatrix = glm::inverse(lightProjection);
816 lightCameraData.rawProjectionMatrix = lightProjection;
817 lightCameraData.rawViewProjection = lightProjection * lightCameraData.viewMatrix;
818 lightCameraData.rawViewCullMatrix = lightCameraData.rawViewProjection;
819 lightCameraData.passOptions = passOptions;
820 lightCameraData.viewportOrigin = { 0, 0 };
821 lightCameraData.viewportSize = { pointShadowResolution, pointShadowResolution };
822 lightCameraData.depthClamp = -std::numeric_limits<float>::max();
823
824 lightCameraData.frustum = Geometry::calculateFrustum<Geometry::Frustum, glm::mat4>(lightCameraData.viewProjection);
825 }
826 }
827 }
828 }
829
830 if (!cascadeInstances) cascadeInstances = 1;
831
832 if (cascadeInstances && (layerCount != currentLayerCount ||
833 cascadeShadowResolution != currentTextureSize ||
834 cascadeShadowFormat != currentShadowFormat))
835 {
836 assert(cascadeArray);
837
838 cascadeArray->description.target = ResourceDimensions::Texture2DArray;
839 cascadeArray->description.layers = layerCount;
840 cascadeArray->description.width = cascadeShadowResolution;
841 cascadeArray->description.height = cascadeShadowResolution;
842 cascadeArray->description.format = cascadeShadowFormat;
843 cascadeArray->description.flags = TextureFlags::DepthBuffer | TextureFlags::Texture;
844 if (layerCount) {
845 cascadeArray->setChanged();
846 }
847 currentLayerCount = layerCount;
848 currentTextureSize = cascadeShadowResolution;
849 currentShadowFormat = cascadeShadowFormat;
850 }
851
852 if (cubeInstances && (cubeInstances != currentCubeCount ||
853 pointShadowResolution != currentPointShadowResolution ||
854 pointShadowFormat != currentPointShadowFormat))
855 {
856 assert(cubeArray);
857 cubeArray->description.target = useTextureCubeArrays ? ResourceDimensions::TextureCubeArray : ResourceDimensions::TextureCube;
858 cubeArray->description.layers = useTextureCubeArrays ? cubeInstances : 1;
859 cubeArray->description.faces = 6;
860 cubeArray->description.width = pointShadowResolution;
861 cubeArray->description.height = pointShadowResolution;
862 cubeArray->description.format = pointShadowFormat;
864 if (cubeInstances) {
865 cubeArray->setChanged();
866 }
867 currentCubeCount = cubeInstances;
868 currentPointShadowResolution = pointShadowResolution;
869 currentPointShadowFormat = pointShadowFormat;
870 }
871
872 for (const auto & light : pool) {
873 auto & lightData = getData(&light);
874 if (!light.enabled) continue;
875 if (!lightData.castShadows) continue;
876
877 if (light.lightType == LightType::Directional) {
878 for (size_t i = 0; i < lightData.numViewports; ++i) {
879 auto & lightCameraData = lightData.lightCameraData[i];
880 context->cullingManager->dispatchCulling(&lightCameraData);
881 }
882 }
883 else if (light.lightType == LightType::Point) {
884 for (size_t i = 0; i < 6; ++i) {
885 auto & lightCameraData = lightData.lightCameraData[i];
886 context->cullingManager->dispatchCulling(&lightCameraData);
887 }
888 }
889 }
890
891}
constexpr void unsetFlag(const uint32_t flag)
Unset the given flag. Does not remove the status of other than the given flags.
Definition: Component.h:371
ComponentType * getComponent() const
Definition: Component.h:159
constexpr void setFlag(const uint32_t flag)
Set the given flags. Does not override the currently set flags.
Definition: Component.h:368
Context * context
Pointer to the Context instance the system lives in.
virtual void initialize(Context *context)
Initialize the system.
void update()
Updates the system state to that of the current frame.
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
std::unique_ptr< class CullingManager > cullingManager
CullingManager instance.
Definition: Context.h:195
class IRenderer * renderer
Renderer.
Definition: Context.h:228
std::unique_ptr< class Bounds > bounds
Bounds service instance.
Definition: Context.h:216
std::unique_ptr< class Variables > variables
Variables service instance.
Definition: Context.h:180
std::unique_ptr< class Time > time
Time service instance.
Definition: Context.h:198
std::unique_ptr< class Engine > engine
Engine instance.
Definition: Context.h:222
virtual glm::mat4 getProjectionMatrix(const glm::mat4 projectionMatrix)=0
Get an adjusted projection matrix used to render.
Defines a single light source and its behavior.
LightType lightType
The type of light.
float shadowBias
Constant term shadow map rasterization bias.
bool castShadows
If this light should cast shadows.
bool enabled
If the light is enabled.
float shadowBiasClamp
Shadow map bias rasterization clamp.
float shadowNearPlane
Shadow near plane. If set to 0 near plane is 1/1000th of the radius.
std::vector< WeakEntityPtr > cameras
Cameras from which the shadow maps will be used, defaults to main camera.
WeakEntityPtr lodReference
Camera entity of which its z-axis define the shadowmap cascade split axis, defaults to main camera.
float shadowSlopedBias
Linear term of shadow map rasterization bias.
float shadowIntensityOffset
Shadow intensity offset.
bool tightShadowBounds
If the shadows should have tight bounds.
glm::vec4 lightColor
Color contribution of the light.
bool dynamicCascadeCount
Dynamically determine how many cascades should be drawn.
LightingLayers lightingLayer
The lighting layer the light belongs to.
float range
Falloff range.
void initialize(Context *context) override
Initialize the system.
void preRender(Context *context)
Cull light system.
Defines a 4x4 transformation matrix for the entity and a global offset for root entities.
glm::quat rotation
Rotation given as a quaternion.
glm::dvec3 getOrigin() const
Gets the Origin offset of the scene.
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
@ Point
Point light source.
@ Directional
Directional light.
@ WebGPU
Graphics device using the WebGPU API Backend.
Contains data describing a Camera instance and its derived data structured such as matrix data and vi...
Definition: CameraSystem.h:67
glm::mat4 rawProjectionMatrix
Projection matrix that has not been adjusted by the renderer, and is thus appropriate for direct scre...
Definition: CameraSystem.h:129
Defines calculated light data.
Definition: LightSystem.h:34
@ DepthBuffer
The texture can be used as a depth target and have depth buffer values written into.
Definition: Flags.h:122
@ Texture
Texture usage, see Default.
Definition: Flags.h:118
@ CubeMap
The texture can be used as a cube map.
Definition: Flags.h:126