feat: Shadow pass——主光方向光阴影(RenderGraph 双 pass 管线首个验证案例)

- RenderGraph 声明 ShadowMap 虚拟资源(D32,2048²固定分辨率,独立于
  swapchain 尺寸),Shadow pass 写深度 → Forward 声明 ShaderRead 采样,
  barrier/layout(DepthAttachment→ShaderReadOnly)全部由图自动预算
- Shadow pass:空 fragment shader + 深度偏移(斜率缩放)抗 shadow acne
- Forward:UBO 增加 lightSpace 矩阵;Set 2 采样阴影图,手动对比 +
  斜率缩放偏差;clamp-to-border 白色边界(光视锥外 = 全亮)
- 光空间矩阵:主光方向反推 30 单位正交盒;深度重映射 [-1,1]→[0,1]
- GetPhysicalView(slot):物理镜像按帧槽位隔离,描述符 Compile 后一次性写入
- 修复 RG 隐蔽 bug:ShaderStage 与 PipelineStage2 位值不同
  (eFragment=0x10 vs eFragmentShader=0x80),位值直转会撞出
  TESSELLATION_CONTROL stage;改为逐位显式映射
- 修复 Camera 投影深度未重映射到 [0,1] 的潜在裁剪问题

validation 零错误零警告(3 passes / 5 barriers / 4 gpu images)
This commit is contained in:
2026-09-22 14:30:50 +08:00
parent 394bfd3d0c
commit d55f644b68
8 changed files with 316 additions and 16 deletions
@@ -220,6 +220,10 @@ namespace Engine::RG {
// 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。
void Execute(const FrameContext& frame);
// 物理镜像访问器:给"按帧槽位预写描述符"的场景用(如阴影深度图采样)。
// 仅 created 资源有效;imported 资源(swapchain)每帧才绑定,无物理视图。
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
// ---- 统计 ----
u32 GetPassCount() const;
+16
View File
@@ -56,6 +56,7 @@ namespace Engine {
private:
void BuildGraph(); // 声明 pass 结构 + 首次 Compile
void HandleResize();
void UpdateShadowDescriptors(); // Compile 后把阴影深度图物理视图写进 Set 2
Window& m_Window;
@@ -78,9 +79,24 @@ namespace Engine {
vk::ShaderEXT m_VertShader{};
vk::ShaderEXT m_FragShader{};
// ---- 阴影 ----
// Shadow pass 只绑定顶点 shader(深度写入,无颜色输出),
// 独立 pipeline layout:空描述符 + 128B push(lightVP + model)
vk::PipelineLayout m_ShadowPipelineLayout;
vk::ShaderEXT m_ShadowVertShader{};
vk::ShaderEXT m_ShadowFragShader{};
// Set 2:阴影深度图采样。每槽位一组描述符,Compile 后用
// GetPhysicalView(slot) 一次性写入(物理镜像按槽位隔离,无需每帧更新)
vk::DescriptorSetLayout m_ShadowSetLayout;
vk::Sampler m_ShadowSampler;
std::vector<vk::DescriptorSet> m_ShadowDescriptorSets; // 每帧槽位一个
glm::mat4 m_LightSpaceMatrix{1.0f}; // 每帧按主光方向重算
// ---- RenderGraph 资源句柄 ----
RG::TextureHandle m_Backbuffer{};
RG::TextureHandle m_Depth{};
RG::TextureHandle m_ShadowMap{};
// ---- 当帧绑定(Render() 期间有效,供 pass 回调读取)----
ECS::Scene* m_BoundScene = nullptr;
+28 -5
View File
@@ -612,6 +612,15 @@ namespace Engine::RG {
// ---------- RenderGraphContext ----------
vk::ImageView RenderGraph::GetPhysicalView(TextureHandle h, u32 frameSlot) const
{
const TextureEntry& entry = GetEntry(h);
if (entry.imported) {
return entry.importedView; // 导入资源回退到当帧绑定视图
}
return entry.physical[frameSlot]->view;
}
vk::ImageView RenderGraphContext::View(TextureHandle h) const
{
const auto& entry = graph->GetEntry(h);
@@ -679,11 +688,25 @@ namespace Engine::RG {
case ResourceUse::DepthAttachment:
return vk::PipelineStageFlagBits2::eEarlyFragmentTests |
vk::PipelineStageFlagBits2::eLateFragmentTests;
case ResourceUse::ShaderRead:
// ShaderStageFlags 与 PipelineStageFlags2 位值兼容,需显式转换
return vk::PipelineStageFlags2(
static_cast<VkPipelineStageFlags2>(
static_cast<VkShaderStageFlags>(use.readStage)));
case ResourceUse::ShaderRead: {
// 关键:ShaderStage 与 PipelineStage2 的位值完全不同!
// ShaderStage::eFragment = 0x10,PipelineStage2::eFragmentShader = 0x80
// 位值直转会变成完全错误的 stage(0x10 在 Pipeline2 里是
// TESSELLATION_CONTROL)。必须逐位显式映射。
const vk::ShaderStageFlags stages = use.readStage;
vk::PipelineStageFlags2 out{};
auto mapStage = [&](vk::ShaderStageFlagBits from,
vk::PipelineStageFlagBits2 to) {
if (stages & from) out |= to;
};
mapStage(vk::ShaderStageFlagBits::eVertex, vk::PipelineStageFlagBits2::eVertexShader);
mapStage(vk::ShaderStageFlagBits::eTessellationControl, vk::PipelineStageFlagBits2::eTessellationControlShader);
mapStage(vk::ShaderStageFlagBits::eTessellationEvaluation, vk::PipelineStageFlagBits2::eTessellationEvaluationShader);
mapStage(vk::ShaderStageFlagBits::eGeometry, vk::PipelineStageFlagBits2::eGeometryShader);
mapStage(vk::ShaderStageFlagBits::eFragment, vk::PipelineStageFlagBits2::eFragmentShader);
mapStage(vk::ShaderStageFlagBits::eCompute, vk::PipelineStageFlagBits2::eComputeShader);
return out;
}
case ResourceUse::TransferSrc:
case ResourceUse::TransferDst:
return vk::PipelineStageFlagBits2::eTransfer;
+214 -8
View File
@@ -40,14 +40,23 @@ namespace Engine
struct UniformBufferObject {
glm::mat4 view;
glm::mat4 proj;
glm::mat4 lightSpace; // 主光(lights[0])光空间变换(阴影用)
glm::vec4 ambientColor;
Light lights[4];
};
// Shadow pass 推送块:lightVP + model = 128B(最小保证上限内)
struct ShadowPushConstants {
glm::mat4 lightVP;
glm::mat4 model;
};
constexpr u32 kUboBinding = 0;
constexpr u32 kUBOSetIndex = 0;
constexpr u32 kTextureSetIndex = 1;
constexpr u32 kShadowSetIndex = 2;
constexpr u32 kMaxLights = 4;
constexpr u32 kShadowMapSize = 2048; // 固定分辨率(独立于 swapchain 尺寸)
}
Renderer::Renderer(Window& window)
@@ -75,9 +84,16 @@ namespace Engine
};
m_TextureSetLayout = m_DescriptorManager->CreateLayout(texBindings);
// 6. PipelineLayout:两个 set
// 5.5 Set 2: 阴影深度图(主 shader 采样对比)
std::vector<DescriptorBinding> shadowBindings = {
{0, vk::DescriptorType::eCombinedImageSampler, vk::ShaderStageFlagBits::eFragment},
};
m_ShadowSetLayout = m_DescriptorManager->CreateLayout(shadowBindings);
// 6. PipelineLayout:三个 set(主 shader 64B push;阴影 shader 单独 128B)
m_PipelineLayout = m_DescriptorManager->CreatePipelineLayout(
{m_UBOSetLayout, m_TextureSetLayout}, sizeof(glm::mat4));
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout},
sizeof(glm::mat4));
// 7. ShaderManager(VK_EXT_shader_object)
m_ShaderManager = CreateScope<ShaderManager>(*m_Device);
@@ -86,19 +102,53 @@ namespace Engine
auto vert = m_ShaderManager->CreateFromFile(
shaderDir + "simple_shader.vert.spv",
vk::ShaderStageFlagBits::eVertex,
{m_UBOSetLayout, m_TextureSetLayout}, sizeof(glm::mat4));
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout},
sizeof(glm::mat4));
auto frag = m_ShaderManager->CreateFromFile(
shaderDir + "simple_shader.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{m_UBOSetLayout, m_TextureSetLayout}, sizeof(glm::mat4));
{m_UBOSetLayout, m_TextureSetLayout, m_ShadowSetLayout},
sizeof(glm::mat4));
m_VertShader = vert.Handle;
m_FragShader = frag.Handle;
if (!m_VertShader || !m_FragShader) {
// 阴影 shader:无描述符,128B push(lightVP + model)
m_ShadowPipelineLayout = m_DescriptorManager->CreatePipelineLayout(
std::vector<vk::DescriptorSetLayout>{}, sizeof(ShadowPushConstants));
auto shadowVert = m_ShaderManager->CreateFromFile(
shaderDir + "shadow.vert.spv",
vk::ShaderStageFlagBits::eVertex,
{}, sizeof(ShadowPushConstants));
auto shadowFrag = m_ShaderManager->CreateFromFile(
shaderDir + "shadow.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{}, sizeof(ShadowPushConstants));
m_ShadowVertShader = shadowVert.Handle;
m_ShadowFragShader = shadowFrag.Handle;
if (!m_VertShader || !m_FragShader || !m_ShadowVertShader || !m_ShadowFragShader) {
ENGINE_FATAL("Failed to create shader objects");
}
// 阴影采样器:clamp-to-border + 白色边界(视锥外 = 全亮),
// 手动对比而非 comparison sampler,逻辑清晰且无 format feature 依赖
vk::SamplerCreateInfo samplerInfo;
samplerInfo.setMagFilter(vk::Filter::eLinear)
.setMinFilter(vk::Filter::eLinear)
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
.setAddressModeU(vk::SamplerAddressMode::eClampToBorder)
.setAddressModeV(vk::SamplerAddressMode::eClampToBorder)
.setAddressModeW(vk::SamplerAddressMode::eClampToBorder)
.setMipLodBias(0.0f)
.setAnisotropyEnable(VK_FALSE)
.setCompareEnable(VK_FALSE)
.setMinLod(0.0f)
.setMaxLod(0.0f)
.setBorderColor(vk::BorderColor::eFloatOpaqueWhite);
m_ShadowSampler = m_Device->GetDevice().createSampler(samplerInfo);
// 8. Swapchain(只管呈现;深度图由 RenderGraph 创建)
m_Swapchain = CreateScope<Swapchain>(*m_Device, *m_Allocator, m_Window);
m_Extent = m_Swapchain->GetExtent();
@@ -121,6 +171,15 @@ namespace Engine
m_UniformBuffer->Get(slot), m_UniformBuffer->GetSize());
}
// 10.5 阴影深度图描述符:每槽位一组。
// Compile 后由 UpdateShadowDescriptors 写入物理视图(槽位间隔离,
// 物理镜像不重建则无需再更新——轮换重编译时会重新调用)。
m_ShadowDescriptorSets.resize(kMaxFramesInFlight);
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_ShadowDescriptorSets[slot] =
m_DescriptorManager->AllocateDescriptorSet(m_ShadowSetLayout);
}
// 11. RenderGraph:声明 + 编译(场景在 Render(Scene&) 时按帧绑定)
BuildGraph();
@@ -137,10 +196,34 @@ namespace Engine
if (m_FragShader) {
m_Device->GetDevice().destroyShaderEXT(m_FragShader);
}
if (m_ShadowVertShader) {
m_Device->GetDevice().destroyShaderEXT(m_ShadowVertShader);
}
if (m_ShadowFragShader) {
m_Device->GetDevice().destroyShaderEXT(m_ShadowFragShader);
}
m_Device->GetDevice().destroyPipelineLayout(m_PipelineLayout);
m_Device->GetDevice().destroyPipelineLayout(m_ShadowPipelineLayout);
m_Device->GetDevice().destroyDescriptorSetLayout(m_UBOSetLayout);
m_Device->GetDevice().destroyDescriptorSetLayout(m_TextureSetLayout);
m_Device->GetDevice().destroyDescriptorSetLayout(m_ShadowSetLayout);
m_Device->GetDevice().destroySampler(m_ShadowSampler);
}
void Renderer::UpdateShadowDescriptors()
{
// 把阴影深度图各槽位的物理视图写进 Set 2。
// 必须在 Compile 之后调用(物理镜像此时才存在);
// swapchain 轮换重编译会重建物理镜像,因此 HandleResize 也要重新调用。
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_DescriptorManager->UpdateTexture(
m_ShadowDescriptorSets[slot], 0,
m_ShadowSampler,
m_RenderGraph->GetPhysicalView(m_ShadowMap, slot));
}
ENGINE_INFO("Shadow descriptors updated ({} slots)", kMaxFramesInFlight);
}
void Renderer::BuildGraph()
@@ -162,6 +245,102 @@ namespace Engine
m_Depth = m_RenderGraph->CreateTexture(
"MainDepth", RG::TextureDesc{depthFormat});
// 阴影深度图:固定分辨率(独立于 swapchain 尺寸),
// usage 由 uses 自动推导:DepthAttachment | Sampled
m_ShadowMap = m_RenderGraph->CreateTexture(
"ShadowMap",
RG::TextureDesc{vk::Format::eD32Sfloat, kShadowMapSize, kShadowMapSize});
// ---- Shadow pass:主光视角写深度 ----
// 只写深度、无颜色附件;只绑顶点 shader 也可以,这里同样绑了
// 空 fragment 以最大化驱动兼容性。
m_RenderGraph->AddPass("Shadow",
[this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd;
vk::ClearValue clearValue;
clearValue.setDepthStencil(vk::ClearDepthStencilValue(1.0f, 0));
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_ShadowMap))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, {kShadowMapSize, kShadowMapSize}})
.setLayerCount(1)
.setPDepthAttachment(&depthAttachment); // 无颜色附件
cmd.beginRendering(renderingInfo);
std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment
};
std::array<vk::ShaderEXT, 2> shaders = {
m_ShadowVertShader, m_ShadowFragShader};
cmd.bindShadersEXT(stages, shaders);
// ---- 动态状态(阴影 pass 自带一套,与 Forward 无关)----
cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_TRUE);
cmd.setDepthWriteEnable(VK_TRUE);
cmd.setDepthCompareOp(vk::CompareOp::eLess);
cmd.setDepthBoundsTestEnable(VK_FALSE);
cmd.setStencilTestEnable(VK_FALSE);
// 深度偏移:缓解 shadow acne(斜率缩放)
cmd.setDepthBiasEnable(VK_TRUE);
cmd.setDepthBias(1.25f, 0.0f, 1.75f);
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
cmd.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
cmd.setPrimitiveRestartEnable(VK_FALSE);
auto binding = Vertex::GetBindingDescription();
auto attributes = Vertex::GetAttributeDescriptions();
cmd.setVertexInputEXT(binding, attributes);
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
vk::SampleMask sampleMask = 0xFFFFFFFF;
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE);
vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(kShadowMapSize),
static_cast<float>(kShadowMapSize),
0.0f, 1.0f);
cmd.setViewportWithCount(viewport);
vk::Rect2D scissor({0, 0}, {kShadowMapSize, kShadowMapSize});
cmd.setScissorWithCount(scissor);
// ---- 绘制:主光视角下的全部渲染体 ----
m_BoundScene->Each<TransformComponent, MeshRendererComponent>(
[&](Entity, TransformComponent& transform,
MeshRendererComponent& meshRenderer) {
Model* model = m_BoundAssets->GetModel(meshRenderer.model);
if (!model) return;
ShadowPushConstants push{m_LightSpaceMatrix, transform.world};
cmd.pushConstants(m_ShadowPipelineLayout,
vk::ShaderStageFlagBits::eVertex,
0, sizeof(ShadowPushConstants), &push);
for (auto& mesh : model->GetMeshes()) {
mesh->Draw(cmd); // 阴影 pass 不需要材质描述符
}
});
cmd.endRendering();
})
.Write(m_ShadowMap, RG::ResourceUse::DepthAttachment);
// ---- Forward pass:直接渲染到 swapchain ----
// 场景数据每帧经 m_BoundScene 从 ECS 读取
m_RenderGraph->AddPass("Forward",
@@ -203,10 +382,13 @@ namespace Engine
std::array<vk::ShaderEXT, 2> shaders = {m_VertShader, m_FragShader};
cmd.bindShadersEXT(stages, shaders);
// Set 0:本帧槽位的 UBO
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影深度图
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_PipelineLayout, kUBOSetIndex,
m_UBODescriptorSets[ctx.frame.frameSlot], {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_PipelineLayout, kShadowSetIndex,
m_ShadowDescriptorSets[ctx.frame.frameSlot], {});
// ---- 动态状态 ----
cmd.setRasterizerDiscardEnable(VK_FALSE);
@@ -287,7 +469,10 @@ namespace Engine
cmd.endRendering();
})
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
.Write(m_Depth, RG::ResourceUse::DepthAttachment);
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
// 采样阴影深度图:RG 自动在 Shadow(写) → Forward(读) 之间
// 插入 eDepthAttachmentOptimal → eShaderReadOnlyOptimal 的 barrier
.Read(m_ShadowMap, RG::ResourceUse::ShaderRead);
// ---- Present pass:transition 到 ePresentSrcKHR ----
m_RenderGraph->AddPass("Present",
@@ -298,6 +483,7 @@ namespace Engine
.Read(m_Backbuffer, RG::ResourceUse::Present);
m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile
UpdateShadowDescriptors(); // 物理镜像就位,写 Set 2
}
void Renderer::HandleResize()
@@ -311,8 +497,9 @@ namespace Engine
m_SyncManager = CreateScope<SyncManager>(
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
// RG 重编译:物理镜像按新尺寸轮换
// RG 重编译:物理镜像按新尺寸轮换(阴影图是固定尺寸,会被重建)
m_RenderGraph->SetTargetExtent(m_Extent);
UpdateShadowDescriptors();
}
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets)
@@ -359,6 +546,25 @@ namespace Engine
ubo.lights[i].color = glm::vec4(0.0f);
}
// 1.5 光空间矩阵:主光(lights[0])视角的正交盒包住场景。
// 光源"位置" = 光方向反向拉远 30 个单位,看向原点。
// 深度重映射 [-1,1] → [0,1](Vulkan clip volume),与相机投影同理。
glm::vec3 lightDir(ubo.lights[0].direction);
if (glm::length(lightDir) > 0.0f) {
glm::mat4 lightView = glm::lookAt(
lightDir * 30.0f, // 沿光方向反推的光源位置
glm::vec3(0.0f), // 看向原点
glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 lightProj = glm::ortho(
-20.0f, 20.0f, -20.0f, 20.0f, 0.1f, 100.0f);
lightProj[2][2] = lightProj[2][2] * 0.5f + 0.5f;
lightProj[3][2] = lightProj[3][2] * 0.5f + 0.5f;
m_LightSpaceMatrix = lightProj * lightView;
}
ubo.lightSpace = m_LightSpaceMatrix;
// 此时本槽位的 fence 尚未 reset——先等它(保证 GPU 已完成本槽位上一轮),
// 再写 UBO,写-读竞争关闭。
m_SyncManager->WaitFence(frameSlot);
+7
View File
@@ -0,0 +1,7 @@
#version 450
// Shadow pass 片段着色器:深度-only pass,无任何输出。
// 注意:不能声明 out 变量——renderingInfo 无颜色附件时写 out 会被 validation 拒绝。
void main() {
}
+15
View File
@@ -0,0 +1,15 @@
#version 450
// Shadow pass 顶点着色器:只关心位置,输出光空间深度。
// push_constant 两个字 mat4 = 128 字节,正好在最小保证上限内。
layout(location = 0) in vec3 inPosition;
layout(push_constant) uniform PushConstants {
mat4 lightVP; // 光空间变换(正交)
mat4 model; // 实体世界矩阵
} pc;
void main() {
gl_Position = pc.lightVP * pc.model * vec4(inPosition, 1.0);
}
+26 -1
View File
@@ -10,18 +10,42 @@ struct Light {
layout(location = 0) in vec2 fragUV;
layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec3 fragWorldPos;
layout(location = 3) in vec4 fragLightPos;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view;
mat4 proj;
mat4 lightSpace;
vec4 ambientColor;
Light lights[MAX_LIGHTS];
} ubo;
layout(set = 1, binding = 0) uniform sampler2D texSampler;
// 阴影深度图:手动对比(非 comparison sampler,逻辑清晰且免 format feature 依赖)
layout(set = 2, binding = 0) uniform sampler2D shadowMap;
// 主光(lights[0])阴影因子:出光视锥外/未被遮挡 = 1.0
float ShadowFactor(vec3 N, vec3 L)
{
vec3 proj = fragLightPos.xyz / fragLightPos.w;
proj = proj * 0.5 + 0.5; // NDC [-1,1] -> UV [0,1]
if (proj.x < 0.0 || proj.x > 1.0 ||
proj.y < 0.0 || proj.y > 1.0 ||
proj.z > 1.0) {
return 1.0; // 光视锥外:不施加阴影(边界由 border color 兜底)
}
// 斜率缩放偏差,缓解 shadow acne
float bias = max(0.004 * (1.0 - dot(N, L)), 0.0008);
float closest = texture(shadowMap, proj.xy).r;
return (proj.z - bias > closest) ? 0.25 : 1.0;
}
void main() {
vec3 albedo = texture(texSampler, fragUV).rgb;
vec3 N = normalize(fragNormal);
@@ -31,7 +55,8 @@ void main() {
for (int i = 0; i < MAX_LIGHTS; ++i) {
vec3 L = normalize(-ubo.lights[i].direction.xyz);
float diff = max(dot(N, L), 0.0);
result += ubo.lights[i].color.rgb * ubo.lights[i].color.a * diff * albedo;
float shadow = (i == 0) ? ShadowFactor(N, L) : 1.0;
result += ubo.lights[i].color.rgb * ubo.lights[i].color.a * diff * albedo * shadow;
}
outColor = vec4(result, 1.0);
+4
View File
@@ -14,6 +14,7 @@ layout(location = 2) in vec2 inUV;
layout(location = 0) out vec2 fragUV;
layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec3 fragWorldPos;
layout(location = 3) out vec4 fragLightPos; // 光空间裁剪坐标(阴影采样用)
layout(push_constant) uniform PushConstants {
mat4 model;
@@ -22,6 +23,7 @@ layout(push_constant) uniform PushConstants {
layout(set = 0, binding = 0) uniform UniformBufferObject {
mat4 view;
mat4 proj;
mat4 lightSpace; // 主光(lights[0])的正交光空间变换
vec4 ambientColor;
Light lights[MAX_LIGHTS];
} ubo;
@@ -36,4 +38,6 @@ void main() {
fragNormal = normalize(normalMatrix * inNormal);
fragUV = inUV;
fragLightPos = ubo.lightSpace * worldPos;
}