diff --git a/documents/docfx.json b/documents/docfx.json index dc96a069..28f27224 100644 --- a/documents/docfx.json +++ b/documents/docfx.json @@ -49,7 +49,8 @@ "_gitContribute": { "repo": "https://github.com/qian-o/Zenith.NET", "branch": "master" - } + }, + "_gitUrlPattern": "https://github.com/qian-o/Zenith.NET/blob/master/{path}#L{line}" }, "output": "_site", "sitemap": { diff --git a/documents/images/compute-shader.png b/documents/images/compute-shader.png index e3ae527c..a75663c9 100644 Binary files a/documents/images/compute-shader.png and b/documents/images/compute-shader.png differ diff --git a/documents/images/indirect-drawing.png b/documents/images/indirect-drawing.png index f6d90769..52d8ec60 100644 Binary files a/documents/images/indirect-drawing.png and b/documents/images/indirect-drawing.png differ diff --git a/documents/images/mesh-shading.png b/documents/images/mesh-shading.png index 2e4be78b..f0e49f79 100644 Binary files a/documents/images/mesh-shading.png and b/documents/images/mesh-shading.png differ diff --git a/documents/images/ray-tracing.png b/documents/images/ray-tracing.png index fdd14fc9..c9c115b5 100644 Binary files a/documents/images/ray-tracing.png and b/documents/images/ray-tracing.png differ diff --git a/documents/images/spinning-cube.png b/documents/images/spinning-cube.png index f53ed728..6b981292 100644 Binary files a/documents/images/spinning-cube.png and b/documents/images/spinning-cube.png differ diff --git a/documents/tutorials/advanced/mesh-shading.md b/documents/tutorials/advanced/mesh-shading.md index 30771e2b..19440d74 100644 --- a/documents/tutorials/advanced/mesh-shading.md +++ b/documents/tutorials/advanced/mesh-shading.md @@ -1,76 +1,71 @@ # Mesh Shading -In this tutorial, you'll learn how to use mesh shading with Zenith.NET. We'll render a simple cube using the mesh shading pipeline, demonstrating the modern GPU-driven geometry processing approach. +In this tutorial, you'll render 1,000 procedural UV spheres using the mesh shader pipeline with GPU-driven frustum culling. This demonstrates the modern mesh shading approach where geometry is generated and culled entirely on the GPU. > [!NOTE] -> This tutorial requires a GPU with mesh shading support. Check `Context.Capabilities.MeshShadingSupported` before using mesh shading features. +> This tutorial requires a GPU with mesh shading support (e.g., NVIDIA Turing+, AMD RDNA 2+, or Apple M3+). ## Overview -We'll create a `MeshShadingRenderer` class that: +This tutorial covers: -- Defines vertex and meshlet data structures -- Creates structured buffers for vertices, indices, and meshlets -- Builds a mesh shading pipeline with mesh and pixel shaders -- Dispatches mesh shading workgroups to render geometry +- Creating a **mesh shading pipeline** with amplification, mesh, and pixel stages +- Generating **procedural sphere geometry** (vertices and triangles) on the CPU +- Implementing **GPU-driven frustum culling** in the amplification shader +- Using `groupshared` memory and atomic operations for visible instance compaction +- Extracting **frustum planes** from the view-projection matrix +- Dispatching mesh groups with `DispatchMesh` ## Key Concepts -### What is Mesh Shading? +### Mesh Shader Pipeline -Mesh shading replaces the traditional vertex processing pipeline (Input Assembler → Vertex Shader → optional tessellation/geometry) with a more flexible compute-like model: +The mesh shader pipeline replaces the traditional vertex/geometry pipeline: -| Traditional Pipeline | Mesh Shading Pipeline | -|---------------------|----------------------| -| Input Assembler | (removed) | -| Vertex Shader | (removed) | -| Hull/Domain Shader | (removed) | -| Geometry Shader | (removed) | -| - | Amplification Shader (optional) | -| - | Mesh Shader | -| Rasterizer | Rasterizer | -| Pixel Shader | Pixel Shader | +| Stage | Role | Thread Group Size | +|-------|------|-------------------| +| **Amplification** | Decides which mesh groups to spawn (culling) | 32 | +| **Mesh** | Outputs vertices and triangles per group | 120 | +| **Pixel** | Standard fragment shading | — | -### Meshlet Architecture +### Frustum Culling -The mesh shading pipeline works with **meshlets** - small chunks of geometry that can be processed independently: +The amplification shader tests each instance's bounding sphere against 6 frustum planes. Only visible instances are passed to mesh shader groups via a payload: ``` -Mesh -├── Meshlet 0 (up to 64-256 vertices, 64-256 primitives) -├── Meshlet 1 -├── Meshlet 2 -└── ... -``` - -Each meshlet contains: -- **VertexOffset**: Starting index in the vertex buffer -- **VertexCount**: Number of vertices in this meshlet -- **PrimitiveOffset**: Starting index in the index buffer -- **PrimitiveCount**: Number of triangles in this meshlet - -### Pipeline Stages +Payload { InstanceIndices[ASGroupSize] } -| Shader Stage | Description | -|--------------|-------------| -| **Amplification** (optional) | Determines how many mesh shading workgroups to spawn (LOD, culling) | -| **Mesh** | Outputs vertices and primitives directly to the rasterizer | -| **Pixel** | Standard fragment shading | +// Amplification: +visible = !IsFrustumCulled(position, radius) +if (visible) payload.InstanceIndices[atomicAdd(count)] = instanceIndex +DispatchMesh(visibleCount, 1, 1, payload) +``` ## The Renderer Class -Create a new file `Renderers/MeshShadingRenderer.cs`: +Create the file `Renderers/MeshShadingRenderer.cs`: ```csharp namespace ZenithTutorials.Renderers; internal unsafe class MeshShadingRenderer : IRenderer { - private const uint MaxPrimitives = 126; + private const uint ASGroupSize = 32; + private const uint MeshGroupSize = 120; + private const uint GridSize = 10; + private const uint TotalInstances = GridSize * GridSize * GridSize; + private const uint DispatchGroupCount = (TotalInstances + ASGroupSize - 1) / ASGroupSize; private const string ShaderSource = """ - static const uint MaxVertices = 64; - static const uint MaxPrimitives = 126; + static const uint GridSize = 10; + static const uint TotalInstances = GridSize * GridSize * GridSize; + static const float InstanceSpacing = 2.5; + static const uint ASGroupSize = 32; + static const float BoundingSphereRadius = 0.5; + + static const uint SphereVertexCount = 62; + static const uint SphereTriangleCount = 120; + static const float GridOffset = float(GridSize - 1) * 0.5 * InstanceSpacing; struct Vertex { @@ -78,97 +73,183 @@ internal unsafe class MeshShadingRenderer : IRenderer private float4 NormalAndPadding; - float2 TexCoord; - - private float padding0; + property float3 Position + { + get { + return PositionAndPadding.xyz; + } + } - private float padding1; + property float3 Normal + { + get { + return NormalAndPadding.xyz; + } + } + }; - property float3 Position { get { return PositionAndPadding.xyz; } } + struct Triangle + { + private uint4 IndicesAndPadding; - property float3 Normal { get { return NormalAndPadding.xyz; } } + property uint3 Indices + { + get { + return IndicesAndPadding.xyz; + } + } }; - struct Meshlet + struct Payload { - uint VertexOffset; + uint InstanceIndices[ASGroupSize]; + }; - uint VertexCount; + struct VertexOutput + { + float4 Position : SV_POSITION; - uint PrimitiveOffset; + float3 WorldNormal : WORLDNORMAL; - uint PrimitiveCount; + float3 Color : COLOR; }; - struct Triangle + struct Constants { - private uint4 IndicesAndPadding; + float4x4 ViewProjection; - property uint3 Indices { get { return IndicesAndPadding.xyz; } } + float4 FrustumPlanes[6]; + + private float4 TimeAndLightDirection; + + property float Time + { + get { + return TimeAndLightDirection.x; + } + } + + property float3 LightDirection + { + get { + return TimeAndLightDirection.yzw; + } + } }; - struct TransformConstants + void DecomposeInstanceID(uint id, out uint x, out uint y, out uint z) { - float4x4 MVP; - }; + x = id % GridSize; + y = (id / GridSize) % GridSize; + z = id / (GridSize * GridSize); + } - struct VertexOutput + float3 InstancePosition(uint id) { - float4 Position : SV_Position; + uint x, y, z; + DecomposeInstanceID(id, x, y, z); + return float3(x, y, z) * InstanceSpacing - GridOffset; + } - float3 Normal : NORMAL; + float3 InstanceColor(uint id) + { + uint x, y, z; + DecomposeInstanceID(id, x, y, z); + return float3(x, y, z) / float(GridSize - 1); + } - float2 TexCoord : TEXCOORD0; - }; + bool IsFrustumCulled(float3 center, float radius) + { + for (uint i = 0; i < 6; i++) + { + float4 plane = constants.FrustumPlanes[i]; + if (dot(plane.xyz, center) + plane.w < -radius) + { + return true; + } + } + return false; + } - ConstantBuffer transform; + ConstantBuffer constants; StructuredBuffer vertices; StructuredBuffer indices; - StructuredBuffer meshlets; + + groupshared Payload s_payload; + groupshared uint s_visibleCount; + + [shader("amplification")] + [numthreads(ASGroupSize, 1, 1)] + void ASMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID) + { + uint instanceIndex = groupID * ASGroupSize + groupThreadID; + + bool visible = false; + if (instanceIndex < TotalInstances) + { + float3 worldPos = InstancePosition(instanceIndex); + visible = !IsFrustumCulled(worldPos, BoundingSphereRadius); + } + + if (groupThreadID == 0) + { + s_visibleCount = 0; + } + + GroupMemoryBarrierWithGroupSync(); + + if (visible) + { + uint offset; + InterlockedAdd(s_visibleCount, 1, offset); + s_payload.InstanceIndices[offset] = instanceIndex; + } + + GroupMemoryBarrierWithGroupSync(); + + DispatchMesh(s_visibleCount, 1, 1, s_payload); + } [shader("mesh")] - [numthreads(MaxPrimitives, 1, 1)] + [numthreads(120, 1, 1)] [outputtopology("triangle")] - void MSMain(in uint groupID : SV_GroupID, - in uint groupThreadID : SV_GroupThreadID, - OutputVertices outVertices, - OutputIndices outIndices) + void MSMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID, in payload Payload meshPayload, + OutputVertices outVertices, OutputIndices outIndices) { - Meshlet meshlet = meshlets[groupID]; + uint instanceIndex = meshPayload.InstanceIndices[groupID]; + float3 instancePos = InstancePosition(instanceIndex); + float3 color = InstanceColor(instanceIndex); - SetMeshOutputCounts(meshlet.VertexCount, meshlet.PrimitiveCount); + SetMeshOutputCounts(SphereVertexCount, SphereTriangleCount); - if (groupThreadID < meshlet.VertexCount) + if (groupThreadID < SphereVertexCount) { - Vertex vertex = vertices[meshlet.VertexOffset + groupThreadID]; + Vertex v = vertices[groupThreadID]; + float3 worldPos = v.Position + instancePos; VertexOutput output; - output.Position = mul(float4(vertex.Position, 1.0), transform.MVP); - output.Normal = vertex.Normal; - output.TexCoord = vertex.TexCoord; + output.Position = mul(float4(worldPos, 1.0), constants.ViewProjection); + output.WorldNormal = v.Normal; + output.Color = color; outVertices[groupThreadID] = output; } - if (groupThreadID < meshlet.PrimitiveCount) + if (groupThreadID < SphereTriangleCount) { - outIndices[groupThreadID] = indices[meshlet.PrimitiveOffset + groupThreadID].Indices; + outIndices[groupThreadID] = indices[groupThreadID].Indices; } } [shader("pixel")] - float4 PSMain(VertexOutput input) : SV_Target + float4 PSMain(VertexOutput input) : SV_TARGET { - // Simple directional lighting - float3 lightDir = normalize(float3(1.0, 1.0, -1.0)); - float ndotl = max(dot(normalize(input.Normal), lightDir), 0.0); - - // Base color from texture coordinates - float3 baseColor = float3(input.TexCoord, 0.5); + float3 lightDir = normalize(constants.LightDirection); + float3 normal = normalize(input.WorldNormal); + float ndotl = max(dot(normal, lightDir), 0.0); - // Ambient + diffuse lighting - float3 ambient = baseColor * 0.2; - float3 diffuse = baseColor * ndotl * 0.8; + float3 ambient = input.Color * 0.15; + float3 diffuse = input.Color * ndotl * 0.85; return float4(ambient + diffuse, 1.0); } @@ -176,14 +257,12 @@ internal unsafe class MeshShadingRenderer : IRenderer private readonly Buffer vertexBuffer; private readonly Buffer indexBuffer; - private readonly Buffer meshletBuffer; - private readonly Buffer constantBuffer; + private readonly Buffer constantsBuffer; private readonly ResourceLayout resourceLayout; private readonly ResourceTable resourceTable; private readonly MeshShadingPipeline pipeline; - private readonly uint meshletCount; - private float rotationAngle; + private float totalTime; public MeshShadingRenderer() { @@ -192,109 +271,87 @@ internal unsafe class MeshShadingRenderer : IRenderer throw new NotSupportedException("Mesh shading is not supported on this device."); } - Vertex[] cubeVertices = - [ - // Front face - new() { Position = new(-0.5f, -0.5f, 0.5f), Normal = new( 0, 0, 1), TexCoord = new(0, 1) }, - new() { Position = new( 0.5f, -0.5f, 0.5f), Normal = new( 0, 0, 1), TexCoord = new(1, 1) }, - new() { Position = new( 0.5f, 0.5f, 0.5f), Normal = new( 0, 0, 1), TexCoord = new(1, 0) }, - new() { Position = new(-0.5f, 0.5f, 0.5f), Normal = new( 0, 0, 1), TexCoord = new(0, 0) }, - - // Back face - new() { Position = new( 0.5f, -0.5f, -0.5f), Normal = new( 0, 0, -1), TexCoord = new(0, 1) }, - new() { Position = new(-0.5f, -0.5f, -0.5f), Normal = new( 0, 0, -1), TexCoord = new(1, 1) }, - new() { Position = new(-0.5f, 0.5f, -0.5f), Normal = new( 0, 0, -1), TexCoord = new(1, 0) }, - new() { Position = new( 0.5f, 0.5f, -0.5f), Normal = new( 0, 0, -1), TexCoord = new(0, 0) }, - - // Left face - new() { Position = new(-0.5f, -0.5f, -0.5f), Normal = new(-1, 0, 0), TexCoord = new(0, 1) }, - new() { Position = new(-0.5f, -0.5f, 0.5f), Normal = new(-1, 0, 0), TexCoord = new(1, 1) }, - new() { Position = new(-0.5f, 0.5f, 0.5f), Normal = new(-1, 0, 0), TexCoord = new(1, 0) }, - new() { Position = new(-0.5f, 0.5f, -0.5f), Normal = new(-1, 0, 0), TexCoord = new(0, 0) }, - - // Right face - new() { Position = new( 0.5f, -0.5f, 0.5f), Normal = new( 1, 0, 0), TexCoord = new(0, 1) }, - new() { Position = new( 0.5f, -0.5f, -0.5f), Normal = new( 1, 0, 0), TexCoord = new(1, 1) }, - new() { Position = new( 0.5f, 0.5f, -0.5f), Normal = new( 1, 0, 0), TexCoord = new(1, 0) }, - new() { Position = new( 0.5f, 0.5f, 0.5f), Normal = new( 1, 0, 0), TexCoord = new(0, 0) }, - - // Top face - new() { Position = new(-0.5f, 0.5f, 0.5f), Normal = new( 0, 1, 0), TexCoord = new(0, 1) }, - new() { Position = new( 0.5f, 0.5f, 0.5f), Normal = new( 0, 1, 0), TexCoord = new(1, 1) }, - new() { Position = new( 0.5f, 0.5f, -0.5f), Normal = new( 0, 1, 0), TexCoord = new(1, 0) }, - new() { Position = new(-0.5f, 0.5f, -0.5f), Normal = new( 0, 1, 0), TexCoord = new(0, 0) }, - - // Bottom face - new() { Position = new(-0.5f, -0.5f, -0.5f), Normal = new( 0, -1, 0), TexCoord = new(0, 1) }, - new() { Position = new( 0.5f, -0.5f, -0.5f), Normal = new( 0, -1, 0), TexCoord = new(1, 1) }, - new() { Position = new( 0.5f, -0.5f, 0.5f), Normal = new( 0, -1, 0), TexCoord = new(1, 0) }, - new() { Position = new(-0.5f, -0.5f, 0.5f), Normal = new( 0, -1, 0), TexCoord = new(0, 0) } - ]; - - Triangle[] cubeTriangles = - [ - // Front face - new() { I0 = 0, I1 = 1, I2 = 2 }, - new() { I0 = 0, I1 = 2, I2 = 3 }, - // Back face - new() { I0 = 4, I1 = 5, I2 = 6 }, - new() { I0 = 4, I1 = 6, I2 = 7 }, - // Left face - new() { I0 = 8, I1 = 9, I2 = 10 }, - new() { I0 = 8, I1 = 10, I2 = 11 }, - // Right face - new() { I0 = 12, I1 = 13, I2 = 14 }, - new() { I0 = 12, I1 = 14, I2 = 15 }, - // Top face - new() { I0 = 16, I1 = 17, I2 = 18 }, - new() { I0 = 16, I1 = 18, I2 = 19 }, - // Bottom face - new() { I0 = 20, I1 = 21, I2 = 22 }, - new() { I0 = 20, I1 = 22, I2 = 23 } - ]; - - Meshlet[] meshlets = - [ - new() + const int lonSegments = 12; + const int latSegments = 6; + const float radius = 0.5f; + + List sphereVertices = []; + List sphereTriangles = []; + + sphereVertices.Add(new() { Position = new(0, radius, 0), Normal = Vector3.UnitY }); + + for (int lat = 1; lat < latSegments; lat++) + { + float phi = MathF.PI * lat / latSegments; + float sinPhi = MathF.Sin(phi); + float cosPhi = MathF.Cos(phi); + + for (int lon = 0; lon < lonSegments; lon++) + { + float theta = 2.0f * MathF.PI * lon / lonSegments; + Vector3 normal = new(sinPhi * MathF.Cos(theta), cosPhi, sinPhi * MathF.Sin(theta)); + + sphereVertices.Add(new() { Position = normal * radius, Normal = normal }); + } + } + + sphereVertices.Add(new() { Position = new(0, -radius, 0), Normal = -Vector3.UnitY }); + + for (int lon = 0; lon < lonSegments; lon++) + { + uint next = (uint)((lon + 1) % lonSegments); + + sphereTriangles.Add(new() { Index0 = 0, Index1 = (uint)(1 + lon), Index2 = 1 + next }); + } + + for (int lat = 0; lat < latSegments - 2; lat++) + { + for (int lon = 0; lon < lonSegments; lon++) { - VertexOffset = 0, - VertexCount = (uint)cubeVertices.Length, - PrimitiveOffset = 0, - PrimitiveCount = (uint)cubeTriangles.Length + uint next = (uint)((lon + 1) % lonSegments); + uint tl = (uint)(1 + (lat * lonSegments) + lon); + uint tr = (uint)(1 + (lat * lonSegments)) + next; + uint bl = (uint)(1 + ((lat + 1) * lonSegments) + lon); + uint br = (uint)(1 + ((lat + 1) * lonSegments)) + next; + + sphereTriangles.Add(new() { Index0 = tl, Index1 = bl, Index2 = tr }); + sphereTriangles.Add(new() { Index0 = tr, Index1 = bl, Index2 = br }); } - ]; - meshletCount = (uint)meshlets.Length; + } + + uint bottomPole = (uint)(sphereVertices.Count - 1); + uint lastRing = 1 + ((latSegments - 2) * lonSegments); + + for (int lon = 0; lon < lonSegments; lon++) + { + uint next = (uint)((lon + 1) % lonSegments); + + sphereTriangles.Add(new() { Index0 = bottomPole, Index1 = lastRing + next, Index2 = lastRing + (uint)lon }); + } + + Vertex[] vertexData = [.. sphereVertices]; + Triangle[] triangleData = [.. sphereTriangles]; - // Create vertex buffer vertexBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)(sizeof(Vertex) * cubeVertices.Length), + SizeInBytes = (uint)(sizeof(Vertex) * vertexData.Length), StrideInBytes = (uint)sizeof(Vertex), Flags = BufferUsageFlags.ShaderResource }); - vertexBuffer.Upload(cubeVertices, 0); + vertexBuffer.Upload(vertexData, 0); - // Create index buffer (Triangle struct per triangle) indexBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)(sizeof(Triangle) * cubeTriangles.Length), + SizeInBytes = (uint)(sizeof(Triangle) * triangleData.Length), StrideInBytes = (uint)sizeof(Triangle), Flags = BufferUsageFlags.ShaderResource }); - indexBuffer.Upload(cubeTriangles, 0); + indexBuffer.Upload(triangleData, 0); - meshletBuffer = App.Context.CreateBuffer(new() + constantsBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)(sizeof(Meshlet) * meshlets.Length), - StrideInBytes = (uint)sizeof(Meshlet), - Flags = BufferUsageFlags.ShaderResource - }); - meshletBuffer.Upload(meshlets, 0); - - constantBuffer = App.Context.CreateBuffer(new() - { - SizeInBytes = (uint)sizeof(TransformConstants), - StrideInBytes = (uint)sizeof(TransformConstants), + SizeInBytes = (uint)sizeof(Constants), + StrideInBytes = (uint)sizeof(Constants), Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite }); @@ -302,8 +359,7 @@ internal unsafe class MeshShadingRenderer : IRenderer { Bindings = BindingHelper.Bindings ( - new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Mesh }, - new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Mesh }, + new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Amplification | ShaderStageFlags.Mesh | ShaderStageFlags.Pixel }, new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Mesh }, new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Mesh } ) @@ -312,9 +368,10 @@ internal unsafe class MeshShadingRenderer : IRenderer resourceTable = App.Context.CreateResourceTable(new() { Layout = resourceLayout, - Resources = [constantBuffer, vertexBuffer, indexBuffer, meshletBuffer] + Resources = [constantsBuffer, vertexBuffer, indexBuffer] }); + using Shader ampShader = App.Context.LoadShaderFromSource(ShaderSource, "ASMain", ShaderStageFlags.Amplification); using Shader meshShader = App.Context.LoadShaderFromSource(ShaderSource, "MSMain", ShaderStageFlags.Mesh); using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel); @@ -326,13 +383,16 @@ internal unsafe class MeshShadingRenderer : IRenderer DepthStencilState = DepthStencilStates.Default, BlendState = BlendStates.Opaque }, - Amplification = null, + Amplification = ampShader, Mesh = meshShader, Pixel = pixelShader, ResourceLayout = resourceLayout, PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output, - MeshThreadGroupSizeX = MaxPrimitives, + Output = App.FrameBuffer.Output, + AmplificationThreadGroupSizeX = ASGroupSize, + AmplificationThreadGroupSizeY = 1, + AmplificationThreadGroupSizeZ = 1, + MeshThreadGroupSizeX = MeshGroupSize, MeshThreadGroupSizeY = 1, MeshThreadGroupSizeZ = 1 }); @@ -340,22 +400,37 @@ internal unsafe class MeshShadingRenderer : IRenderer public void Update(double deltaTime) { - rotationAngle += (float)deltaTime; + totalTime += (float)deltaTime; + + float angle = totalTime * 0.3f; + + Vector3 cameraPos = new(35.0f * MathF.Sin(angle), 20.0f * MathF.Sin(totalTime * 0.2f), 35.0f * MathF.Cos(angle)); + + Matrix4x4 view = Matrix4x4.CreateLookAt(cameraPos, Vector3.Zero, Vector3.UnitY); + Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 200.0f); + Matrix4x4 viewProjection = view * projection; + + constantsBuffer.Upload([new Constants() + { + ViewProjection = viewProjection, + FrustumPlane0 = NormalizePlane(new(viewProjection.M11 + viewProjection.M14, viewProjection.M21 + viewProjection.M24, viewProjection.M31 + viewProjection.M34, viewProjection.M41 + viewProjection.M44)), + FrustumPlane1 = NormalizePlane(new(viewProjection.M14 - viewProjection.M11, viewProjection.M24 - viewProjection.M21, viewProjection.M34 - viewProjection.M31, viewProjection.M44 - viewProjection.M41)), + FrustumPlane2 = NormalizePlane(new(viewProjection.M12 + viewProjection.M14, viewProjection.M22 + viewProjection.M24, viewProjection.M32 + viewProjection.M34, viewProjection.M42 + viewProjection.M44)), + FrustumPlane3 = NormalizePlane(new(viewProjection.M14 - viewProjection.M12, viewProjection.M24 - viewProjection.M22, viewProjection.M34 - viewProjection.M32, viewProjection.M44 - viewProjection.M42)), + FrustumPlane4 = NormalizePlane(new(viewProjection.M13, viewProjection.M23, viewProjection.M33, viewProjection.M43)), + FrustumPlane5 = NormalizePlane(new(viewProjection.M14 - viewProjection.M13, viewProjection.M24 - viewProjection.M23, viewProjection.M34 - viewProjection.M33, viewProjection.M44 - viewProjection.M43)), + Time = totalTime, + LightDirection = -Vector3.Normalize(cameraPos) + }], 0); } public void Render() { - Matrix4x4 model = Matrix4x4.CreateRotationY(rotationAngle) * Matrix4x4.CreateRotationX(rotationAngle * 0.5f); - Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 3), Vector3.Zero, Vector3.UnitY); - Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f); - - constantBuffer.Upload([new TransformConstants() { MVP = model * view * projection }], 0); - CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); - commandBuffer.BeginRenderPass(App.SwapChain.FrameBuffer, new() + commandBuffer.BeginRenderPass(App.FrameBuffer, new() { - ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)], + ColorValues = [new(0.05f, 0.05f, 0.08f, 1.0f)], Depth = 1.0f, Stencil = 0, Flags = ClearFlags.All @@ -363,7 +438,7 @@ internal unsafe class MeshShadingRenderer : IRenderer commandBuffer.SetPipeline(pipeline); commandBuffer.SetResourceTable(resourceTable); - commandBuffer.DispatchMesh(meshletCount, 1, 1); + commandBuffer.DispatchMesh(DispatchGroupCount, 1, 1); commandBuffer.EndRenderPass(); @@ -379,17 +454,18 @@ internal unsafe class MeshShadingRenderer : IRenderer pipeline.Dispose(); resourceTable.Dispose(); resourceLayout.Dispose(); - constantBuffer.Dispose(); - meshletBuffer.Dispose(); + constantsBuffer.Dispose(); indexBuffer.Dispose(); vertexBuffer.Dispose(); } + + private static Vector4 NormalizePlane(Vector4 plane) + { + return plane / new Vector3(plane.X, plane.Y, plane.Z).Length(); + } } -/// -/// Vertex structure with position and normal. -/// -[StructLayout(LayoutKind.Explicit, Size = 48)] +[StructLayout(LayoutKind.Explicit, Size = 32)] file struct Vertex { [FieldOffset(0)] @@ -397,71 +473,56 @@ file struct Vertex [FieldOffset(16)] public Vector3 Normal; - - [FieldOffset(32)] - public Vector2 TexCoord; } -/// -/// Triangle indices for mesh shading. -/// [StructLayout(LayoutKind.Explicit, Size = 16)] file struct Triangle { [FieldOffset(0)] - public uint I0; + public uint Index0; [FieldOffset(4)] - public uint I1; + public uint Index1; [FieldOffset(8)] - public uint I2; + public uint Index2; } -/// -/// Meshlet structure defining a chunk of geometry. -/// -[StructLayout(LayoutKind.Explicit, Size = 16)] -file struct Meshlet +[StructLayout(LayoutKind.Explicit, Size = 176)] +file struct Constants { [FieldOffset(0)] - public uint VertexOffset; + public Matrix4x4 ViewProjection; - [FieldOffset(4)] - public uint VertexCount; + [FieldOffset(64)] + public Vector4 FrustumPlane0; - [FieldOffset(8)] - public uint PrimitiveOffset; - - [FieldOffset(12)] - public uint PrimitiveCount; -} + [FieldOffset(80)] + public Vector4 FrustumPlane1; -/// -/// Transform constants for the mesh. -/// -[StructLayout(LayoutKind.Explicit, Size = 64)] -file struct TransformConstants -{ - [FieldOffset(0)] - public Matrix4x4 MVP; -} -``` + [FieldOffset(96)] + public Vector4 FrustumPlane2; -## Running the Tutorial + [FieldOffset(112)] + public Vector4 FrustumPlane3; -Update your `Program.cs` to run the `MeshShadingRenderer`: + [FieldOffset(128)] + public Vector4 FrustumPlane4; -```csharp -using ZenithTutorials; -using ZenithTutorials.Renderers; + [FieldOffset(144)] + public Vector4 FrustumPlane5; -App.Run(); + [FieldOffset(160)] + public float Time; -App.Cleanup(); + [FieldOffset(164)] + public Vector3 LightDirection; +} ``` -Run the application: +## Running the Tutorial + +Run the application and select **7. Mesh Shading** from the menu: ```bash dotnet run @@ -469,151 +530,163 @@ dotnet run ## Result -![mesh-shading](../../images/mesh-shading.png) +![Mesh Shading](../../images/mesh-shading.png) ## Code Breakdown -### Checking Mesh Shading Support +### Procedural Sphere Geometry + +The sphere is generated as a UV sphere with 12 longitude and 6 latitude segments, producing 62 vertices and 120 triangles: ```csharp -if (!App.Context.Capabilities.MeshShadingSupported) +sphereVertices.Add(new() { Position = new(0, radius, 0), Normal = Vector3.UnitY }); + +for (int lat = 1; lat < latSegments; lat++) { - throw new NotSupportedException("Mesh shading is not supported on this device."); + float phi = MathF.PI * lat / latSegments; + float sinPhi = MathF.Sin(phi); + float cosPhi = MathF.Cos(phi); + + for (int lon = 0; lon < lonSegments; lon++) + { + float theta = 2.0f * MathF.PI * lon / lonSegments; + Vector3 normal = new(sinPhi * MathF.Cos(theta), cosPhi, sinPhi * MathF.Sin(theta)); + + sphereVertices.Add(new() { Position = normal * radius, Normal = normal }); + } } + +sphereVertices.Add(new() { Position = new(0, -radius, 0), Normal = -Vector3.UnitY }); ``` -Always check `Capabilities.MeshShadingSupported` before using mesh shading features. +The vertex and index data are stored in `StructuredBuffer` resources (not vertex/index buffers), since mesh shaders read geometry data directly. -### Meshlet Data Structure +### Mesh Shading Pipeline + +The pipeline configuration specifies thread group sizes for both amplification and mesh stages: ```csharp -[StructLayout(LayoutKind.Explicit, Size = 16)] -file struct Meshlet +pipeline = App.Context.CreateMeshShadingPipeline(new() { - [FieldOffset(0)] - public uint VertexOffset; - - [FieldOffset(4)] - public uint VertexCount; + RenderStates = new() + { + RasterizerState = RasterizerStates.CullBack, + DepthStencilState = DepthStencilStates.Default, + BlendState = BlendStates.Opaque + }, + Amplification = ampShader, + Mesh = meshShader, + Pixel = pixelShader, + ResourceLayout = resourceLayout, + PrimitiveTopology = PrimitiveTopology.TriangleList, + Output = App.FrameBuffer.Output, + AmplificationThreadGroupSizeX = ASGroupSize, + AmplificationThreadGroupSizeY = 1, + AmplificationThreadGroupSizeZ = 1, + MeshThreadGroupSizeX = MeshGroupSize, + MeshThreadGroupSizeY = 1, + MeshThreadGroupSizeZ = 1 +}); +``` - [FieldOffset(8)] - public uint PrimitiveOffset; +### Amplification Shader (Culling) - [FieldOffset(12)] - public uint PrimitiveCount; -} -``` +The amplification shader tests each instance against the camera frustum and only dispatches mesh groups for visible instances: -Each meshlet describes a chunk of geometry: -- **VertexOffset/Count**: Range of vertices in the vertex buffer -- **PrimitiveOffset/Count**: Range of triangles in the index buffer +```csharp +[shader("amplification")] +[numthreads(ASGroupSize, 1, 1)] +void ASMain(uint groupID: SV_GroupID, uint groupThreadID: SV_GroupThreadID) +{ + uint instanceIndex = groupID * ASGroupSize + groupThreadID; -### Mesh Shader Entry Point + bool visible = false; + if (instanceIndex < TotalInstances) + { + float3 worldPos = InstancePosition(instanceIndex); + visible = !IsFrustumCulled(worldPos, BoundingSphereRadius); + } -```slang -[shader("mesh")] -[numthreads(MaxPrimitives, 1, 1)] -[outputtopology("triangle")] -void MSMain(in uint groupID : SV_GroupID, - in uint groupThreadID : SV_GroupThreadID, - OutputVertices outVertices, - OutputIndices outIndices) -``` + if (groupThreadID == 0) + { + s_visibleCount = 0; + } -Key attributes: + GroupMemoryBarrierWithGroupSync(); -| Attribute | Description | -|-----------|-------------| -| `[shader("mesh")]` | Marks this as a mesh shader entry point | -| `[numthreads(X,Y,Z)]` | Thread group size (typically MaxPrimitives threads) | -| `[outputtopology("triangle")]` | Output primitive type | -| `OutputVertices` | Output vertex array (max N vertices) | -| `OutputIndices` | Output index array (max N triangles) | + if (visible) + { + uint offset; + InterlockedAdd(s_visibleCount, 1, offset); + s_payload.InstanceIndices[offset] = instanceIndex; + } -### Setting Output Counts + GroupMemoryBarrierWithGroupSync(); -```slang -SetMeshOutputCounts(meshlet.VertexCount, meshlet.PrimitiveCount); + DispatchMesh(s_visibleCount, 1, 1, s_payload); +} ``` -This must be called once per workgroup to declare how many vertices and primitives will be output. +**Key steps:** +1. Each thread checks one instance against 6 frustum planes +2. Visible instances are compacted into a `groupshared` payload using `InterlockedAdd` +3. `DispatchMesh` spawns only as many mesh groups as there are visible instances + +### Frustum Plane Extraction -### Dispatching Mesh Shading +Frustum planes are extracted from the view-projection matrix on the CPU: ```csharp -commandBuffer.DispatchMesh(meshletCount, 1, 1); +FrustumPlane0 = NormalizePlane(new(viewProjection.M11 + viewProjection.M14, viewProjection.M21 + viewProjection.M24, viewProjection.M31 + viewProjection.M34, viewProjection.M41 + viewProjection.M44)), ``` -Unlike traditional `Draw` calls, mesh shading uses `DispatchMesh(X, Y, Z)` to launch workgroups: -- Each workgroup processes one meshlet -- Total workgroups = `meshletCount × 1 × 1` +| Plane | Extraction | +|-------|-----------| +| Left | Row 4 + Row 1 | +| Right | Row 4 - Row 1 | +| Bottom | Row 4 + Row 2 | +| Top | Row 4 - Row 2 | +| Near | Row 3 | +| Far | Row 4 - Row 3 | -### Creating the Pipeline +### Constants Layout + +The `Constants` struct packs all per-frame data into 176 bytes: ```csharp -pipeline = App.Context.CreateMeshShadingPipeline(new() +[StructLayout(LayoutKind.Explicit, Size = 176)] +file struct Constants { - RenderStates = new() { ... }, - Amplification = null, - Mesh = meshShader, - Pixel = pixelShader, - ResourceLayout = resourceLayout, - PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output, - MeshThreadGroupSizeX = MaxPrimitives, - MeshThreadGroupSizeY = 1, - MeshThreadGroupSizeZ = 1 -}); -``` + [FieldOffset(0)] + public Matrix4x4 ViewProjection; -The `MeshShadingPipelineDesc` requires: -- `Amplification` - The compiled amplification shader (optional) -- `Mesh` - The compiled mesh shader -- `Pixel` - The compiled pixel shader -- `ResourceLayout` - Resource bindings (same as graphics pipelines) -- `AmplificationThreadGroupSizeX/Y/Z` - Must match `[numthreads()]` in the amplification shader (if used) -- `MeshThreadGroupSizeX/Y/Z` - Must match `[numthreads()]` in the mesh shader + [FieldOffset(64)] + public Vector4 FrustumPlane0; -## Amplification Shader (Optional) + [FieldOffset(80)] + public Vector4 FrustumPlane1; -For more advanced scenarios, you can add an amplification shader to dynamically control meshlet dispatch: + [FieldOffset(96)] + public Vector4 FrustumPlane2; -```slang -struct AmplificationPayload -{ - uint MeshletIndices[32]; -}; + [FieldOffset(112)] + public Vector4 FrustumPlane3; -[shader("amplification")] -[numthreads(32, 1, 1)] -void ASMain(in uint groupID : SV_GroupID, - in uint groupThreadID : SV_GroupThreadID) -{ - // Frustum culling, LOD selection, etc. - bool visible = /* culling logic */; + [FieldOffset(128)] + public Vector4 FrustumPlane4; - if (visible) - { - AmplificationPayload payload; - payload.MeshletIndices[groupThreadID] = groupID * 32 + groupThreadID; + [FieldOffset(144)] + public Vector4 FrustumPlane5; - // Dispatch mesh shading workgroups - DispatchMesh(visibleCount, 1, 1, payload); - } + [FieldOffset(160)] + public float Time; + + [FieldOffset(164)] + public Vector3 LightDirection; } ``` -## Best Practices - -1. **Meshlet Size**: Keep meshlets within hardware limits (typically 64-256 vertices, 64-126 primitives) -2. **Thread Utilization**: Size `numthreads` to match your maximum primitive count -3. **Early Out**: Check thread bounds before writing to output arrays -4. **Preprocessing**: Generate meshlets offline for complex models -5. **Culling**: Use amplification shaders for GPU-driven culling - -## Next Steps - -Congratulations! You've completed all Zenith.NET tutorials. +The constant buffer is shared across all three shader stages (`Amplification | Mesh | Pixel`), so the amplification shader can read frustum planes while the pixel shader reads the light direction. ## Source Code diff --git a/documents/tutorials/advanced/ray-tracing.md b/documents/tutorials/advanced/ray-tracing.md index 3095dcb6..4b24c3a9 100644 --- a/documents/tutorials/advanced/ray-tracing.md +++ b/documents/tutorials/advanced/ray-tracing.md @@ -1,55 +1,60 @@ # Ray Tracing -In this tutorial, you'll learn how to use hardware-accelerated ray tracing with Zenith.NET. We'll render a scene with a checkered floor and a sphere, demonstrating acceleration structure construction and compute-shader-based ray tracing with `RayQuery`. +In this tutorial, you'll build a real-time ray tracer using hardware-accelerated ray tracing. The scene features three colored spheres on a checkerboard floor with an animated orbiting camera, soft shadows, reflections, and ACES tone mapping — all driven by a compute shader using `RayQuery`. > [!NOTE] -> This tutorial requires a GPU with ray tracing support. Check `Context.Capabilities.RayTracingSupported` before using ray tracing features. +> This tutorial requires a GPU with ray tracing support (e.g., NVIDIA RTX, AMD RDNA 2+, or Apple M1+). ## Overview -We'll create a `RayTracingRenderer` class that: +This tutorial covers: -- Creates floor geometry using triangle buffers -- Creates a sphere using procedural AABBs -- Builds separate BLAS for floor and sphere, combined in a TLAS -- Uses a compute shader with `RayQuery` for ray tracing -- Implements shadow rays for hard shadows -- Copies the result to the swap chain for display +- Building **Bottom-Level** and **Top-Level Acceleration Structures** (BLAS/TLAS) +- Using **triangle geometry** for the floor and **procedural AABBs** for spheres +- Tracing rays with `RayQuery` in a compute shader +- Implementing **soft shadows**, **reflections**, and **Fresnel** effects +- Applying **ACES tone mapping** for cinematic color grading +- Dynamically **resizing** the output texture on window resize ## Key Concepts -### Ray Tracing with RayQuery +### Acceleration Structure Hierarchy -Zenith.NET uses `RayQuery` for ray tracing. This approach binds the acceleration structure as a regular resource and performs all ray tracing logic within a single shader: +Ray tracing uses a two-level acceleration structure: -| Aspect | Description | -|--------|-------------| -| **Shader Stage** | Any shader (typically compute) | -| **Setup** | Bind acceleration structure to any pipeline | -| **Hit/Miss Logic** | All logic in one shader using `RayQuery` API | -| **Best For** | Shadows, AO, GI, reflections — any ray tracing workload | +| Level | Purpose | Content | +|-------|---------|---------| +| **BLAS** (Bottom-Level) | Geometry containers | Triangle meshes or procedural AABBs | +| **TLAS** (Top-Level) | Scene graph | References to BLAS instances with transforms | -### Acceleration Structures +This tutorial builds two BLAS: +- **Floor BLAS**: A triangle mesh (2 triangles forming a 100×100 quad) +- **Sphere BLAS**: 3 procedural AABBs (bounding boxes for sphere intersection) -Ray tracing uses a two-level acceleration structure hierarchy: +Both are combined into one TLAS for the scene. -- **BLAS (Bottom-Level Acceleration Structure)**: Contains the actual geometry data. Each BLAS can store either triangle meshes or axis-aligned bounding boxes (AABBs) for procedural geometry. -- **TLAS (Top-Level Acceleration Structure)**: Contains instances that reference one or more BLAS with transform matrices. Multiple instances can share the same BLAS with different transforms. +### RayQuery + +Instead of using a dedicated ray tracing pipeline, Zenith.NET uses `RayQuery` in compute shaders. This inline approach traces rays within any shader stage: ``` -TLAS (scene) -├── Instance 0 → BLAS 0 (floor, triangles) -├── Instance 1 → BLAS 1 (sphere, AABBs) -├── Instance 2 → BLAS 0 (same geometry, different transform) -└── ... -``` +RayQuery query; +query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, ray); + +while (query.Proceed()) +{ + // Handle procedural intersections +} -> [!IMPORTANT] -> Acceleration structure transforms only support rotation and scale. Translation is **not supported** - use the geometry's world-space coordinates directly. +if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT) +{ + // Handle triangle hit +} +``` ## The Renderer Class -Create a new file `Renderers/RayTracingRenderer.cs`: +Create the file `Renderers/RayTracingRenderer.cs`: ```csharp namespace ZenithTutorials.Renderers; @@ -59,47 +64,223 @@ internal unsafe class RayTracingRenderer : IRenderer private const uint ThreadGroupSize = 16; private const string ShaderSource = """ + static const float RayEpsilon = 0.001; + static const float TwoPi = 6.2831853; + static const uint SphereCount = 3; + static const uint ShadowSamples = 6; + static const uint ReflectionSamples = 4; + static const float ShadowMin = 0.3; + static const float SunRadius = 0.04; + static const float SphereRoughness = 0.05; + static const float SphereF0 = 0.15; + static const float FloorFadeStart = 8.0; + static const float FloorFadeRange = 20.0; + + static const float3 FloorNormal = float3(0.0, 1.0, 0.0); + static const float3 LightDir = float3(0.6667, 0.6667, -0.3333); + static const float3 LightColor = float3(1.0, 0.98, 0.95); + static const float3 AmbientColor = float3(0.15, 0.15, 0.2); + + struct Constants + { + private float4 PositionAndPadding; + + property float3 Position + { + get { + return PositionAndPadding.xyz; + } + } + }; + struct Sphere { private float4 CenterAndRadius; private float4 ColorAndPadding; - property float3 Center { get { return CenterAndRadius.xyz; } } + property float3 Center + { + get { + return CenterAndRadius.xyz; + } + } - property float Radius { get { return CenterAndRadius.w; } } + property float Radius + { + get { + return CenterAndRadius.w; + } + } - property float3 Color { get { return ColorAndPadding.xyz; } } + property float3 Color + { + get { + return ColorAndPadding.xyz; + } + } }; RaytracingAccelerationStructure scene; + ConstantBuffer constants; StructuredBuffer spheres; RWTexture2D outputTexture; - static const float3 LightDir = normalize(float3(1.0, 1.0, -0.5)); - static const float3 LightColor = float3(1.0, 0.98, 0.95); - static const float3 AmbientColor = float3(0.1, 0.1, 0.15); + float3 SampleSky(float3 direction) + { + float t = 0.5 * (direction.y + 1.0); + float3 horizon = float3(0.7, 0.85, 1.0); + float3 zenith = float3(0.3, 0.5, 1.0); + float3 sky = lerp(horizon, zenith, saturate(t)); + + float sunDot = dot(direction, LightDir); + sky += LightColor * smoothstep(0.995, 0.999, sunDot) * 3.0; + + return sky; + } + + float3 ACESFilm(float3 x) + { + x *= 1.6; + float3 a = x * (x * 2.51 + 0.03); + float3 b = x * (x * 2.43 + 0.59) + 0.14; + float3 result = saturate(a / b); + + float luma = dot(result, float3(0.2126, 0.7152, 0.0722)); + result = saturate(lerp(float3(luma, luma, luma), result, 1.5)); + + return result; + } + + float SchlickFresnel(float cosTheta, float f0) + { + return f0 + (1.0 - f0) * pow(1.0 - cosTheta, 5.0); + } + + float3 ShadeCheckerboard(float3 hitPoint, float3 normal, float3 rayDirection, bool softShadow, out float shadow) + { + float2 fw = max(abs(fwidth_approx(hitPoint.xz)), 0.001); + float2 fractPos = fract(hitPoint.xz) - 0.5; + float2 filtered = clamp(fractPos / fw, -0.5, 0.5); + float checker = 0.5 - 0.5 * filtered.x * filtered.y; + float3 baseColor = lerp(float3(0.787, 0.787, 0.787), float3(0.1, 0.1, 0.1), checker); + + float NdotL = max(dot(normal, LightDir), 0.0); + float3 shadowOrigin = hitPoint + normal * RayEpsilon; + shadow = softShadow ? lerp(ShadowMin, 1.0, TraceSoftShadow(shadowOrigin, LightDir, hitPoint.xz * 100.0)) : + (TraceShadowRay(shadowOrigin, LightDir) ? ShadowMin : 1.0); + + float3 litColor = baseColor * AmbientColor + baseColor * LightColor * NdotL * shadow; + + float ao = 1.0; + for (uint i = 0; i < SphereCount; i++) + { + float3 toSphere = spheres[i].Center - hitPoint; + float horizDist = length(toSphere.xz); + float r = spheres[i].Radius; + float occl = saturate(1.0 - horizDist / (r * 2.0)); + float hFactor = saturate(1.0 - toSphere.y / (r * 3.0)); + ao -= occl * hFactor * 0.4; + } + + litColor *= max(ao, 0.3); + + float dist = length(hitPoint.xz); + float fade = saturate((dist - FloorFadeStart) / FloorFadeRange); + return lerp(litColor, SampleSky(rayDirection), fade); + } + + float3 ShadeSphere(float3 hitPoint, float3 normal, float3 sphereColor, float3 viewDir, bool softShadow) + { + float NdotL = max(dot(normal, LightDir), 0.0); + + float3 halfDir = normalize(LightDir + viewDir); + float spec = pow(max(dot(normal, halfDir), 0.0), 64.0); + + float3 shadowOrigin = hitPoint + normal * RayEpsilon; + float shadow = softShadow ? lerp(ShadowMin, 1.0, TraceSoftShadow(shadowOrigin, LightDir, hitPoint.xz * 100.0)) : + (TraceShadowRay(shadowOrigin, LightDir) ? ShadowMin : 1.0); + + float3 diffuse = sphereColor * LightColor * NdotL * shadow; + float3 specular = LightColor * spec * shadow; + float3 ambient = sphereColor * AmbientColor; + + return ambient + diffuse + specular; + } + + float3 TraceReflection(float3 origin, float3 direction) + { + RayDesc reflectRay; + reflectRay.Origin = origin; + reflectRay.Direction = direction; + reflectRay.TMin = RayEpsilon; + reflectRay.TMax = 1000.0; + + float3 sphereNormal = float3(0.0); + float3 sphereColor = float3(0.0); + + RayQuery query; + query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, reflectRay); + + while (query.Proceed()) + { + if (query.CandidateType() == CANDIDATE_PROCEDURAL_PRIMITIVE) + { + uint sphereIndex = query.CandidatePrimitiveIndex(); + Sphere sphere = spheres[sphereIndex]; + + float3 ro = query.CandidateObjectRayOrigin(); + float3 rd = query.CandidateObjectRayDirection(); + + float t = IntersectSphere(ro, rd, sphere); + + if (t >= query.RayTMin() && t <= query.CommittedRayT()) + { + float3 hitPoint = ro + rd * t; + sphereNormal = normalize(hitPoint - sphere.Center); + sphereColor = sphere.Color; + query.CommitProceduralPrimitiveHit(t); + } + } + } + + if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT) + { + float3 hitPoint = reflectRay.Origin + reflectRay.Direction * query.CommittedRayT(); + float unused; + return ShadeCheckerboard(hitPoint, FloorNormal, reflectRay.Direction, false, unused); + } + else if (query.CommittedStatus() == COMMITTED_PROCEDURAL_PRIMITIVE_HIT) + { + float3 hitPoint = reflectRay.Origin + reflectRay.Direction * query.CommittedRayT(); + float3 viewDir = normalize(origin - hitPoint); + return ShadeSphere(hitPoint, sphereNormal, sphereColor, viewDir, false); + } + else + { + return SampleSky(direction); + } + } float IntersectSphere(float3 origin, float3 direction, Sphere sphere) { float3 oc = origin - sphere.Center; - float a = dot(direction, direction); float b = dot(oc, direction); float c = dot(oc, oc) - sphere.Radius * sphere.Radius; - float discriminant = b * b - a * c; + float discriminant = b * b - c; if (discriminant > 0.0) { float sqrtD = sqrt(discriminant); - float t1 = (-b - sqrtD) / a; + float t1 = -b - sqrtD; if (t1 > 0.0) { return t1; } - float t2 = (-b + sqrtD) / a; + float t2 = -b + sqrtD; if (t2 > 0.0) { @@ -110,12 +291,26 @@ internal unsafe class RayTracingRenderer : IRenderer return -1.0; } + float2 fwidth_approx(float2 p) + { + float2 dx = float2(0.02, 0.0); + float2 dy = float2(0.0, 0.02); + return abs(fract(p + dx) - fract(p)) + abs(fract(p + dy) - fract(p)); + } + + float Hash(float2 p) + { + float3 p3 = fract(float3(p.xyx) * 0.1031); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); + } + bool TraceShadowRay(float3 origin, float3 direction) { RayDesc shadowRay; shadowRay.Origin = origin; shadowRay.Direction = direction; - shadowRay.TMin = 0.001; + shadowRay.TMin = RayEpsilon; shadowRay.TMax = 1000.0; RayQuery shadowQuery; @@ -143,8 +338,81 @@ internal unsafe class RayTracingRenderer : IRenderer return shadowQuery.CommittedStatus() != COMMITTED_NOTHING; } + float TraceSoftShadow(float3 origin, float3 direction, float2 pixelSeed) + { + float3 tangent = normalize(cross(direction, float3(0.0, 1.0, 0.0))); + float3 bitangent = cross(direction, tangent); + + float lit = 0.0; + for (uint i = 0; i < ShadowSamples; i++) + { + float h = Hash(pixelSeed + float2(float(i) * 7.13, float(i) * 3.71)); + float angle = (float(i) + h) * (TwoPi / float(ShadowSamples)); + float radius = sqrt(Hash(pixelSeed + float2(float(i) * 11.07, 0.0))) * SunRadius; + float3 jitteredDir = normalize(direction + tangent * cos(angle) * radius + bitangent * sin(angle) * radius); + + if (!TraceShadowRay(origin, jitteredDir)) + { + lit += 1.0; + } + } + + return lit / float(ShadowSamples); + } + + float3 TraceRoughReflection(float3 origin, float3 reflectDir, float3 normal, float roughness, float2 pixelSeed) + { + float3 tangent = normalize(cross(reflectDir, normal)); + float3 bitangent = cross(reflectDir, tangent); + + float3 accum = float3(0.0); + for (uint i = 0; i < ReflectionSamples; i++) + { + float h1 = Hash(pixelSeed + float2(float(i) * 5.17, float(i) * 9.23)); + float h2 = Hash(pixelSeed + float2(float(i) * 13.37, float(i) * 2.91)); + float angle = h1 * TwoPi; + float radius = sqrt(h2) * roughness; + float3 jitteredDir = normalize(reflectDir + tangent * cos(angle) * radius + bitangent * sin(angle) * radius); + accum += TraceReflection(origin, jitteredDir); + } + + return accum / float(ReflectionSamples); + } + + float3 ShadeFloor(float3 hitPoint, float3 rayDir, float3 cameraPos) + { + float shadow; + float3 directColor = ShadeCheckerboard(hitPoint, FloorNormal, rayDir, true, shadow); + + float3 viewDir = normalize(cameraPos - hitPoint); + float3 halfDir = normalize(LightDir + viewDir); + float floorSpec = pow(max(dot(FloorNormal, halfDir), 0.0), 128.0); + float specDist = length(hitPoint.xz); + float specFade = 1.0 - saturate((specDist - FloorFadeStart) / FloorFadeRange); + directColor += LightColor * floorSpec * 0.4 * specFade * shadow; + + float3 reflectDir = reflect(rayDir, FloorNormal); + float3 reflectColor = TraceReflection(hitPoint + FloorNormal * RayEpsilon, reflectDir); + float fresnel = SchlickFresnel(max(dot(FloorNormal, viewDir), 0.0), 0.02); + + return lerp(directColor, reflectColor, fresnel); + } + + float3 ShadePrimarySphere(float3 hitPoint, float3 rayDir, float3 cameraPos, float3 normal, float3 sphereColor) + { + float3 viewDir = normalize(cameraPos - hitPoint); + + float3 directColor = ShadeSphere(hitPoint, normal, sphereColor, viewDir, true); + + float3 reflectDir = reflect(rayDir, normal); + float3 reflectColor = TraceRoughReflection(hitPoint + normal * RayEpsilon, reflectDir, normal, SphereRoughness, hitPoint.xz * 100.0); + float fresnel = SchlickFresnel(max(dot(normal, viewDir), 0.0), SphereF0); + + return lerp(directColor, reflectColor, fresnel); + } + [numthreads(16, 16, 1)] - void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) + void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID) { uint2 pixelCoord = dispatchThreadID.xy; @@ -163,8 +431,8 @@ internal unsafe class RayTracingRenderer : IRenderer float aspectRatio = float(width) / float(height); float fov = tan(radians(45.0) * 0.5); - float3 cameraPos = float3(0.0, 4.0, -12.0); - float3 cameraTarget = float3(0.0, 0.0, 0.0); + float3 cameraPos = constants.Position; + float3 cameraTarget = float3(0.0, 0.5, 0.0); float3 cameraUp = float3(0.0, 1.0, 0.0); float3 forward = normalize(cameraTarget - cameraPos); @@ -176,7 +444,7 @@ internal unsafe class RayTracingRenderer : IRenderer RayDesc ray; ray.Origin = cameraPos; ray.Direction = rayDir; - ray.TMin = 0.001; + ray.TMin = RayEpsilon; ray.TMax = 1000.0; float3 sphereHitNormal = float3(0.0); @@ -214,48 +482,19 @@ internal unsafe class RayTracingRenderer : IRenderer if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT) { float3 hitPoint = ray.Origin + ray.Direction * query.CommittedRayT(); - - float scale = 1.0; - int checkX = int(floor(hitPoint.x * scale)); - int checkZ = int(floor(hitPoint.z * scale)); - bool isWhite = ((checkX + checkZ) & 1) == 0; - float3 baseColor = isWhite ? float3(0.9, 0.9, 0.9) : float3(0.2, 0.2, 0.2); - - float3 normal = float3(0.0, 1.0, 0.0); - float NdotL = max(dot(normal, LightDir), 0.0); - - float3 shadowOrigin = hitPoint + normal * 0.001; - bool inShadow = TraceShadowRay(shadowOrigin, LightDir); - - float shadow = inShadow ? 0.3 : 1.0; - float3 diffuse = baseColor * LightColor * NdotL * shadow; - float3 ambient = baseColor * AmbientColor; - - color = ambient + diffuse; + color = ShadeFloor(hitPoint, rayDir, cameraPos); } else if (query.CommittedStatus() == COMMITTED_PROCEDURAL_PRIMITIVE_HIT) { float3 hitPoint = ray.Origin + ray.Direction * query.CommittedRayT(); - - float NdotL = max(dot(sphereHitNormal, LightDir), 0.0); - - float3 shadowOrigin = hitPoint + sphereHitNormal * 0.001; - bool inShadow = TraceShadowRay(shadowOrigin, LightDir); - - float shadow = inShadow ? 0.3 : 1.0; - float3 diffuse = sphereHitColor * LightColor * NdotL * shadow; - float3 ambient = sphereHitColor * AmbientColor; - - color = ambient + diffuse; + color = ShadePrimarySphere(hitPoint, rayDir, cameraPos, sphereHitNormal, sphereHitColor); } else { - float t = 0.5 * (rayDir.y + 1.0); - - color = lerp(float3(1.0, 1.0, 1.0), float3(0.5, 0.7, 1.0), t); + color = SampleSky(rayDir); } - color = pow(color, 1.0 / 2.2); + color = ACESFilm(color); outputTexture[pixelCoord] = float4(color, 1.0); } @@ -263,15 +502,18 @@ internal unsafe class RayTracingRenderer : IRenderer private readonly Buffer floorVertexBuffer; private readonly Buffer floorIndexBuffer; - private readonly Buffer sphereBuffer; private readonly Buffer aabbBuffer; private readonly BottomLevelAccelerationStructure floorBlas; private readonly BottomLevelAccelerationStructure sphereBlas; private readonly TopLevelAccelerationStructure tlas; + private readonly Buffer constantsBuffer; + private readonly Buffer sphereBuffer; private readonly ResourceLayout resourceLayout; private readonly ComputePipeline pipeline; + private Texture? outputTexture; private ResourceTable? resourceTable; + private float totalTime; public RayTracingRenderer() { @@ -282,10 +524,10 @@ internal unsafe class RayTracingRenderer : IRenderer Vector3[] floorVertices = [ - new(-5.0f, 0.0f, -5.0f), - new( 5.0f, 0.0f, -5.0f), - new( 5.0f, 0.0f, 5.0f), - new(-5.0f, 0.0f, 5.0f) + new(-50.0f, 0.0f, -50.0f), + new( 50.0f, 0.0f, -50.0f), + new( 50.0f, 0.0f, 50.0f), + new(-50.0f, 0.0f, 50.0f) ]; uint[] floorIndices = [0, 1, 2, 0, 2, 3]; @@ -305,38 +547,31 @@ internal unsafe class RayTracingRenderer : IRenderer }); floorIndexBuffer.Upload(floorIndices, 0); - Sphere[] sphereData = + Sphere[] spheres = [ - new() { Center = new(-1.5f, 1.0f, 0.0f), Radius = 1.0f, Color = new(0.8f, 0.2f, 0.2f) }, - new() { Center = new( 1.5f, 1.0f, 0.0f), Radius = 1.0f, Color = new(0.2f, 0.4f, 0.8f) } + new() { Center = new(-2.0f, 1.0f, 1.0f), Radius = 1.0f, Color = new(0.8f, 0.2f, 0.2f) }, + new() { Center = new( 2.0f, 1.2f, -1.0f), Radius = 1.2f, Color = new(0.2f, 0.4f, 0.8f) }, + new() { Center = new( 0.0f, 0.6f, -3.0f), Radius = 0.6f, Color = new(0.9f, 0.7f, 0.2f) } ]; - sphereBuffer = App.Context.CreateBuffer(new() - { - SizeInBytes = (uint)(sizeof(Sphere) * sphereData.Length), - StrideInBytes = (uint)sizeof(Sphere), - Flags = BufferUsageFlags.ShaderResource - }); - sphereBuffer.Upload(sphereData, 0); - - Vector3[] aabbData = new Vector3[sphereData.Length * 2]; - for (int i = 0; i < sphereData.Length; i++) + Vector3[] aabbs = new Vector3[spheres.Length * 2]; + for (int i = 0; i < spheres.Length; i++) { - aabbData[i * 2] = sphereData[i].Center - new Vector3(sphereData[i].Radius); - aabbData[(i * 2) + 1] = sphereData[i].Center + new Vector3(sphereData[i].Radius); + aabbs[i * 2] = spheres[i].Center - new Vector3(spheres[i].Radius); + aabbs[(i * 2) + 1] = spheres[i].Center + new Vector3(spheres[i].Radius); } aabbBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)(sizeof(Vector3) * aabbData.Length), + SizeInBytes = (uint)(sizeof(Vector3) * aabbs.Length), StrideInBytes = (uint)(sizeof(Vector3) * 2), Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.AccelerationStructure }); - aabbBuffer.Upload(aabbData, 0); + aabbBuffer.Upload(aabbs, 0); - CommandBuffer buildCmd = App.Context.Graphics.CommandBuffer(); + CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); - floorBlas = buildCmd.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc + floorBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc { Geometries = [ @@ -360,7 +595,7 @@ internal unsafe class RayTracingRenderer : IRenderer Flags = AccelerationStructureBuildFlags.PreferFastTrace }); - sphereBlas = buildCmd.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc + sphereBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc { Geometries = [ @@ -370,7 +605,7 @@ internal unsafe class RayTracingRenderer : IRenderer AABBs = new() { Buffer = aabbBuffer, - Count = (uint)sphereData.Length, + Count = (uint)spheres.Length, StrideInBytes = (uint)(sizeof(Vector3) * 2) }, Flags = RayTracingGeometryFlags.Opaque @@ -379,7 +614,7 @@ internal unsafe class RayTracingRenderer : IRenderer Flags = AccelerationStructureBuildFlags.PreferFastTrace }); - tlas = buildCmd.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc + tlas = commandBuffer.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc { Instances = [ @@ -403,13 +638,29 @@ internal unsafe class RayTracingRenderer : IRenderer Flags = AccelerationStructureBuildFlags.PreferFastTrace }); - buildCmd.Submit(waitForCompletion: true); + commandBuffer.Submit(waitForCompletion: true); + + constantsBuffer = App.Context.CreateBuffer(new() + { + SizeInBytes = (uint)sizeof(Constants), + StrideInBytes = (uint)sizeof(Constants), + Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite + }); + + sphereBuffer = App.Context.CreateBuffer(new() + { + SizeInBytes = (uint)(sizeof(Sphere) * spheres.Length), + StrideInBytes = (uint)sizeof(Sphere), + Flags = BufferUsageFlags.ShaderResource + }); + sphereBuffer.Upload(spheres, 0); resourceLayout = App.Context.CreateResourceLayout(new() { Bindings = BindingHelper.Bindings ( new() { Type = ResourceType.AccelerationStructure, Count = 1, StageFlags = ShaderStageFlags.Compute }, + new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute } ) @@ -429,6 +680,14 @@ internal unsafe class RayTracingRenderer : IRenderer public void Update(double deltaTime) { + totalTime += (float)deltaTime; + + float angle = totalTime * 0.3f; + + constantsBuffer.Upload([new Constants() + { + Position = new(12.0f * MathF.Sin(angle), 4.0f + MathF.Sin(totalTime * 0.2f), -12.0f * MathF.Cos(angle)) + }], 0); } public void Render() @@ -449,7 +708,7 @@ internal unsafe class RayTracingRenderer : IRenderer resourceTable ??= App.Context.CreateResourceTable(new() { Layout = resourceLayout, - Resources = [tlas, sphereBuffer, outputTexture] + Resources = [tlas, constantsBuffer, sphereBuffer, outputTexture] }); CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); @@ -462,12 +721,10 @@ internal unsafe class RayTracingRenderer : IRenderer commandBuffer.Dispatch(dispatchX, dispatchY, 1); - Texture colorTarget = App.SwapChain.FrameBuffer.Desc.ColorAttachments[0].Target; - commandBuffer.CopyTexture(outputTexture, default, default, - colorTarget, + App.FrameBuffer.Desc.ColorAttachments[0].Target, default, default, new() { Width = App.Width, Height = App.Height, Depth = 1 }); @@ -479,6 +736,7 @@ internal unsafe class RayTracingRenderer : IRenderer { resourceTable?.Dispose(); resourceTable = null; + outputTexture?.Dispose(); outputTexture = null; } @@ -490,19 +748,24 @@ internal unsafe class RayTracingRenderer : IRenderer pipeline.Dispose(); resourceLayout.Dispose(); + sphereBuffer.Dispose(); + constantsBuffer.Dispose(); tlas.Dispose(); sphereBlas.Dispose(); floorBlas.Dispose(); aabbBuffer.Dispose(); - sphereBuffer.Dispose(); floorIndexBuffer.Dispose(); floorVertexBuffer.Dispose(); } } -/// -/// Sphere definition for procedural geometry. -/// +[StructLayout(LayoutKind.Explicit, Size = 16)] +file struct Constants +{ + [FieldOffset(0)] + public Vector3 Position; +} + [StructLayout(LayoutKind.Explicit, Size = 32)] file struct Sphere { @@ -519,18 +782,7 @@ file struct Sphere ## Running the Tutorial -Update your `Program.cs` to run the `RayTracingRenderer`: - -```csharp -using ZenithTutorials; -using ZenithTutorials.Renderers; - -App.Run(); - -App.Cleanup(); -``` - -Run the application: +Run the application and select **6. Ray Tracing** from the menu: ```bash dotnet run @@ -538,28 +790,16 @@ dotnet run ## Result -![ray-tracing](../../images/ray-tracing.png) +![Ray Tracing](../../images/ray-tracing.png) ## Code Breakdown -### Checking Ray Tracing Support - -```csharp -if (!App.Context.Capabilities.RayTracingSupported) -{ - throw new NotSupportedException("Ray tracing is not supported on this device."); -} -``` - -Always check `Capabilities.RayTracingSupported` before using ray tracing features. - -### Acceleration Structure Setup +### Acceleration Structures -Build a two-level acceleration structure hierarchy: +The floor uses triangle geometry, while spheres use procedural AABBs: ```csharp -// Floor BLAS (triangle geometry) -floorBlas = buildCmd.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc +floorBlas = commandBuffer.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc { Geometries = [ @@ -582,32 +822,26 @@ floorBlas = buildCmd.BuildAccelerationStructure(new BottomLevelAccelerationStruc ], Flags = AccelerationStructureBuildFlags.PreferFastTrace }); +``` + +For procedural geometry, AABBs (axis-aligned bounding boxes) are provided as min/max pairs. The actual intersection is computed in the shader: + +```csharp +Vector3[] aabbs = new Vector3[spheres.Length * 2]; -// Sphere BLAS (procedural AABB geometry) -sphereBlas = buildCmd.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc +for (int i = 0; i < spheres.Length; i++) { - Geometries = - [ - new() - { - Type = RayTracingGeometryType.AABBs, - AABBs = new() - { - Buffer = aabbBuffer, - Count = (uint)sphereData.Length, - StrideInBytes = (uint)(sizeof(Vector3) * 2) - }, - Flags = RayTracingGeometryFlags.Opaque - } - ], - Flags = AccelerationStructureBuildFlags.PreferFastTrace -}); + aabbs[i * 2] = spheres[i].Center - new Vector3(spheres[i].Radius); + aabbs[(i * 2) + 1] = spheres[i].Center + new Vector3(spheres[i].Radius); +} ``` -Combine BLAS into a TLAS: +### TLAS Assembly + +The top-level structure combines both BLAS instances: ```csharp -tlas = buildCmd.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc +tlas = commandBuffer.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc { Instances = [ @@ -617,7 +851,7 @@ tlas = buildCmd.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc ID = 0, Mask = 0xFF, Transform = Matrix4x4.Identity, - ... + Flags = RayTracingInstanceFlags.None }, new() { @@ -625,187 +859,89 @@ tlas = buildCmd.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc ID = 1, Mask = 0xFF, Transform = Matrix4x4.Identity, - ... + Flags = RayTracingInstanceFlags.None } ], Flags = AccelerationStructureBuildFlags.PreferFastTrace }); ``` -### Ray Tracing with RayQuery +### Resource Layout -Ray tracing in Zenith.NET uses `RayQuery` within a compute shader. Procedural geometry requires a custom intersection function: +The compute shader accesses four resources — acceleration structure, constants, sphere data, and the output texture: -```slang -float IntersectSphere(float3 origin, float3 direction, Sphere sphere) +```csharp +resourceLayout = App.Context.CreateResourceLayout(new() { - float3 oc = origin - sphere.Center; - - float a = dot(direction, direction); - float b = dot(oc, direction); - float c = dot(oc, oc) - sphere.Radius * sphere.Radius; - float discriminant = b * b - a * c; - - if (discriminant > 0.0) - { - float sqrtD = sqrt(discriminant); - float t1 = (-b - sqrtD) / a; - - if (t1 > 0.0) - { - return t1; - } - - float t2 = (-b + sqrtD) / a; - - if (t2 > 0.0) - { - return t2; - } - } - - return -1.0; -} + Bindings = BindingHelper.Bindings + ( + new() { Type = ResourceType.AccelerationStructure, Count = 1, StageFlags = ShaderStageFlags.Compute }, + new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, + new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, + new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute } + ) +}); ``` -The main ray query loop processes only procedural candidates, storing hit information for later use: +### Animated Camera -```slang -float3 sphereHitNormal = float3(0.0); -float3 sphereHitColor = float3(0.0); +The camera orbits the scene, creating a cinematic flythrough: -RayQuery query; -query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, ray); - -while (query.Proceed()) +```csharp +public void Update(double deltaTime) { - if (query.CandidateType() == CANDIDATE_PROCEDURAL_PRIMITIVE) - { - uint sphereIndex = query.CandidatePrimitiveIndex(); - Sphere sphere = spheres[sphereIndex]; + totalTime += (float)deltaTime; - float3 ro = query.CandidateObjectRayOrigin(); - float3 rd = query.CandidateObjectRayDirection(); + float angle = totalTime * 0.3f; - float t = IntersectSphere(ro, rd, sphere); - - if (t >= query.RayTMin() && t <= query.CommittedRayT()) - { - float3 hitPoint = ro + rd * t; - - sphereHitNormal = normalize(hitPoint - sphere.Center); - sphereHitColor = sphere.Color; - - query.CommitProceduralPrimitiveHit(t); - } - } -} - -// Check result -if (query.CommittedStatus() == COMMITTED_TRIANGLE_HIT) -{ - // Handle triangle hit -} -else if (query.CommittedStatus() == COMMITTED_PROCEDURAL_PRIMITIVE_HIT) -{ - // Handle procedural hit using stored normal and color + constantsBuffer.Upload([new Constants() + { + Position = new(12.0f * MathF.Sin(angle), 4.0f + MathF.Sin(totalTime * 0.2f), -12.0f * MathF.Cos(angle)) + }], 0); } ``` -Key elements: - -| Element | Description | -|---------|-------------| -| `RayQuery` | Declares a ray query with template ray flags | -| `TraceRayInline` | Initiates the ray traversal | -| `Proceed()` | Advances traversal; returns `true` while candidates remain | -| `CandidateType()` | Returns the type of the current candidate hit | -| `CommitNonOpaqueTriangleHit()` | Accepts a triangle hit | -| `CommitProceduralPrimitiveHit(t)` | Accepts a procedural hit at distance `t` | -| `CommittedStatus()` | Returns the final hit result after traversal | +The camera position traces a circle of radius 12 with vertical bobbing. -### Shadow Rays with RayQuery +### Dynamic Resize -Shadow rays test visibility to a light source. Note that shadow rays must also handle procedural geometry intersections: +The output texture and resource table are recreated when the window resizes: -```slang -bool TraceShadowRay(float3 origin, float3 direction) +```csharp +public void Resize(uint width, uint height) { - RayDesc shadowRay; - shadowRay.Origin = origin; - shadowRay.Direction = direction; - shadowRay.TMin = 0.001; - shadowRay.TMax = 1000.0; - - RayQuery shadowQuery; - shadowQuery.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, shadowRay); - - while (shadowQuery.Proceed()) - { - if (shadowQuery.CandidateType() == CANDIDATE_PROCEDURAL_PRIMITIVE) - { - uint sphereIndex = shadowQuery.CandidatePrimitiveIndex(); - Sphere sphere = spheres[sphereIndex]; - - float3 ro = shadowQuery.CandidateObjectRayOrigin(); - float3 rd = shadowQuery.CandidateObjectRayDirection(); - - float t = IntersectSphere(ro, rd, sphere); - - if (t >= shadowQuery.RayTMin() && t <= shadowQuery.CommittedRayT()) - { - shadowQuery.CommitProceduralPrimitiveHit(t); - } - } - } + resourceTable?.Dispose(); + resourceTable = null; - return shadowQuery.CommittedStatus() != COMMITTED_NOTHING; + outputTexture?.Dispose(); + outputTexture = null; } ``` -Key optimization: `RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH` stops at the first hit since we only need to know if something blocks the light. - -### Compute Pipeline for Ray Tracing +Using nullable fields with `??=` in `Render()` provides lazy reallocation: ```csharp -resourceLayout = App.Context.CreateResourceLayout(new() -{ - Bindings = BindingHelper.Bindings - ( - new() { Type = ResourceType.AccelerationStructure, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute } - ) -}); - -pipeline = App.Context.CreateComputePipeline(new() -{ - Compute = computeShader, - ResourceLayout = resourceLayout, - ThreadGroupSizeX = ThreadGroupSize, - ThreadGroupSizeY = ThreadGroupSize, - ThreadGroupSizeZ = 1 -}); +outputTexture ??= App.Context.CreateTexture(new() { ... }); +resourceTable ??= App.Context.CreateResourceTable(new() { ... }); ``` -Note that all resource bindings use `ShaderStageFlags.Compute` since ray tracing runs entirely within a compute shader. +### Shader Rendering Techniques -### Dispatching and Display - -```csharp -uint dispatchX = (App.Width + ThreadGroupSize - 1) / ThreadGroupSize; -uint dispatchY = (App.Height + ThreadGroupSize - 1) / ThreadGroupSize; - -commandBuffer.SetPipeline(pipeline); -commandBuffer.SetResourceTable(resourceTable); -commandBuffer.Dispatch(dispatchX, dispatchY, 1); -``` +The shader implements several rendering techniques: -The compute shader processes each pixel independently. The result is then copied to the swap chain's color target for display. +| Technique | Function | Description | +|-----------|----------|-------------| +| Sky gradient | `SampleSky` | Horizon-to-zenith color blend with sun disk | +| Soft shadows | `TraceSoftShadow` | Jittered shadow rays simulating area light | +| Reflections | `TraceReflection` / `TraceRoughReflection` | Single and multi-sample reflection rays | +| Fresnel | `SchlickFresnel` | Angle-dependent reflectivity | +| Tone mapping | `ACESFilm` | ACES filmic curve with saturation boost | +| Checkerboard | `ShadeCheckerboard` | Anti-aliased procedural floor pattern | +| Sphere AO | Per-sphere loop | Contact-based ambient occlusion on floor | ## Next Steps -- [Mesh Shading](mesh-shading.md) - Process geometry in meshlets using the modern mesh shading pipeline +- [Mesh Shading](mesh-shading.md) - Use the modern mesh shader pipeline with GPU-driven culling ## Source Code diff --git a/documents/tutorials/getting-started/hello-triangle.md b/documents/tutorials/getting-started/hello-triangle.md index 31a81796..1bc4f4b3 100644 --- a/documents/tutorials/getting-started/hello-triangle.md +++ b/documents/tutorials/getting-started/hello-triangle.md @@ -1,21 +1,20 @@ # Hello Triangle -In this tutorial, you'll learn how to render a colored triangle using Zenith.NET. This is the classic "Hello World" of graphics programming. +In this tutorial, you'll create a renderer that draws a single colored triangle on screen. This is the classic starting point for graphics programming — establishing the graphics pipeline, defining vertex data, and issuing a draw call. ## Overview -We'll create a `HelloTriangleRenderer` class that: +This tutorial covers: -- Creates vertex data and uploads it to a GPU buffer -- Compiles vertex and pixel shaders using Slang -- Builds a graphics pipeline -- Records and submits draw commands - -This class-based approach makes it easy to extend for future tutorials. +- Defining a **Slang shader** with vertex and pixel stages +- Creating a **vertex buffer** with position and color data +- Configuring an **input layout** to describe vertex attributes +- Building a **graphics pipeline** with render states +- Recording and submitting **command buffers** to render a frame ## The Renderer Class -Create a new file `Renderers/HelloTriangleRenderer.cs`: +Create the file `Renderers/HelloTriangleRenderer.cs`: ```csharp namespace ZenithTutorials.Renderers; @@ -27,14 +26,14 @@ internal unsafe class HelloTriangleRenderer : IRenderer { float3 Position : POSITION0; - float4 Color : COLOR0; + float4 Color : COLOR0; }; struct PSInput { float4 Position : SV_POSITION; - float4 Color : COLOR0; + float4 Color : COLOR; }; PSInput VSMain(VSInput input) @@ -57,12 +56,11 @@ internal unsafe class HelloTriangleRenderer : IRenderer public HelloTriangleRenderer() { - // Define triangle vertices (NDC coordinates: -1 to 1) Vertex[] vertices = [ - new(new( 0.0f, 0.5f, 0.0f), new(1.0f, 0.0f, 0.0f, 1.0f)), // Top - Red - new(new( 0.5f, -0.5f, 0.0f), new(0.0f, 1.0f, 0.0f, 1.0f)), // Right - Green - new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 0.0f, 1.0f, 1.0f)), // Left - Blue + new(new( 0.0f, 0.5f, 0.0f), new(1.0f, 0.0f, 0.0f, 1.0f)), + new(new( 0.5f, -0.5f, 0.0f), new(0.0f, 1.0f, 0.0f, 1.0f)), + new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 0.0f, 1.0f, 1.0f)), ]; vertexBuffer = App.Context.CreateBuffer(new() @@ -73,7 +71,6 @@ internal unsafe class HelloTriangleRenderer : IRenderer }); vertexBuffer.Upload(vertices, 0); - // Define vertex input layout (must match shader VSInput) InputLayout inputLayout = new(); inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position }); inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Color }); @@ -85,16 +82,16 @@ internal unsafe class HelloTriangleRenderer : IRenderer { RenderStates = new() { - RasterizerState = RasterizerStates.CullNone, // Disable back-face culling - DepthStencilState = DepthStencilStates.Default, // Enable depth testing - BlendState = BlendStates.Opaque // No alpha blending + RasterizerState = RasterizerStates.CullNone, + DepthStencilState = DepthStencilStates.Default, + BlendState = BlendStates.Opaque }, Vertex = vertexShader, Pixel = pixelShader, ResourceLayout = null, InputLayouts = [inputLayout], PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output + Output = App.FrameBuffer.Output }); } @@ -106,7 +103,7 @@ internal unsafe class HelloTriangleRenderer : IRenderer { CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); - commandBuffer.BeginRenderPass(App.SwapChain.FrameBuffer, new() + commandBuffer.BeginRenderPass(App.FrameBuffer, new() { ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)], Depth = 1.0f, @@ -134,9 +131,6 @@ internal unsafe class HelloTriangleRenderer : IRenderer } } -/// -/// Vertex structure with position and color data. -/// [StructLayout(LayoutKind.Sequential)] file struct Vertex(Vector3 position, Vector4 color) { @@ -148,30 +142,71 @@ file struct Vertex(Vector3 position, Vector4 color) ## Running the Tutorial -Update your `Program.cs` to run the `HelloTriangleRenderer`: +Run the application and select **1. Hello Triangle** from the menu: + +```bash +dotnet run +``` + +## Result + +![Hello Triangle](../../images/hello-triangle.png) + +## Code Breakdown + +### Shader + +The shader is written inline as a Slang source string. It defines two stages: ```csharp -using ZenithTutorials; -using ZenithTutorials.Renderers; +private const string ShaderSource = """ + struct VSInput + { + float3 Position : POSITION0; -App.Run(); + float4 Color : COLOR0; + }; -App.Cleanup(); -``` + struct PSInput + { + float4 Position : SV_POSITION; -Run the application: + float4 Color : COLOR; + }; -```bash -dotnet run + PSInput VSMain(VSInput input) + { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.Color = input.Color; + + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return input.Color; + } + """; ``` -## Result +- **VSMain**: Converts the 3D position to clip space and passes the color through +- **PSMain**: Outputs the interpolated vertex color -![hello-triangle](../../images/hello-triangle.png) +### Vertex Data -## Code Breakdown +Three vertices define the triangle with red, green, and blue colors: -### Vertex Structure +```csharp +Vertex[] vertices = +[ + new(new( 0.0f, 0.5f, 0.0f), new(1.0f, 0.0f, 0.0f, 1.0f)), + new(new( 0.5f, -0.5f, 0.0f), new(0.0f, 1.0f, 0.0f, 1.0f)), + new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 0.0f, 1.0f, 1.0f)), +]; +``` + +The `Vertex` struct is defined as a `file`-scoped type with sequential layout: ```csharp [StructLayout(LayoutKind.Sequential)] @@ -183,10 +218,10 @@ file struct Vertex(Vector3 position, Vector4 color) } ``` -The `Vertex` struct uses the `file` keyword to limit its visibility to the current source file. It uses a primary constructor and `LayoutKind.Sequential` ensures the memory layout matches what the GPU expects. - ### Vertex Buffer +The buffer is created with `Vertex | MapWrite` flags. `MapWrite` enables CPU-side uploads: + ```csharp vertexBuffer = App.Context.CreateBuffer(new() { @@ -194,73 +229,92 @@ vertexBuffer = App.Context.CreateBuffer(new() StrideInBytes = (uint)sizeof(Vertex), Flags = BufferUsageFlags.Vertex | BufferUsageFlags.MapWrite }); - vertexBuffer.Upload(vertices, 0); ``` -Create a GPU buffer to hold vertex data using `App.Context`. `BufferUsageFlags.Vertex` indicates it will be used as a vertex buffer. +### Input Layout -### Shaders +The input layout tells the pipeline how to interpret vertex data. The order must match the shader's `VSInput`: -The Slang shader defines: - -- **Vertex Shader (`VSMain`)**: Transforms vertex positions and passes colors to the pixel shader -- **Pixel Shader (`PSMain`)**: Outputs the interpolated color for each pixel +```csharp +InputLayout inputLayout = new(); +inputLayout.Add(new() { Format = ElementFormat.Float3, Semantic = ElementSemantic.Position }); +inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Color }); +``` ### Graphics Pipeline +The pipeline binds everything together — shaders, render states, input layout, and output format: + ```csharp pipeline = App.Context.CreateGraphicsPipeline(new() { - RenderStates = new() { ... }, + RenderStates = new() + { + RasterizerState = RasterizerStates.CullNone, + DepthStencilState = DepthStencilStates.Default, + BlendState = BlendStates.Opaque + }, Vertex = vertexShader, Pixel = pixelShader, ResourceLayout = null, InputLayouts = [inputLayout], PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output + Output = App.FrameBuffer.Output }); ``` -The pipeline combines shaders, render states, and input layout into a complete rendering configuration. +| Property | Value | Purpose | +|----------|-------|---------| +| `RasterizerState` | `CullNone` | No face culling (both sides visible) | +| `DepthStencilState` | `Default` | Standard depth testing | +| `BlendState` | `Opaque` | No transparency | +| `ResourceLayout` | `null` | No bound resources needed | +| `PrimitiveTopology` | `TriangleList` | Every 3 vertices form a triangle | + +### Rendering -### Render Method +Each frame, a command buffer records the draw commands: ```csharp -public void Render() +CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); + +commandBuffer.BeginRenderPass(App.FrameBuffer, new() { - CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); + ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)], + Depth = 1.0f, + Stencil = 0, + Flags = ClearFlags.All +}); - commandBuffer.BeginRenderPass(App.SwapChain.FrameBuffer, new() - { - ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)], - Depth = 1.0f, - Stencil = 0, - Flags = ClearFlags.All - }); +commandBuffer.SetPipeline(pipeline); +commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0); +commandBuffer.Draw(3, 1, 0, 0); + +commandBuffer.EndRenderPass(); + +commandBuffer.Submit(waitForCompletion: true); +``` - commandBuffer.SetPipeline(pipeline); - commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0); - commandBuffer.Draw(3, 1, 0, 0); +`Draw(3, 1, 0, 0)` draws 3 vertices, 1 instance, starting at vertex 0 and instance 0. - commandBuffer.EndRenderPass(); +Note that `BeginRenderPass` does not pass a `ResourceTable` because this renderer has no bound resources. - commandBuffer.Submit(waitForCompletion: true); +### Resource Cleanup + +All GPU resources must be disposed in reverse order of creation: + +```csharp +public void Dispose() +{ + pipeline.Dispose(); + vertexBuffer.Dispose(); } ``` -Each frame: -1. `CommandBuffer()` - Get a command buffer from the graphics queue -2. `BeginRenderPass` - Clear and prepare for rendering (using `ColorValues` array and `ClearFlags`) -3. `SetPipeline` / `SetVertexBuffer` / `Draw` - Record draw commands -4. `EndRenderPass` - Finish the render pass -5. `Submit` - Submit commands to the GPU - ## Next Steps -Congratulations! You've rendered your first triangle with Zenith.NET. - -- [Textured Quad](textured-quad.md) - Load textures, create samplers, and use resource binding +- [Textured Quad](textured-quad.md) - Add textures, index buffers, and samplers ## Source Code diff --git a/documents/tutorials/getting-started/prerequisites.md b/documents/tutorials/getting-started/prerequisites.md index bc61952b..90ca0bbb 100644 --- a/documents/tutorials/getting-started/prerequisites.md +++ b/documents/tutorials/getting-started/prerequisites.md @@ -1,33 +1,17 @@ # Prerequisites -This guide covers the environment setup required before working with Zenith.NET. +Before starting the tutorials, you need to set up the project and create the shared framework code that all tutorials will use. -## System Requirements +## Development Environment -### Hardware - -Zenith.NET supports multiple graphics backends across platforms: - -| Platform | DirectX 12 | Metal 4 | Vulkan 1.4 | -|----------|:----------:|:-------:|:----------:| -| Windows | Yes | No | Yes | -| Apple | No | Yes | Yes | -| Android | No | No | Yes | -| Linux | No | No | Yes | +- .NET 10.0 SDK or later +- A GPU with DirectX 12, Metal 4, or Vulkan 1.4 support +- Visual Studio 2026, VS Code, or JetBrains Rider > [!NOTE] -> These tutorials are designed for desktop platforms (Windows, macOS, and Linux). - -### Software - -- **.NET SDK**: 10.0 or later -- **IDE**: Visual Studio 2026, VS Code, or JetBrains Rider +> These tutorials target desktop platforms: Windows, macOS, and Linux. -## Building the Tutorials - -The example code in these tutorials is designed to be extensible. We'll create a base project structure that all tutorials will share. - -### Creating the Project +## Creating the Project ```bash dotnet new console -n ZenithTutorials @@ -36,12 +20,11 @@ cd ZenithTutorials ### Required Packages -Install the following NuGet packages: - ```bash dotnet add package Zenith.NET.DirectX12 dotnet add package Zenith.NET.Metal dotnet add package Zenith.NET.Vulkan +dotnet add package Zenith.NET.Extensions.ImageSharp dotnet add package Zenith.NET.Extensions.Slang dotnet add package Silk.NET.Windowing dotnet add package Silk.NET.Input @@ -49,7 +32,7 @@ dotnet add package Silk.NET.Input ### Project Configuration -Update your `.csproj` file: +Your `.csproj` should look like this: ```xml @@ -63,12 +46,19 @@ Update your `.csproj` file: - - - - - - + + + + + + + + + + + + PreserveNewest + @@ -79,36 +69,51 @@ Update your `.csproj` file: ## Project Structure -Organize your project with the following directory structure: - ``` ZenithTutorials/ -├── Program.cs # Application entry point -├── App.cs # Application framework -├── IRenderer.cs # Renderer interface -├── BindingHelper.cs # Cross-platform resource binding helper -├── CocoaHelper.cs # macOS CAMetalLayer helper -├── Usings.cs # Global using statements -└── Renderers/ # All tutorial renderers +├── Program.cs +├── App.cs +├── IRenderer.cs +├── BindingHelper.cs +├── CocoaHelper.cs +├── Usings.cs +├── Assets/ +│ └── shoko.png +└── Renderers/ + ├── HelloTriangleRenderer.cs + ├── TexturedQuadRenderer.cs + ├── SpinningCubeRenderer.cs + ├── ComputeShaderRenderer.cs + ├── IndirectDrawingRenderer.cs + ├── RayTracingRenderer.cs + └── MeshShadingRenderer.cs ``` -## Global Usings +### Asset File + +Save the following image as `Assets/shoko.png` in your project (right-click → Save As): + +![shoko.png](../../images/shoko.png) -Create `Usings.cs` for shared using statements across all files: +## Framework Code + +The following files provide the shared infrastructure for all tutorials. Copy each file into your project. + +### Usings.cs ```csharp global using System.Numerics; +global using System.Runtime.CompilerServices; global using System.Runtime.InteropServices; global using Zenith.NET; +global using Zenith.NET.Extensions.ImageSharp; global using Zenith.NET.Extensions.Slang; global using Buffer = Zenith.NET.Buffer; ``` -This eliminates repetitive using statements in each renderer file. +### IRenderer.cs -## Renderer Interface - -All tutorial renderers implement a common interface. Create `IRenderer.cs`: +All tutorial renderers implement this interface: ```csharp namespace ZenithTutorials; @@ -123,22 +128,150 @@ internal interface IRenderer : IDisposable } ``` -This interface ensures all renderers follow a consistent pattern: +| Method | Called | Purpose | +|--------|-------|---------| +| `Update` | Every frame | Update logic (animations, transforms) | +| `Render` | Every frame | Issue GPU commands | +| `Resize` | On window resize | Recreate size-dependent resources | +| `Dispose` | On exit | Clean up GPU resources | + +### App.cs + +The application framework manages window creation, graphics context initialization, and the render loop: + +```csharp +using Silk.NET.Windowing; +using Zenith.NET.DirectX12; +using Zenith.NET.Metal; +using Zenith.NET.Vulkan; + +namespace ZenithTutorials; + +internal static class App +{ + private static readonly IWindow window; + private static readonly SwapChain swapChain; + + static App() + { + if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux()) + { + throw new PlatformNotSupportedException("This application only supports Windows, macOS, and Linux."); + } + + if (OperatingSystem.IsWindows()) + { + Context = GraphicsContext.CreateDirectX12(useValidationLayer: true); + } + else if (OperatingSystem.IsMacOS()) + { + Context = GraphicsContext.CreateMetal(useValidationLayer: true); + } + else + { + Context = GraphicsContext.CreateVulkan(useValidationLayer: true); + } + + Context.ValidationMessage += static (sender, args) => Console.WriteLine($"[{args.Source} - {args.Severity}] {args.Message}"); + + window = Window.Create(WindowOptions.Default with + { + API = GraphicsAPI.None, + Title = "Zenith Tutorials", + Size = new(1280, 720) + }); + window.Initialize(); + window.Center(); + + Surface surface; + if (OperatingSystem.IsWindows()) + { + surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height); + } + else if (OperatingSystem.IsMacOS()) + { + surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height); + } + else + { + surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height); + } + + swapChain = Context.CreateSwapChain(new() { Surface = surface, ColorTargetFormat = PixelFormat.B8G8R8A8UNorm, DepthStencilTargetFormat = PixelFormat.D32FloatS8UInt }); + } + + public static GraphicsContext Context { get; } + + public static uint Width => (uint)window.FramebufferSize.X; + + public static uint Height => (uint)window.FramebufferSize.Y; + + public static FrameBuffer FrameBuffer => swapChain.FrameBuffer; + + public static void Run() where TRenderer : IRenderer, new() + { + try + { + using TRenderer renderer = new(); + + window.Update += delta => + { + if (Width is 0 || Height is 0) + { + return; + } + + renderer.Update(delta); + }; + + window.Render += delta => + { + if (Width is 0 || Height is 0) + { + return; + } + + renderer.Render(); + swapChain.Present(); + }; + + window.Resize += size => + { + if (Width is 0 || Height is 0) + { + return; + } + + renderer.Resize(Width, Height); + swapChain.Resize(Width, Height); + }; + + window.Run(); + } + finally + { + swapChain.Dispose(); + window.Dispose(); + + Context.Dispose(); + } + } +} +``` + +`App` provides: -- `Update` - Called each frame for logic updates (animations, input handling) -- `Render` - Called each frame to record and submit draw commands -- `Resize` - Called when the window size changes -- `Dispose` - Cleanup GPU resources +| Member | Description | +|--------|-------------| +| `Context` | The `GraphicsContext` for the current platform | +| `Width` / `Height` | Current framebuffer dimensions | +| `FrameBuffer` | The swap chain's current frame buffer | +| `Run()` | Creates a renderer, runs the window loop, and cleans up on exit | -## Binding Helper +### BindingHelper.cs -Different graphics backends use different indexing schemes for resource bindings: +Each graphics backend (DirectX 12, Metal, Vulkan) uses different resource binding index conventions. `BindingHelper` assigns the correct indices automatically: -| Backend | Index Scheme | -|---------|--------------| -| DirectX12 | Per-type: CBV, SRV, UAV, Sampler each start at 0 | -| Metal | Per-category: Buffer, Texture, Sampler each start at 0 | -| Vulkan | Global: All resources share index space (0, 1, 2, ...) | ```csharp namespace ZenithTutorials; @@ -197,7 +330,8 @@ internal static class BindingHelper { ResourceType.ConstantBuffer or ResourceType.StructuredBuffer or - ResourceType.StructuredBufferReadWrite => bufferIndex++, + ResourceType.StructuredBufferReadWrite or + ResourceType.AccelerationStructure => bufferIndex++, ResourceType.Texture or ResourceType.TextureReadWrite => textureIndex++, @@ -228,32 +362,15 @@ internal static class BindingHelper } ``` -Usage example: +| Backend | Index Strategy | +|---------|---------------| +| **DirectX 12** | Separate counters per register type (CBV, SRV, UAV, Sampler) | +| **Metal** | Separate counters per resource category (Buffer, Texture, Sampler) | +| **Vulkan** | Sequential binding indices | -```csharp -resourceLayout = App.Context.CreateResourceLayout(new() -{ - Bindings = BindingHelper.Bindings - ( - new() { Type = ResourceType.Texture, Count = 1, StageFlags = ShaderStageFlags.Pixel }, - new() { Type = ResourceType.Sampler, Count = 1, StageFlags = ShaderStageFlags.Pixel } - ) -}); - -resourceTable = App.Context.CreateResourceTable(new() -{ - Layout = resourceLayout, - Resources = [texture, sampler] -}); -``` - -The helper automatically assigns the correct `Index` values based on the current backend, so you don't need to specify them manually. - -## Cocoa Helper +### CocoaHelper.cs -On macOS, creating a rendering surface requires a `CAMetalLayer`. Silk.NET.Windowing doesn't expose this directly, so we need a helper to create it using Objective-C runtime interop. - -Create `CocoaHelper.cs`: +Required for macOS to create a `CAMetalLayer` for the window surface: ```csharp namespace ZenithTutorials; @@ -291,169 +408,53 @@ internal static partial class CocoaHelper } ``` -The `CAMetalLayer` can be used with both Metal and Vulkan backends on macOS. - -## Application Framework - -All tutorials share a common application framework that handles window creation, graphics context initialization, and the main loop. - -### App.cs - -Create `App.cs` as the reusable application framework: - -```csharp -using Silk.NET.Windowing; -using Zenith.NET.DirectX12; -using Zenith.NET.Metal; -using Zenith.NET.Vulkan; - -namespace ZenithTutorials; - -internal static class App -{ - private static readonly IWindow window; - - static App() - { - // Ensure platform is supported - if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux()) - { - throw new PlatformNotSupportedException("This tutorial only supports Windows, macOS, and Linux."); - } - - // Create window with no graphics API (we manage rendering ourselves) - window = Window.Create(WindowOptions.Default with - { - API = GraphicsAPI.None, - Title = "Zenith.NET Tutorial", - Size = new(1280, 720) - }); - - window.Initialize(); - - // Create graphics context and surface based on platform - Surface surface; - if (OperatingSystem.IsWindows()) - { - Context = GraphicsContext.CreateDirectX12(useValidationLayer: true); - - surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height); - } - else if (OperatingSystem.IsMacOS()) - { - Context = GraphicsContext.CreateMetal(useValidationLayer: true); - - surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height); - } - else - { - Context = GraphicsContext.CreateVulkan(useValidationLayer: true); - - surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height); - } - - // Log validation messages for debugging - Context.ValidationMessage += (sender, args) => - { - Console.WriteLine($"[{args.Source} - {args.Severity}] {args.Message}"); - }; - - // Create swap chain for double-buffered rendering - SwapChain = Context.CreateSwapChain(new() - { - Surface = surface, - ColorTargetFormat = PixelFormat.B8G8R8A8UNorm, - DepthStencilTargetFormat = PixelFormat.D32FloatS8UInt - }); - } - - public static GraphicsContext Context { get; } - - public static SwapChain SwapChain { get; } - - public static uint Width => (uint)window.Size.X; - - public static uint Height => (uint)window.Size.Y; - - public static void Run() where TRenderer : IRenderer, new() - { - using TRenderer renderer = new(); - - window.Update += renderer.Update; - - window.Render += delta => - { - // Skip rendering when window is minimized - if (Width <= 0 || Height <= 0) - { - return; - } - - renderer.Render(); - SwapChain.Present(); - }; - - window.Resize += size => - { - if (Width <= 0 || Height <= 0) - { - return; - } - - // Notify renderer first, then resize swap chain - renderer.Resize(Width, Height); - SwapChain.Resize(Width, Height); - }; - - window.Run(); - } - - public static void Cleanup() - { - SwapChain.Dispose(); - Context.Dispose(); - window.Dispose(); - } -} -``` +> [!NOTE] +> On Windows and Linux, this file is not used but must be present to compile. ### Program.cs -Create `Program.cs` as the simple entry point: +The entry point provides an interactive tutorial selector: ```csharp using ZenithTutorials; using ZenithTutorials.Renderers; -App.Run(); - -App.Cleanup(); -``` +(string Name, Action Run)[] tutorials = +[ + ("Hello Triangle", App.Run), + ("Textured Quad", App.Run), + ("Spinning Cube", App.Run), + ("Compute Shader", App.Run), + ("Indirect Drawing", App.Run), + ("Ray Tracing", App.Run), + ("Mesh Shading", App.Run) +]; + +for (int i = 0; i < tutorials.Length; i++) +{ + Console.WriteLine($"{i + 1}. {tutorials[i].Name}"); +} -> [!NOTE] -> `HelloTriangleRenderer` will be created in the [next tutorial](hello-triangle.md). +Console.Write("Select a tutorial to run: "); -This framework provides: +if (int.TryParse(Console.ReadKey().KeyChar.ToString(), out int choice) && choice >= 1 && choice <= tutorials.Length) +{ + Console.WriteLine($"\nRunning '{tutorials[choice - 1].Name}' tutorial..."); -- **Platform validation** - Ensures only supported platforms (Windows, macOS, Linux) are used -- **Window creation** with Silk.NET (1280×720 default size) -- **Cross-platform backend selection** (DirectX12 on Windows, Metal on macOS, Vulkan on Linux) -- **SwapChain management** for presenting frames -- **Resize handling** for responsive rendering -- **Generic renderer pattern** using `App.Run()` for easy tutorial switching -- **Static access** to `App.Context` and `App.SwapChain` from renderers + tutorials[choice - 1].Run(); +} +``` -## Verify Installation +> [!TIP] +> If you are following the tutorials sequentially, comment out renderers you haven't implemented yet to avoid build errors. -Before continuing, verify your setup compiles correctly: +## Next Steps -```bash -dotnet build -``` +With the framework in place, you're ready to start the first tutorial: -If the build succeeds, you're ready to start [Hello Triangle](hello-triangle.md)! +- [Hello Triangle](hello-triangle.md) - Render your first triangle with a graphics pipeline ## Source Code > [!TIP] -> The complete source code for all tutorials is available on GitHub: [ZenithTutorials](https://github.com/qian-o/ZenithTutorials) +> View the complete tutorial project on GitHub: [ZenithTutorials](https://github.com/qian-o/ZenithTutorials) diff --git a/documents/tutorials/getting-started/spinning-cube.md b/documents/tutorials/getting-started/spinning-cube.md index 9a94e529..b1fae9b3 100644 --- a/documents/tutorials/getting-started/spinning-cube.md +++ b/documents/tutorials/getting-started/spinning-cube.md @@ -1,19 +1,19 @@ # Spinning Cube -In this tutorial, you'll learn how to render a rotating 3D cube using Zenith.NET. We'll introduce constant buffers for passing transformation matrices to the GPU. +In this tutorial, you'll render a spinning 3D cube with per-vertex colors. This introduces constant buffers for uploading transformation matrices, and per-frame updates for animation. ## Overview -We'll create a `SpinningCubeRenderer` class that: +This tutorial covers: -- Defines 3D cube geometry with vertex and index buffers -- Creates a constant buffer for MVP (Model-View-Projection) matrices -- Updates the rotation every frame -- Uses depth testing for correct 3D rendering +- Building **Model/View/Projection** transformation matrices +- Creating and updating a **constant buffer** each frame +- Using **back-face culling** and **depth testing** for 3D rendering +- Animating object rotation over time in the `Update` loop ## The Renderer Class -Create a new file `Renderers/SpinningCubeRenderer.cs`: +Create the file `Renderers/SpinningCubeRenderer.cs`: ```csharp namespace ZenithTutorials.Renderers; @@ -21,15 +21,6 @@ namespace ZenithTutorials.Renderers; internal unsafe class SpinningCubeRenderer : IRenderer { private const string ShaderSource = """ - struct MVPConstants - { - float4x4 Model; - - float4x4 View; - - float4x4 Projection; - }; - struct VSInput { float3 Position : POSITION0; @@ -41,18 +32,26 @@ internal unsafe class SpinningCubeRenderer : IRenderer { float4 Position : SV_POSITION; - float4 Color : COLOR0; + float4 Color : COLOR; + }; + + struct Constants + { + float4x4 Model; + + float4x4 View; + + float4x4 Projection; }; - ConstantBuffer mvp; + ConstantBuffer constants; PSInput VSMain(VSInput input) { - float4 worldPos = mul(float4(input.Position, 1.0), mvp.Model); - float4 viewPos = mul(worldPos, mvp.View); - + float4x4 mvp = mul(mul(constants.Model, constants.View), constants.Projection); + PSInput output; - output.Position = mul(viewPos, mvp.Projection); + output.Position = mul(float4(input.Position, 1.0), mvp); output.Color = input.Color; return output; @@ -66,7 +65,7 @@ internal unsafe class SpinningCubeRenderer : IRenderer private readonly Buffer vertexBuffer; private readonly Buffer indexBuffer; - private readonly Buffer constantBuffer; + private readonly Buffer constantsBuffer; private readonly ResourceLayout resourceLayout; private readonly ResourceTable resourceTable; private readonly GraphicsPipeline pipeline; @@ -77,12 +76,10 @@ internal unsafe class SpinningCubeRenderer : IRenderer { Vertex[] vertices = [ - // Front face new(new(-0.5f, -0.5f, 0.5f), new(1.0f, 0.0f, 0.0f, 1.0f)), new(new( 0.5f, -0.5f, 0.5f), new(0.0f, 1.0f, 0.0f, 1.0f)), new(new( 0.5f, 0.5f, 0.5f), new(0.0f, 0.0f, 1.0f, 1.0f)), new(new(-0.5f, 0.5f, 0.5f), new(1.0f, 1.0f, 0.0f, 1.0f)), - // Back face new(new(-0.5f, -0.5f, -0.5f), new(1.0f, 0.0f, 1.0f, 1.0f)), new(new( 0.5f, -0.5f, -0.5f), new(0.0f, 1.0f, 1.0f, 1.0f)), new(new( 0.5f, 0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), @@ -91,17 +88,11 @@ internal unsafe class SpinningCubeRenderer : IRenderer uint[] indices = [ - // Front 0, 1, 2, 0, 2, 3, - // Back 5, 4, 7, 5, 7, 6, - // Left 4, 0, 3, 4, 3, 7, - // Right 1, 5, 6, 1, 6, 2, - // Top 3, 2, 6, 3, 6, 7, - // Bottom 4, 5, 1, 4, 1, 0 ]; @@ -121,10 +112,10 @@ internal unsafe class SpinningCubeRenderer : IRenderer }); indexBuffer.Upload(indices, 0); - constantBuffer = App.Context.CreateBuffer(new() + constantsBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)sizeof(MVPConstants), - StrideInBytes = (uint)sizeof(MVPConstants), + SizeInBytes = (uint)sizeof(Constants), + StrideInBytes = (uint)sizeof(Constants), Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite }); @@ -139,7 +130,7 @@ internal unsafe class SpinningCubeRenderer : IRenderer resourceTable = App.Context.CreateResourceTable(new() { Layout = resourceLayout, - Resources = [constantBuffer] + Resources = [constantsBuffer] }); InputLayout inputLayout = new(); @@ -162,26 +153,26 @@ internal unsafe class SpinningCubeRenderer : IRenderer ResourceLayout = resourceLayout, InputLayouts = [inputLayout], PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output + Output = App.FrameBuffer.Output }); } public void Update(double deltaTime) { rotationAngle += (float)deltaTime; - } - public void Render() - { Matrix4x4 model = Matrix4x4.CreateRotationY(rotationAngle) * Matrix4x4.CreateRotationX(rotationAngle * 0.5f); Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 3), Vector3.Zero, Vector3.UnitY); Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f); - constantBuffer.Upload([new MVPConstants() { Model = model, View = view, Projection = projection }], 0); + constantsBuffer.Upload([new Constants() { Model = model, View = view, Projection = projection }], 0); + } + public void Render() + { CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); - commandBuffer.BeginRenderPass(App.SwapChain.FrameBuffer, new() + commandBuffer.BeginRenderPass(App.FrameBuffer, new() { ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)], Depth = 1.0f, @@ -209,15 +200,12 @@ internal unsafe class SpinningCubeRenderer : IRenderer pipeline.Dispose(); resourceTable.Dispose(); resourceLayout.Dispose(); - constantBuffer.Dispose(); + constantsBuffer.Dispose(); indexBuffer.Dispose(); vertexBuffer.Dispose(); } } -/// -/// Vertex structure with position and color data. -/// [StructLayout(LayoutKind.Sequential)] file struct Vertex(Vector3 position, Vector4 color) { @@ -226,11 +214,8 @@ file struct Vertex(Vector3 position, Vector4 color) public Vector4 Color = color; } -/// -/// MVP transformation matrices. -/// [StructLayout(LayoutKind.Explicit, Size = 192)] -file struct MVPConstants +file struct Constants { [FieldOffset(0)] public Matrix4x4 Model; @@ -245,18 +230,7 @@ file struct MVPConstants ## Running the Tutorial -Update your `Program.cs` to run the `SpinningCubeRenderer`: - -```csharp -using ZenithTutorials; -using ZenithTutorials.Renderers; - -App.Run(); - -App.Cleanup(); -``` - -Run the application: +Run the application and select **3. Spinning Cube** from the menu: ```bash dotnet run @@ -264,137 +238,111 @@ dotnet run ## Result -![spinning-cube](../../images/spinning-cube.png) +![Spinning Cube](../../images/spinning-cube.png) ## Code Breakdown -### MVP Constants Structure +### Shader + +The vertex shader computes the Model-View-Projection transform: ```csharp -[StructLayout(LayoutKind.Explicit, Size = 192)] -file struct MVPConstants -{ - [FieldOffset(0)] - public Matrix4x4 Model; +private const string ShaderSource = """ + struct Constants + { + float4x4 Model; - [FieldOffset(64)] - public Matrix4x4 View; + float4x4 View; - [FieldOffset(128)] - public Matrix4x4 Projection; -} -``` + float4x4 Projection; + }; -The MVP (Model-View-Projection) matrices transform vertices from object space to screen space: + ConstantBuffer constants; -| Matrix | Purpose | -|--------|---------| -| **Model** | Object rotation, scale, and position in the world | -| **View** | Camera position and orientation | -| **Projection** | 3D to 2D projection (perspective or orthographic) | + PSInput VSMain(VSInput input) + { + float4x4 mvp = mul(mul(constants.Model, constants.View), constants.Projection); + + PSInput output; + output.Position = mul(float4(input.Position, 1.0), mvp); + output.Color = input.Color; + + return output; + } + """; +``` + +`ConstantBuffer` gives the shader access to the CPU-uploaded matrices. ### Constant Buffer +A constant buffer is created for the MVP matrices, updated every frame: + ```csharp -constantBuffer = App.Context.CreateBuffer(new() +constantsBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)sizeof(MVPConstants), - StrideInBytes = (uint)sizeof(MVPConstants), + SizeInBytes = (uint)sizeof(Constants), + StrideInBytes = (uint)sizeof(Constants), Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite }); ``` -Constant buffers pass data from the CPU to shaders. Use `BufferUsageFlags.Constant` and upload new data each frame as needed. - -### Cube Geometry +The `Constants` struct uses explicit layout to match HLSL/Slang packing rules: ```csharp -// 8 vertices for the cube corners -Vertex[] vertices = [ ... ]; - -// 36 indices (6 faces × 2 triangles × 3 vertices) -uint[] indices = -[ - // Front - 0, 1, 2, 0, 2, 3, - // Back - 5, 4, 7, 5, 7, 6, - // ... remaining faces -]; -``` - -A cube has 8 unique vertices and 6 faces. Each face is made of 2 triangles, requiring 6 indices per face (36 total). - -### Shader MVP Transformation - -```slang -ConstantBuffer mvp; - -PSInput VSMain(VSInput input) +[StructLayout(LayoutKind.Explicit, Size = 192)] +file struct Constants { - float4 worldPos = mul(float4(input.Position, 1.0), mvp.Model); - float4 viewPos = mul(worldPos, mvp.View); + [FieldOffset(0)] + public Matrix4x4 Model; - PSInput output; - output.Position = mul(viewPos, mvp.Projection); - output.Color = input.Color; + [FieldOffset(64)] + public Matrix4x4 View; - return output; + [FieldOffset(128)] + public Matrix4x4 Projection; } ``` -C# `Matrix4x4` and Slang (with `-matrix-layout-row-major`) both use row-major layout, so the multiplication order is `vector * matrix`. The vertex shader applies transformations in order: Model → View → Projection. Use `ConstantBuffer` in Slang to access structured constant data. +Each `Matrix4x4` is 64 bytes (4x4 floats), giving a total size of 192 bytes. + +### Animation -### Frame Update +The `Update` method accumulates time and builds transformation matrices: ```csharp public void Update(double deltaTime) { rotationAngle += (float)deltaTime; -} -``` - -The `Update` method is called each frame with the elapsed time. Accumulating `deltaTime` creates smooth, frame-rate-independent rotation. -### Creating Matrices + Matrix4x4 model = Matrix4x4.CreateRotationY(rotationAngle) * Matrix4x4.CreateRotationX(rotationAngle * 0.5f); + Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 3), Vector3.Zero, Vector3.UnitY); + Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f); -```csharp -Matrix4x4 model = Matrix4x4.CreateRotationY(rotationAngle) * Matrix4x4.CreateRotationX(rotationAngle * 0.5f); -Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 3), Vector3.Zero, Vector3.UnitY); -Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f); + constantsBuffer.Upload([new Constants() { Model = model, View = view, Projection = projection }], 0); +} ``` -| Method | Description | -|--------|-------------| -| `CreateRotationY/X` | Rotate around an axis | -| `CreateLookAt` | Position camera at (0,0,3) looking at origin | -| `CreatePerspectiveFieldOfView` | 45° FOV perspective projection | +| Matrix | Purpose | +|--------|---------| +| **Model** | Combined Y and X rotation, creating a tumbling effect | +| **View** | Camera at `(0, 0, 3)` looking at the origin | +| **Projection** | Perspective with 45-degree FOV | + +### Render States -### Back-Face Culling +The pipeline now uses `CullBack` instead of `CullNone`: ```csharp -RasterizerState = RasterizerStates.CullBack +RasterizerState = RasterizerStates.CullBack, +DepthStencilState = DepthStencilStates.Default, ``` -For closed 3D objects, enable back-face culling to skip rendering triangles facing away from the camera, improving performance. - -## What You've Learned - -Congratulations! You've completed the Getting Started tutorials. You now understand: - -- Creating vertex and index buffers -- Compiling shaders with Slang -- Building graphics pipelines -- Loading textures and creating samplers -- Resource binding with layouts and tables -- Using constant buffers for per-frame data -- MVP transformations for 3D rendering +Back-face culling discards triangles facing away from the camera, which is essential for 3D rendering performance. ## Next Steps -Continue with intermediate topics: - -- [Compute Shader](../intermediate/compute-shader.md) - Run general-purpose GPU computations for image processing +- [Compute Shader](../intermediate/compute-shader.md) - Process textures on the GPU with compute pipelines ## Source Code diff --git a/documents/tutorials/getting-started/textured-quad.md b/documents/tutorials/getting-started/textured-quad.md index b077d649..eafffde4 100644 --- a/documents/tutorials/getting-started/textured-quad.md +++ b/documents/tutorials/getting-started/textured-quad.md @@ -1,63 +1,20 @@ # Textured Quad -In this tutorial, you'll learn how to render a textured quad using Zenith.NET. We'll load an image, create a texture and sampler, and bind them to the shader using resource layouts. +In this tutorial, you'll render a textured quad using an index buffer, a texture loaded from file, and a sampler. This introduces resource binding — connecting GPU resources like textures and samplers to shaders through resource layouts and tables. ## Overview -We'll create a `TexturedQuadRenderer` class that: +This tutorial covers: -- Uses an index buffer to draw a quad with 4 vertices -- Loads an image and uploads it to a GPU texture -- Creates a sampler for texture filtering -- Binds texture and sampler using `ResourceLayout` and `ResourceTable` - -## Project Setup - -### Required Package - -Add the ImageSharp extension for loading images: - -```bash -dotnet add package Zenith.NET.Extensions.ImageSharp -``` - -Then add the global using to `Usings.cs`: - -```csharp -global using Zenith.NET.Extensions.ImageSharp; -``` - -### Assets Configuration - -Update your `.csproj` to copy assets to the output directory: - -```xml - - - PreserveNewest - - -``` - -### Sample Image - -This tutorial uses the following sample image. Right-click to save it to your project's `Assets` folder: - -shoko - -Your project structure should now look like this: - -``` -ZenithTutorials/ -├── Assets/ -│ └── shoko.png # Save the image above as shoko.png -└── Renderers/ - └── TexturedQuadRenderer.cs -``` +- Using an **index buffer** to share vertices between triangles +- Loading a **texture** from an image file +- Creating a **sampler** with filtering and address modes +- Defining a **resource layout** and **resource table** to bind resources to shaders +- Using `BindingHelper` for cross-backend resource binding ## The Renderer Class -Create a new file `Renderers/TexturedQuadRenderer.cs`: +Create the file `Renderers/TexturedQuadRenderer.cs`: ```csharp namespace ZenithTutorials.Renderers; @@ -76,11 +33,11 @@ internal unsafe class TexturedQuadRenderer : IRenderer { float4 Position : SV_POSITION; - float2 TexCoord : TEXCOORD0; + float2 TexCoord : TEXCOORD; }; - Texture2D shaderTexture; - SamplerState samplerState; + Texture2D texture; + SamplerState sampler; PSInput VSMain(VSInput input) { @@ -93,7 +50,7 @@ internal unsafe class TexturedQuadRenderer : IRenderer float4 PSMain(PSInput input) : SV_TARGET { - return shaderTexture.Sample(samplerState, input.TexCoord); + return texture.Sample(sampler, input.TexCoord); } """; @@ -107,7 +64,6 @@ internal unsafe class TexturedQuadRenderer : IRenderer public TexturedQuadRenderer() { - // UV origin (0,0) is top-left, (1,1) is bottom-right Vertex[] vertices = [ new(new(-0.5f, 0.5f, 0.0f), new(0.0f, 0.0f)), @@ -180,7 +136,7 @@ internal unsafe class TexturedQuadRenderer : IRenderer ResourceLayout = resourceLayout, InputLayouts = [inputLayout], PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output + Output = App.FrameBuffer.Output }); } @@ -192,7 +148,7 @@ internal unsafe class TexturedQuadRenderer : IRenderer { CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); - commandBuffer.BeginRenderPass(App.SwapChain.FrameBuffer, new() + commandBuffer.BeginRenderPass(App.FrameBuffer, new() { ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)], Depth = 1.0f, @@ -227,9 +183,6 @@ internal unsafe class TexturedQuadRenderer : IRenderer } } -/// -/// Vertex structure with position and texture coordinates. -/// [StructLayout(LayoutKind.Sequential)] file struct Vertex(Vector3 position, Vector2 texCoord) { @@ -241,18 +194,7 @@ file struct Vertex(Vector3 position, Vector2 texCoord) ## Running the Tutorial -Update your `Program.cs` to run the `TexturedQuadRenderer`: - -```csharp -using ZenithTutorials; -using ZenithTutorials.Renderers; - -App.Run(); - -App.Cleanup(); -``` - -Run the application: +Run the application and select **2. Textured Quad** from the menu: ```bash dotnet run @@ -260,50 +202,76 @@ dotnet run ## Result -![textured-quad](../../images/textured-quad.png) +![Textured Quad](../../images/textured-quad.png) ## Code Breakdown -### Vertex Structure +### Shader + +The pixel shader samples a texture using UV coordinates: ```csharp -[StructLayout(LayoutKind.Sequential)] -file struct Vertex(Vector3 position, Vector2 texCoord) -{ - public Vector3 Position = position; +private const string ShaderSource = """ + struct VSInput + { + float3 Position : POSITION0; - public Vector2 TexCoord = texCoord; -} -``` + float2 TexCoord : TEXCOORD0; + }; -Unlike the triangle tutorial, we now use `Vector2 TexCoord` instead of color. Texture coordinates (UVs) range from `(0,0)` at the top-left to `(1,1)` at the bottom-right. + struct PSInput + { + float4 Position : SV_POSITION; -### Index Buffer + float2 TexCoord : TEXCOORD; + }; -```csharp -uint[] indices = [0, 1, 2, 0, 2, 3]; + Texture2D texture; + SamplerState sampler; -indexBuffer = App.Context.CreateBuffer(new() -{ - SizeInBytes = (uint)(sizeof(uint) * indices.Length), - StrideInBytes = sizeof(uint), - Flags = BufferUsageFlags.Index | BufferUsageFlags.MapWrite -}); + PSInput VSMain(VSInput input) + { + PSInput output; + output.Position = float4(input.Position, 1.0); + output.TexCoord = input.TexCoord; + + return output; + } + + float4 PSMain(PSInput input) : SV_TARGET + { + return texture.Sample(sampler, input.TexCoord); + } + """; ``` -A quad requires 6 indices (2 triangles × 3 vertices). Using an index buffer reduces vertex data from 6 to 4 vertices by reusing shared vertices. +`Texture2D` and `SamplerState` are declared as global resources. The pixel shader uses `texture.Sample(sampler, uv)` to fetch filtered texel colors. -### Loading Textures +### Index Buffer + +Instead of duplicating vertices, an index buffer references shared vertices: ```csharp -texture = App.Context.LoadTextureFromFile(Path.Combine(AppContext.BaseDirectory, "Assets", "shoko.png"), generateMipMaps: true); +Vertex[] vertices = +[ + new(new(-0.5f, 0.5f, 0.0f), new(0.0f, 0.0f)), + new(new( 0.5f, 0.5f, 0.0f), new(1.0f, 0.0f)), + new(new( 0.5f, -0.5f, 0.0f), new(1.0f, 1.0f)), + new(new(-0.5f, -0.5f, 0.0f), new(0.0f, 1.0f)) +]; + +uint[] indices = [0, 1, 2, 0, 2, 3]; ``` -The `Zenith.NET.Extensions.ImageSharp` extension provides convenient methods to load images. Setting `generateMipMaps: true` creates smaller versions of the texture for better quality at different distances. +Two triangles (indices `0,1,2` and `0,2,3`) share vertices 0 and 2 to form the quad. -### Sampler +### Texture and Sampler + +The texture is loaded from a file with mipmaps generated automatically: ```csharp +texture = App.Context.LoadTextureFromFile(Path.Combine(AppContext.BaseDirectory, "Assets", "shoko.png"), generateMipMaps: true); + sampler = App.Context.CreateSampler(new() { U = AddressMode.Clamp, @@ -314,18 +282,17 @@ sampler = App.Context.CreateSampler(new() }); ``` -Samplers control how textures are read: - -| Property | Description | -|----------|-------------| -| `U/V/W` | How to handle coordinates outside 0-1 range | -| `Filter` | Interpolation method (linear = smooth, point = pixelated) | -| `MaxLod` | Maximum mipmap level to use | +| Property | Value | Purpose | +|----------|-------|---------| +| `AddressMode.Clamp` | U, V, W | Clamp UVs to `[0,1]` — no texture wrapping | +| `Filter` | `MinLinearMagLinearMipLinear` | Trilinear filtering for smooth sampling | +| `MaxLod` | `uint.MaxValue` | Allow all mipmap levels | ### Resource Binding +Resources are exposed to shaders through a layout and table: + ```csharp -// 1. Define the layout using BindingHelper for cross-platform compatibility resourceLayout = App.Context.CreateResourceLayout(new() { Bindings = BindingHelper.Bindings @@ -335,60 +302,40 @@ resourceLayout = App.Context.CreateResourceLayout(new() ) }); -// 2. Create the table (bind actual resources) resourceTable = App.Context.CreateResourceTable(new() { Layout = resourceLayout, Resources = [texture, sampler] }); - -// 3. Bind during rendering -commandBuffer.SetResourceTable(resourceTable); ``` -This three-step process connects your GPU resources to shader variables: - -1. **ResourceLayout** - Describes the structure (types and binding slots) -2. **ResourceTable** - Binds actual resources to the layout -3. **SetResourceTable** - Activates the binding during rendering - -The `BindingHelper.Bindings()` method (defined in [Prerequisites](prerequisites.md)) automatically assigns the correct `Index` values based on the current backend, so you don't need to specify them manually. +`BindingHelper.Bindings` assigns the correct binding indices per backend. `StageFlags` controls which shader stages can access each resource. -### Resource Preprocessing +### Rendering -When beginning a render pass, you can pass resource tables to the `preprocessResourceTables` parameter: +The render pass now receives the `resourceTable`, and uses indexed drawing: ```csharp -commandBuffer.BeginRenderPass(App.SwapChain.FrameBuffer, new() +commandBuffer.BeginRenderPass(App.FrameBuffer, new() { ColorValues = [new(0.1f, 0.1f, 0.1f, 1.0f)], Depth = 1.0f, Stencil = 0, Flags = ClearFlags.All }, resourceTable); -``` - -This allows Zenith.NET to optimize the resources in the table for shader access before the render pass begins, eliminating the need for manual resource management. - -### Shader Texture Sampling - -```slang -Texture2D shaderTexture; -SamplerState samplerState; -float4 PSMain(PSInput input) : SV_TARGET -{ - return shaderTexture.Sample(samplerState, input.TexCoord); -} +commandBuffer.SetPipeline(pipeline); +commandBuffer.SetResourceTable(resourceTable); +commandBuffer.SetVertexBuffer(vertexBuffer, 0, 0); +commandBuffer.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32); +commandBuffer.DrawIndexed(6, 1, 0, 0, 0); ``` -In Slang, resources are declared as global variables after the struct definitions, without explicit `register` bindings. The binding order is determined by declaration order and matches the order in `ResourceLayout.Bindings`. The pixel shader samples the texture at the interpolated UV coordinates. +`DrawIndexed(6, 1, 0, 0, 0)` draws 6 indices (2 triangles), 1 instance. ## Next Steps -Now that you understand texturing and resource binding, the next tutorial covers 3D rendering: - -- [Spinning Cube](spinning-cube.md) - Render a 3D cube with index buffers and MVP transformation matrices +- [Spinning Cube](spinning-cube.md) - Add 3D transformations with constant buffers ## Source Code diff --git a/documents/tutorials/index.md b/documents/tutorials/index.md index 74d10369..77947dd4 100644 --- a/documents/tutorials/index.md +++ b/documents/tutorials/index.md @@ -28,8 +28,8 @@ Explore cutting-edge GPU features for modern rendering (requires hardware suppor | Tutorial | Description | Requirement | |----------|-------------|-------------| -| [Ray Tracing](advanced/ray-tracing.md) | Build acceleration structures (BLAS/TLAS), use `RayQuery` for ray tracing, and implement hard shadows | `RayTracingSupported` | -| [Mesh Shading](advanced/mesh-shading.md) | Use meshlet-based geometry processing with mesh shading pipelines | `MeshShadingSupported` | +| [Ray Tracing](advanced/ray-tracing.md) | Build acceleration structures (BLAS/TLAS), use `RayQuery` for ray tracing with soft shadows, reflections, and ACES tonemapping | `RayTracingSupported` | +| [Mesh Shading](advanced/mesh-shading.md) | Render 1,000 sphere instances with amplification shader frustum culling and mesh shading pipeline | `MeshShadingSupported` | ## Tutorial Structure @@ -42,7 +42,7 @@ Each tutorial follows a consistent pattern: 5. **Result** - Screenshot of the expected output 6. **Code Breakdown** - Step-by-step explanation of important code sections -All tutorials share the same `App` framework, making it easy to switch between examples by changing a single line in `Program.cs`. +All tutorials share the same `App` framework. Run `dotnet run` and select a tutorial from the interactive menu in `Program.cs`. ## Learning Path @@ -54,8 +54,8 @@ All tutorials share the same `App` framework, making it easy to switch between e | **Spinning Cube** | Pass data to shaders via constant buffers, implement 3D transformations, and enable depth testing | | **Compute Shader** | Run general-purpose GPU computations for image processing | | **Indirect Drawing** | Let the GPU control draw parameters for efficient multi-instance rendering | -| **Ray Tracing** | Build acceleration structures, trace rays with `RayQuery`, handle intersections, and implement shadows | -| **Mesh Shading** | Process geometry in meshlets using the modern mesh shading pipeline | +| **Ray Tracing** | Build acceleration structures, trace rays with `RayQuery`, implement soft shadows, reflections, Fresnel, and ACES tonemapping | +| **Mesh Shading** | Use amplification shaders for GPU-driven frustum culling with mesh shading at scale (1,000 instances) | ## Requirements diff --git a/documents/tutorials/intermediate/compute-shader.md b/documents/tutorials/intermediate/compute-shader.md index 5d0d51b6..8d3be520 100644 --- a/documents/tutorials/intermediate/compute-shader.md +++ b/documents/tutorials/intermediate/compute-shader.md @@ -1,20 +1,20 @@ # Compute Shader -In this tutorial, you'll learn how to use compute shaders with Zenith.NET. We'll create a simple image processing effect that converts a color image to grayscale on the GPU. +In this tutorial, you'll use a compute pipeline to process an image on the GPU — converting it from color to grayscale. This introduces compute shaders, read/write textures, and dispatching work groups. ## Overview -We'll create a `ComputeShaderRenderer` class that: +This tutorial covers: -- Loads an image as an input texture -- Creates an output texture with read/write access -- Builds a compute pipeline -- Dispatches compute work to process the image -- Copies the result to the swap chain for display +- Creating a **compute pipeline** with thread group configuration +- Using `Texture2D` (read-only) and `RWTexture2D` (read-write) resources +- **Dispatching** compute work groups based on texture dimensions +- Performing **linearize → grayscale → gamma** color conversion +- Copying the processed texture to the frame buffer with centered placement ## The Renderer Class -Create a new file `Renderers/ComputeShaderRenderer.cs`: +Create the file `Renderers/ComputeShaderRenderer.cs`: ```csharp namespace ZenithTutorials.Renderers; @@ -23,29 +23,27 @@ internal class ComputeShaderRenderer : IRenderer { private const uint ThreadGroupSize = 16; - private const string ComputeShaderSource = """ + private const string ShaderSource = """ Texture2D inputTexture; RWTexture2D outputTexture; [numthreads(16, 16, 1)] - void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) + void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID) { uint width, height; outputTexture.GetDimensions(width, height); - // Bounds check if (dispatchThreadID.x >= width || dispatchThreadID.y >= height) { return; } - // Read input pixel float4 color = inputTexture[dispatchThreadID.xy]; - // Convert to grayscale using luminance weights - float gray = dot(color.rgb, float3(0.299, 0.587, 0.114)); + float3 linear = pow(color.rgb, 2.2); + float gray = dot(linear, float3(0.2126, 0.7152, 0.0722)); + gray = pow(gray, 1.0 / 2.2); - // Write to output outputTexture[dispatchThreadID.xy] = float4(gray, gray, gray, color.a); } """; @@ -90,7 +88,7 @@ internal class ComputeShaderRenderer : IRenderer Resources = [inputTexture, outputTexture] }); - using Shader computeShader = App.Context.LoadShaderFromSource(ComputeShaderSource, "CSMain", ShaderStageFlags.Compute); + using Shader computeShader = App.Context.LoadShaderFromSource(ShaderSource, "CSMain", ShaderStageFlags.Compute); pipeline = App.Context.CreateComputePipeline(new() { @@ -122,14 +120,11 @@ internal class ComputeShaderRenderer : IRenderer processed = true; } - // Copy the processed texture to the swap chain's color target (centered) - Texture colorTarget = App.SwapChain.FrameBuffer.Desc.ColorAttachments[0].Target; + Texture colorTarget = App.FrameBuffer.Desc.ColorAttachments[0].Target; - // Clamp copy region to fit within both textures uint copyWidth = Math.Min(outputTexture.Desc.Width, App.Width); uint copyHeight = Math.Min(outputTexture.Desc.Height, App.Height); - // Center the copy region uint srcX = (outputTexture.Desc.Width - copyWidth) / 2; uint srcY = (outputTexture.Desc.Height - copyHeight) / 2; uint destX = (App.Width - copyWidth) / 2; @@ -163,18 +158,7 @@ internal class ComputeShaderRenderer : IRenderer ## Running the Tutorial -Update your `Program.cs` to run the `ComputeShaderRenderer`: - -```csharp -using ZenithTutorials; -using ZenithTutorials.Renderers; - -App.Run(); - -App.Cleanup(); -``` - -Run the application: +Run the application and select **4. Compute Shader** from the menu: ```bash dotnet run @@ -182,81 +166,50 @@ dotnet run ## Result -![compute-shader](../../images/compute-shader.png) +![Compute Shader](../../images/compute-shader.png) ## Code Breakdown -### Compute Shader +### Shader -```slang -Texture2D inputTexture; -RWTexture2D outputTexture; +The compute shader processes each pixel independently in 16×16 thread groups: -[numthreads(16, 16, 1)] -void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) -{ - uint width, height; - outputTexture.GetDimensions(width, height); +```csharp +private const string ShaderSource = """ + Texture2D inputTexture; + RWTexture2D outputTexture; - // Bounds check - if (dispatchThreadID.x >= width || dispatchThreadID.y >= height) + [numthreads(16, 16, 1)] + void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID) { - return; - } + uint width, height; + outputTexture.GetDimensions(width, height); - // Read, process, write - float4 color = inputTexture[dispatchThreadID.xy]; - float gray = dot(color.rgb, float3(0.299, 0.587, 0.114)); - outputTexture[dispatchThreadID.xy] = float4(gray, gray, gray, color.a); -} -``` - -Key elements: + if (dispatchThreadID.x >= width || dispatchThreadID.y >= height) + { + return; + } -| Element | Description | -|---------|-------------| -| `Texture2D` | Read-only input texture | -| `RWTexture2D` | Read/write output texture | -| `[numthreads(16, 16, 1)]` | Thread group size (16×16 threads) | -| `SV_DispatchThreadID` | Global thread index across all groups | + float4 color = inputTexture[dispatchThreadID.xy]; -### Output Texture Creation + float3 linear = pow(color.rgb, 2.2); + float gray = dot(linear, float3(0.2126, 0.7152, 0.0722)); + gray = pow(gray, 1.0 / 2.2); -```csharp -outputTexture = App.Context.CreateTexture(new() -{ - Type = TextureType.Texture2D, - Format = PixelFormat.B8G8R8A8UNorm, - Width = inputTexture.Desc.Width, - Height = inputTexture.Desc.Height, - Depth = 1, - MipLevels = 1, - ArrayLayers = 1, - SampleCount = SampleCount.Count1, - Flags = TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess -}); + outputTexture[dispatchThreadID.xy] = float4(gray, gray, gray, color.a); + } + """; ``` -`TextureUsageFlags.UnorderedAccess` is required for textures that will be written to in compute shaders. +The grayscale conversion follows three steps: -### Compute Resource Layout +1. **Linearize**: `pow(color.rgb, 2.2)` removes sRGB gamma +2. **Luminance**: `dot(linear, float3(0.2126, 0.7152, 0.0722))` computes perceptual brightness using Rec. 709 coefficients +3. **Re-encode**: `pow(gray, 1.0 / 2.2)` applies gamma correction -```csharp -resourceLayout = App.Context.CreateResourceLayout(new() -{ - Bindings = BindingHelper.Bindings - ( - new() { Type = ResourceType.Texture, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute } - ) -}); -``` - -Note the differences from graphics shaders: -- `ShaderStageFlags.Compute` instead of `Vertex` or `Pixel` -- `ResourceType.TextureReadWrite` for writable textures +### Compute Pipeline -### Compute Pipeline Creation +Unlike the graphics pipeline, a compute pipeline has no vertex/pixel stages or render states: ```csharp pipeline = App.Context.CreateComputePipeline(new() @@ -269,64 +222,57 @@ pipeline = App.Context.CreateComputePipeline(new() }); ``` -The `ComputePipelineDesc` requires: -- `Compute` - The compiled compute shader -- `ResourceLayout` - Resource bindings (same as graphics pipelines) -- `ThreadGroupSizeX/Y/Z` - Must match `[numthreads()]` in the shader +The thread group size (16×16×1) defines how many threads run per group. This must match the `[numthreads]` attribute in the shader. -### Dispatching Compute Work +### Output Texture -```csharp -uint dispatchX = (inputTexture.Desc.Width + ThreadGroupSize - 1) / ThreadGroupSize; -uint dispatchY = (inputTexture.Desc.Height + ThreadGroupSize - 1) / ThreadGroupSize; +The output texture is created with `UnorderedAccess` to allow compute shader writes: -commandBuffer.SetPipeline(pipeline); -commandBuffer.SetResourceTable(resourceTable); -commandBuffer.Dispatch(dispatchX, dispatchY, 1); +```csharp +outputTexture = App.Context.CreateTexture(new() +{ + Type = TextureType.Texture2D, + Format = PixelFormat.B8G8R8A8UNorm, + Width = inputTexture.Desc.Width, + Height = inputTexture.Desc.Height, + Depth = 1, + MipLevels = 1, + ArrayLayers = 1, + SampleCount = SampleCount.Count1, + Flags = TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess +}); ``` -The `Dispatch` call executes the compute shader: -- `dispatchX` × `dispatchY` × `dispatchZ` = total thread groups -- Each group runs `ThreadGroupSize` × `ThreadGroupSize` × 1 threads -- The formula `(size + groupSize - 1) / groupSize` ensures full coverage +| Flag | Purpose | +|------|---------| +| `ShaderResource` | Can be read as `Texture2D` in shaders | +| `UnorderedAccess` | Can be written as `RWTexture2D` in compute shaders | -### Copying to the Swap Chain +### Dispatch and Copy + +The compute shader runs once, then the result is copied centered to the frame buffer each frame: ```csharp -Texture colorTarget = App.SwapChain.FrameBuffer.Desc.ColorAttachments[0].Target; - -// Clamp copy region to fit within both textures -uint copyWidth = Math.Min(outputTexture.Desc.Width, App.Width); -uint copyHeight = Math.Min(outputTexture.Desc.Height, App.Height); - -// Center the copy region -uint srcX = (outputTexture.Desc.Width - copyWidth) / 2; -uint srcY = (outputTexture.Desc.Height - copyHeight) / 2; -uint destX = (App.Width - copyWidth) / 2; -uint destY = (App.Height - copyHeight) / 2; - -commandBuffer.CopyTexture(outputTexture, - default, - new() { X = srcX, Y = srcY, Z = 0 }, - colorTarget, - default, - new() { X = destX, Y = destY, Z = 0 }, - new() { Width = copyWidth, Height = copyHeight, Depth = 1 }); -``` +if (!processed) +{ + uint dispatchX = (inputTexture.Desc.Width + ThreadGroupSize - 1) / ThreadGroupSize; + uint dispatchY = (inputTexture.Desc.Height + ThreadGroupSize - 1) / ThreadGroupSize; -Instead of using a full-screen quad with a graphics pipeline, we directly copy the processed texture to the swap chain's color target: + commandBuffer.SetPipeline(pipeline); + commandBuffer.SetResourceTable(resourceTable); + commandBuffer.Dispatch(dispatchX, dispatchY, 1); + + processed = true; +} +``` -- `App.SwapChain.FrameBuffer.Desc.ColorAttachments[0].Target` - Gets the swap chain's render target texture -- `CopyTexture` - Efficiently copies texture data on the GPU without needing shaders or render passes -- `copyWidth` / `copyHeight` - Clamps the copy region to fit within both source and destination textures -- `srcX` / `srcY` - Centers the source region when the texture is larger than the window -- `destX` / `destY` - Centers the destination region when the texture is smaller than the window +The dispatch count is computed as `ceil(dimension / threadGroupSize)` to ensure all pixels are covered. -This approach is simpler and more efficient when you just need to display a texture without additional processing. +The `CopyTexture` call copies the result centered within the swap chain's color target, handling cases where the image and window have different sizes. ## Next Steps -- [Indirect Drawing](indirect-drawing.md) - Let the GPU control draw parameters for efficient multi-instance rendering +- [Indirect Drawing](indirect-drawing.md) - Draw multiple instances with GPU-driven indirect commands ## Source Code diff --git a/documents/tutorials/intermediate/indirect-drawing.md b/documents/tutorials/intermediate/indirect-drawing.md index a601c712..f2f2380c 100644 --- a/documents/tutorials/intermediate/indirect-drawing.md +++ b/documents/tutorials/intermediate/indirect-drawing.md @@ -1,70 +1,71 @@ # Indirect Drawing -In this tutorial, you'll learn how to use indirect drawing with Zenith.NET. Indirect drawing allows the GPU to control draw parameters, enabling GPU-driven rendering techniques. +In this tutorial, you'll render a 5×5 grid of spinning cubes using indirect drawing and GPU instancing. This introduces indirect draw buffers, structured buffers for per-instance data, and shows how to drive draw calls from GPU-accessible memory. ## Overview -We'll create an `IndirectDrawingRenderer` class that: +This tutorial covers: -- Creates multiple instances of geometry with different transforms -- Stores draw arguments in a GPU buffer -- Uses `DrawIndexedIndirect` to render all instances in a single call -- Demonstrates GPU-driven rendering patterns +- Creating an **indirect draw buffer** with `IndirectDrawIndexedArgs` +- Using a **structured buffer** to store per-instance transforms and colors +- Updating instance data per-frame for independent animations +- Issuing a single `DrawIndexedIndirect` call to render all instances +- Setting up view/projection matrices in `Resize` for window-independent rendering ## The Renderer Class -Create a new file `Renderers/IndirectDrawingRenderer.cs`: +Create the file `Renderers/IndirectDrawingRenderer.cs`: ```csharp namespace ZenithTutorials.Renderers; internal unsafe class IndirectDrawingRenderer : IRenderer { - private const int InstanceCount = 25; // 5x5 grid of cubes + private const int InstanceCount = 25; private const string ShaderSource = """ - struct ViewConstants + struct VSInput { - float4x4 View; + float3 Position : POSITION0; - float4x4 Projection; + float4 Color : COLOR0; + + uint InstanceID : SV_InstanceID; }; - struct InstanceData + struct PSInput { - float4x4 Model; + float4 Position : SV_POSITION; - float4 Color; + float4 Color : COLOR; }; - struct VSInput + struct Constants { - float3 Position : POSITION0; - - float4 Color : COLOR0; + float4x4 View; - uint InstanceID : SV_InstanceID; + float4x4 Projection; }; - struct PSInput + struct Instance { - float4 Position : SV_POSITION; + float4x4 Model; - float4 Color : COLOR0; + float4 Color; }; - ConstantBuffer view; - StructuredBuffer instances; + ConstantBuffer constants; + StructuredBuffer instances; PSInput VSMain(VSInput input) { - InstanceData instance = instances[input.InstanceID]; + Instance instance = instances[input.InstanceID]; float4 worldPos = mul(float4(input.Position, 1.0), instance.Model); - float4 viewPos = mul(worldPos, view.View); + float4 viewPos = mul(worldPos, constants.View); PSInput output; - output.Position = mul(viewPos, view.Projection); + output.Position = mul(viewPos, constants.Projection); output.Color = input.Color * instance.Color; return output; @@ -79,7 +80,7 @@ internal unsafe class IndirectDrawingRenderer : IRenderer private readonly Buffer vertexBuffer; private readonly Buffer indexBuffer; private readonly Buffer indirectBuffer; - private readonly Buffer viewConstantsBuffer; + private readonly Buffer constantsBuffer; private readonly Buffer instanceBuffer; private readonly ResourceLayout resourceLayout; private readonly ResourceTable resourceTable; @@ -91,12 +92,10 @@ internal unsafe class IndirectDrawingRenderer : IRenderer { Vertex[] vertices = [ - // Front face new(new(-0.5f, -0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), new(new( 0.5f, -0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), new(new( 0.5f, 0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), new(new(-0.5f, 0.5f, 0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), - // Back face new(new(-0.5f, -0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), new(new( 0.5f, -0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), new(new( 0.5f, 0.5f, -0.5f), new(1.0f, 1.0f, 1.0f, 1.0f)), @@ -145,17 +144,18 @@ internal unsafe class IndirectDrawingRenderer : IRenderer FirstInstance = 0 }], 0); - viewConstantsBuffer = App.Context.CreateBuffer(new() + constantsBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)sizeof(ViewConstants), - StrideInBytes = (uint)sizeof(ViewConstants), + SizeInBytes = (uint)sizeof(Constants), + StrideInBytes = (uint)sizeof(Constants), Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite }); + Resize(App.Width, App.Height); instanceBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)(sizeof(InstanceData) * InstanceCount), - StrideInBytes = (uint)sizeof(InstanceData), + SizeInBytes = (uint)(sizeof(Instance) * InstanceCount), + StrideInBytes = (uint)sizeof(Instance), Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.MapWrite }); @@ -171,7 +171,7 @@ internal unsafe class IndirectDrawingRenderer : IRenderer resourceTable = App.Context.CreateResourceTable(new() { Layout = resourceLayout, - Resources = [viewConstantsBuffer, instanceBuffer] + Resources = [constantsBuffer, instanceBuffer] }); InputLayout inputLayout = new(); @@ -181,7 +181,6 @@ internal unsafe class IndirectDrawingRenderer : IRenderer using Shader vertexShader = App.Context.LoadShaderFromSource(ShaderSource, "VSMain", ShaderStageFlags.Vertex); using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel); - // Create graphics pipeline pipeline = App.Context.CreateGraphicsPipeline(new() { RenderStates = new() @@ -195,7 +194,7 @@ internal unsafe class IndirectDrawingRenderer : IRenderer ResourceLayout = resourceLayout, InputLayouts = [inputLayout], PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output + Output = App.FrameBuffer.Output }); } @@ -203,7 +202,8 @@ internal unsafe class IndirectDrawingRenderer : IRenderer { rotationAngle += (float)deltaTime; - InstanceData[] instances = new InstanceData[InstanceCount]; + Instance[] instances = new Instance[InstanceCount]; + int index = 0; int gridSize = (int)Math.Sqrt(InstanceCount); @@ -233,14 +233,9 @@ internal unsafe class IndirectDrawingRenderer : IRenderer public void Render() { - Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 8), Vector3.Zero, Vector3.UnitY); - Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)App.Width / App.Height, 0.1f, 100.0f); - - viewConstantsBuffer.Upload([new ViewConstants() { View = view, Projection = projection }], 0); - CommandBuffer commandBuffer = App.Context.Graphics.CommandBuffer(); - commandBuffer.BeginRenderPass(App.SwapChain.FrameBuffer, new() + commandBuffer.BeginRenderPass(App.FrameBuffer, new() { ColorValues = [new(0.1f, 0.1f, 0.15f, 1.0f)], Depth = 1.0f, @@ -261,6 +256,10 @@ internal unsafe class IndirectDrawingRenderer : IRenderer public void Resize(uint width, uint height) { + Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 8), Vector3.Zero, Vector3.UnitY); + Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)width / height, 0.1f, 100.0f); + + constantsBuffer.Upload([new Constants() { View = view, Projection = projection }], 0); } public void Dispose() @@ -269,16 +268,13 @@ internal unsafe class IndirectDrawingRenderer : IRenderer resourceTable.Dispose(); resourceLayout.Dispose(); instanceBuffer.Dispose(); - viewConstantsBuffer.Dispose(); + constantsBuffer.Dispose(); indirectBuffer.Dispose(); indexBuffer.Dispose(); vertexBuffer.Dispose(); } } -/// -/// Vertex structure with position and color data. -/// [StructLayout(LayoutKind.Sequential)] file struct Vertex(Vector3 position, Vector4 color) { @@ -287,47 +283,30 @@ file struct Vertex(Vector3 position, Vector4 color) public Vector4 Color = color; } -/// -/// Per-instance transformation and color data. -/// -[StructLayout(LayoutKind.Explicit, Size = 80)] -file struct InstanceData +[StructLayout(LayoutKind.Explicit, Size = 128)] +file struct Constants { [FieldOffset(0)] - public Matrix4x4 Model; + public Matrix4x4 View; [FieldOffset(64)] - public Vector4 Color; + public Matrix4x4 Projection; } -/// -/// View and projection matrices. -/// -[StructLayout(LayoutKind.Explicit, Size = 128)] -file struct ViewConstants +[StructLayout(LayoutKind.Explicit, Size = 80)] +file struct Instance { [FieldOffset(0)] - public Matrix4x4 View; + public Matrix4x4 Model; [FieldOffset(64)] - public Matrix4x4 Projection; + public Vector4 Color; } ``` ## Running the Tutorial -Update your `Program.cs` to run the `IndirectDrawingRenderer`: - -```csharp -using ZenithTutorials; -using ZenithTutorials.Renderers; - -App.Run(); - -App.Cleanup(); -``` - -Run the application: +Run the application and select **5. Indirect Drawing** from the menu: ```bash dotnet run @@ -335,34 +314,38 @@ dotnet run ## Result -![indirect-drawing](../../images/indirect-drawing.png) +![Indirect Drawing](../../images/indirect-drawing.png) ## Code Breakdown -### Indirect Draw Arguments +### Shader + +The vertex shader reads per-instance data from a `StructuredBuffer`: ```csharp -indirectBuffer.Upload([new IndirectDrawIndexedArgs() +ConstantBuffer constants; +StructuredBuffer instances; + +PSInput VSMain(VSInput input) { - IndexCount = (uint)indices.Length, - InstanceCount = InstanceCount, - FirstIndex = 0, - VertexOffset = 0, - FirstInstance = 0 -}], 0); + Instance instance = instances[input.InstanceID]; + + float4 worldPos = mul(float4(input.Position, 1.0), instance.Model); + float4 viewPos = mul(worldPos, constants.View); + + PSInput output; + output.Position = mul(viewPos, constants.Projection); + output.Color = input.Color * instance.Color; + + return output; +} ``` -The `IndirectDrawIndexedArgs` structure matches the GPU's expected format for indexed indirect draws: +`SV_InstanceID` provides the instance index, used to look up the per-instance model matrix and color from the structured buffer. -| Field | Description | -|-------|-------------| -| `IndexCount` | Number of indices to draw per instance | -| `InstanceCount` | Number of instances to draw | -| `FirstIndex` | Starting index in the index buffer | -| `VertexOffset` | Value added to each index before fetching vertices | -| `FirstInstance` | Starting instance ID | +### Indirect Draw Buffer -### Indirect Buffer Creation +The draw arguments are stored in a GPU buffer instead of being passed as CPU parameters: ```csharp indirectBuffer = App.Context.CreateBuffer(new() @@ -371,77 +354,78 @@ indirectBuffer = App.Context.CreateBuffer(new() StrideInBytes = (uint)sizeof(IndirectDrawIndexedArgs), Flags = BufferUsageFlags.Indirect | BufferUsageFlags.MapWrite }); + +indirectBuffer.Upload([new IndirectDrawIndexedArgs() +{ + IndexCount = (uint)indices.Length, + InstanceCount = InstanceCount, + FirstIndex = 0, + VertexOffset = 0, + FirstInstance = 0 +}], 0); ``` -`BufferUsageFlags.Indirect` is required for buffers used with indirect draw commands. +`IndirectDrawIndexedArgs` mirrors the standard GPU indirect draw structure. Using `DrawIndexedIndirect` instead of `DrawIndexed` allows the GPU to read draw parameters from a buffer, enabling GPU-driven rendering scenarios. + +### Structured Buffer -### Instance Data Buffer +Per-instance data (model matrix + color) is uploaded to a structured buffer each frame: ```csharp instanceBuffer = App.Context.CreateBuffer(new() { - SizeInBytes = (uint)(sizeof(InstanceData) * InstanceCount), - StrideInBytes = (uint)sizeof(InstanceData), + SizeInBytes = (uint)(sizeof(Instance) * InstanceCount), + StrideInBytes = (uint)sizeof(Instance), Flags = BufferUsageFlags.ShaderResource | BufferUsageFlags.MapWrite }); ``` -Instance data is stored in a `StructuredBuffer` accessed by the vertex shader using `SV_InstanceID`. +The `Instance` struct is 80 bytes — a 64-byte `Matrix4x4` plus a 16-byte `Vector4`: -### Shader Instance Access - -```slang -StructuredBuffer instances; - -PSInput VSMain(VSInput input) +```csharp +[StructLayout(LayoutKind.Explicit, Size = 80)] +file struct Instance { - InstanceData instance = instances[input.InstanceID]; + [FieldOffset(0)] + public Matrix4x4 Model; - float4 worldPos = mul(float4(input.Position, 1.0), instance.Model); - // ... + [FieldOffset(64)] + public Vector4 Color; } ``` -The shader reads per-instance data using `SV_InstanceID` as an index into the structured buffer. +### Per-Instance Animation -### Indirect Draw Call +Each cube gets a unique rotation speed and color based on its grid position: ```csharp -commandBuffer.DrawIndexedIndirect(indirectBuffer, 0, 1); +instances[index] = new() +{ + Model = Matrix4x4.CreateScale(0.4f) + * Matrix4x4.CreateRotationY(rotation) + * Matrix4x4.CreateRotationX(rotation * 0.5f) + * Matrix4x4.CreateTranslation(offsetX, offsetY, 0), + Color = new((float)x / gridSize, (float)y / gridSize, 1.0f - ((float)x / gridSize), 1.0f) +}; ``` -| Parameter | Description | -|-----------|-------------| -| `indirectBuffer` | Buffer containing draw arguments | -| `offsetInBytes` | Byte offset into the buffer | -| `drawCount` | Number of draw commands to execute | - -### Available Indirect Commands - -Zenith.NET provides several indirect drawing methods: - -| Method | Description | -|--------|-------------| -| `DrawIndirect` | Non-indexed indirect draw | -| `DrawIndexedIndirect` | Indexed indirect draw | -| `DispatchIndirect` | Indirect compute dispatch | -| `DispatchMeshIndirect` | Indirect mesh shading dispatch | - -### GPU-Driven Rendering +### View/Projection in Resize -Indirect drawing enables GPU-driven rendering where the GPU itself generates draw parameters: +View and projection matrices are set in `Resize` rather than `Update`, since the camera is static and only the aspect ratio changes: -1. A compute shader performs culling and generates visible instance list -2. Another compute shader writes `IndirectDrawIndexedArgs` to the indirect buffer -3. `DrawIndexedIndirect` renders only visible instances +```csharp +public void Resize(uint width, uint height) +{ + Matrix4x4 view = Matrix4x4.CreateLookAt(new(0, 0, 8), Vector3.Zero, Vector3.UnitY); + Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(float.DegreesToRadians(45.0f), (float)width / height, 0.1f, 100.0f); -This eliminates CPU-GPU synchronization for visibility determination. + constantsBuffer.Upload([new Constants() { View = view, Projection = projection }], 0); +} +``` ## Next Steps -Continue with advanced GPU features: - -- [Ray Tracing](../advanced/ray-tracing.md) - Build acceleration structures, trace rays with `RayQuery`, and implement shadows +- [Ray Tracing](../advanced/ray-tracing.md) - Cast rays with hardware acceleration structures ## Source Code diff --git a/sources/Experiments/CornellBox/App.cs b/sources/Experiments/CornellBox/App.cs index c6c1ddf1..8646e411 100644 --- a/sources/Experiments/CornellBox/App.cs +++ b/sources/Experiments/CornellBox/App.cs @@ -27,6 +27,11 @@ internal static class App static App() { + if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux()) + { + throw new PlatformNotSupportedException("This application only supports Windows, macOS, and Linux."); + } + if (OperatingSystem.IsWindows()) { Context = GraphicsContext.CreateDirectX12(useValidationLayer: true); @@ -42,8 +47,12 @@ static App() Context.ValidationMessage += static (sender, args) => Console.WriteLine($"[{args.Source} - {args.Severity}] {args.Message}"); - window = Window.Create(WindowOptions.Default with { API = GraphicsAPI.None }); - window.Size = new(1280, 720); + window = Window.Create(WindowOptions.Default with + { + API = GraphicsAPI.None, + Title = "Cornell Box - Zenith.NET", + Size = new(1280, 720) + }); window.Initialize(); window.Center(); diff --git a/sources/Experiments/CornellBox/Assets/Shaders/PathTracing.slang b/sources/Experiments/CornellBox/Assets/Shaders/PathTracing.slang new file mode 100644 index 00000000..c171acc2 --- /dev/null +++ b/sources/Experiments/CornellBox/Assets/Shaders/PathTracing.slang @@ -0,0 +1,488 @@ +static const float3 LightMin = float3(213.0, 547.0, 227.0); +static const float3 LightMax = float3(343.0, 547.0, 332.0); +static const float LightArea = (343.0 - 213.0) * (332.0 - 227.0); +static const float3 LightNormal = float3(0.0, -1.0, 0.0); +static const float PI = 3.14159265; + +struct Vertex +{ + private float4 PositionAndPadding; + + private float4 NormalAndMaterialID; + + property float3 Position + { + get { + return PositionAndPadding.xyz; + } + } + + property float3 Normal + { + get { + return NormalAndMaterialID.xyz; + } + } + + property uint MaterialID + { + get { + return asuint(NormalAndMaterialID.w); + } + } +}; + +struct Material +{ + private float4 AlbedoAndEmission; + + float Metallic; + + float Roughness; + + private float padding0; + + private float padding1; + + property float3 Albedo + { + get { + return AlbedoAndEmission.xyz; + } + } + + property float Emission + { + get { + return AlbedoAndEmission.w; + } + } +}; + +struct CameraParams +{ + float4x4 InvView; + + float4x4 InvProjection; + + private float4 PositionAndPadding; + + uint FrameCount; + + uint Width; + + uint Height; + + private float padding0; + + property float3 Position + { + get { + return PositionAndPadding.xyz; + } + } +}; + +RaytracingAccelerationStructure scene; +ConstantBuffer camera; +StructuredBuffer vertices; +StructuredBuffer indices; +StructuredBuffer materials; +RWTexture2D accumTexture; +RWTexture2D outputTexture; + +float DistributionGGX(float NdotH, float roughness) +{ + float a = roughness * roughness; + float a2 = a * a; + float denom = NdotH * NdotH * (a2 - 1.0) + 1.0; + + return a2 / (PI * denom * denom); +} + +float GeometrySchlickGGX(float NdotX, float roughness) +{ + float r = roughness + 1.0; + float k = (r * r) / 8.0; + + return NdotX / (NdotX * (1.0 - k) + k); +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) +{ + return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); +} + +float3 FresnelSchlick(float cosTheta, float3 F0) +{ + return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); +} + +float3 evaluateBRDF(float3 N, float3 V, float3 L, Material mat) +{ + float roughness = max(mat.Roughness, 0.04); + float NdotL = max(dot(N, L), 0.0); + float NdotV = max(dot(N, V), 0.001); + float3 H = normalize(V + L); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + float3 F0 = lerp(float3(0.04, 0.04, 0.04), mat.Albedo, mat.Metallic); + + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 specular = D * G * F / (4.0 * NdotV * NdotL + 0.0001); + + float3 kD = (1.0 - F) * (1.0 - mat.Metallic); + float3 diffuse = kD * mat.Albedo / PI; + + return diffuse + specular; +} + +float powerHeuristic(float pdfA, float pdfB) +{ + float a2 = pdfA * pdfA; + + return a2 / (a2 + pdfB * pdfB + 0.0001); +} + +float computeBrdfPdf(float3 N, float3 V, float3 L, float roughness, float specProb) +{ + float NdotL = max(dot(N, L), 0.0); + if (NdotL <= 0.0) + { + return 0.0; + } + + float3 H = normalize(V + L); + float NdotH = max(dot(N, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + float a = roughness * roughness; + float a2 = a * a; + float denom = NdotH * NdotH * (a2 - 1.0) + 1.0; + float D = a2 / (PI * denom * denom); + + float pdfSpec = D * NdotH / (4.0 * HdotV + 0.0001); + float pdfDiff = NdotL / PI; + + return specProb * pdfSpec + (1.0 - specProb) * pdfDiff; +} + +float3 sampleGGXHalfVector(float3 N, float roughness, inout uint seed) +{ + float a = roughness * roughness; + + float r1 = randomFloat(seed); + float r2 = randomFloat(seed); + + float phi = 2.0 * PI * r1; + float cosTheta = sqrt((1.0 - r2) / (1.0 + (a * a - 1.0) * r2)); + float sinTheta = sqrt(1.0 - cosTheta * cosTheta); + + float3 w = N; + float3 helper = abs(w.x) > 0.99 ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0); + float3 u = normalize(cross(helper, w)); + float3 v = cross(w, u); + + return normalize(u * cos(phi) * sinTheta + v * sin(phi) * sinTheta + w * cosTheta); +} + +uint pcgHash(uint input) +{ + uint state = input * 747796405u + 2891336453u; + uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; + + return (word >> 22u) ^ word; +} + +float randomFloat(inout uint seed) +{ + seed = pcgHash(seed); + + return float(seed) / 4294967295.0; +} + +float3 cosineSampleHemisphere(float3 normal, inout uint seed) +{ + float r1 = randomFloat(seed); + float r2 = randomFloat(seed); + + float phi = 2.0 * PI * r1; + float sinTheta = sqrt(r2); + float cosTheta = sqrt(1.0 - r2); + + float3 w = normal; + float3 helper = abs(w.x) > 0.99 ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0); + float3 u = normalize(cross(helper, w)); + float3 v = cross(w, u); + + return normalize(u * cos(phi) * sinTheta + v * sin(phi) * sinTheta + w * cosTheta); +} + +bool traceShadowRay(float3 origin, float3 direction, float maxDist) +{ + RayDesc shadowRay; + shadowRay.Origin = origin; + shadowRay.Direction = direction; + shadowRay.TMin = 0.001; + shadowRay.TMax = maxDist - 0.01; + + if (shadowRay.TMax <= shadowRay.TMin) + { + return false; + } + + RayQuery shadowQuery; + shadowQuery.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, shadowRay); + + while (shadowQuery.Proceed()) + { + } + + return shadowQuery.CommittedStatus() != COMMITTED_NOTHING; +} + +float3 sampleLightDirect(float3 hitPos, float3 hitNormal, float3 geoNormal, float3 V, Material mat, float roughness, + float specProb, inout uint rng) +{ + float r1 = randomFloat(rng); + float r2 = randomFloat(rng); + + float3 lightPoint = float3(lerp(LightMin.x, LightMax.x, r1), LightMin.y, lerp(LightMin.z, LightMax.z, r2)); + + float3 toLight = lightPoint - hitPos; + float dist = length(toLight); + float3 L = toLight / dist; + + float NdotL = dot(hitNormal, L); + if (NdotL <= 0.0) + { + return float3(0.0, 0.0, 0.0); + } + + float lightCosine = max(dot(LightNormal, -L), 0.0); + if (lightCosine <= 0.0) + { + return float3(0.0, 0.0, 0.0); + } + + float3 shadowOrigin = dot(geoNormal, L) > 0.0 ? hitPos + geoNormal * 0.001 : hitPos - geoNormal * 0.001; + if (traceShadowRay(shadowOrigin, L, dist)) + { + return float3(0.0, 0.0, 0.0); + } + + Material lightMat = materials[3]; + float3 lightEmission = lightMat.Albedo * lightMat.Emission; + + float pdfLight = (dist * dist) / (lightCosine * LightArea); + float pdfBrdf = computeBrdfPdf(hitNormal, V, L, roughness, specProb); + float misWeight = powerHeuristic(pdfLight, pdfBrdf); + + float3 brdf = evaluateBRDF(hitNormal, V, L, mat); + + return lightEmission * brdf * NdotL * misWeight / pdfLight; +} + +float3 tracePath(float3 origin, float3 direction, inout uint rng) +{ + float3 throughput = float3(1.0, 1.0, 1.0); + float3 radiance = float3(0.0, 0.0, 0.0); + + for (int bounce = 0; bounce < 8; bounce++) + { + RayDesc ray; + ray.Origin = origin; + ray.Direction = direction; + ray.TMin = 0.001; + ray.TMax = 100000.0; + + RayQuery query; + query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, ray); + + while (query.Proceed()) + { + } + + if (query.CommittedStatus() == COMMITTED_NOTHING) + { + float3 skyColor = lerp(float3(0.15, 0.12, 0.10), float3(0.30, 0.35, 0.45), saturate(direction.y * 0.5 + 0.5)); + radiance += throughput * skyColor; + + break; + } + + uint primIdx = query.CommittedPrimitiveIndex(); + float2 bary = query.CommittedTriangleBarycentrics(); + float t = query.CommittedRayT(); + float3 hitPos = origin + direction * t; + + uint i0 = indices[primIdx * 3 + 0]; + uint i1 = indices[primIdx * 3 + 1]; + uint i2 = indices[primIdx * 3 + 2]; + + Vertex v0 = vertices[i0]; + Vertex v1 = vertices[i1]; + Vertex v2 = vertices[i2]; + + float3 baryWeights = float3(1.0 - bary.x - bary.y, bary.x, bary.y); + float3 normal = normalize(v0.Normal * baryWeights.x + v1.Normal * baryWeights.y + v2.Normal * baryWeights.z); + + float3 geoNormal = normal; + if (dot(normal, direction) > 0.0) + { + normal = -normal; + } + + Material mat = materials[v0.MaterialID]; + + if (mat.Emission > 0.0) + { + if (bounce == 0) + { + radiance += throughput * mat.Albedo * mat.Emission; + } + + break; + } + + float3 V = -direction; + + float3 F0 = lerp(float3(0.04, 0.04, 0.04), mat.Albedo, mat.Metallic); + float roughness = max(mat.Roughness, 0.04); + float specWeight = max(F0.r, max(F0.g, F0.b)); + float diffWeight = (1.0 - specWeight) * (1.0 - mat.Metallic); + float total = specWeight + diffWeight; + float specProb = specWeight / total; + + radiance += throughput * sampleLightDirect(hitPos, normal, geoNormal, V, mat, roughness, specProb, rng); + + float3 newDir; + float NdotV = max(dot(normal, V), 0.001); + + if (randomFloat(rng) < specProb) + { + float3 H = sampleGGXHalfVector(normal, roughness, rng); + newDir = reflect(-V, H); + + float NdotL = dot(normal, newDir); + if (NdotL <= 0.0) + { + break; + } + + float NdotH = max(dot(normal, H), 0.0); + float HdotV = max(dot(H, V), 0.0); + + float3 F = FresnelSchlick(HdotV, F0); + float G = GeometrySmith(NdotV, NdotL, roughness); + + float3 specThroughput = F * G * HdotV / (NdotV * NdotH * specProb + 0.0001); + throughput *= min(specThroughput, float3(10.0, 10.0, 10.0)); + } + else + { + newDir = cosineSampleHemisphere(normal, rng); + + float3 H = normalize(V + newDir); + float HdotV = max(dot(H, V), 0.0); + + float3 F = FresnelSchlick(HdotV, F0); + float3 kD = (1.0 - F) * (1.0 - mat.Metallic); + + throughput *= kD * mat.Albedo / (1.0 - specProb + 0.0001); + } + + origin = hitPos + normal * 0.001; + direction = newDir; + + if (bounce >= 2) + { + float p = max(throughput.r, max(throughput.g, throughput.b)); + + if (randomFloat(rng) > p) + { + break; + } + + throughput /= p; + } + } + + return radiance; +} + +float halton(uint index, uint base) +{ + float result = 0.0; + float f = 1.0; + + uint i = index; + + while (i > 0) + { + f /= float(base); + result += f * float(i % base); + i /= base; + } + + return result; +} + +[numthreads(16, 16, 1)] +void CSMain(uint3 dispatchThreadID: SV_DispatchThreadID) +{ + uint2 pixel = dispatchThreadID.xy; + + if (pixel.x >= camera.Width || pixel.y >= camera.Height) + { + return; + } + + uint rng = pcgHash(pixel.x + pixel.y * camera.Width + camera.FrameCount * camera.Width * camera.Height); + + uint sampleIndex = camera.FrameCount + 1; + float hx = halton(sampleIndex, 2); + float hy = halton(sampleIndex, 3); + float ox = randomFloat(rng) * 0.5 - 0.25; + float oy = randomFloat(rng) * 0.5 - 0.25; + float2 jitter = frac(float2(hx + ox, hy + oy)); + float2 uv = (float2(pixel) + jitter) / float2(camera.Width, camera.Height); + float2 ndc = uv * 2.0 - 1.0; + ndc.y = -ndc.y; + + float4 target = mul(float4(ndc, 1.0, 1.0), camera.InvProjection); + float3 localDir = normalize(target.xyz / target.w); + float3 direction = normalize(mul(float4(localDir, 0.0), camera.InvView).xyz); + float3 origin = camera.Position; + + float3 color = tracePath(origin, direction, rng); + color = min(color, float3(30.0, 30.0, 30.0)); + + float4 prev = accumTexture[pixel]; + float4 accumulated; + + if (camera.FrameCount == 0) + { + accumulated = float4(color, 1.0); + } + else + { + accumulated = prev + float4(color, 1.0); + } + + accumTexture[pixel] = accumulated; + + float3 avg = accumulated.rgb / float(camera.FrameCount + 1); + + // ACES tonemapping + float3 a = avg * (avg * 2.51 + 0.03); + float3 b = avg * (avg * 2.43 + 0.59) + 0.14; + avg = saturate(a / b); + + avg = pow(avg, 1.0 / 2.2); + outputTexture[pixel] = float4(avg, 1.0); +} diff --git a/sources/Experiments/CornellBox/Assets/Shaders/Rasterization.slang b/sources/Experiments/CornellBox/Assets/Shaders/Rasterization.slang new file mode 100644 index 00000000..17135159 --- /dev/null +++ b/sources/Experiments/CornellBox/Assets/Shaders/Rasterization.slang @@ -0,0 +1,205 @@ +struct Material +{ + private float4 AlbedoAndEmission; + + float Metallic; + + float Roughness; + + private float padding0; + + private float padding1; + + property float3 Albedo + { + get { + return AlbedoAndEmission.xyz; + } + } + + property float Emission + { + get { + return AlbedoAndEmission.w; + } + } +}; + +struct RasterConstants +{ + float4x4 Model; + + float4x4 View; + + float4x4 Projection; + + private float4 LightPosAndPadding; + + private float4 LightColorAndPadding; + + private float4 CameraPosAndPadding; + + property float3 LightPos + { + get { + return LightPosAndPadding.xyz; + } + } + + property float3 LightColor + { + get { + return LightColorAndPadding.xyz; + } + } + + property float3 CameraPos + { + get { + return CameraPosAndPadding.xyz; + } + } +}; + +struct VSInput +{ + private float4 PositionAndPadding : POSITION0; + + private float4 NormalAndMaterialID : NORMAL0; + + property float3 Position + { + get { + return PositionAndPadding.xyz; + } + } + + property float3 Normal + { + get { + return NormalAndMaterialID.xyz; + } + } + + property uint MaterialID + { + get { + return asuint(NormalAndMaterialID.w); + } + } +}; + +struct PSInput +{ + float4 Position : SV_POSITION; + + float3 WorldPos : TEXCOORD0; + + float3 Normal : TEXCOORD1; + + nointerpolation uint MaterialID : TEXCOORD2; +}; + +ConstantBuffer raster; +StructuredBuffer materials; + +static const float PI = 3.14159265; + +float3 ACESFilm(float3 x) +{ + return saturate((x * (x * 2.51 + 0.03)) / (x * (x * 2.43 + 0.59) + 0.14)); +} + +float3 toSRGB(float3 linear) +{ + return pow(linear, 1.0 / 2.2); +} + +float DistributionGGX(float NdotH, float roughness) +{ + float a = roughness * roughness; + float a2 = a * a; + float d = NdotH * NdotH * (a2 - 1.0) + 1.0; + return a2 / (PI * d * d); +} + +float GeometrySchlickGGX(float NdotV, float roughness) +{ + float r = roughness + 1.0; + float k = (r * r) / 8.0; + return NdotV / (NdotV * (1.0 - k) + k); +} + +float GeometrySmith(float NdotV, float NdotL, float roughness) +{ + return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); +} + +float3 FresnelSchlick(float cosTheta, float3 F0) +{ + return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); +} + +PSInput VSMain(VSInput input) +{ + float4 worldPos = mul(float4(input.Position, 1.0), raster.Model); + + PSInput output; + output.Position = mul(mul(worldPos, raster.View), raster.Projection); + output.WorldPos = worldPos.xyz; + output.Normal = normalize(mul(float4(input.Normal, 0.0), raster.Model).xyz); + output.MaterialID = input.MaterialID; + + return output; +} + +float4 PSMain(PSInput input) : SV_TARGET +{ + Material mat = materials[input.MaterialID]; + + if (mat.Emission > 0.0) + { + float3 emissive = mat.Albedo * mat.Emission; + float3 mapped = emissive / (emissive + 1.0); + + return float4(toSRGB(mapped), 1.0); + } + + float3 N = normalize(input.Normal); + float3 worldPos = input.WorldPos; + float3 V = normalize(raster.CameraPos - worldPos); + float3 toLight = raster.LightPos - worldPos; + float dist = length(toLight); + float3 L = toLight / dist; + float3 H = normalize(L + V); + + float NdotL = max(dot(N, L), 0.0); + float NdotH = max(dot(N, H), 0.0); + float NdotV = max(dot(N, V), 0.001); + float HdotV = max(dot(H, V), 0.0); + + float roughness = max(mat.Roughness, 0.04); + float metallic = mat.Metallic; + float3 F0 = lerp(float3(0.04), mat.Albedo, metallic); + + float D = DistributionGGX(NdotH, roughness); + float G = GeometrySmith(NdotV, NdotL, roughness); + float3 F = FresnelSchlick(HdotV, F0); + + float3 specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.0001); + + float3 kD = (1.0 - F) * (1.0 - metallic); + float3 diffuse = kD * mat.Albedo / PI; + + float atten = 1.0 / (dist * dist) * 80000.0; + float3 Lo = (diffuse + specular) * raster.LightColor * NdotL * atten; + + float hemiFactor = N.y * 0.5 + 0.5; + float3 ambient = mat.Albedo * lerp(0.03, 0.10, hemiFactor) * (1.0 - metallic * 0.7); + + float3 color = ambient + Lo; + + color = toSRGB(ACESFilm(color)); + + return float4(color, 1.0); +} diff --git a/sources/Experiments/CornellBox/CONVENTIONS.md b/sources/Experiments/CornellBox/CONVENTIONS.md deleted file mode 100644 index 95ecf1ee..00000000 --- a/sources/Experiments/CornellBox/CONVENTIONS.md +++ /dev/null @@ -1,792 +0,0 @@ -# CornellBox Development Conventions - -## Overview - -A dual-mode Cornell Box renderer built on the Zenith.NET multi-backend GPU framework (DirectX12 / Metal / Vulkan). -Two rendering modes switchable via ImGui radio buttons at runtime: - -- **Path Tracing** (mode 0): `ComputePipeline` + inline `RayQuery<>`, progressive accumulation with NEE (Next Event Estimation), Cook-Torrance PBR BRDF (GGX + Schlick Fresnel + Smith G), GGX importance sampling for specular, cosine-weighted hemisphere for diffuse, Russian roulette, environment sky light on ray miss. Only available when `Context.Capabilities.RayTracingSupported` is true. -- **Rasterization** (mode 1): `GraphicsPipeline` + Blinn-Phong lighting, point light at ceiling, hemisphere ambient, ACES tonemapping. Always available as fallback. - -## Current State (2026-03-30) - -- All files implemented and compiling successfully -- DirectX12 validation layer clean -- Camera initial position: (278, 273, -800), Speed=240, FarPlane=2000, looks into the box -- Swap chain format: B8G8R8A8UNorm color + D32FloatS8UInt depth/stencil -- `Renderer` abstract base class unifies both renderers (`Update` / `Render` / `Resize` + `IDisposable`) -- `activeRenderer` field in App.cs dispatches calls polymorphically - -### Render Loop (App.cs) - -1. `imGui.Update()` → `camera.Update()` → `activeRenderer.Update(camera)` -2. ImGui window: backend info, render mode radio buttons, SPP counter (path tracing only), FPS -3. Create `CommandBuffer` -4. `imGui.Render(commandBuffer, swapChain.FrameBuffer, ClearValues.Default)` — ImGui clears swap chain and renders UI overlay -5. `activeRenderer.Render(commandBuffer)` — renderer writes to its own Color texture, displayed via ImGui `AddImage` -6. `commandBuffer.Submit(true)` → `swapChain.Present()` - -### Disposal Order - -`pathTracer` → `rasterizer` → `imGui` → `swapChain` → `input` → `window` → `Context` - -## Project Structure - -``` -CornellBox/ -├── App.cs # Lifecycle, ImGui mode switching, activeRenderer dispatch -├── Program.cs # Entry point -├── CONVENTIONS.md # This file -├── Renderers/ -│ ├── Renderer.cs # Abstract base class: Color / DepthStencil / FrameBuffer management -│ ├── PathTracingRenderer.cs # ComputePipeline + RayQuery path tracing (NEE + PBR BRDF) -│ └── RasterizationRenderer.cs # GraphicsPipeline + Blinn-Phong -├── Handlers/ -│ ├── CameraHandler.cs # 6DOF camera (WASD+QE, right-click mouselook) -│ └── ImGuiHandler.cs # ImGui integration (input forwarding, font loading) -├── Helpers/ -│ ├── BindingHelper.cs # Multi-backend resource binding index assignment -│ ├── CornellBoxGeometry.cs # Shared geometry factory (Vertex, Material) -│ ├── CocoaHelper.cs # macOS Metal layer creation -│ └── Extensions.cs # ImGui.Overlay extension -└── Assets/ - └── Fonts/msyh.ttf # Chinese font for ImGui -``` - -## GPU Alignment Rules - -### Slang Side - -- **Never** use bare `float3` in ConstantBuffer / StructuredBuffer structs -- Pack `float3` into `float4`, use `private` field + `property` accessor -- If the next field after `float3` is a scalar (`float` / `uint`), merge them into one `float4` with a meaningful combined name (e.g. `NormalAndMaterialID`, `AlbedoAndEmission`); if there is no natural scalar to pair, use `XXXAndPadding` -- **Only** `float3` needs the `float4` + property pattern; scalar types (`float`, `uint`, `int`, `float4x4`, etc.) can be declared directly as normal fields -- Pad trailing bytes with `private float paddingN` to reach 16-byte boundary -- Vertex I/O structs (VSInput) also use `private float4` + `property` for consistency — semantic annotations go on the backing field -- PSInput interpolators can use `float3` directly (controlled by semantic output) -- Attributes go **above** properties, blank line between fields - -```slang -struct Vertex -{ - private float4 PositionAndPadding; - - private float4 NormalAndMaterialID; - - property float3 Position { get { return PositionAndPadding.xyz; } } - - property float3 Normal { get { return NormalAndMaterialID.xyz; } } - - property uint MaterialID { get { return asuint(NormalAndMaterialID.w); } } -}; - -struct Material -{ - private float4 AlbedoAndEmission; - - float Metallic; - - float Roughness; - - private float padding0; - - private float padding1; - - property float3 Albedo { get { return AlbedoAndEmission.xyz; } } - - property float Emission { get { return AlbedoAndEmission.w; } } -}; - -struct CameraParams -{ - float4x4 InvView; - - float4x4 InvProjection; - - private float4 PositionAndPadding; - - uint FrameCount; - - uint Width; - - uint Height; - - private float padding0; - - property float3 Position { get { return PositionAndPadding.xyz; } } -}; -``` - -### C# Side - -- Use `LayoutKind.Explicit` + `FieldOffset` for precise offset control -- Specify `Size` to ensure total size matches Slang side -- C# structs use split, human-readable fields (`Position`, `Normal`, `MaterialID`) — the GPU-side packing is handled by `FieldOffset` matching the Slang `float4` layout -- ConstantBuffer requires 256-byte alignment (`BufferUsageFlags.Constant`) -- Attributes go **above** the field, blank line between fields - -```csharp -[StructLayout(LayoutKind.Explicit, Size = 160)] -file struct CameraParams -{ - [FieldOffset(0)] - public Matrix4x4 InvView; - - [FieldOffset(64)] - public Matrix4x4 InvProjection; - - [FieldOffset(128)] - public Vector3 Position; - - [FieldOffset(144)] - public uint FrameCount; - - [FieldOffset(148)] - public uint Width; - - [FieldOffset(152)] - public uint Height; -} -``` - -### Alignment Quick Reference - -| Type | Size | Alignment | Notes | -|------|------|-----------|-------| -| `float` / `uint` / `int` | 4B | 4B | | -| `float2` / `uint2` | 8B | 8B | | -| `float4` / `uint4` | 16B | 16B | Use instead of float3 | -| `float4x4` | 64B | 16B | `Matrix4x4` | -| struct | - | 16B boundary | Total size must be multiple of 16 | - -### Vertex Input Layout - -Vertex buffers use `InputLayout` + `ElementFormat`. The C# struct uses split fields with `FieldOffset`, while Slang uses `private float4` + `property` for both StructuredBuffer and vertex input: - -```csharp -// 32 bytes — used by both renderers -[StructLayout(LayoutKind.Explicit, Size = 32)] -internal struct Vertex -{ - [FieldOffset(0)] - public Vector3 Position; // maps to PositionAndPadding.xyz - - [FieldOffset(16)] - public Vector3 Normal; // maps to NormalAndMaterialID.xyz - - [FieldOffset(28)] - public uint MaterialID; // maps to NormalAndMaterialID.w -} -``` - -Slang StructuredBuffer struct (path tracing): - -```slang -struct Vertex -{ - private float4 PositionAndPadding; - - private float4 NormalAndMaterialID; - - property float3 Position { get { return PositionAndPadding.xyz; } } - - property float3 Normal { get { return NormalAndMaterialID.xyz; } } - - property uint MaterialID { get { return asuint(NormalAndMaterialID.w); } } -}; -``` - -Slang vertex input struct (rasterization) — same layout with semantic annotations: - -```slang -struct VSInput -{ - private float4 PositionAndPadding : POSITION0; - - private float4 NormalAndMaterialID : NORMAL0; - - property float3 Position { get { return PositionAndPadding.xyz; } } - - property float3 Normal { get { return NormalAndMaterialID.xyz; } } - - property uint MaterialID { get { return asuint(NormalAndMaterialID.w); } } -}; -``` - -`InputLayout` matches the `float4 + float4` backing fields: - -```csharp -InputLayout inputLayout = new(); -inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Position }); -inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Normal }); -``` - -## Resource Creation Patterns - -### Buffer - -```csharp -// Vertex buffer (AccelerationStructure flag needed for ray tracing) -buffer = App.Context.CreateBuffer(new() -{ - SizeInBytes = (uint)(sizeof(Vertex) * vertices.Length), - StrideInBytes = (uint)sizeof(Vertex), - Flags = BufferUsageFlags.Vertex | BufferUsageFlags.AccelerationStructure -}); -buffer.Upload(vertices, 0); - -// ConstantBuffer (256B aligned, MapWrite for per-frame update) -cbuffer = App.Context.CreateBuffer(new() -{ - SizeInBytes = (uint)sizeof(CameraParams), - StrideInBytes = (uint)sizeof(CameraParams), - Flags = BufferUsageFlags.Constant | BufferUsageFlags.MapWrite -}); - -// StructuredBuffer (read-only) -sbuffer = App.Context.CreateBuffer(new() -{ - SizeInBytes = (uint)(sizeof(Material) * count), - StrideInBytes = (uint)sizeof(Material), - Flags = BufferUsageFlags.ShaderResource -}); -``` - -### Texture (UAV / Accumulation Buffer) - -```csharp -texture = App.Context.CreateTexture(new() -{ - Type = TextureType.Texture2D, - Format = PixelFormat.R32G32B32A32Float, - Width = width, Height = height, Depth = 1, - MipLevels = 1, ArrayLayers = 1, - SampleCount = SampleCount.Count1, - Flags = TextureUsageFlags.ShaderResource | TextureUsageFlags.UnorderedAccess -}); -``` - -## Acceleration Structure Build Pattern - -```csharp -CommandBuffer buildCmd = App.Context.Graphics.CommandBuffer(); - -// BLAS — one per geometry group -blas = buildCmd.BuildAccelerationStructure(new BottomLevelAccelerationStructureDesc -{ - Geometries = [new() - { - Type = RayTracingGeometryType.Triangles, - Triangles = new() - { - VertexBuffer = vertexBuffer, - VertexFormat = PixelFormat.R32G32B32Float, - VertexCount = vertexCount, - VertexStrideInBytes = (uint)sizeof(Vertex), - IndexBuffer = indexBuffer, - IndexFormat = IndexFormat.UInt32, - IndexCount = indexCount, - Transform = Matrix4x4.Identity - }, - Flags = RayTracingGeometryFlags.Opaque - }], - Flags = AccelerationStructureBuildFlags.PreferFastTrace -}); - -// TLAS — references all BLAS instances -tlas = buildCmd.BuildAccelerationStructure(new TopLevelAccelerationStructureDesc -{ - Instances = [ - new() - { - AccelerationStructure = blas, - ID = 0, - Mask = 0xFF, - Transform = Matrix4x4.Identity, - Flags = RayTracingInstanceFlags.None - } - ], - Flags = AccelerationStructureBuildFlags.PreferFastTrace -}); - -buildCmd.Submit(waitForCompletion: true); -``` - -## Pipeline Creation Patterns - -### ComputePipeline (Path Tracing) - -```csharp -resourceLayout = App.Context.CreateResourceLayout(new() -{ - Bindings = BindingHelper.Bindings( - new() { Type = ResourceType.AccelerationStructure, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.ConstantBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.StructuredBuffer, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute }, - new() { Type = ResourceType.TextureReadWrite, Count = 1, StageFlags = ShaderStageFlags.Compute } - ) -}); - -using Shader cs = App.Context.LoadShaderFromSource(ShaderSource, "CSMain", ShaderStageFlags.Compute); -pipeline = App.Context.CreateComputePipeline(new() -{ - Compute = cs, - ResourceLayout = resourceLayout, - ThreadGroupSizeX = 16, ThreadGroupSizeY = 16, ThreadGroupSizeZ = 1 -}); -``` - -### GraphicsPipeline (Rasterization) - -```csharp -using Shader vs = App.Context.LoadShaderFromSource(ShaderSource, "VSMain", ShaderStageFlags.Vertex); -using Shader ps = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel); - -pipeline = App.Context.CreateGraphicsPipeline(new() -{ - RenderStates = new() - { - RasterizerState = RasterizerStates.CullBack, - DepthStencilState = DepthStencilStates.Default, - BlendState = BlendStates.Opaque - }, - Vertex = vs, Pixel = ps, - ResourceLayout = resourceLayout, - InputLayouts = [inputLayout], - PrimitiveTopology = PrimitiveTopology.TriangleList, - Output = App.SwapChain.FrameBuffer.Output -}); -``` - -## Resource Binding Pattern - -Declaration order in ResourceLayout Bindings = declaration order in shader = resource order in ResourceTable. - -```csharp -// Layout declaration order -Bindings = BindingHelper.Bindings( - new() { Type = ResourceType.AccelerationStructure, ... }, // [0] scene - new() { Type = ResourceType.ConstantBuffer, ... }, // [1] camera - new() { Type = ResourceType.StructuredBuffer, ... }, // [2] vertices - new() { Type = ResourceType.StructuredBuffer, ... }, // [3] indices - new() { Type = ResourceType.StructuredBuffer, ... }, // [4] materials - new() { Type = ResourceType.TextureReadWrite, ... }, // [5] accumTexture - new() { Type = ResourceType.TextureReadWrite, ... }, // [6] outputTexture -); - -// Shader declares in same order: -// RaytracingAccelerationStructure scene; -// ConstantBuffer camera; -// StructuredBuffer vertices; -// StructuredBuffer indices; -// StructuredBuffer materials; -// RWTexture2D accumTexture; -// RWTexture2D outputTexture; - -// ResourceTable passes in same order -resourceTable = App.Context.CreateResourceTable(new() -{ - Layout = resourceLayout, - Resources = [tlas, cameraBuffer, vertexBuffer, indexBuffer, materialBuffer, accumTexture, Color] -}); -``` - -`BindingHelper.Bindings()` handles per-backend index differences transparently. - -## Render Loop Patterns - -### Compute Output → Color Texture - -```csharp -cmd.SetPipeline(computePipeline); -cmd.SetResourceTable(resourceTable); -cmd.Dispatch(dispatchX, dispatchY, 1); -``` - -The compute shader writes directly to the base class `Color` texture (bound as `outputTexture` UAV in the ResourceTable). No `CopyTexture` needed — ImGui displays it via `AddImage`. - -### Graphics RenderPass → SwapChain - -```csharp -cmd.BeginRenderPass(App.SwapChain.FrameBuffer, new() -{ - ColorValues = [new(0.51f, 0.518f, 0.557f, 1)], - Depth = 1.0f, Stencil = 0, - Flags = ClearFlags.All -}); -cmd.SetPipeline(graphicsPipeline); -cmd.SetResourceTable(resourceTable); -cmd.SetVertexBuffer(vertexBuffer, 0, 0); -cmd.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32); -cmd.DrawIndexed(indexCount, 1, 0, 0, 0); -cmd.EndRenderPass(); -``` - -## Resize Pattern - -Resources to rebuild on size change: - -- Texture (accumulationTexture) -- ResourceTable (references Texture) -- Reset path tracing FrameCount to 0 - -No rebuild needed: Buffer, Pipeline, ResourceLayout, acceleration structures. - -```csharp -public void Resize(uint width, uint height) -{ - base.Resize(width, height); // recreates Color + DepthStencil + FrameBuffer - - resourceTable?.Dispose(); - resourceTable = null; - accumulationTexture?.Dispose(); - accumulationTexture = null; - - FrameCount = 0; - // Lazy rebuild on next Render call -} -``` - -## Dispose Order - -Release in reverse creation order — downstream first, upstream last: - -``` -ResourceTable → Pipeline → ResourceLayout -→ Texture (accumulation) -→ TLAS → BLAS[] -→ Buffer (camera / material / index / vertex) -``` - -## Cornell Box Geometry Data (CornellBoxGeometry.cs) - -- Coordinate range 0~560 (standard Cornell Box specification) -- 16 quads (64 vertices, 96 indices): 5 walls + 5 short block faces + 5 tall block faces + 1 light -- 6 material groups: 0=red left wall, 1=green right wall, 2=white surfaces (ceiling/floor/back wall), 3=light, 4=short block (smooth diffuse), 5=tall block (metallic mirror) -- Each quad → 4 vertices + 6 indices (2 triangles) -- Normals auto-computed via `normalize(cross(v1-v0, v2-v0))` -- Material ID stored in `Vertex.MaterialID` (C#), packed into `NormalAndMaterialID.w` on GPU via `FieldOffset(28)` overlapping the `float4` w-component -- Material colors: red(0.63,0.06,0.06), green(0.14,0.45,0.09), white(0.73,0.71,0.68), light(1.0,0.85,0.6)+emission=25, short block(0.73,0.71,0.68)+roughness=0.3, tall block(0.95,0.93,0.88)+metallic=1.0+roughness=0.05 - -### Shared Data Types - -```csharp -// 32 bytes — used by both renderers -[StructLayout(LayoutKind.Explicit, Size = 32)] -internal struct Vertex -{ - [FieldOffset(0)] - public Vector3 Position; - - [FieldOffset(16)] - public Vector3 Normal; - - [FieldOffset(28)] - public uint MaterialID; -} - -// 32 bytes — PBR material with metallic/roughness -[StructLayout(LayoutKind.Explicit, Size = 32)] -internal struct Material -{ - [FieldOffset(0)] - public Vector3 Albedo; - - [FieldOffset(12)] - public float Emission; - - [FieldOffset(16)] - public float Metallic; - - [FieldOffset(20)] - public float Roughness; -} -``` - -## Renderer Base Class (Renderer.cs) - -```csharp -internal abstract class Renderer : IDisposable -{ - public Texture Color { get; private set; } - public Texture DepthStencil { get; private set; } - public FrameBuffer FrameBuffer { get; private set; } - - abstract void Update(CameraHandler camera); - abstract void Render(CommandBuffer commandBuffer); - virtual void Resize(uint width, uint height); // recreates Color + DepthStencil + FrameBuffer - virtual void Dispose(); // disposes FrameBuffer + DepthStencil + Color -} -``` - -- Constructor calls `Resize(App.Width, App.Height)` to create initial resources -- `Resize()`: Disposes then recreates `Color` (B8G8R8A8UNorm, RenderTarget | ShaderResource | UnorderedAccess), `DepthStencil` (D32FloatS8UInt), and `FrameBuffer` -- `Color` texture is used by path tracer as compute output target, and displayed via ImGui `AddImage` in App.cs -- Subclasses call `base.Resize()` / `base.Dispose()` to manage these shared resources - -## PathTracingRenderer Details - -### Overview - -- **Pipeline**: `ComputePipeline` with `[numthreads(16,16,1)]`, entry point `CSMain` -- **Shader**: Inline Slang raw string literal -- **Algorithm**: 8-bounce path tracing + NEE (Next Event Estimation) + Cook-Torrance PBR BRDF + Russian roulette (bounce ≥ 2) -- **Accumulation**: R32G32B32A32Float UAV texture, progressive average with jittered subpixel sampling -- **Tonemapping**: ACES filmic + gamma correction -- **Environment**: Gradient sky light on ray miss (warm ground → cool sky) -- **Output**: Tonemapped + gamma-corrected to `Color` texture (base class, B8G8R8A8UNorm) - -### Shader Structs - -```slang -struct Vertex // StructuredBuffer — private float4 + property -struct Material // StructuredBuffer — private float4 + property -struct CameraParams // ConstantBuffer — float4x4 × 2, private float4 Position, uint × 3 + padding -``` - -### Resource Bindings (7 slots) - -| Index | Type | Shader Variable | Description | -|-------|------|-----------------|-------------| -| 0 | AccelerationStructure | `scene` | TLAS for ray queries | -| 1 | ConstantBuffer | `camera` | CameraParams (160B) | -| 2 | StructuredBuffer | `vertices` | Vertex[] (32B stride) | -| 3 | StructuredBuffer | `indices` | uint[] | -| 4 | StructuredBuffer | `materials` | Material[] (32B stride) | -| 5 | TextureReadWrite | `accumTexture` | R32G32B32A32Float accumulation buffer | -| 6 | TextureReadWrite | `outputTexture` | Base class `Color` texture (compute write target) | - -### Shader Functions - -| Function | Purpose | -|----------|---------| -| `pcgHash(uint)` | PCG hash for RNG seed | -| `randomFloat(inout uint)` | Returns [0,1) float, advances seed | -| `cosineSampleHemisphere(float3, inout uint)` | Cosine-weighted hemisphere sampling around normal | -| `traceShadowRay(float3, float3, float)` | Shadow ray test using `RayQuery` | -| `sampleLightDirect(float3, float3, float3, Material, inout uint)` | NEE: uniform sample on ceiling light quad, Cook-Torrance BRDF, geometry term | -| `DistributionGGX(float, float)` | GGX normal distribution function | -| `GeometrySchlickGGX(float, float)` | Schlick-GGX geometry sub-function | -| `GeometrySmith(float, float, float)` | Smith geometry function (combined) | -| `FresnelSchlick(float, float3)` | Schlick Fresnel approximation | -| `evaluateBRDF(float3, float3, float3, Material)` | Full Cook-Torrance BRDF evaluation (diffuse + specular) | -| `sampleGGXHalfVector(float3, float, inout uint)` | GGX importance sampling for specular half-vector | -| `tracePath(float3, float3, inout uint)` | Main path tracing loop (8 bounces max) | -| `CSMain(uint3)` | Entry point: generate ray, trace, accumulate, ACES tonemap, gamma-correct | - -### Path Tracing Algorithm (`tracePath`) - -1. For each bounce (max 8): - - Trace primary ray via `RayQuery` - - On miss → add environment sky contribution (`lerp` warm ground to cool sky based on `direction.y`) → break - - Compute hit position, interpolate normal via barycentric weights from index/vertex buffers - - Flip normal if back-facing (`dot(normal, direction) > 0`) - - If emissive material: accumulate emission on bounce 0 only, then break (prevents double-counting with NEE) - - Non-emissive: add NEE contribution via `sampleLightDirect()` using Cook-Torrance BRDF - - Probabilistic BRDF sampling: compute `specProb` from F0 and metallic, then: - - With probability `specProb`: GGX importance sample half-vector → reflect → specular throughput (clamped to 10) - - Otherwise: cosine-weighted hemisphere → diffuse throughput - - Russian roulette (bounce ≥ 2): survival probability = max component of throughput - - Per-sample radiance clamped to 30 to suppress fireflies - -### Light Constants (hardcoded) - -```slang -static const float3 LightMin = float3(213.0, 548.6, 227.0); -static const float3 LightMax = float3(343.0, 548.6, 332.0); -static const float LightArea = 13650.0; // (343-213) * (332-227) -static const float3 LightNormal = float3(0.0, -1.0, 0.0); -``` - -### Camera Ray Generation (`CSMain`) - -1. RNG seed: `pcgHash(pixel.x + pixel.y * Width + FrameCount * Width * Height)` -2. Jittered subpixel offset → UV → NDC (y-flipped) -3. `InvProjection` → local direction → `InvView` → world direction -4. Origin = `camera.Position` - -### Accumulation - -- `FrameCount == 0`: overwrite `accumTexture` with new sample -- `FrameCount > 0`: add to existing accumulation -- Running average: `accumulated.rgb / (FrameCount + 1)` -- ACES tonemapping: `saturate((x * (2.51x + 0.03)) / (x * (2.43x + 0.59) + 0.14))` -- Gamma correction: `pow(avg, 1/2.2)` → `outputTexture` - -### Camera Change Detection - -```csharp -if (view != lastView || projection != lastProjection) -{ - lastView = view; - lastProjection = projection; - FrameCount = 0; // reset accumulation -} -``` - -### C# Side CameraParams (160B) - -```csharp -[StructLayout(LayoutKind.Explicit, Size = 160)] -file struct CameraParams -{ - [FieldOffset(0)] public Matrix4x4 InvView; - [FieldOffset(64)] public Matrix4x4 InvProjection; - [FieldOffset(128)] public Vector3 Position; - [FieldOffset(144)] public uint FrameCount; - [FieldOffset(148)] public uint Width; - [FieldOffset(152)] public uint Height; -} -``` - -### Buffer Setup - -- `vertexBuffer`: ShaderResource | AccelerationStructure -- `indexBuffer`: ShaderResource | AccelerationStructure -- `materialBuffer`: ShaderResource -- `cameraBuffer`: Constant | MapWrite (per-frame upload) -- Acceleration structures: single BLAS (all geometry, PreferFastTrace) → single TLAS (one instance) - -### Resize / Lifecycle - -- `Resize()`: calls `base.Resize()`, disposes `resourceTable` + `accumulationTexture`, set to null for lazy rebuild -- `Render()`: if resources are null → create `accumulationTexture` (R32G32B32A32Float) + `resourceTable`, reset `FrameCount` -- `Dispatch()`: `ceil(Width/16) × ceil(Height/16) × 1` -- `FrameCount++` after each dispatch - -### Dispose Order - -``` -base (FrameBuffer + DepthStencil + Color) -→ resourceTable → accumulationTexture -→ pipeline → resourceLayout -→ tlas → blas -→ cameraBuffer → materialBuffer → indexBuffer → vertexBuffer -``` - -## RasterizationRenderer Details - -### Overview - -- **Pipeline**: `GraphicsPipeline` with Vertex + Pixel shaders, entry points `VSMain` / `PSMain` -- **Shader**: Inline Slang raw string literal -- **Algorithm**: Blinn-Phong shading with point light, hemisphere ambient, ACES tonemapping -- **Output**: Renders directly to `FrameBuffer` (base class) via render pass - -### Shader Structs - -```slang -struct Material // StructuredBuffer — private float4 + property -struct RasterConstants // ConstantBuffer — float4x4 × 3, private float4 × 3 + properties -struct VSInput // Vertex input — private float4 × 2 + properties (Position, Normal, MaterialID) -struct PSInput // Interpolated — float4 Position, float3 WorldPos, float3 Normal, nointerpolation uint MaterialID -``` - -### Resource Bindings (2 slots) - -| Index | Type | Shader Variable | Stages | Description | -|-------|------|-----------------|--------|-------------| -| 0 | ConstantBuffer | `cb` | Vertex + Pixel | RasterConstants (240B) | -| 1 | StructuredBuffer | `materials` | Pixel | Material[] (32B stride) | - -### Vertex Shader (`VSMain`) - -1. Transform position: `worldPos = mul(float4(input.Position, 1.0), cb.Model)` -2. Clip space: `mul(mul(worldPos, cb.View), cb.Projection)` -3. Pass world position, transformed normal, MaterialID to PSInput -4. MaterialID via `asuint(NormalAndMaterialID.w)` through VSInput property, passed as `nointerpolation uint` - -### Pixel Shader (`PSMain`) - -1. **Emissive check**: if `mat.Emission > 0` → Reinhard tonemapping: `color / (color + 1)` → gamma correct → return -2. **Blinn-Phong**: - - Hemisphere ambient: `albedo * lerp(0.06, 0.15, N.y * 0.5 + 0.5)` (brighter on upward-facing surfaces) - - Diffuse: `albedo * lightColor * NdotL * atten` - - Specular: `lightColor * pow(NdotH, 64) * atten * 0.1` - - Distance attenuation: `1 / (1 + 0.000005 * dist²)` -3. ACES tonemapping: `saturate((x * (2.51x + 0.03)) / (x * (2.43x + 0.59) + 0.14))` -4. Gamma correction: `pow(color, 1/2.2)` - -### Light Configuration - -- Point light position: (278, 548, 280) -- Light color: (2.0, 1.8, 1.4) -- Ambient factor: hemisphere-based (0.06 bottom to 0.15 top) -- Specular exponent: 64, weight: 0.1 - -### C# Side RasterConstants (240B) - -```csharp -[StructLayout(LayoutKind.Explicit, Size = 240)] -file struct RasterConstants -{ - [FieldOffset(0)] public Matrix4x4 Model; // Identity - [FieldOffset(64)] public Matrix4x4 View; - [FieldOffset(128)] public Matrix4x4 Projection; - [FieldOffset(192)] public Vector3 LightPos; // maps to private float4 LightPosAndPadding - [FieldOffset(208)] public Vector3 LightColor; // maps to private float4 LightColorAndPadding - [FieldOffset(224)] public Vector3 CameraPos; // maps to private float4 CameraPosAndPadding -} -``` - -### Pipeline Configuration - -- RasterizerState: `CullNone` (camera is inside the box) -- DepthStencilState: `Default` -- BlendState: `Opaque` -- PrimitiveTopology: `TriangleList` -- InputLayout: Float4 (POSITION) + Float4 (NORMAL), stride = 32 - -### Buffer Setup - -- `vertexBuffer`: Vertex only -- `indexBuffer`: Index only -- `materialBuffer`: ShaderResource -- `constantBuffer`: Constant | MapWrite (per-frame upload) -- `resourceTable`: created once in constructor (no size-dependent resources) - -### Render Pass - -```csharp -cmd.BeginRenderPass(FrameBuffer, clearValues, resourceTable); -cmd.SetPipeline(pipeline); -cmd.SetResourceTable(resourceTable); -cmd.SetVertexBuffer(vertexBuffer, 0, 0); -cmd.SetIndexBuffer(indexBuffer, 0, IndexFormat.UInt32); -cmd.DrawIndexed(indexCount, 1, 0, 0, 0); -cmd.EndRenderPass(); -``` - -Clear values: color (0.51, 0.518, 0.557, 1) matching environment sky, depth 1.0, stencil 0, ClearFlags.All - -### Resize / Lifecycle - -- `Resize()`: only `base.Resize()` — no renderer-specific size-dependent resources -- No `accumulationTexture`, no `resourceTable` rebuild needed - -### Dispose Order - -``` -base (FrameBuffer + DepthStencil + Color) -→ pipeline → resourceTable → resourceLayout -→ constantBuffer → materialBuffer → indexBuffer → vertexBuffer -``` - -## DirectX12 Specific Notes - -- `BindingHelper` assigns DirectX12 register indices by type: CBV(b), SRV(t), UAV(u), Sampler(s) independently numbered -- Metal uses argument buffer index; Vulkan numbers all bindings sequentially - -## C# Code Style - -- Prefer `is` / `is not` pattern matching over `==` / `!=` for comparisons -- Use full `using` imports, short type names in code (no `Namespace.Type` inline references) -- `file struct` for shader-mirrored GPU structs (scoped to the file that uses them) -- Shaders inlined as `const string ShaderSource = """...""";` (raw string literal) -- Collection expressions: `[]` for empty, `[.. spread]` for conversion -- Target-typed `new()` for object initializers -- `static readonly` fields for framework objects created once -- Blank line between each field / property / method diff --git a/sources/Experiments/CornellBox/Handlers/CameraHandler.cs b/sources/Experiments/CornellBox/Handlers/CameraHandler.cs index 769c4cdb..e4d15527 100644 --- a/sources/Experiments/CornellBox/Handlers/CameraHandler.cs +++ b/sources/Experiments/CornellBox/Handlers/CameraHandler.cs @@ -12,9 +12,9 @@ internal class CameraHandler public CameraHandler(IInputContext input, Matrix4x4 initial) { IMouse mouse = input.Mice[0]; - mouse.MouseDown += Mouse_MouseDown; - mouse.MouseUp += Mouse_MouseUp; - mouse.MouseMove += Mouse_MouseMove; + mouse.MouseDown += OnMouseDown; + mouse.MouseUp += OnMouseUp; + mouse.MouseMove += OnMouseMove; IKeyboard keyboard = input.Keyboards[0]; keyboard.KeyDown += OnKeyDown; @@ -86,7 +86,7 @@ public void Update(double delta, uint width, uint height) } } - private void Mouse_MouseDown(IMouse arg1, MouseButton arg2) + private void OnMouseDown(IMouse arg1, MouseButton arg2) { if (arg2 is MouseButton.Right) { @@ -94,7 +94,7 @@ private void Mouse_MouseDown(IMouse arg1, MouseButton arg2) } } - private void Mouse_MouseUp(IMouse arg1, MouseButton arg2) + private void OnMouseUp(IMouse arg1, MouseButton arg2) { if (arg2 is MouseButton.Right) { @@ -102,7 +102,7 @@ private void Mouse_MouseUp(IMouse arg1, MouseButton arg2) } } - private void Mouse_MouseMove(IMouse mouse, Vector2 vector) + private void OnMouseMove(IMouse mouse, Vector2 vector) { const float clipRadians = 89.0f * MathF.PI / 180.0f; diff --git a/sources/Experiments/CornellBox/Helpers/BindingHelper.cs b/sources/Experiments/CornellBox/Helpers/BindingHelper.cs index a4b30688..bcfd50ef 100644 --- a/sources/Experiments/CornellBox/Helpers/BindingHelper.cs +++ b/sources/Experiments/CornellBox/Helpers/BindingHelper.cs @@ -41,17 +41,6 @@ ResourceType.StructuredBufferReadWrite or } break; - case Backend.Vulkan: - { - for (int i = 0; i < bindings.Length; i++) - { - ref ResourceBinding binding = ref bindings[i]; - - binding = binding with { Index = (uint)i }; - } - } - break; - case Backend.Metal: { uint bufferIndex = 0; @@ -82,6 +71,17 @@ ResourceType.Texture or } } break; + + case Backend.Vulkan: + { + for (int i = 0; i < bindings.Length; i++) + { + ref ResourceBinding binding = ref bindings[i]; + + binding = binding with { Index = (uint)i }; + } + } + break; } return bindings; diff --git a/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs b/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs index 2291e636..0d378131 100644 --- a/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs +++ b/sources/Experiments/CornellBox/Helpers/CornellBoxGeometry.cs @@ -72,10 +72,10 @@ public static void Create(out Vertex[] vertices, out uint[] indices, out Materia // 15: Light AddQuad(verticesList, indicesList, - new(343.0f, 548.6f, 227.0f), - new(343.0f, 548.6f, 332.0f), - new(213.0f, 548.6f, 332.0f), - new(213.0f, 548.6f, 227.0f), + new(343.0f, 547.0f, 227.0f), + new(343.0f, 547.0f, 332.0f), + new(213.0f, 547.0f, 332.0f), + new(213.0f, 547.0f, 227.0f), 3); vertices = [.. verticesList]; diff --git a/sources/Experiments/CornellBox/Helpers/Extensions.cs b/sources/Experiments/CornellBox/Helpers/Extensions.cs deleted file mode 100644 index 1ae53d40..00000000 --- a/sources/Experiments/CornellBox/Helpers/Extensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Hexa.NET.ImGui; - -namespace CornellBox.Helpers; - -internal static class Extensions -{ - private const ImGuiWindowFlags OverlayFlags = ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.AlwaysAutoResize - | ImGuiWindowFlags.NoSavedSettings - | ImGuiWindowFlags.NoFocusOnAppearing - | ImGuiWindowFlags.NoInputs - | ImGuiWindowFlags.NoMove; - - extension(ImGui) - { - public static void Overlay(string name, Action action) - { - ImGui.SetNextWindowPos(new(10, 10), ImGuiCond.Always, new(0, 0)); - ImGui.SetNextWindowBgAlpha(0.35f); - - if (ImGui.Begin(name, OverlayFlags)) - { - action(); - - ImGui.End(); - } - } - } -} diff --git a/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs b/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs index 161535ca..d75ec775 100644 --- a/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs +++ b/sources/Experiments/CornellBox/Renderers/PathTracingRenderer.cs @@ -12,405 +12,6 @@ internal unsafe class PathTracingRenderer : Renderer { private const uint ThreadGroupSize = 16; - private const string ShaderSource = """ - struct Vertex - { - private float4 PositionAndPadding; - - private float4 NormalAndMaterialID; - - property float3 Position { get { return PositionAndPadding.xyz; } } - - property float3 Normal { get { return NormalAndMaterialID.xyz; } } - - property uint MaterialID { get { return asuint(NormalAndMaterialID.w); } } - }; - - struct Material - { - private float4 AlbedoAndEmission; - - float Metallic; - - float Roughness; - - private float padding0; - - private float padding1; - - property float3 Albedo { get { return AlbedoAndEmission.xyz; } } - - property float Emission { get { return AlbedoAndEmission.w; } } - }; - - struct CameraParams - { - float4x4 InvView; - - float4x4 InvProjection; - - private float4 PositionAndPadding; - - uint FrameCount; - - uint Width; - - uint Height; - - private float padding0; - - property float3 Position { get { return PositionAndPadding.xyz; } } - }; - - RaytracingAccelerationStructure scene; - ConstantBuffer camera; - StructuredBuffer vertices; - StructuredBuffer indices; - StructuredBuffer materials; - RWTexture2D accumTexture; - RWTexture2D outputTexture; - - // Light geometry constants (hardcoded ceiling light quad) - static const float3 LightMin = float3(213.0, 548.6, 227.0); - static const float3 LightMax = float3(343.0, 548.6, 332.0); - static const float LightArea = (343.0 - 213.0) * (332.0 - 227.0); - static const float3 LightNormal = float3(0.0, -1.0, 0.0); - static const float PI = 3.14159265; - - float DistributionGGX(float NdotH, float roughness) - { - float a = roughness * roughness; - float a2 = a * a; - float denom = NdotH * NdotH * (a2 - 1.0) + 1.0; - return a2 / (PI * denom * denom); - } - - float GeometrySchlickGGX(float NdotX, float roughness) - { - float r = roughness + 1.0; - float k = (r * r) / 8.0; - return NdotX / (NdotX * (1.0 - k) + k); - } - - float GeometrySmith(float NdotV, float NdotL, float roughness) - { - return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); - } - - float3 FresnelSchlick(float cosTheta, float3 F0) - { - return F0 + (1.0 - F0) * pow(saturate(1.0 - cosTheta), 5.0); - } - - float3 evaluateBRDF(float3 N, float3 V, float3 L, Material mat) - { - float roughness = max(mat.Roughness, 0.04); - float NdotL = max(dot(N, L), 0.0); - float NdotV = max(dot(N, V), 0.001); - float3 H = normalize(V + L); - float NdotH = max(dot(N, H), 0.0); - float HdotV = max(dot(H, V), 0.0); - - float3 F0 = lerp(float3(0.04, 0.04, 0.04), mat.Albedo, mat.Metallic); - - float D = DistributionGGX(NdotH, roughness); - float G = GeometrySmith(NdotV, NdotL, roughness); - float3 F = FresnelSchlick(HdotV, F0); - - float3 specular = D * G * F / (4.0 * NdotV * NdotL + 0.0001); - - float3 kD = (1.0 - F) * (1.0 - mat.Metallic); - float3 diffuse = kD * mat.Albedo / PI; - - return diffuse + specular; - } - - float3 sampleGGXHalfVector(float3 N, float roughness, inout uint seed) - { - float a = roughness * roughness; - - float r1 = randomFloat(seed); - float r2 = randomFloat(seed); - - float phi = 2.0 * PI * r1; - float cosTheta = sqrt((1.0 - r2) / (1.0 + (a * a - 1.0) * r2)); - float sinTheta = sqrt(1.0 - cosTheta * cosTheta); - - float3 w = N; - float3 helper = abs(w.x) > 0.99 ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0); - float3 u = normalize(cross(helper, w)); - float3 v = cross(w, u); - - return normalize(u * cos(phi) * sinTheta + v * sin(phi) * sinTheta + w * cosTheta); - } - - uint pcgHash(uint input) - { - uint state = input * 747796405u + 2891336453u; - uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u; - return (word >> 22u) ^ word; - } - - float randomFloat(inout uint seed) - { - seed = pcgHash(seed); - return float(seed) / 4294967295.0; - } - - float3 cosineSampleHemisphere(float3 normal, inout uint seed) - { - float r1 = randomFloat(seed); - float r2 = randomFloat(seed); - - float phi = 2.0 * 3.14159265 * r1; - float sinTheta = sqrt(r2); - float cosTheta = sqrt(1.0 - r2); - - float3 w = normal; - float3 helper = abs(w.x) > 0.99 ? float3(0.0, 1.0, 0.0) : float3(1.0, 0.0, 0.0); - float3 u = normalize(cross(helper, w)); - float3 v = cross(w, u); - - return normalize(u * cos(phi) * sinTheta + v * sin(phi) * sinTheta + w * cosTheta); - } - - bool traceShadowRay(float3 origin, float3 direction, float maxDist) - { - RayDesc shadowRay; - shadowRay.Origin = origin; - shadowRay.Direction = direction; - shadowRay.TMin = 0.001; - shadowRay.TMax = maxDist - 0.001; - - RayQuery shadowQuery; - shadowQuery.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, shadowRay); - - while (shadowQuery.Proceed()) {} - - return shadowQuery.CommittedStatus() != COMMITTED_NOTHING; - } - - float3 sampleLightDirect(float3 hitPos, float3 hitNormal, float3 V, Material mat, inout uint rng) - { - float u = randomFloat(rng); - float v = randomFloat(rng); - - float3 lightPoint = float3( - lerp(LightMin.x, LightMax.x, u), - LightMin.y, - lerp(LightMin.z, LightMax.z, v) - ); - - float3 toLight = lightPoint - hitPos; - float dist = length(toLight); - float3 L = toLight / dist; - - float NdotL = dot(hitNormal, L); - if (NdotL <= 0.0) - { - return float3(0.0, 0.0, 0.0); - } - - float lightCosine = max(dot(LightNormal, -L), 0.0); - if (lightCosine <= 0.0) - { - return float3(0.0, 0.0, 0.0); - } - - if (traceShadowRay(hitPos + hitNormal * 0.001, L, dist)) - { - return float3(0.0, 0.0, 0.0); - } - - Material lightMat = materials[3]; - float3 lightEmission = lightMat.Albedo * lightMat.Emission; - - float pdf = 1.0 / LightArea; - float3 brdf = evaluateBRDF(hitNormal, V, L, mat); - float geometryTerm = NdotL * lightCosine / (dist * dist); - - return lightEmission * brdf * geometryTerm / pdf; - } - - float3 tracePath(float3 origin, float3 direction, inout uint rng) - { - float3 throughput = float3(1.0, 1.0, 1.0); - float3 radiance = float3(0.0, 0.0, 0.0); - - for (int bounce = 0; bounce < 8; bounce++) - { - RayDesc ray; - ray.Origin = origin; - ray.Direction = direction; - ray.TMin = 0.001; - ray.TMax = 100000.0; - - RayQuery query; - query.TraceRayInline(scene, RAY_FLAG_NONE, 0xFF, ray); - - while (query.Proceed()) {} - - if (query.CommittedStatus() == COMMITTED_NOTHING) - { - float3 skyColor = lerp(float3(0.15, 0.12, 0.10), float3(0.30, 0.35, 0.45), saturate(direction.y * 0.5 + 0.5)); - radiance += throughput * skyColor; - break; - } - - uint primIdx = query.CommittedPrimitiveIndex(); - float2 bary = query.CommittedTriangleBarycentrics(); - float t = query.CommittedRayT(); - float3 hitPos = origin + direction * t; - - uint i0 = indices[primIdx * 3 + 0]; - uint i1 = indices[primIdx * 3 + 1]; - uint i2 = indices[primIdx * 3 + 2]; - - Vertex v0 = vertices[i0]; - Vertex v1 = vertices[i1]; - Vertex v2 = vertices[i2]; - - float3 baryWeights = float3(1.0 - bary.x - bary.y, bary.x, bary.y); - float3 normal = normalize( - v0.Normal * baryWeights.x + - v1.Normal * baryWeights.y + - v2.Normal * baryWeights.z - ); - - if (dot(normal, direction) > 0.0) - { - normal = -normal; - } - - Material mat = materials[v0.MaterialID]; - - if (mat.Emission > 0.0) - { - if (bounce == 0) - { - radiance += throughput * mat.Albedo * mat.Emission; - } - - break; - } - - float3 V = -direction; - - radiance += throughput * sampleLightDirect(hitPos, normal, V, mat, rng); - - float3 F0 = lerp(float3(0.04, 0.04, 0.04), mat.Albedo, mat.Metallic); - float roughness = max(mat.Roughness, 0.04); - float specWeight = max(F0.r, max(F0.g, F0.b)); - float diffWeight = (1.0 - specWeight) * (1.0 - mat.Metallic); - float total = specWeight + diffWeight; - float specProb = specWeight / total; - - float3 newDir; - float NdotV = max(dot(normal, V), 0.001); - - if (randomFloat(rng) < specProb) - { - float3 H = sampleGGXHalfVector(normal, roughness, rng); - newDir = reflect(-V, H); - - float NdotL = dot(normal, newDir); - if (NdotL <= 0.0) - { - break; - } - - float NdotH = max(dot(normal, H), 0.0); - float HdotV = max(dot(H, V), 0.0); - - float3 F = FresnelSchlick(HdotV, F0); - float G = GeometrySmith(NdotV, NdotL, roughness); - - float3 specThroughput = F * G * HdotV / (NdotV * NdotH * specProb + 0.0001); - throughput *= min(specThroughput, float3(10.0, 10.0, 10.0)); - } - else - { - newDir = cosineSampleHemisphere(normal, rng); - - float3 H = normalize(V + newDir); - float HdotV = max(dot(H, V), 0.0); - - float3 F = FresnelSchlick(HdotV, F0); - float3 kD = (1.0 - F) * (1.0 - mat.Metallic); - - throughput *= kD * mat.Albedo / (1.0 - specProb + 0.0001); - } - - origin = hitPos + normal * 0.001; - direction = newDir; - - if (bounce >= 2) - { - float p = max(throughput.r, max(throughput.g, throughput.b)); - - if (randomFloat(rng) > p) - { - break; - } - - throughput /= p; - } - } - - return radiance; - } - - [numthreads(16, 16, 1)] - void CSMain(uint3 dispatchThreadID : SV_DispatchThreadID) - { - uint2 pixel = dispatchThreadID.xy; - - if (pixel.x >= camera.Width || pixel.y >= camera.Height) - { - return; - } - - uint rng = pcgHash(pixel.x + pixel.y * camera.Width + camera.FrameCount * camera.Width * camera.Height); - - float2 jitter = float2(randomFloat(rng), randomFloat(rng)); - float2 uv = (float2(pixel) + jitter) / float2(camera.Width, camera.Height); - float2 ndc = uv * 2.0 - 1.0; - ndc.y = -ndc.y; - - float4 target = mul(float4(ndc, 1.0, 1.0), camera.InvProjection); - float3 localDir = normalize(target.xyz / target.w); - float3 direction = normalize(mul(float4(localDir, 0.0), camera.InvView).xyz); - float3 origin = camera.Position; - - float3 color = tracePath(origin, direction, rng); - color = min(color, float3(30.0, 30.0, 30.0)); - - float4 prev = accumTexture[pixel]; - float4 accumulated; - - if (camera.FrameCount == 0) - { - accumulated = float4(color, 1.0); - } - else - { - accumulated = prev + float4(color, 1.0); - } - - accumTexture[pixel] = accumulated; - - float3 avg = accumulated.rgb / float(camera.FrameCount + 1); - - // ACES tonemapping - float3 a = avg * (avg * 2.51 + 0.03); - float3 b = avg * (avg * 2.43 + 0.59) + 0.14; - avg = saturate(a / b); - - avg = pow(avg, 1.0 / 2.2); - outputTexture[pixel] = float4(avg, 1.0); - } - """; - private readonly Buffer vertexBuffer; private readonly Buffer indexBuffer; private readonly Buffer materialBuffer; @@ -519,7 +120,7 @@ public PathTracingRenderer() ) }); - using Shader computeShader = App.Context.LoadShaderFromSource(ShaderSource, "CSMain", ShaderStageFlags.Compute); + using Shader computeShader = App.Context.LoadShaderFromFile(ShaderPath("PathTracing.slang"), "CSMain", ShaderStageFlags.Compute); pipeline = App.Context.CreateComputePipeline(new() { diff --git a/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs b/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs index 5bb33d23..ce9a8cc4 100644 --- a/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs +++ b/sources/Experiments/CornellBox/Renderers/RasterizationRenderer.cs @@ -10,126 +10,6 @@ namespace CornellBox.Renderers; internal unsafe class RasterizationRenderer : Renderer { - private const string ShaderSource = """ - struct Material - { - private float4 AlbedoAndEmission; - - float Metallic; - - float Roughness; - - private float padding0; - - private float padding1; - - property float3 Albedo { get { return AlbedoAndEmission.xyz; } } - - property float Emission { get { return AlbedoAndEmission.w; } } - }; - - struct RasterConstants - { - float4x4 Model; - - float4x4 View; - - float4x4 Projection; - - private float4 LightPosAndPadding; - - private float4 LightColorAndPadding; - - private float4 CameraPosAndPadding; - - property float3 LightPos { get { return LightPosAndPadding.xyz; } } - - property float3 LightColor { get { return LightColorAndPadding.xyz; } } - - property float3 CameraPos { get { return CameraPosAndPadding.xyz; } } - }; - - ConstantBuffer cb; - StructuredBuffer materials; - - struct VSInput - { - private float4 PositionAndPadding : POSITION0; - - private float4 NormalAndMaterialID : NORMAL0; - - property float3 Position { get { return PositionAndPadding.xyz; } } - - property float3 Normal { get { return NormalAndMaterialID.xyz; } } - - property uint MaterialID { get { return asuint(NormalAndMaterialID.w); } } - }; - - struct PSInput - { - float4 Position : SV_POSITION; - - float3 WorldPos : TEXCOORD0; - - float3 Normal : TEXCOORD1; - - nointerpolation uint MaterialID : TEXCOORD2; - }; - - PSInput VSMain(VSInput input) - { - float4 worldPos = mul(float4(input.Position, 1.0), cb.Model); - - PSInput output; - output.Position = mul(mul(worldPos, cb.View), cb.Projection); - output.WorldPos = worldPos.xyz; - output.Normal = normalize(mul(float4(input.Normal, 0.0), cb.Model).xyz); - output.MaterialID = input.MaterialID; - - return output; - } - - float4 PSMain(PSInput input) : SV_TARGET - { - Material mat = materials[input.MaterialID]; - - if (mat.Emission > 0.0) - { - float3 emissive = mat.Albedo * mat.Emission; - float3 mapped = emissive / (emissive + 1.0); - return float4(pow(mapped, 1.0 / 2.2), 1.0); - } - - float3 N = normalize(input.Normal); - float3 worldPos = input.WorldPos; - float3 L = normalize(cb.LightPos - worldPos); - float3 V = normalize(cb.CameraPos - worldPos); - float3 H = normalize(L + V); - - float NdotL = max(dot(N, L), 0.0); - float NdotH = max(dot(N, H), 0.0); - float spec = pow(NdotH, 64.0); - - float dist = length(cb.LightPos - worldPos); - float atten = 1.0 / (1.0 + 0.000005 * dist * dist); - - float hemiFactor = N.y * 0.5 + 0.5; - float3 ambient = mat.Albedo * lerp(0.06, 0.15, hemiFactor); - float3 diffuse = mat.Albedo * cb.LightColor * NdotL * atten; - float3 specular = cb.LightColor * spec * atten * 0.1; - - float3 color = ambient + diffuse + specular; - - // ACES tonemapping - float3 a = color * (color * 2.51 + 0.03); - float3 b = color * (color * 2.43 + 0.59) + 0.14; - color = saturate(a / b); - - color = pow(color, 1.0 / 2.2); - return float4(color, 1.0); - } - """; - private readonly Buffer vertexBuffer; private readonly Buffer indexBuffer; private readonly Buffer materialBuffer; @@ -195,8 +75,8 @@ public RasterizationRenderer() inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Position }); inputLayout.Add(new() { Format = ElementFormat.Float4, Semantic = ElementSemantic.Normal }); - using Shader vertexShader = App.Context.LoadShaderFromSource(ShaderSource, "VSMain", ShaderStageFlags.Vertex); - using Shader pixelShader = App.Context.LoadShaderFromSource(ShaderSource, "PSMain", ShaderStageFlags.Pixel); + using Shader vertexShader = App.Context.LoadShaderFromFile(ShaderPath("Rasterization.slang"), "VSMain", ShaderStageFlags.Vertex); + using Shader pixelShader = App.Context.LoadShaderFromFile(ShaderPath("Rasterization.slang"), "PSMain", ShaderStageFlags.Pixel); pipeline = App.Context.CreateGraphicsPipeline(new() { @@ -222,7 +102,7 @@ public override void Update(CameraHandler camera) Model = Matrix4x4.Identity, View = camera.View, Projection = camera.Projection, - LightPos = new(278.0f, 548.0f, 280.0f), + LightPos = new(278.0f, 547.0f, 280.0f), LightColor = new(2.0f, 1.8f, 1.4f), CameraPos = camera.Position }], 0); diff --git a/sources/Experiments/CornellBox/Renderers/Renderer.cs b/sources/Experiments/CornellBox/Renderers/Renderer.cs index 9558e16a..4dd9bd99 100644 --- a/sources/Experiments/CornellBox/Renderers/Renderer.cs +++ b/sources/Experiments/CornellBox/Renderers/Renderer.cs @@ -65,4 +65,9 @@ public virtual void Dispose() DepthStencil.Dispose(); Color.Dispose(); } + + protected static string ShaderPath(params string[] paths) + { + return Path.Combine([AppContext.BaseDirectory, "Assets", "Shaders", .. paths]); + } } diff --git a/sources/NuGet.Packaging.props b/sources/NuGet.Packaging.props index cf795345..116220c4 100644 --- a/sources/NuGet.Packaging.props +++ b/sources/NuGet.Packaging.props @@ -7,7 +7,7 @@ $(MSBuildThisFileDirectory)..\.nuget - 0.0.7 + 0.0.8 qian-o Copyright (c) 2026 qian-o Zenith.NET is a modern, cross-platform graphics and compute library for .NET. It provides a unified GPU programming interface supporting DirectX12, Metal, and Vulkan backends. diff --git a/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs b/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs index a53d0575..cdedcc74 100644 --- a/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs +++ b/sources/Zenith.NET.DirectX12/DXBottomLevelAccelerationStructure.cs @@ -21,7 +21,7 @@ public DXBottomLevelAccelerationStructure(DXGraphicsContext context, BottomLevel MappedMemory mappedMemory = TransformBuffer.Map(); - desc.Geometries.Select(static item => *(Matrix3X4*)&item.Triangles.Transform).ToArray().CopyTo(new Span>((Matrix3X4*)mappedMemory.Pointer, (int)geometryCount)); + desc.Geometries.Select(static item => DXFormats.DirectX12(item.Triangles.Transform)).ToArray().CopyTo(new Span>((Matrix3X4*)mappedMemory.Pointer, (int)geometryCount)); TransformBuffer.Unmap(); diff --git a/sources/Zenith.NET.DirectX12/DXFormats.cs b/sources/Zenith.NET.DirectX12/DXFormats.cs index 13a0c234..eba4d384 100644 --- a/sources/Zenith.NET.DirectX12/DXFormats.cs +++ b/sources/Zenith.NET.DirectX12/DXFormats.cs @@ -1,10 +1,12 @@ -using Silk.NET.Core.Native; +using System.Numerics; +using Silk.NET.Core.Native; using Silk.NET.Direct3D12; using Silk.NET.DXGI; +using Silk.NET.Maths; namespace Zenith.NET.DirectX12; -internal static class DXFormats +internal static unsafe class DXFormats { public static (ResourceFlags Flags, ResourceStates States, HeapType Type) DirectX12(BufferUsageFlags bufferUsageFlags) { @@ -533,6 +535,30 @@ QueryType.Occlusion or ); } + public static Matrix3X4 DirectX12(Matrix4x4 matrix4x4) + { + Matrix3X4 result; + + float* pResult = (float*)&result; + + pResult[0] = matrix4x4.M11; + pResult[1] = matrix4x4.M21; + pResult[2] = matrix4x4.M31; + pResult[3] = matrix4x4.M41; + + pResult[4] = matrix4x4.M12; + pResult[5] = matrix4x4.M22; + pResult[6] = matrix4x4.M32; + pResult[7] = matrix4x4.M42; + + pResult[8] = matrix4x4.M13; + pResult[9] = matrix4x4.M23; + pResult[10] = matrix4x4.M33; + pResult[11] = matrix4x4.M43; + + return result; + } + public static RaytracingGeometryType DirectX12(RayTracingGeometryType rayTracingGeometryType) { return rayTracingGeometryType switch diff --git a/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs b/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs index ba557aad..1c21312e 100644 --- a/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs +++ b/sources/Zenith.NET.DirectX12/DXTopLevelAccelerationStructure.cs @@ -1,4 +1,5 @@ using Silk.NET.Direct3D12; +using Silk.NET.Maths; namespace Zenith.NET.DirectX12; @@ -136,7 +137,7 @@ private void FillInstanceBuffer(TopLevelAccelerationStructureDesc desc, out Buil AccelerationStructure = instance.AccelerationStructure.DirectX12().AccelerationStructureBuffer.GPUVirtualAddress }; - new ReadOnlySpan(&instance.Transform, 12).CopyTo(new(instances[i].Transform, 12)); + *(Matrix3X4*)instances[i].Transform = DXFormats.DirectX12(instance.Transform); } InstanceBuffer.Unmap(); diff --git a/sources/Zenith.NET.Metal/MTLBottomLevelAccelerationStructure.cs b/sources/Zenith.NET.Metal/MTLBottomLevelAccelerationStructure.cs index 18b2ff2e..03322d0f 100644 --- a/sources/Zenith.NET.Metal/MTLBottomLevelAccelerationStructure.cs +++ b/sources/Zenith.NET.Metal/MTLBottomLevelAccelerationStructure.cs @@ -19,7 +19,7 @@ public unsafe MTLBottomLevelAccelerationStructure(MTLGraphicsContext context, Bo MappedMemory mappedMemory = TransformBuffer.Map(); - desc.Geometries.Select(static item => *(MTLPackedFloat4x3*)&item.Triangles.Transform).ToArray().CopyTo(new Span((MTLPackedFloat4x3*)mappedMemory.Pointer, (int)geometryCount)); + desc.Geometries.Select(static item => MTLFormats.Metal(item.Triangles.Transform)).ToArray().CopyTo(new Span((MTLPackedFloat4x3*)mappedMemory.Pointer, (int)geometryCount)); TransformBuffer.Unmap(); diff --git a/sources/Zenith.NET.Metal/MTLCommandEncoder.cs b/sources/Zenith.NET.Metal/MTLCommandEncoder.cs index d3e86628..e0cc86ee 100644 --- a/sources/Zenith.NET.Metal/MTLCommandEncoder.cs +++ b/sources/Zenith.NET.Metal/MTLCommandEncoder.cs @@ -253,7 +253,7 @@ public void Bind() if (currentResourceTable is MTLResourceTable resourceTable) { - Render?.SetArgumentTable(resourceTable.ArgumentTable, MTLRenderStages.Object | MTLRenderStages.Mesh); + Render?.SetArgumentTable(resourceTable.ArgumentTable, MTLRenderStages.Object | MTLRenderStages.Mesh | MTLRenderStages.Fragment); } } break; diff --git a/sources/Zenith.NET.Metal/MTLFormats.cs b/sources/Zenith.NET.Metal/MTLFormats.cs index aff8fa59..56a7f965 100644 --- a/sources/Zenith.NET.Metal/MTLFormats.cs +++ b/sources/Zenith.NET.Metal/MTLFormats.cs @@ -1,8 +1,9 @@ -using Metal.NET; +using System.Numerics; +using Metal.NET; namespace Zenith.NET.Metal; -internal static class MTLFormats +internal static unsafe class MTLFormats { public static MTLResourceOptions Metal(BufferUsageFlags bufferUsageFlags) { @@ -452,6 +453,30 @@ public static MTLVisibilityResultMode Metal(QueryType queryType) }; } + public static MTLPackedFloat4x3 Metal(Matrix4x4 matrix4x4) + { + MTLPackedFloat4x3 result; + + float* pResult = (float*)&result; + + pResult[0] = matrix4x4.M11; + pResult[1] = matrix4x4.M21; + pResult[2] = matrix4x4.M31; + pResult[3] = matrix4x4.M41; + + pResult[4] = matrix4x4.M12; + pResult[5] = matrix4x4.M22; + pResult[6] = matrix4x4.M32; + pResult[7] = matrix4x4.M42; + + pResult[8] = matrix4x4.M13; + pResult[9] = matrix4x4.M23; + pResult[10] = matrix4x4.M33; + pResult[11] = matrix4x4.M43; + + return result; + } + public static MTLAccelerationStructureUsage Metal(AccelerationStructureBuildFlags accelerationStructureBuildFlags) { MTLAccelerationStructureUsage result = MTLAccelerationStructureUsage.None; diff --git a/sources/Zenith.NET.Metal/MTLTopLevelAccelerationStructure.cs b/sources/Zenith.NET.Metal/MTLTopLevelAccelerationStructure.cs index 4707a81a..04d703dd 100644 --- a/sources/Zenith.NET.Metal/MTLTopLevelAccelerationStructure.cs +++ b/sources/Zenith.NET.Metal/MTLTopLevelAccelerationStructure.cs @@ -95,7 +95,7 @@ private void FillInstanceBuffer(TopLevelAccelerationStructureDesc desc) instances[i] = new() { - TransformationMatrix = *(MTLPackedFloat4x3*)&instance.Transform, + TransformationMatrix = MTLFormats.Metal(instance.Transform), Options = MTLFormats.Metal(instance.Flags), Mask = instance.Mask, UserID = instance.ID, diff --git a/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs b/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs index 468a79a1..b29a9464 100644 --- a/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs +++ b/sources/Zenith.NET.Vulkan/VKBottomLevelAccelerationStructure.cs @@ -23,7 +23,7 @@ public VKBottomLevelAccelerationStructure(VKGraphicsContext context, BottomLevel MappedMemory mappedMemory = TransformBuffer.Map(); - desc.Geometries.Select(static item => *(TransformMatrixKHR*)&item.Triangles.Transform).ToArray().CopyTo(new Span((TransformMatrixKHR*)mappedMemory.Pointer, (int)geometryCount)); + desc.Geometries.Select(static item => VKFormats.Vulkan(item.Triangles.Transform)).ToArray().CopyTo(new Span((TransformMatrixKHR*)mappedMemory.Pointer, (int)geometryCount)); TransformBuffer.Unmap(); diff --git a/sources/Zenith.NET.Vulkan/VKFormats.cs b/sources/Zenith.NET.Vulkan/VKFormats.cs index 3eac604e..fc83082d 100644 --- a/sources/Zenith.NET.Vulkan/VKFormats.cs +++ b/sources/Zenith.NET.Vulkan/VKFormats.cs @@ -1,8 +1,9 @@ -using Silk.NET.Vulkan; +using System.Numerics; +using Silk.NET.Vulkan; namespace Zenith.NET.Vulkan; -internal static class VKFormats +internal static unsafe class VKFormats { public static VkShaderStageFlags Vulkan(ShaderStageFlags shaderStageFlags) { @@ -613,6 +614,30 @@ public static IndexType Vulkan(IndexFormat indexFormat) }; } + public static TransformMatrixKHR Vulkan(Matrix4x4 matrix4x4) + { + TransformMatrixKHR result; + + float* pResult = (float*)&result; + + pResult[0] = matrix4x4.M11; + pResult[1] = matrix4x4.M21; + pResult[2] = matrix4x4.M31; + pResult[3] = matrix4x4.M41; + + pResult[4] = matrix4x4.M12; + pResult[5] = matrix4x4.M22; + pResult[6] = matrix4x4.M32; + pResult[7] = matrix4x4.M42; + + pResult[8] = matrix4x4.M13; + pResult[9] = matrix4x4.M23; + pResult[10] = matrix4x4.M33; + pResult[11] = matrix4x4.M43; + + return result; + } + public static GeometryTypeKHR Vulkan(RayTracingGeometryType rayTracingGeometryType) { return rayTracingGeometryType switch diff --git a/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs b/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs index 04994252..4139bbd1 100644 --- a/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs +++ b/sources/Zenith.NET.Vulkan/VKTopLevelAccelerationStructure.cs @@ -179,7 +179,7 @@ private void FillInstanceBuffer(TopLevelAccelerationStructureDesc desc, out Acce instances[i] = new() { - Transform = *(TransformMatrixKHR*)&instance.Transform, + Transform = VKFormats.Vulkan(instance.Transform), InstanceCustomIndex = instance.ID, Mask = instance.Mask, Flags = VKFormats.Vulkan(instance.Flags),