Merge branch 'CSM-new'

CSM-dev is clipmap shadow map and it works
This commit is contained in:
2025-10-23 00:35:44 +09:00
11 changed files with 128 additions and 163 deletions

View File

@@ -8,7 +8,8 @@ layout(location=0) out vec4 outColor;
layout(set=1, binding=0) uniform sampler2D posTex; layout(set=1, binding=0) uniform sampler2D posTex;
layout(set=1, binding=1) uniform sampler2D normalTex; layout(set=1, binding=1) uniform sampler2D normalTex;
layout(set=1, binding=2) uniform sampler2D albedoTex; layout(set=1, binding=2) uniform sampler2D albedoTex;
layout(set=2, binding=0) uniform sampler2D shadowTex[MAX_CASCADES]; // Mixed near + CSM: shadowTex[0] is the near/simple map, 1..N-1 are cascades
layout(set=2, binding=0) uniform sampler2D shadowTex[4];
const float PI = 3.14159265359; const float PI = 3.14159265359;
@@ -29,17 +30,27 @@ vec2(0.0281, -0.2468), vec2(-0.2104, 0.0573),
vec2(0.1197, 0.0779), vec2(-0.0905, -0.1203) vec2(0.1197, 0.0779), vec2(-0.0905, -0.1203)
); );
// Clipmap selection: choose the smallest level whose light-space XY NDC contains the point.
uint selectCascadeIndex(vec3 worldPos)
{
for (uint i = 0u; i < 4u; ++i)
{
vec4 lclip = sceneData.lightViewProjCascades[i] * vec4(worldPos, 1.0);
vec3 ndc = lclip.xyz / max(lclip.w, 1e-6);
if (abs(ndc.x) <= 1.0 && abs(ndc.y) <= 1.0 && ndc.z >= 0.0 && ndc.z <= 1.0)
{
return i;
}
}
return 3u; // fallback to farthest level
}
float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L) float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
{ {
// Choose cascade based on view-space depth uint ci = selectCascadeIndex(worldPos);
float viewDepth = - (sceneData.view * vec4(worldPos, 1.0)).z;// positive mat4 lightMat = sceneData.lightViewProjCascades[ci];
int ci = 0;
if (viewDepth > sceneData.cascadeSplitsView.x) ci = 1;
if (viewDepth > sceneData.cascadeSplitsView.y) ci = 2;
if (viewDepth > sceneData.cascadeSplitsView.z) ci = 3;
ci = clamp(ci, 0, MAX_CASCADES-1);
vec4 lclip = sceneData.lightViewProjCascades[ci] * vec4(worldPos, 1.0); vec4 lclip = lightMat * vec4(worldPos, 1.0);
vec3 ndc = lclip.xyz / lclip.w; vec3 ndc = lclip.xyz / lclip.w;
vec2 suv = ndc.xy * 0.5 + 0.5; vec2 suv = ndc.xy * 0.5 + 0.5;
@@ -60,14 +71,15 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
vec2 texelSize = 1.0 / vec2(dim); vec2 texelSize = 1.0 / vec2(dim);
float baseRadius = 1.25; float baseRadius = 1.25;
float radius = mix(baseRadius, baseRadius * 4.0, current); // Slightly increase filter for farther cascades
float radius = mix(baseRadius, baseRadius * 3.0, float(ci) / 3.0);
float ang = hash12(suv * 4096.0) * 6.2831853; float ang = hash12(suv * 4096.0) * 6.2831853;
vec2 r = vec2(cos(ang), sin(ang)); vec2 r = vec2(cos(ang), sin(ang));
mat2 rot = mat2(r.x, -r.y, r.y, r.x); mat2 rot = mat2(r.x, -r.y, r.y, r.x);
const int TAP_COUNT = 16; const int TAP_COUNT = 16;
float occluded = 0.0; float visible = 0.0;
float wsum = 0.0; float wsum = 0.0;
for (int i = 0; i < TAP_COUNT; ++i) for (int i = 0; i < TAP_COUNT; ++i)
@@ -79,19 +91,14 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
float w = 1.0 - smoothstep(0.0, 0.65, pr); float w = 1.0 - smoothstep(0.0, 0.65, pr);
float mapD = texture(shadowTex[ci], suv + off).r; float mapD = texture(shadowTex[ci], suv + off).r;
float vis = step(mapD, current + bias);
// Forward-Z depth shadow map: visible += vis * w;
// - Occluded when current + bias > mapD
// - Unoccluded otherwise
// Use step(edge, x): returns 1 when x >= edge. Make occ=1 for occluded.
float occ = step(mapD + bias, current);
occluded += occ * w;
wsum += w; wsum += w;
} }
float shadow = (wsum > 0.0) ? (occluded / wsum) : 0.0; float visibility = (wsum > 0.0) ? (visible / wsum) : 1.0;
return 1.0 - shadow; return visibility;
} }
vec3 fresnelSchlick(float cosTheta, vec3 F0) vec3 fresnelSchlick(float cosTheta, vec3 F0)

View File

@@ -6,13 +6,16 @@ layout(set = 0, binding = 0) uniform SceneData{
mat4 view; mat4 view;
mat4 proj; mat4 proj;
mat4 viewproj; mat4 viewproj;
mat4 lightViewProj; // legacy single-shadow for fallback // Legacy single shadow matrix (used for near range in mixed mode)
mat4 lightViewProj;
vec4 ambientColor; vec4 ambientColor;
vec4 sunlightDirection; //w for sun power vec4 sunlightDirection; //w for sun power
vec4 sunlightColor; vec4 sunlightColor;
// CSM data
mat4 lightViewProjCascades[MAX_CASCADES]; // Cascaded shadow matrices (0 = near/simple map, 1..N-1 = CSM)
vec4 cascadeSplitsView; // positive view-space distances of far plane per cascade mat4 lightViewProjCascades[4];
// View-space split distances for selecting cascades (x,y,z,w)
vec4 cascadeSplitsView;
} sceneData; } sceneData;
layout(set = 1, binding = 0) uniform GLTFMaterialData{ layout(set = 1, binding = 0) uniform GLTFMaterialData{

View File

@@ -18,14 +18,16 @@ layout(buffer_reference, std430) readonly buffer VertexBuffer{
layout(push_constant) uniform PushConsts { layout(push_constant) uniform PushConsts {
mat4 render_matrix; mat4 render_matrix;
VertexBuffer vertexBuffer; VertexBuffer vertexBuffer;
uint cascadeIndex; uint cascadeIndex; // which cascade this pass renders
// pad to 16-byte boundary implicitly
} PC; } PC;
void main() void main()
{ {
Vertex v = PC.vertexBuffer.vertices[gl_VertexIndex]; Vertex v = PC.vertexBuffer.vertices[gl_VertexIndex];
vec4 worldPos = PC.render_matrix * vec4(v.position, 1.0); vec4 worldPos = PC.render_matrix * vec4(v.position, 1.0);
uint ci = min(PC.cascadeIndex, uint(MAX_CASCADES-1)); // Use cascaded matrix; cascade 0 is the legacy near/simple map
uint ci = min(PC.cascadeIndex, uint(3));
gl_Position = sceneData.lightViewProjCascades[ci] * worldPos; gl_Position = sceneData.lightViewProjCascades[ci] * worldPos;
} }

View File

@@ -10,10 +10,18 @@ inline constexpr bool kUseValidationLayers = true;
// Shadow mapping configuration // Shadow mapping configuration
inline constexpr int kShadowCascadeCount = 4; inline constexpr int kShadowCascadeCount = 4;
// Maximum shadow distance for CSM in view-space units // Maximum shadow distance for CSM in view-space units
inline constexpr float kShadowCSMFar = 50.0f; inline constexpr float kShadowCSMFar = 400.0f;
// Shadow map resolution used for stabilization (texel snapping). Must match actual image size. // Shadow map resolution used for stabilization (texel snapping). Must match actual image size.
inline constexpr float kShadowMapResolution = 4096.0f; inline constexpr float kShadowMapResolution = 2048.0f;
// Extra XY expansion for cascade footprint (safety against FOV/aspect changes) // Extra XY expansion for cascade footprint (safety against FOV/aspect changes)
inline constexpr float kShadowCascadeRadiusScale = 1.15f; inline constexpr float kShadowCascadeRadiusScale = 2.5f;
// Additive XY margin in world units (light-space) beyond scaled radius // Additive XY margin in world units (light-space) beyond scaled radius
inline constexpr float kShadowCascadeRadiusMargin = 10.0f; inline constexpr float kShadowCascadeRadiusMargin = 40.0f;
// Clipmap shadow configuration (used when cascades operate in clipmap mode)
// Base coverage radius of level 0 around the camera (world units). Each level doubles the radius.
inline constexpr float kShadowClipBaseRadius = 20.0f;
// Pullback distance of the light eye from the clipmap center along the light direction (world units)
inline constexpr float kShadowClipLightPullback = 160.0f;
// Additional Z padding for the orthographic frustum along light direction
inline constexpr float kShadowClipZPadding = 80.0f;

View File

@@ -18,6 +18,8 @@
#include "render/vk_pipelines.h" #include "render/vk_pipelines.h"
#include <iostream> #include <iostream>
#include <glm/gtx/transform.hpp> #include <glm/gtx/transform.hpp>
#include "config.h"
#include "render/primitives.h" #include "render/primitives.h"
#include "vk_mem_alloc.h" #include "vk_mem_alloc.h"
@@ -126,7 +128,7 @@ void VulkanEngine::init()
auto imguiPass = std::make_unique<ImGuiPass>(); auto imguiPass = std::make_unique<ImGuiPass>();
_renderPassManager->setImGuiPass(std::move(imguiPass)); _renderPassManager->setImGuiPass(std::move(imguiPass));
const std::string structurePath = _assetManager->modelPath("seoul_high.glb"); const std::string structurePath = _assetManager->modelPath("police_office.glb");
const auto structureFile = _assetManager->loadGLTF(structurePath); const auto structureFile = _assetManager->loadGLTF(structurePath);
assert(structureFile.has_value()); assert(structureFile.has_value());
@@ -317,6 +319,7 @@ void VulkanEngine::draw()
RGImageHandle hGBufferAlbedo = _renderGraph->import_gbuffer_albedo(); RGImageHandle hGBufferAlbedo = _renderGraph->import_gbuffer_albedo();
RGImageHandle hSwapchain = _renderGraph->import_swapchain_image(swapchainImageIndex); RGImageHandle hSwapchain = _renderGraph->import_swapchain_image(swapchainImageIndex);
// Create a transient shadow depth target (fixed resolution for now)
// Create transient depth targets for cascaded shadow maps // Create transient depth targets for cascaded shadow maps
const VkExtent2D shadowExtent{2048, 2048}; const VkExtent2D shadowExtent{2048, 2048};
std::array<RGImageHandle, kShadowCascadeCount> hShadowCascades{}; std::array<RGImageHandle, kShadowCascadeCount> hShadowCascades{};

View File

@@ -35,11 +35,9 @@ void SamplerManager::init(DeviceManager *deviceManager)
sh.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; sh.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
sh.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; sh.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
sh.compareEnable = VK_FALSE; // manual PCF sh.compareEnable = VK_FALSE; // manual PCF
// Depth shadow maps are single-level; keep base LOD only and avoid mip filtering.
sh.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sh.maxLod = 0.0f;
sh.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE; sh.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
vkCreateSampler(_deviceManager->device(), &sh, nullptr, &_shadowLinearClamp); vkCreateSampler(_deviceManager->device(), &sh, nullptr, &_shadowLinearClamp);
} }
void SamplerManager::cleanup() void SamplerManager::cleanup()
@@ -56,6 +54,7 @@ void SamplerManager::cleanup()
vkDestroySampler(_deviceManager->device(), _defaultSamplerLinear, nullptr); vkDestroySampler(_deviceManager->device(), _defaultSamplerLinear, nullptr);
_defaultSamplerLinear = VK_NULL_HANDLE; _defaultSamplerLinear = VK_NULL_HANDLE;
} }
if (_shadowLinearClamp) if (_shadowLinearClamp)
{ {
vkDestroySampler(_deviceManager->device(), _shadowLinearClamp, nullptr); vkDestroySampler(_deviceManager->device(), _shadowLinearClamp, nullptr);

View File

@@ -15,6 +15,7 @@ public:
VkSampler defaultNearest() const { return _defaultSamplerNearest; } VkSampler defaultNearest() const { return _defaultSamplerNearest; }
VkSampler shadowLinearClamp() const { return _shadowLinearClamp; } VkSampler shadowLinearClamp() const { return _shadowLinearClamp; }
private: private:
DeviceManager *_deviceManager = nullptr; DeviceManager *_deviceManager = nullptr;
VkSampler _defaultSamplerLinear = VK_NULL_HANDLE; VkSampler _defaultSamplerLinear = VK_NULL_HANDLE;

View File

@@ -685,6 +685,8 @@ void RenderGraph::execute(VkCommandBuffer cmd)
{ {
depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
} }
if (p.depthAttachment.clearOnLoad) depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
else depthInfo.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
if (!p.depthAttachment.store) depthInfo.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; if (!p.depthAttachment.store) depthInfo.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
hasDepth = true; hasDepth = true;
if (rec->extent.width && rec->extent.height) chosenExtent = clamp_min(chosenExtent, rec->extent); if (rec->extent.width && rec->extent.height) chosenExtent = clamp_min(chosenExtent, rec->extent);

View File

@@ -19,8 +19,7 @@ public:
RGImageHandle drawHandle, RGImageHandle drawHandle,
RGImageHandle gbufferPosition, RGImageHandle gbufferPosition,
RGImageHandle gbufferNormal, RGImageHandle gbufferNormal,
RGImageHandle gbufferAlbedo, RGImageHandle gbufferAlbedo, std::span<RGImageHandle> shadowCascades);
std::span<RGImageHandle> shadowCascades);
private: private:
EngineContext *_context = nullptr; EngineContext *_context = nullptr;

View File

@@ -45,11 +45,11 @@ void ShadowPass::init(EngineContext *context)
b.set_cull_mode(VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE); b.set_cull_mode(VK_CULL_MODE_BACK_BIT, VK_FRONT_FACE_CLOCKWISE);
b.set_multisampling_none(); b.set_multisampling_none();
b.disable_blending(); b.disable_blending();
// Forward-Z for shadow maps only (engine uses reversed-Z elsewhere)
// We clear depth to 1.0 and use LESS_OR_EQUAL so the nearest depth wins. b.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL);
b.enable_depthtest(true, VK_COMPARE_OP_LESS_OR_EQUAL);
b.set_depth_format(VK_FORMAT_D32_SFLOAT); b.set_depth_format(VK_FORMAT_D32_SFLOAT);
// Static depth bias to help with surface acne (tune later)
b._rasterizer.depthBiasEnable = VK_TRUE; b._rasterizer.depthBiasEnable = VK_TRUE;
b._rasterizer.depthBiasConstantFactor = 2.0f; b._rasterizer.depthBiasConstantFactor = 2.0f;
b._rasterizer.depthBiasSlopeFactor = 2.0f; b._rasterizer.depthBiasSlopeFactor = 2.0f;
@@ -85,8 +85,7 @@ void ShadowPass::register_graph(RenderGraph *graph, std::span<RGImageHandle> cas
RGPassType::Graphics, RGPassType::Graphics,
[shadowDepth](RGPassBuilder &builder, EngineContext *ctx) [shadowDepth](RGPassBuilder &builder, EngineContext *ctx)
{ {
// Forward-Z in shadow pass: clear depth to 1.0 (far) VkClearValue clear{}; clear.depthStencil = {0.f, 0};
VkClearValue clear{}; clear.depthStencil = {1.f, 0};
builder.write_depth(shadowDepth, true, clear); builder.write_depth(shadowDepth, true, clear);
// Ensure index/vertex buffers are tracked as reads (like Geometry) // Ensure index/vertex buffers are tracked as reads (like Geometry)

View File

@@ -4,6 +4,7 @@
#include "vk_swapchain.h" #include "vk_swapchain.h"
#include "core/engine_context.h" #include "core/engine_context.h"
#include "core/config.h"
#include "glm/gtx/transform.hpp" #include "glm/gtx/transform.hpp"
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
@@ -106,123 +107,64 @@ void SceneManager::update_scene()
sceneData.proj = projection; sceneData.proj = projection;
sceneData.viewproj = projection * view; sceneData.viewproj = projection * view;
// Build cascaded directional light view-projection matrices // Clipmap shadow setup (directional). Each level i covers a square region
// around the camera in the light's XY plane with radius R_i = R0 * 2^i.
// The region center is snapped to the light-space texel grid for stability.
{ {
using namespace glm; const glm::mat4 invView = glm::inverse(view);
const vec3 camPos = vec3(inverse(view)[3]); const glm::vec3 camPos = glm::vec3(invView[3]);
// Use light-ray direction (from light to scene).
// Shaders compute per-fragment L as -sunlightDirection (vector to light).
vec3 L = normalize(vec3(sceneData.sunlightDirection));
if (!glm::all(glm::isfinite(L)) || glm::length2(L) < 1e-10f)
L = glm::vec3(0.0f, -1.0f, 0.0f);
const glm::vec3 worldUp(0, 1, 0), altUp(0, 0, 1); glm::vec3 L = glm::normalize(-glm::vec3(sceneData.sunlightDirection));
glm::vec3 upPick = (std::abs(glm::dot(worldUp, L)) > 0.99f) ? altUp : worldUp; if (glm::length(L) < 1e-5f) L = glm::vec3(0.0f, -1.0f, 0.0f);
glm::vec3 right = glm::normalize(glm::cross(upPick, L)); const glm::vec3 worldUp(0.0f, 1.0f, 0.0f);
glm::vec3 up = glm::normalize(glm::cross(L, right)); glm::vec3 right = glm::cross(L, worldUp);
if (glm::length2(right) < 1e-6f) right = glm::vec3(1, 0, 0);
right = glm::normalize(right);
glm::vec3 up = glm::normalize(glm::cross(right, L));
const float csmFar = kShadowCSMFar; auto level_radius = [](int level) {
const float lambda = 0.5f; return kShadowClipBaseRadius * powf(2.0f, float(level));
const int cascades = kShadowCascadeCount;
float splits[4] = {0, 0, 0, 0};
for (int i = 1; i <= cascades; ++i)
{
float p = (float) i / (float) cascades;
float logd = nearPlane * std::pow(csmFar / nearPlane, p);
float lind = nearPlane + (csmFar - nearPlane) * p;
float d = glm::mix(lind, logd, lambda);
if (i - 1 < 4) splits[i - 1] = d;
}
sceneData.cascadeSplitsView = vec4(splits[0], splits[1], splits[2], splits[3]);
mat4 invView = inverse(view);
float baseWorldTexel = 0.0f;
auto buildCascade = [&](int idx, float nearD, float farD) -> mat4 {
float tanHalf = tanf(fov * 0.5f);
float yf = tanHalf * farD;
float xf = yf * aspect;
float rStable = 1.05f * sqrtf(xf * xf + yf * yf);
float texelWorld;
if (idx == 0)
{
baseWorldTexel = (2.0f * rStable) / kShadowMapResolution;
baseWorldTexel = powf(2.0f, ceilf(log2f(baseWorldTexel)));
texelWorld = baseWorldTexel;
}
else
{
texelWorld = baseWorldTexel * (1 << idx);
rStable = 0.5f * texelWorld * kShadowMapResolution;
}
vec3 cornersV[8]; {
float tanHalfFov = tanf(fov * 0.5f);
float yn = tanHalfFov * nearD, xn = yn * aspect;
float yf = tanHalfFov * farD, xf = yf * aspect;
cornersV[0] = {-xn, -yn, -nearD};
cornersV[1] = {xn, -yn, -nearD};
cornersV[2] = {xn, yn, -nearD};
cornersV[3] = {-xn, yn, -nearD};
cornersV[4] = {-xf, -yf, -farD};
cornersV[5] = {xf, -yf, -farD};
cornersV[6] = {xf, yf, -farD};
cornersV[7] = {-xf, yf, -farD};
}
mat4 invView = inverse(view);
vec3 cornersW[8], centerWS(0);
for (int i = 0; i < 8; ++i)
{
cornersW[i] = vec3(invView * vec4(cornersV[i], 1));
centerWS += cornersW[i];
}
centerWS *= 1.0f / 8.0f;
float lightDist = rStable + 50.0f;
vec3 lightPos = centerWS - L * lightDist;
mat4 viewLight = lookAtRH(lightPos, centerWS, up);
vec2 centerLS = vec2(viewLight * vec4(centerWS, 1));
vec2 snapped = floor(centerLS / texelWorld) * texelWorld;
vec2 deltaLS = snapped - centerLS;
vec3 shiftWS = right * deltaLS.x + up * deltaLS.y;
vec3 centerSnapped = centerWS + shiftWS;
lightPos = centerSnapped - L * lightDist;
viewLight = lookAtRH(lightPos, centerSnapped, up);
float radius = ceil(rStable / texelWorld) * texelWorld;
vec2 cLS = vec2(viewLight * vec4(centerSnapped, 1));
float left = cLS.x - radius, rightE = cLS.x + radius;
float bottom = cLS.y - radius, top = cLS.y + radius;
float minZ = 1e9f, maxZ = -1e9f;
for (int i = 0; i < 8; ++i)
{
vec3 p = vec3(viewLight * vec4(cornersW[i], 1));
minZ = std::min(minZ, p.z);
maxZ = std::max(maxZ, p.z);
}
float sliceLen = farD - nearD;
float zPad = std::max(10.0f, 0.2f * sliceLen);
float casterExtrude = 100.0f;
float nearLS = 0.01f;
float farLS = -minZ + zPad + casterExtrude;
mat4 projLight = orthoRH_ZO(left, rightE, bottom, top, nearLS, farLS);
return projLight * viewLight;
}; };
for (int i = 0; i < cascades; ++i) // Keep a copy of level radii in cascadeSplitsView for debug/visualization
sceneData.cascadeSplitsView = glm::vec4(
level_radius(0), level_radius(1), level_radius(2), level_radius(3));
for (int ci = 0; ci < kShadowCascadeCount; ++ci)
{ {
float nearD = (i == 0) ? nearPlane : splits[i - 1]; const float radius = level_radius(ci);
float farD = splits[i];
sceneData.lightViewProjCascades[i] = buildCascade(i, nearD, farD); // Compute camera coordinates in light's orthonormal basis (world -> light XY)
const float u = glm::dot(camPos, right);
const float v = glm::dot(camPos, up);
// Texel size in light-space at this level
const float texel = (2.0f * radius) / float(kShadowMapResolution);
const float uSnapped = floorf(u / texel) * texel;
const float vSnapped = floorf(v / texel) * texel;
const float du = uSnapped - u;
const float dv = vSnapped - v;
// World-space snapped center of this clip level
const glm::vec3 center = camPos + right * du + up * dv;
// Build light view matrix looking at the snapped center
const glm::vec3 eye = center - L * kShadowClipLightPullback;
const glm::mat4 V = glm::lookAtRH(eye, center, up);
// Conservative Z range along light direction
const float zNear = 0.1f;
const float zFar = kShadowClipLightPullback + kShadowClipZPadding;
const glm::mat4 P = glm::orthoRH_ZO(-radius, radius, -radius, radius, zNear, zFar);
const glm::mat4 lightVP = P * V;
sceneData.lightViewProjCascades[ci] = lightVP;
if (ci == 0)
{
sceneData.lightViewProj = lightVP;
}
} }
sceneData.lightViewProj = sceneData.lightViewProjCascades[0];
} }
auto end = std::chrono::system_clock::now(); auto end = std::chrono::system_clock::now();