diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index 120a7b4..7229682 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -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; diff --git a/src/Engine.Core/Components/Camera.cs b/src/Engine.Core/Components/Camera.cs new file mode 100644 index 0000000..5571a43 --- /dev/null +++ b/src/Engine.Core/Components/Camera.cs @@ -0,0 +1,46 @@ +using System.Numerics; + +namespace Engine.Core.Components; + +/// +/// A perspective camera component for the ECS. +/// Provides view and projection matrices for the renderer. +/// +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); + } +} diff --git a/src/Engine.Graphics/MeshRenderer.cs b/src/Engine.Graphics/MeshRenderer.cs index ee59e2b..c034084 100644 --- a/src/Engine.Graphics/MeshRenderer.cs +++ b/src/Engine.Graphics/MeshRenderer.cs @@ -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); diff --git a/src/Engine.Graphics/Shaders/vertex.spv b/src/Engine.Graphics/Shaders/vertex.spv index 5a0dd52..5219475 100644 Binary files a/src/Engine.Graphics/Shaders/vertex.spv and b/src/Engine.Graphics/Shaders/vertex.spv differ diff --git a/src/Engine.Graphics/Shaders/vertex.vert b/src/Engine.Graphics/Shaders/vertex.vert index 0031f34..6adc1ff 100644 --- a/src/Engine.Graphics/Shaders/vertex.vert +++ b/src/Engine.Graphics/Shaders/vertex.vert @@ -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; } diff --git a/src/Engine.Graphics/Swapchain.cs b/src/Engine.Graphics/Swapchain.cs index 6f4fc3d..b16111e 100644 --- a/src/Engine.Graphics/Swapchain.cs +++ b/src/Engine.Graphics/Swapchain.cs @@ -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) diff --git a/src/Engine.Graphics/VulkanPipeline.cs b/src/Engine.Graphics/VulkanPipeline.cs index b4dfb95..59e8e6f 100644 --- a/src/Engine.Graphics/VulkanPipeline.cs +++ b/src/Engine.Graphics/VulkanPipeline.cs @@ -5,7 +5,7 @@ using Silk.NET.Vulkan; namespace Engine.Graphics; /// -/// 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. /// 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,