feat: shadow mapping — depth-only render pass from light POV + PCF

- VulkanShadowMap.cs: 2048x2048 D32_SFLOAT image, sampler (clamp-to-border),
  separate shadow pipeline (depth-only, no color, depth bias enabled, CULL_NONE)
- Shadow shaders: shadow.vert (lightViewProj * model * pos), shadow.frag (empty)
- Main shaders updated: fragLightSpacePos output from vertex, PCF 3x3 in fragment
- Push constants expanded to 160B: model(64) + lightPos(16) + lightColor(16) + lightViewProj(64)
- 3 push constant ranges: Vertex(model), Fragment(light), Vertex|Fragment(lightViewProj)
- Descriptor set: 2 bindings — UBO(vp) + CombinedImageSampler(shadowMap)
- Shadow pass: before main pass, renders scene depth from light position
- Image transitions: shadow map UNDEFINED→DEPTH→SHADER_READ_ONLY each frame
- vkCmdSetDepthBias(1.25, 0, 1.75) for acne prevention
- Light VP: CreateLookAt(lightPos, origin, up) * Perspective(60°, 1.0, 0.1, 60)
- 0 validation errors on build
This commit is contained in:
emil28092005
2026-06-18 20:09:32 +03:00
parent 1dde764342
commit e6e305dfa9
13 changed files with 518 additions and 15 deletions
@@ -0,0 +1,4 @@
#version 450
void main() {
}
Binary file not shown.
@@ -0,0 +1,14 @@
#version 450
layout(location = 0) in vec3 inPosition;
layout(push_constant) uniform PC {
mat4 model;
vec4 lightPos;
vec4 lightColor;
mat4 lightViewProj;
} pc;
void main() {
gl_Position = pc.lightViewProj * pc.model * vec4(inPosition, 1.0);
}
Binary file not shown.
@@ -3,6 +3,7 @@
layout(location = 0) in vec3 fragWorldPos; layout(location = 0) in vec3 fragWorldPos;
layout(location = 1) in vec3 fragNormal; layout(location = 1) in vec3 fragNormal;
layout(location = 2) in vec3 fragAlbedo; layout(location = 2) in vec3 fragAlbedo;
layout(location = 3) in vec4 fragLightSpacePos;
layout(location = 0) out vec4 outColor; layout(location = 0) out vec4 outColor;
@@ -10,10 +11,13 @@ layout(set = 0, binding = 0) uniform CameraUBO {
mat4 vp; mat4 vp;
}; };
layout(set = 0, binding = 1) uniform sampler2D shadowMap;
layout(push_constant) uniform PC { layout(push_constant) uniform PC {
mat4 model; mat4 model;
vec4 lightPos; vec4 lightPos;
vec4 lightColor; vec4 lightColor;
mat4 lightViewProj;
} pc; } pc;
const vec3 AMBIENT = vec3(0.01, 0.01, 0.02); const vec3 AMBIENT = vec3(0.01, 0.01, 0.02);
@@ -60,6 +64,33 @@ vec3 acesTonemap(vec3 color)
return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0); return clamp((color * (a * color + b)) / (color * (c * color + d) + e), 0.0, 1.0);
} }
float calcShadow(vec4 lightSpacePos, vec3 N, vec3 L)
{
vec3 projCoords = lightSpacePos.xyz / lightSpacePos.w;
projCoords = projCoords * 0.5 + 0.5;
if (projCoords.x < 0.0 || projCoords.x > 1.0) return 1.0;
if (projCoords.y < 0.0 || projCoords.y > 1.0) return 1.0;
if (projCoords.z > 1.0) return 1.0;
float bias = max(0.005 * (1.0 - dot(N, L)), 0.0005);
float currentDepth = projCoords.z;
// PCF 3x3
float shadow = 0.0;
vec2 texelSize = 1.0 / vec2(2048.0, 2048.0);
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
float closestDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > closestDepth ? 0.0 : 1.0;
}
}
shadow /= 9.0;
return shadow;
}
void main() void main()
{ {
vec3 N = normalize(fragNormal); vec3 N = normalize(fragNormal);
@@ -68,8 +99,6 @@ void main()
float roughness = 0.5; float roughness = 0.5;
float metallic = 0.1; float metallic = 0.1;
vec3 color = AMBIENT * albedo;
vec3 lightPos = pc.lightPos.xyz; vec3 lightPos = pc.lightPos.xyz;
float lightIntensity = pc.lightPos.w; float lightIntensity = pc.lightPos.w;
vec3 lightColor = pc.lightColor.xyz; vec3 lightColor = pc.lightColor.xyz;
@@ -82,6 +111,9 @@ void main()
float attenuation = pow(clamp(1.0 - dist / max(lightRange, 0.001), 0.0, 1.0), 2.0); float attenuation = pow(clamp(1.0 - dist / max(lightRange, 0.001), 0.0, 1.0), 2.0);
vec3 radiance = lightColor * lightIntensity * attenuation; vec3 radiance = lightColor * lightIntensity * attenuation;
// Shadow
float shadow = calcShadow(fragLightSpacePos, N, L);
vec3 H = normalize(V + L); vec3 H = normalize(V + L);
vec3 F0 = mix(vec3(0.04), albedo, metallic); vec3 F0 = mix(vec3(0.04), albedo, metallic);
@@ -97,7 +129,9 @@ void main()
vec3 kD = (vec3(1.0) - kS) * (1.0 - metallic); vec3 kD = (vec3(1.0) - kS) * (1.0 - metallic);
float NdotL = max(dot(N, L), 0.0); float NdotL = max(dot(N, L), 0.0);
color += (kD * albedo / PI + specular) * radiance * NdotL; vec3 lighting = (kD * albedo / PI + specular) * radiance * NdotL * shadow;
vec3 color = AMBIENT * albedo + lighting;
color = acesTonemap(color); color = acesTonemap(color);
color = pow(color, vec3(1.0 / 2.2)); color = pow(color, vec3(1.0 / 2.2));
Binary file not shown.
@@ -7,6 +7,7 @@ layout(location = 2) in vec3 inNormal;
layout(location = 0) out vec3 fragWorldPos; layout(location = 0) out vec3 fragWorldPos;
layout(location = 1) out vec3 fragNormal; layout(location = 1) out vec3 fragNormal;
layout(location = 2) out vec3 fragAlbedo; layout(location = 2) out vec3 fragAlbedo;
layout(location = 3) out vec4 fragLightSpacePos;
layout(set = 0, binding = 0) uniform CameraUBO { layout(set = 0, binding = 0) uniform CameraUBO {
mat4 vp; mat4 vp;
@@ -16,13 +17,14 @@ layout(push_constant) uniform PC {
mat4 model; mat4 model;
vec4 lightPos; vec4 lightPos;
vec4 lightColor; vec4 lightColor;
mat4 lightViewProj;
} pc; } pc;
void main() { void main() {
vec4 worldPos = pc.model * vec4(inPosition, 1.0); vec4 worldPos = pc.model * vec4(inPosition, 1.0);
gl_Position = pc.model * vec4(inPosition, 1.0);
gl_Position = vp * worldPos; gl_Position = vp * worldPos;
fragWorldPos = worldPos.xyz; fragWorldPos = worldPos.xyz;
fragNormal = mat3(pc.model) * inNormal; fragNormal = mat3(pc.model) * inNormal;
fragAlbedo = inColor; fragAlbedo = inColor;
fragLightSpacePos = pc.lightViewProj * worldPos;
} }
Binary file not shown.
+3
View File
@@ -64,6 +64,7 @@ internal static unsafe class Vk
public delegate void VkCmdDraw(VkCommandBuffer commandBuffer, uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance); public delegate void VkCmdDraw(VkCommandBuffer commandBuffer, uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance);
public delegate void VkCmdDrawIndexed(VkCommandBuffer commandBuffer, uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance); public delegate void VkCmdDrawIndexed(VkCommandBuffer commandBuffer, uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance);
public delegate void VkCmdBindIndexBuffer(VkCommandBuffer commandBuffer, VkBuffer buffer, ulong offset, int indexType); public delegate void VkCmdBindIndexBuffer(VkCommandBuffer commandBuffer, VkBuffer buffer, ulong offset, int indexType);
public delegate void VkCmdSetDepthBias(VkCommandBuffer commandBuffer, float depthBiasConstantFactor, float depthBiasClamp, float depthBiasSlopeFactor);
public delegate void VkCmdBeginRendering(VkCommandBuffer commandBuffer, VkRenderingInfo* pRenderingInfo); public delegate void VkCmdBeginRendering(VkCommandBuffer commandBuffer, VkRenderingInfo* pRenderingInfo);
public delegate void VkCmdEndRendering(VkCommandBuffer commandBuffer); public delegate void VkCmdEndRendering(VkCommandBuffer commandBuffer);
public delegate void VkCmdPipelineBarrier2(VkCommandBuffer commandBuffer, VkDependencyInfo* pDependencyInfo); public delegate void VkCmdPipelineBarrier2(VkCommandBuffer commandBuffer, VkDependencyInfo* pDependencyInfo);
@@ -148,6 +149,7 @@ internal static unsafe class Vk
public static VkCmdDraw vkCmdDraw; public static VkCmdDraw vkCmdDraw;
public static VkCmdDrawIndexed vkCmdDrawIndexed; public static VkCmdDrawIndexed vkCmdDrawIndexed;
public static VkCmdBindIndexBuffer vkCmdBindIndexBuffer; public static VkCmdBindIndexBuffer vkCmdBindIndexBuffer;
public static VkCmdSetDepthBias vkCmdSetDepthBias;
public static VkCmdBeginRendering vkCmdBeginRendering; public static VkCmdBeginRendering vkCmdBeginRendering;
public static VkCmdEndRendering vkCmdEndRendering; public static VkCmdEndRendering vkCmdEndRendering;
public static VkCmdPipelineBarrier2 vkCmdPipelineBarrier2; public static VkCmdPipelineBarrier2 vkCmdPipelineBarrier2;
@@ -239,6 +241,7 @@ internal static unsafe class Vk
vkCmdDraw = LoadDev<VkCmdDraw>(p, "vkCmdDraw"); vkCmdDraw = LoadDev<VkCmdDraw>(p, "vkCmdDraw");
vkCmdDrawIndexed = LoadDev<VkCmdDrawIndexed>(p, "vkCmdDrawIndexed"); vkCmdDrawIndexed = LoadDev<VkCmdDrawIndexed>(p, "vkCmdDrawIndexed");
vkCmdBindIndexBuffer = LoadDev<VkCmdBindIndexBuffer>(p, "vkCmdBindIndexBuffer"); vkCmdBindIndexBuffer = LoadDev<VkCmdBindIndexBuffer>(p, "vkCmdBindIndexBuffer");
vkCmdSetDepthBias = LoadDev<VkCmdSetDepthBias>(p, "vkCmdSetDepthBias");
vkCmdBeginRendering = LoadDev<VkCmdBeginRendering>(p, "vkCmdBeginRendering"); vkCmdBeginRendering = LoadDev<VkCmdBeginRendering>(p, "vkCmdBeginRendering");
vkCmdEndRendering = LoadDev<VkCmdEndRendering>(p, "vkCmdEndRendering"); vkCmdEndRendering = LoadDev<VkCmdEndRendering>(p, "vkCmdEndRendering");
vkCmdPipelineBarrier2 = LoadDev<VkCmdPipelineBarrier2>(p, "vkCmdPipelineBarrier2"); vkCmdPipelineBarrier2 = LoadDev<VkCmdPipelineBarrier2>(p, "vkCmdPipelineBarrier2");
@@ -140,19 +140,25 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
private void CreateDescriptorPoolAndSets(VkDescriptorSetLayout layout) private void CreateDescriptorPoolAndSets(VkDescriptorSetLayout layout)
{ {
var poolSize = new VkDescriptorPoolSize var poolSizes = stackalloc VkDescriptorPoolSize[2];
poolSizes[0] = new VkDescriptorPoolSize
{ {
type = VkDescriptorType.UniformBuffer, type = VkDescriptorType.UniformBuffer,
descriptorCount = MaxFramesInFlight, descriptorCount = MaxFramesInFlight,
}; };
poolSizes[1] = new VkDescriptorPoolSize
{
type = VkDescriptorType.CombinedImageSampler,
descriptorCount = MaxFramesInFlight,
};
var poolInfo = new VkDescriptorPoolCreateInfo var poolInfo = new VkDescriptorPoolCreateInfo
{ {
sType = VkStructureType.DescriptorPoolCreateInfo, sType = VkStructureType.DescriptorPoolCreateInfo,
flags = VkDescriptorPoolCreateFlags.FreeDescriptorSet, flags = VkDescriptorPoolCreateFlags.FreeDescriptorSet,
maxSets = MaxFramesInFlight, maxSets = MaxFramesInFlight,
poolSizeCount = 1, poolSizeCount = 2,
pPoolSizes = &poolSize, pPoolSizes = poolSizes,
}; };
var descPool = VkDescriptorPool.Null; var descPool = VkDescriptorPool.Null;
@@ -204,6 +210,29 @@ internal sealed unsafe class VulkanFrameResources : IDisposable
} }
} }
public void UpdateShadowDescriptor(int frameIndex, VkSampler sampler, VkImageView shadowView)
{
var imageInfo = new VkDescriptorImageInfo
{
sampler = sampler,
imageView = shadowView,
imageLayout = VkImageLayout.ShaderReadOnlyOptimal,
};
var write = new VkWriteDescriptorSet
{
sType = VkStructureType.WriteDescriptorSet,
dstSet = DescriptorSets[frameIndex],
dstBinding = 1,
dstArrayElement = 0,
descriptorCount = 1,
descriptorType = VkDescriptorType.CombinedImageSampler,
pImageInfo = (nint)(&imageInfo),
};
Vk.vkUpdateDescriptorSets(_device, 1, &write, 0, 0);
}
public void UpdateUbo(int frameIndex, void* data, ulong size) public void UpdateUbo(int frameIndex, void* data, ulong size)
{ {
void* pData = null; void* pData = null;
+19 -5
View File
@@ -150,7 +150,7 @@ internal sealed unsafe class VulkanPipeline : IDisposable
pDynamicStates = dynamicStates, pDynamicStates = dynamicStates,
}; };
var pushConstantRanges = stackalloc VkPushConstantRange[2]; var pushConstantRanges = stackalloc VkPushConstantRange[3];
pushConstantRanges[0] = new VkPushConstantRange pushConstantRanges[0] = new VkPushConstantRange
{ {
stageFlags = VkShaderStageFlags.Vertex, stageFlags = VkShaderStageFlags.Vertex,
@@ -163,6 +163,12 @@ internal sealed unsafe class VulkanPipeline : IDisposable
offset = 64, offset = 64,
size = 32, size = 32,
}; };
pushConstantRanges[2] = new VkPushConstantRange
{
stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment,
offset = 96,
size = 64,
};
var descLayout = DescriptorSetLayout; var descLayout = DescriptorSetLayout;
var layoutInfo = new VkPipelineLayoutCreateInfo var layoutInfo = new VkPipelineLayoutCreateInfo
@@ -170,7 +176,7 @@ internal sealed unsafe class VulkanPipeline : IDisposable
sType = VkStructureType.PipelineLayoutCreateInfo, sType = VkStructureType.PipelineLayoutCreateInfo,
setLayoutCount = 1, setLayoutCount = 1,
pSetLayouts = &descLayout, pSetLayouts = &descLayout,
pushConstantRangeCount = 2, pushConstantRangeCount = 3,
pPushConstantRanges = (nint)pushConstantRanges, pPushConstantRanges = (nint)pushConstantRanges,
}; };
@@ -221,19 +227,27 @@ internal sealed unsafe class VulkanPipeline : IDisposable
private void CreateDescriptorSetLayout() private void CreateDescriptorSetLayout()
{ {
var binding = new VkDescriptorSetLayoutBinding var bindings = stackalloc VkDescriptorSetLayoutBinding[2];
bindings[0] = new VkDescriptorSetLayoutBinding
{ {
binding = 0, binding = 0,
descriptorType = VkDescriptorType.UniformBuffer, descriptorType = VkDescriptorType.UniformBuffer,
descriptorCount = 1, descriptorCount = 1,
stageFlags = VkShaderStageFlags.Vertex, stageFlags = VkShaderStageFlags.Vertex,
}; };
bindings[1] = new VkDescriptorSetLayoutBinding
{
binding = 1,
descriptorType = VkDescriptorType.CombinedImageSampler,
descriptorCount = 1,
stageFlags = VkShaderStageFlags.Fragment,
};
var info = new VkDescriptorSetLayoutCreateInfo var info = new VkDescriptorSetLayoutCreateInfo
{ {
sType = VkStructureType.DescriptorSetLayoutCreateInfo, sType = VkStructureType.DescriptorSetLayoutCreateInfo,
bindingCount = 1, bindingCount = 2,
pBindings = &binding, pBindings = bindings,
}; };
var descLayout = VkDescriptorSetLayout.Null; var descLayout = VkDescriptorSetLayout.Null;
+101 -3
View File
@@ -13,6 +13,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
private readonly VulkanSwapchain _swapchain; private readonly VulkanSwapchain _swapchain;
private readonly VulkanPipeline _pipeline; private readonly VulkanPipeline _pipeline;
private readonly VulkanFrameResources _frameResources; private readonly VulkanFrameResources _frameResources;
private readonly VulkanShadowMap _shadowMap;
internal readonly VulkanImGui? _imGui; internal readonly VulkanImGui? _imGui;
private readonly Dictionary<ulong, (VulkanVertexBuffer vb, VulkanIndexBuffer ib, uint indexCount)> _meshCache = new(); private readonly Dictionary<ulong, (VulkanVertexBuffer vb, VulkanIndexBuffer ib, uint indexCount)> _meshCache = new();
@@ -39,6 +40,11 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
_frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex, _frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex,
swapchain.ImageCount, ctx, _pipeline.DescriptorSetLayout); swapchain.ImageCount, ctx, _pipeline.DescriptorSetLayout);
_shadowMap = new VulkanShadowMap(ctx.Device, ctx, _pipeline.DescriptorSetLayout);
for (int i = 0; i < VulkanFrameResources.MaxFramesInFlight; i++)
_frameResources.UpdateShadowDescriptor(i, _shadowMap.ShadowSampler, _shadowMap.DepthImageView);
_imGui = new VulkanImGui(ctx, _frameResources.CommandPool, swapchain.Format, swapchain.DepthFormat); _imGui = new VulkanImGui(ctx, _frameResources.CommandPool, swapchain.Format, swapchain.DepthFormat);
} }
@@ -105,6 +111,13 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
var vpCopy = vp; var vpCopy = vp;
System.Buffer.MemoryCopy(&vpCopy, uboData, 64, 64); System.Buffer.MemoryCopy(&vpCopy, uboData, 64, 64);
// Compute light view-projection matrix for shadow mapping
var lightPosVec = new Vector3(lightPos.X, lightPos.Y, lightPos.Z);
var lightView = Matrix4x4.CreateLookAt(lightPosVec, Vector3.Zero, Vector3.UnitY);
var lightProj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1.0f, 0.1f, 60f);
lightProj.M22 *= -1;
var lightViewProj = lightView * lightProj;
var drawCalls = new List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)>(); var drawCalls = new List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)>();
world.Each((Entity e, ref Transform t, ref Mesh m) => world.Each((Entity e, ref Transform t, ref Mesh m) =>
@@ -123,10 +136,10 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
drawCalls.Add((entry.vb.Buffer, entry.ib.Buffer, entry.indexCount, t.GetMatrix())); drawCalls.Add((entry.vb.Buffer, entry.ib.Buffer, entry.indexCount, t.GetMatrix()));
}); });
Render(vp, drawCalls, uboData, lightPos, lightColor); Render(vp, drawCalls, uboData, lightPos, lightColor, lightViewProj);
} }
private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls, byte* uboData, Vector4 lightPos, Vector4 lightColor) private void Render(Matrix4x4 vp, List<(VkBuffer vertexBuf, VkBuffer indexBuf, uint indexCount, Matrix4x4 model)> drawCalls, byte* uboData, Vector4 lightPos, Vector4 lightColor, Matrix4x4 lightViewProj)
{ {
_frameResources.WaitFrame(_frameIndex); _frameResources.WaitFrame(_frameIndex);
@@ -138,7 +151,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
{ {
_swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width, _swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width,
_ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height); _ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height);
Render(vp, drawCalls, uboData, lightPos, lightColor); Render(vp, drawCalls, uboData, lightPos, lightColor, lightViewProj);
return; return;
} }
@@ -159,6 +172,86 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
}; };
Vk.vkBeginCommandBuffer(cmd, &beginInfo); Vk.vkBeginCommandBuffer(cmd, &beginInfo);
// === SHADOW PASS ===
TransitionImageLayoutDepth(cmd, _shadowMap.DepthImage,
VkImageLayout.Undefined, VkImageLayout.DepthStencilAttachmentOptimal,
0, 0, 0x100, 0x200);
var shadowDepthClear = new VkClearValue
{
DepthStencil = new VkClearDepthStencilValue { Depth = 1.0f, Stencil = 0 },
};
var shadowDepthAttachment = new VkRenderingAttachmentInfo
{
sType = VkStructureType.RenderingAttachmentInfo,
imageView = _shadowMap.DepthImageView,
imageLayout = VkImageLayout.DepthStencilAttachmentOptimal,
loadOp = VkAttachmentLoadOp.Clear,
storeOp = VkAttachmentStoreOp.Store,
clearValue = shadowDepthClear,
};
var shadowRenderingInfo = new VkRenderingInfo
{
sType = VkStructureType.RenderingInfo,
renderArea = new VkRect2D
{
Offset = new VkOffset2D { X = 0, Y = 0 },
Extent = new VkExtent2D { Width = VulkanShadowMap.ShadowMapSize, Height = VulkanShadowMap.ShadowMapSize },
},
layerCount = 1,
colorAttachmentCount = 0,
pColorAttachments = null,
pDepthAttachment = &shadowDepthAttachment,
};
Vk.vkCmdBeginRendering(cmd, &shadowRenderingInfo);
Vk.vkCmdBindPipeline(cmd, VkPipelineBindPoint.Graphics, _shadowMap.Pipeline);
var shadowViewport = new VkViewport
{
X = 0, Y = 0,
Width = VulkanShadowMap.ShadowMapSize,
Height = VulkanShadowMap.ShadowMapSize,
MinDepth = 0, MaxDepth = 1,
};
Vk.vkCmdSetViewport(cmd, 0, 1, &shadowViewport);
var shadowScissor = new VkRect2D
{
Offset = new VkOffset2D { X = 0, Y = 0 },
Extent = new VkExtent2D { Width = VulkanShadowMap.ShadowMapSize, Height = VulkanShadowMap.ShadowMapSize },
};
Vk.vkCmdSetScissor(cmd, 0, 1, &shadowScissor);
Vk.vkCmdSetDepthBias(cmd, 1.25f, 0.0f, 1.75f);
foreach (var dc in drawCalls)
{
var vertexBuf = dc.vertexBuf;
ulong offset = 0;
Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuf, &offset);
Vk.vkCmdBindIndexBuffer(cmd, dc.indexBuf, 0, 1);
var model = dc.model;
Vk.vkCmdPushConstants(cmd, _shadowMap.PipelineLayout, VkShaderStageFlags.Vertex, 0, 64, &model);
var lvpCopy = lightViewProj;
Vk.vkCmdPushConstants(cmd, _shadowMap.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 96, 64, &lvpCopy);
Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0);
}
Vk.vkCmdEndRendering(cmd);
// Transition shadow map: depth attachment → shader read
TransitionImageLayoutDepth(cmd, _shadowMap.DepthImage,
VkImageLayout.DepthStencilAttachmentOptimal, VkImageLayout.ShaderReadOnlyOptimal,
0x100, 0x200, 0x8, 0x20);
// === MAIN PASS ===
TransitionImageLayout(cmd, _swapchain.Images[imageIndex], TransitionImageLayout(cmd, _swapchain.Images[imageIndex],
VkImageLayout.Undefined, VkImageLayout.ColorAttachmentOptimal, VkImageLayout.Undefined, VkImageLayout.ColorAttachmentOptimal,
0, 0, 0, 0,
@@ -253,6 +346,10 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
var lcCopy = lightColor; var lcCopy = lightColor;
Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Fragment, 80, 16, &lcCopy); Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Fragment, 80, 16, &lcCopy);
// Light view-proj at offset 96 (vertex + fragment)
var lvpCopy = lightViewProj;
Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 96, 64, &lvpCopy);
Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0);
} }
@@ -442,6 +539,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen
} }
_meshCache.Clear(); _meshCache.Clear();
_shadowMap?.Dispose();
_imGui?.Dispose(); _imGui?.Dispose();
_frameResources?.Dispose(); _frameResources?.Dispose();
_pipeline?.Dispose(); _pipeline?.Dispose();
@@ -0,0 +1,305 @@
using System.Runtime.InteropServices;
namespace Engine.Graphics.Vulkan;
internal sealed unsafe class VulkanShadowMap : IDisposable
{
public const uint ShadowMapSize = 2048;
public VkImage DepthImage;
public VkDeviceMemory DepthImageMemory;
public VkImageView DepthImageView;
public VkSampler ShadowSampler;
public VkPipeline Pipeline;
public VkPipelineLayout PipelineLayout;
public VkShaderModule VertModule;
private readonly VkDevice _device;
private readonly VulkanContext _ctx;
private bool _disposed;
public VulkanShadowMap(VkDevice device, VulkanContext ctx, VkDescriptorSetLayout descLayout)
{
_device = device;
_ctx = ctx;
CreateShadowImage();
CreateShadowSampler();
CreateShadowPipeline(descLayout);
Console.WriteLine("[Vulkan] Shadow map created (2048x2048 D32_SFLOAT)");
}
private void CreateShadowImage()
{
var imageInfo = new VkImageCreateInfo
{
sType = VkStructureType.ImageCreateInfo,
imageType = VkImageType.Type2D,
format = VkFormat.D32Sfloat,
extent = new VkExtent3D { Width = ShadowMapSize, Height = ShadowMapSize, Depth = 1 },
mipLevels = 1,
arrayLayers = 1,
samples = VkSampleCountFlags.Count1,
tiling = VkImageTiling.Optimal,
usage = VkImageUsageFlags.DepthStencilAttachment | VkImageUsageFlags.Sampled,
sharingMode = VkSharingMode.Exclusive,
initialLayout = VkImageLayout.Undefined,
};
var img = VkImage.Null;
Vk.vkCreateImage(_device, &imageInfo, 0, &img);
DepthImage = img;
var reqs = new VkMemoryRequirements();
Vk.vkGetImageMemoryRequirements(_device, DepthImage, &reqs);
var memTypeIndex = _ctx.FindMemoryType(reqs.memoryTypeBits, VkMemoryPropertyFlags.DeviceLocal);
var allocInfo = new VkMemoryAllocateInfo
{
sType = VkStructureType.MemoryAllocateInfo,
allocationSize = reqs.size,
memoryTypeIndex = memTypeIndex,
};
var mem = VkDeviceMemory.Null;
Vk.vkAllocateMemory(_device, &allocInfo, 0, &mem);
DepthImageMemory = mem;
Vk.vkBindImageMemory(_device, DepthImage, DepthImageMemory, 0);
var viewInfo = new VkImageViewCreateInfo
{
sType = VkStructureType.ImageViewCreateInfo,
image = DepthImage,
viewType = VkImageViewType.Type2D,
format = VkFormat.D32Sfloat,
components = new VkComponentMapping { R = VkComponentSwizzle.Identity, G = VkComponentSwizzle.Identity, B = VkComponentSwizzle.Identity, A = VkComponentSwizzle.Identity },
subresourceRange = new VkImageSubresourceRange { AspectMask = VkImageAspectFlags.Depth, BaseMipLevel = 0, LevelCount = 1, BaseArrayLayer = 0, LayerCount = 1 },
};
var view = VkImageView.Null;
Vk.vkCreateImageView(_device, &viewInfo, 0, &view);
DepthImageView = view;
}
private void CreateShadowSampler()
{
var samplerInfo = new VkSamplerCreateInfo
{
sType = VkStructureType.SamplerCreateInfo,
magFilter = VkFilter.Linear,
minFilter = VkFilter.Linear,
mipmapMode = VkSamplerMipmapMode.Linear,
addressModeU = VkSamplerAddressMode.ClampToBorder,
addressModeV = VkSamplerAddressMode.ClampToBorder,
addressModeW = VkSamplerAddressMode.ClampToBorder,
minLod = 0,
maxLod = 1,
borderColor = 1, // VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE
};
var samp = VkSampler.Null;
Vk.vkCreateSampler(_device, &samplerInfo, 0, &samp);
ShadowSampler = samp;
}
private void CreateShadowPipeline(VkDescriptorSetLayout descLayout)
{
var vertSpv = LoadShader("Shaders/shadow.vert.spv");
var fragSpv = LoadShader("Shaders/shadow.frag.spv");
VertModule = CreateShaderModule(vertSpv);
var fragModule = CreateShaderModule(fragSpv);
fixed (byte* pName = "main\0"u8)
{
var stages = stackalloc VkPipelineShaderStageCreateInfo[2];
stages[0] = new VkPipelineShaderStageCreateInfo
{
sType = VkStructureType.PipelineShaderStageCreateInfo,
stage = VkShaderStageFlags.Vertex,
module = VertModule,
pName = pName,
};
stages[1] = new VkPipelineShaderStageCreateInfo
{
sType = VkStructureType.PipelineShaderStageCreateInfo,
stage = VkShaderStageFlags.Fragment,
module = fragModule,
pName = pName,
};
var bindings = stackalloc VkVertexInputBindingDescription[1];
bindings[0] = new VkVertexInputBindingDescription { binding = 0, stride = (uint)sizeof(Engine.Core.Vertex), inputRate = VkVertexInputRate.Vertex };
var attributes = stackalloc VkVertexInputAttributeDescription[1];
attributes[0] = new VkVertexInputAttributeDescription { location = 0, binding = 0, format = VkFormat.R32G32B32Sfloat, offset = 0 };
var vertexInputState = new VkPipelineVertexInputStateCreateInfo
{
sType = VkStructureType.PipelineVertexInputStateCreateInfo,
vertexBindingDescriptionCount = 1,
pVertexBindingDescriptions = bindings,
vertexAttributeDescriptionCount = 1,
pVertexAttributeDescriptions = attributes,
};
var inputAssemblyState = new VkPipelineInputAssemblyStateCreateInfo
{
sType = VkStructureType.PipelineInputAssemblyStateCreateInfo,
topology = VkPrimitiveTopology.TriangleList,
primitiveRestartEnable = VkBool32.False,
};
var viewportState = new VkPipelineViewportStateCreateInfo
{
sType = VkStructureType.PipelineViewportStateCreateInfo,
viewportCount = 1,
scissorCount = 1,
};
var rasterizationState = new VkPipelineRasterizationStateCreateInfo
{
sType = VkStructureType.PipelineRasterizationStateCreateInfo,
depthClampEnable = VkBool32.False,
rasterizerDiscardEnable = VkBool32.False,
polygonMode = VkPolygonMode.Fill,
cullMode = VkCullModeFlags.None,
frontFace = VkFrontFace.CounterClockwise,
depthBiasEnable = VkBool32.True,
lineWidth = 1.0f,
};
var multisampleState = new VkPipelineMultisampleStateCreateInfo
{
sType = VkStructureType.PipelineMultisampleStateCreateInfo,
rasterizationSamples = VkSampleCountFlags.Count1,
};
var depthStencilState = new VkPipelineDepthStencilStateCreateInfo
{
sType = VkStructureType.PipelineDepthStencilStateCreateInfo,
depthTestEnable = VkBool32.True,
depthWriteEnable = VkBool32.True,
depthCompareOp = VkCompareOp.LessOrEqual,
depthBoundsTestEnable = VkBool32.False,
stencilTestEnable = VkBool32.False,
};
var colorBlendState = new VkPipelineColorBlendStateCreateInfo
{
sType = VkStructureType.PipelineColorBlendStateCreateInfo,
logicOpEnable = VkBool32.False,
attachmentCount = 0,
pAttachments = null,
};
var dynamicStates = stackalloc VkDynamicState[3];
dynamicStates[0] = VkDynamicState.Viewport;
dynamicStates[1] = VkDynamicState.Scissor;
dynamicStates[2] = VkDynamicState.DepthBias;
var dynamicState = new VkPipelineDynamicStateCreateInfo
{
sType = VkStructureType.PipelineDynamicStateCreateInfo,
dynamicStateCount = 3,
pDynamicStates = dynamicStates,
};
var pushConstantRanges = stackalloc VkPushConstantRange[2];
pushConstantRanges[0] = new VkPushConstantRange { stageFlags = VkShaderStageFlags.Vertex, offset = 0, size = 64 };
pushConstantRanges[1] = new VkPushConstantRange { stageFlags = VkShaderStageFlags.Fragment, offset = 64, size = 96 };
var layoutInfo = new VkPipelineLayoutCreateInfo
{
sType = VkStructureType.PipelineLayoutCreateInfo,
setLayoutCount = 0,
pushConstantRangeCount = 2,
pPushConstantRanges = (nint)pushConstantRanges,
};
var pl = VkPipelineLayout.Null;
Vk.vkCreatePipelineLayout(_device, &layoutInfo, 0, &pl);
PipelineLayout = pl;
var depthFormat = VkFormat.D32Sfloat;
var renderingInfo = new VkPipelineRenderingCreateInfo
{
sType = VkStructureType.PipelineRenderingCreateInfo,
colorAttachmentCount = 0,
pColorAttachmentFormats = null,
depthAttachmentFormat = depthFormat,
};
var pipelineInfo = new VkGraphicsPipelineCreateInfo
{
sType = VkStructureType.GraphicsPipelineCreateInfo,
pNext = (nint)(&renderingInfo),
stageCount = 2,
pStages = stages,
pVertexInputState = &vertexInputState,
pInputAssemblyState = &inputAssemblyState,
pViewportState = &viewportState,
pRasterizationState = &rasterizationState,
pMultisampleState = &multisampleState,
pDepthStencilState = &depthStencilState,
pColorBlendState = &colorBlendState,
pDynamicState = &dynamicState,
layout = PipelineLayout,
renderPass = new VkRenderPass { Handle = 0 },
subpass = 0,
};
var pp = VkPipeline.Null;
Vk.vkCreateGraphicsPipelines(_device, 0, 1, &pipelineInfo, 0, &pp);
Pipeline = pp;
Vk.vkDestroyShaderModule(_device, fragModule, 0);
}
}
private VkShaderModule CreateShaderModule(byte[] spv)
{
fixed (byte* pCode = spv)
{
var info = new VkShaderModuleCreateInfo
{
sType = VkStructureType.ShaderModuleCreateInfo,
codeSize = (nuint)spv.Length,
pCode = (uint*)pCode,
};
var module = VkShaderModule.Null;
Vk.vkCreateShaderModule(_device, &info, 0, &module);
return module;
}
}
private static byte[] LoadShader(string path)
{
if (!File.Exists(path))
{
var altPath = Path.Combine(AppContext.BaseDirectory, path);
if (!File.Exists(altPath))
{
altPath = Path.Combine(AppContext.BaseDirectory, "Shaders", Path.GetFileName(path));
if (!File.Exists(altPath))
throw new FileNotFoundException($"Shader file not found: {path}");
}
return File.ReadAllBytes(altPath);
}
return File.ReadAllBytes(path);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (Pipeline.Handle != 0) Vk.vkDestroyPipeline(_device, Pipeline, 0);
if (PipelineLayout.Handle != 0) Vk.vkDestroyPipelineLayout(_device, PipelineLayout, 0);
if (VertModule.Handle != 0) Vk.vkDestroyShaderModule(_device, VertModule, 0);
if (ShadowSampler.Handle != 0) Vk.vkDestroySampler(_device, ShadowSampler, 0);
if (DepthImageView.Handle != 0) Vk.vkDestroyImageView(_device, DepthImageView, 0);
if (DepthImage.Handle != 0) Vk.vkDestroyImage(_device, DepthImage, 0);
if (DepthImageMemory.Handle != 0) Vk.vkFreeMemory(_device, DepthImageMemory, 0);
}
}