feat: add depth buffer, camera and MVP push constants
- Add Camera ECS component with view/projection matrices. - Add Vulkan depth buffer to Swapchain: image, memory, view, render pass, framebuffers. - Update VulkanPipeline with depth stencil testing and push-constant layout. - Update vertex shader to use push-constant MVP matrix. - MeshRenderer finds Camera, computes MVP per entity and pushes it. - Program.cs creates a camera entity and keeps the cube rotating. Build and run verified: window opens and FPS is reported.
This commit is contained in:
@@ -35,6 +35,16 @@ class Program
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, new Vector3(0.5f)))
|
||||
.Set(mesh);
|
||||
|
||||
var camera = world.Entity("Camera")
|
||||
.Set(new Camera(
|
||||
new Vector3(0.0f, 0.0f, -2.0f),
|
||||
Vector3.Zero,
|
||||
Vector3.UnitY,
|
||||
MathF.PI / 4.0f,
|
||||
1280.0f / 720.0f,
|
||||
0.1f,
|
||||
100.0f));
|
||||
|
||||
var frames = 0;
|
||||
var lastFpsTime = 0.0;
|
||||
var lastWidth = window.Width;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Engine.Core.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A perspective camera component for the ECS.
|
||||
/// Provides view and projection matrices for the renderer.
|
||||
/// </summary>
|
||||
public record struct Camera
|
||||
{
|
||||
public Vector3 Position;
|
||||
public Vector3 Target;
|
||||
public Vector3 Up;
|
||||
public float FieldOfView;
|
||||
public float AspectRatio;
|
||||
public float NearPlane;
|
||||
public float FarPlane;
|
||||
|
||||
public Camera(
|
||||
Vector3 position,
|
||||
Vector3 target,
|
||||
Vector3 up,
|
||||
float fieldOfView = MathF.PI / 4.0f,
|
||||
float aspectRatio = 16.0f / 9.0f,
|
||||
float nearPlane = 0.1f,
|
||||
float farPlane = 100.0f)
|
||||
{
|
||||
Position = position;
|
||||
Target = target;
|
||||
Up = up;
|
||||
FieldOfView = fieldOfView;
|
||||
AspectRatio = aspectRatio;
|
||||
NearPlane = nearPlane;
|
||||
FarPlane = farPlane;
|
||||
}
|
||||
|
||||
public Matrix4x4 GetViewMatrix()
|
||||
{
|
||||
return Matrix4x4.CreateLookAt(Position, Target, Up);
|
||||
}
|
||||
|
||||
public Matrix4x4 GetProjectionMatrix()
|
||||
{
|
||||
return Matrix4x4.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlane, FarPlane);
|
||||
}
|
||||
}
|
||||
@@ -141,17 +141,26 @@ public sealed unsafe class MeshRenderer : IDisposable
|
||||
};
|
||||
_context.Vk.BeginCommandBuffer(cmd, &beginInfo);
|
||||
|
||||
var clearColor = new ClearValue(new ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f));
|
||||
var clearValues = new[]
|
||||
{
|
||||
new ClearValue(new ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f)),
|
||||
new ClearValue { DepthStencil = new ClearDepthStencilValue(1.0f, 0) }
|
||||
};
|
||||
|
||||
var renderPassInfo = new RenderPassBeginInfo
|
||||
{
|
||||
SType = StructureType.RenderPassBeginInfo,
|
||||
RenderPass = _swapchain.RenderPass,
|
||||
Framebuffer = _swapchain.Framebuffers[imageIndex],
|
||||
RenderArea = new Rect2D(new Offset2D(0, 0), _swapchain.Extent),
|
||||
ClearValueCount = 1,
|
||||
PClearValues = &clearColor
|
||||
ClearValueCount = (uint)clearValues.Length
|
||||
};
|
||||
|
||||
fixed (ClearValue* pClearValues = clearValues)
|
||||
{
|
||||
renderPassInfo.PClearValues = pClearValues;
|
||||
}
|
||||
|
||||
_context.Vk.CmdBeginRenderPass(cmd, &renderPassInfo, SubpassContents.Inline);
|
||||
_context.Vk.CmdBindPipeline(cmd, PipelineBindPoint.Graphics, _pipeline.Handle);
|
||||
|
||||
@@ -160,7 +169,11 @@ public sealed unsafe class MeshRenderer : IDisposable
|
||||
_context.Vk.CmdSetViewport(cmd, 0, 1, &viewport);
|
||||
_context.Vk.CmdSetScissor(cmd, 0, 1, &scissor);
|
||||
|
||||
var camera = GetCamera(world);
|
||||
var view = camera.GetViewMatrix();
|
||||
var proj = camera.GetProjectionMatrix();
|
||||
var drawCmd = cmd;
|
||||
|
||||
world.Each((Entity e, ref Mesh mesh, ref Transform transform) =>
|
||||
{
|
||||
if (!_buffers.TryGetValue(e, out var buffers))
|
||||
@@ -172,6 +185,11 @@ public sealed unsafe class MeshRenderer : IDisposable
|
||||
var bytes = BuildMeshVertices(mesh, transform);
|
||||
buffers.VertexBuffer.Update(bytes);
|
||||
|
||||
var model = transform.GetMatrix();
|
||||
var mvp = Matrix4x4.Multiply(Matrix4x4.Multiply(model, view), proj);
|
||||
var mvpT = Matrix4x4.Transpose(mvp);
|
||||
_context.Vk.CmdPushConstants(drawCmd, _pipeline.Layout, ShaderStageFlags.VertexBit, 0, 64, &mvpT);
|
||||
|
||||
var vertexBuffer = buffers.VertexBuffer.Buffer;
|
||||
var offset = 0ul;
|
||||
_context.Vk.CmdBindVertexBuffers(drawCmd, 0, 1, &vertexBuffer, &offset);
|
||||
@@ -265,6 +283,27 @@ public sealed unsafe class MeshRenderer : IDisposable
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private Camera GetCamera(World world)
|
||||
{
|
||||
var camera = new Camera(
|
||||
new Vector3(0.0f, 0.0f, -2.0f),
|
||||
Vector3.Zero,
|
||||
Vector3.UnitY,
|
||||
MathF.PI / 4.0f,
|
||||
(float)_swapchain.Extent.Width / _swapchain.Extent.Height,
|
||||
0.1f,
|
||||
100.0f);
|
||||
|
||||
world.Each((Entity e, ref Camera cam) =>
|
||||
{
|
||||
camera = cam;
|
||||
});
|
||||
|
||||
// Always keep the aspect ratio in sync with the swapchain.
|
||||
camera.AspectRatio = (float)_swapchain.Extent.Width / _swapchain.Extent.Height;
|
||||
return camera;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Vk.DeviceWaitIdle(_context.Device);
|
||||
|
||||
Binary file not shown.
@@ -5,8 +5,13 @@ layout(location = 1) in vec3 inColor;
|
||||
|
||||
layout(location = 0) out vec3 fragColor;
|
||||
|
||||
layout(push_constant) uniform PushConstants
|
||||
{
|
||||
mat4 mvp;
|
||||
} push;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(inPosition, 1.0);
|
||||
gl_Position = push.mvp * vec4(inPosition, 1.0);
|
||||
fragColor = inColor;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
private Image[] _images = null!;
|
||||
private ImageView[] _imageViews = null!;
|
||||
private Framebuffer[] _framebuffers = null!;
|
||||
private Image _depthImage;
|
||||
private DeviceMemory _depthMemory;
|
||||
private ImageView _depthImageView;
|
||||
private Format _depthFormat;
|
||||
private SurfaceFormatKHR _surfaceFormat;
|
||||
private PresentModeKHR _presentMode;
|
||||
private Extent2D _extent;
|
||||
@@ -31,6 +35,7 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
{
|
||||
_context = context;
|
||||
_surfaceFormat = ChooseSurfaceFormat();
|
||||
_depthFormat = FindDepthFormat();
|
||||
CreateRenderPass();
|
||||
Recreate(1280, 720);
|
||||
}
|
||||
@@ -77,6 +82,8 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
_imageViews = new ImageView[_images.Length];
|
||||
_framebuffers = new Framebuffer[_images.Length];
|
||||
|
||||
CreateDepthResources();
|
||||
|
||||
for (var i = 0; i < _images.Length; i++)
|
||||
{
|
||||
_imageViews[i] = CreateImageView(_images[i], _surfaceFormat.Format);
|
||||
@@ -107,6 +114,113 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
return images;
|
||||
}
|
||||
|
||||
private Format FindDepthFormat()
|
||||
{
|
||||
var candidates = new[] { Format.D32Sfloat, Format.D32SfloatS8Uint, Format.D24UnormS8Uint };
|
||||
foreach (var format in candidates)
|
||||
{
|
||||
FormatProperties props;
|
||||
_context.Vk.GetPhysicalDeviceFormatProperties(_context.PhysicalDevice, format, &props);
|
||||
if ((props.OptimalTilingFeatures & FormatFeatureFlags.DepthStencilAttachmentBit) != 0)
|
||||
return format;
|
||||
}
|
||||
throw new InvalidOperationException("No supported depth format found.");
|
||||
}
|
||||
|
||||
private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties)
|
||||
{
|
||||
PhysicalDeviceMemoryProperties memoryProperties;
|
||||
_context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties);
|
||||
for (var i = 0; i < memoryProperties.MemoryTypeCount; i++)
|
||||
{
|
||||
if ((typeFilter & (1u << i)) != 0 &&
|
||||
(memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties)
|
||||
{
|
||||
return (uint)i;
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Failed to find suitable memory type.");
|
||||
}
|
||||
|
||||
private void CreateDepthResources()
|
||||
{
|
||||
CreateDepthImage();
|
||||
CreateDepthImageView();
|
||||
}
|
||||
|
||||
private void CreateDepthImage()
|
||||
{
|
||||
var createInfo = new ImageCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageCreateInfo,
|
||||
ImageType = ImageType.Type2D,
|
||||
Extent = new Extent3D(_extent.Width, _extent.Height, 1),
|
||||
MipLevels = 1,
|
||||
ArrayLayers = 1,
|
||||
Format = _depthFormat,
|
||||
Tiling = ImageTiling.Optimal,
|
||||
InitialLayout = ImageLayout.Undefined,
|
||||
Usage = ImageUsageFlags.DepthStencilAttachmentBit,
|
||||
Samples = SampleCountFlags.Count1Bit,
|
||||
SharingMode = SharingMode.Exclusive
|
||||
};
|
||||
|
||||
Image image;
|
||||
var result = _context.Vk.CreateImage(_context.Device, &createInfo, null, &image);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkCreateImage failed: {result}");
|
||||
_depthImage = image;
|
||||
|
||||
MemoryRequirements memRequirements;
|
||||
_context.Vk.GetImageMemoryRequirements(_context.Device, image, &memRequirements);
|
||||
|
||||
var memoryTypeIndex = FindMemoryType(memRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit);
|
||||
var allocInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = memRequirements.Size,
|
||||
MemoryTypeIndex = memoryTypeIndex
|
||||
};
|
||||
|
||||
DeviceMemory memory;
|
||||
result = _context.Vk.AllocateMemory(_context.Device, &allocInfo, null, &memory);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkAllocateMemory failed: {result}");
|
||||
_depthMemory = memory;
|
||||
|
||||
result = _context.Vk.BindImageMemory(_context.Device, image, memory, 0);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkBindImageMemory failed: {result}");
|
||||
}
|
||||
|
||||
private void CreateDepthImageView()
|
||||
{
|
||||
var createInfo = new ImageViewCreateInfo
|
||||
{
|
||||
SType = StructureType.ImageViewCreateInfo,
|
||||
Image = _depthImage,
|
||||
ViewType = ImageViewType.Type2D,
|
||||
Format = _depthFormat,
|
||||
SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.DepthBit, 0, 1, 0, 1)
|
||||
};
|
||||
|
||||
ImageView imageView;
|
||||
var result = _context.Vk.CreateImageView(_context.Device, &createInfo, null, &imageView);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkCreateImageView failed: {result}");
|
||||
_depthImageView = imageView;
|
||||
}
|
||||
|
||||
private void CleanupDepthResources()
|
||||
{
|
||||
if (_depthImageView.Handle != 0)
|
||||
_context.Vk.DestroyImageView(_context.Device, _depthImageView, null);
|
||||
if (_depthImage.Handle != 0)
|
||||
_context.Vk.DestroyImage(_context.Device, _depthImage, null);
|
||||
if (_depthMemory.Handle != 0)
|
||||
_context.Vk.FreeMemory(_context.Device, _depthMemory, null);
|
||||
}
|
||||
|
||||
private void CreateRenderPass()
|
||||
{
|
||||
var colorAttachment = new AttachmentDescription
|
||||
@@ -121,17 +235,38 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
FinalLayout = ImageLayout.PresentSrcKhr
|
||||
};
|
||||
|
||||
var depthAttachment = new AttachmentDescription
|
||||
{
|
||||
Format = _depthFormat,
|
||||
Samples = SampleCountFlags.Count1Bit,
|
||||
LoadOp = AttachmentLoadOp.Clear,
|
||||
StoreOp = AttachmentStoreOp.DontCare,
|
||||
StencilLoadOp = AttachmentLoadOp.DontCare,
|
||||
StencilStoreOp = AttachmentStoreOp.DontCare,
|
||||
InitialLayout = ImageLayout.Undefined,
|
||||
FinalLayout = ImageLayout.DepthStencilAttachmentOptimal
|
||||
};
|
||||
|
||||
var attachments = new[] { colorAttachment, depthAttachment };
|
||||
|
||||
var colorAttachmentRef = new AttachmentReference
|
||||
{
|
||||
Attachment = 0,
|
||||
Layout = ImageLayout.ColorAttachmentOptimal
|
||||
};
|
||||
|
||||
var depthAttachmentRef = new AttachmentReference
|
||||
{
|
||||
Attachment = 1,
|
||||
Layout = ImageLayout.DepthStencilAttachmentOptimal
|
||||
};
|
||||
|
||||
var subpass = new SubpassDescription
|
||||
{
|
||||
PipelineBindPoint = PipelineBindPoint.Graphics,
|
||||
ColorAttachmentCount = 1,
|
||||
PColorAttachments = &colorAttachmentRef
|
||||
PColorAttachments = &colorAttachmentRef,
|
||||
PDepthStencilAttachment = &depthAttachmentRef
|
||||
};
|
||||
|
||||
var dependency = new SubpassDependency
|
||||
@@ -144,22 +279,25 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
DstAccessMask = AccessFlags.ColorAttachmentWriteBit
|
||||
};
|
||||
|
||||
var createInfo = new RenderPassCreateInfo
|
||||
fixed (AttachmentDescription* pAttachments = attachments)
|
||||
{
|
||||
SType = StructureType.RenderPassCreateInfo,
|
||||
AttachmentCount = 1,
|
||||
PAttachments = &colorAttachment,
|
||||
SubpassCount = 1,
|
||||
PSubpasses = &subpass,
|
||||
DependencyCount = 1,
|
||||
PDependencies = &dependency
|
||||
};
|
||||
var createInfo = new RenderPassCreateInfo
|
||||
{
|
||||
SType = StructureType.RenderPassCreateInfo,
|
||||
AttachmentCount = (uint)attachments.Length,
|
||||
PAttachments = pAttachments,
|
||||
SubpassCount = 1,
|
||||
PSubpasses = &subpass,
|
||||
DependencyCount = 1,
|
||||
PDependencies = &dependency
|
||||
};
|
||||
|
||||
RenderPass renderPass;
|
||||
var result = _context.Vk.CreateRenderPass(_context.Device, &createInfo, null, &renderPass);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkCreateRenderPass failed: {result}");
|
||||
_renderPass = renderPass;
|
||||
RenderPass renderPass;
|
||||
var result = _context.Vk.CreateRenderPass(_context.Device, &createInfo, null, &renderPass);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkCreateRenderPass failed: {result}");
|
||||
_renderPass = renderPass;
|
||||
}
|
||||
}
|
||||
|
||||
private ImageView CreateImageView(Image image, Format format)
|
||||
@@ -183,22 +321,26 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
|
||||
private Framebuffer CreateFramebuffer(ImageView imageView)
|
||||
{
|
||||
var createInfo = new FramebufferCreateInfo
|
||||
var attachments = new[] { imageView, _depthImageView };
|
||||
fixed (ImageView* pAttachments = attachments)
|
||||
{
|
||||
SType = StructureType.FramebufferCreateInfo,
|
||||
RenderPass = _renderPass,
|
||||
AttachmentCount = 1,
|
||||
PAttachments = &imageView,
|
||||
Width = _extent.Width,
|
||||
Height = _extent.Height,
|
||||
Layers = 1
|
||||
};
|
||||
var createInfo = new FramebufferCreateInfo
|
||||
{
|
||||
SType = StructureType.FramebufferCreateInfo,
|
||||
RenderPass = _renderPass,
|
||||
AttachmentCount = (uint)attachments.Length,
|
||||
PAttachments = pAttachments,
|
||||
Width = _extent.Width,
|
||||
Height = _extent.Height,
|
||||
Layers = 1
|
||||
};
|
||||
|
||||
Framebuffer framebuffer;
|
||||
var result = _context.Vk.CreateFramebuffer(_context.Device, &createInfo, null, &framebuffer);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkCreateFramebuffer failed: {result}");
|
||||
return framebuffer;
|
||||
Framebuffer framebuffer;
|
||||
var result = _context.Vk.CreateFramebuffer(_context.Device, &createInfo, null, &framebuffer);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkCreateFramebuffer failed: {result}");
|
||||
return framebuffer;
|
||||
}
|
||||
}
|
||||
|
||||
private SurfaceFormatKHR ChooseSurfaceFormat()
|
||||
@@ -275,6 +417,8 @@ public sealed unsafe class Swapchain : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
CleanupDepthResources();
|
||||
|
||||
if (_imageViews != null)
|
||||
{
|
||||
foreach (var view in _imageViews)
|
||||
|
||||
@@ -5,7 +5,7 @@ using Silk.NET.Vulkan;
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Simple graphics pipeline for indexed meshes with vec3 position + vec3 color.
|
||||
/// Graphics pipeline for indexed meshes with push-constant MVP and depth testing.
|
||||
/// Uses Silk.NET.Vulkan.
|
||||
/// </summary>
|
||||
public sealed unsafe class VulkanPipeline : IDisposable
|
||||
@@ -55,11 +55,19 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
|
||||
private PipelineLayout CreatePipelineLayout()
|
||||
{
|
||||
var pushConstantRange = new PushConstantRange
|
||||
{
|
||||
StageFlags = ShaderStageFlags.VertexBit,
|
||||
Offset = 0,
|
||||
Size = (uint)(16 * sizeof(float))
|
||||
};
|
||||
|
||||
var createInfo = new PipelineLayoutCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineLayoutCreateInfo,
|
||||
SetLayoutCount = 0,
|
||||
PushConstantRangeCount = 0
|
||||
PushConstantRangeCount = 1,
|
||||
PPushConstantRanges = &pushConstantRange
|
||||
};
|
||||
|
||||
PipelineLayout layout;
|
||||
@@ -170,6 +178,18 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
PAttachments = &colorBlendAttachment
|
||||
};
|
||||
|
||||
var depthStencil = new PipelineDepthStencilStateCreateInfo
|
||||
{
|
||||
SType = StructureType.PipelineDepthStencilStateCreateInfo,
|
||||
DepthTestEnable = true,
|
||||
DepthWriteEnable = true,
|
||||
DepthCompareOp = CompareOp.Less,
|
||||
DepthBoundsTestEnable = false,
|
||||
StencilTestEnable = false,
|
||||
Back = new StencilOpState(),
|
||||
Front = new StencilOpState()
|
||||
};
|
||||
|
||||
var dynamicStates = new[] { DynamicState.Viewport, DynamicState.Scissor };
|
||||
PipelineDynamicStateCreateInfo dynamicState;
|
||||
fixed (DynamicState* pDynamic = dynamicStates)
|
||||
@@ -195,6 +215,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
PViewportState = &viewportState,
|
||||
PRasterizationState = &rasterizer,
|
||||
PMultisampleState = &multisampling,
|
||||
PDepthStencilState = &depthStencil,
|
||||
PColorBlendState = &colorBlending,
|
||||
PDynamicState = &dynamicState,
|
||||
Layout = Layout,
|
||||
|
||||
Reference in New Issue
Block a user