feat: Step 4 load .obj and glTF models into ECS
- Add Vertex struct and Mesh ECS component (vertices + indices). - Add Vulkan IndexBuffer for indexed draws. - Add MeshRenderer that draws Mesh + Transform entities with vkCmdDrawIndexed. - Add minimal .obj loader (positions, faces) and glTF/glTF-binary loader via SharpGLTF.Core. - Rewrite shaders to 3D position + color; recompile to SPIR-V. - Update VulkanPipeline for new vertex format. - Ship a sample Content/cube.obj and wire Program.cs to load it. - Remove TriangleRenderer (replaced by MeshRenderer).
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
|
||||
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release'" />
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using Silk.NET.Core;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// GPU index buffer for indexed draws.
|
||||
/// Uses 32-bit indices.
|
||||
/// </summary>
|
||||
public sealed unsafe class IndexBuffer : IDisposable
|
||||
{
|
||||
private readonly VulkanContext _context;
|
||||
public Silk.NET.Vulkan.Buffer Buffer { get; }
|
||||
public DeviceMemory Memory { get; }
|
||||
public ulong Size { get; }
|
||||
public uint Count { get; }
|
||||
|
||||
public IndexBuffer(VulkanContext context, ReadOnlySpan<byte> data, uint count)
|
||||
{
|
||||
_context = context;
|
||||
Size = (ulong)data.Length;
|
||||
Count = count;
|
||||
|
||||
Buffer = CreateBuffer(Size, BufferUsageFlags.IndexBufferBit);
|
||||
var memoryRequirements = GetMemoryRequirements(Buffer);
|
||||
Memory = AllocateMemory(memoryRequirements, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit);
|
||||
|
||||
var result = _context.Vk.BindBufferMemory(_context.Device, Buffer, Memory, 0);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkBindBufferMemory failed: {result}");
|
||||
|
||||
CopyData(data);
|
||||
}
|
||||
|
||||
private Silk.NET.Vulkan.Buffer CreateBuffer(ulong size, BufferUsageFlags usage)
|
||||
{
|
||||
var createInfo = new BufferCreateInfo
|
||||
{
|
||||
SType = StructureType.BufferCreateInfo,
|
||||
Size = size,
|
||||
Usage = usage,
|
||||
SharingMode = SharingMode.Exclusive
|
||||
};
|
||||
|
||||
Silk.NET.Vulkan.Buffer buffer;
|
||||
var result = _context.Vk.CreateBuffer(_context.Device, &createInfo, null, &buffer);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkCreateBuffer failed: {result}");
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private MemoryRequirements GetMemoryRequirements(Silk.NET.Vulkan.Buffer buffer)
|
||||
{
|
||||
MemoryRequirements requirements;
|
||||
_context.Vk.GetBufferMemoryRequirements(_context.Device, buffer, &requirements);
|
||||
return requirements;
|
||||
}
|
||||
|
||||
private DeviceMemory AllocateMemory(MemoryRequirements requirements, MemoryPropertyFlags properties)
|
||||
{
|
||||
var memoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, properties);
|
||||
var allocateInfo = new MemoryAllocateInfo
|
||||
{
|
||||
SType = StructureType.MemoryAllocateInfo,
|
||||
AllocationSize = requirements.Size,
|
||||
MemoryTypeIndex = memoryTypeIndex
|
||||
};
|
||||
|
||||
DeviceMemory memory;
|
||||
var result = _context.Vk.AllocateMemory(_context.Device, &allocateInfo, null, &memory);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkAllocateMemory failed: {result}");
|
||||
return memory;
|
||||
}
|
||||
|
||||
private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties)
|
||||
{
|
||||
PhysicalDeviceMemoryProperties memoryProperties;
|
||||
_context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties);
|
||||
for (var i = 0; i < memoryProperties.MemoryTypeCount; i++)
|
||||
{
|
||||
if ((typeFilter & (1u << i)) != 0 &&
|
||||
(memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties)
|
||||
{
|
||||
return (uint)i;
|
||||
}
|
||||
}
|
||||
throw new InvalidOperationException("Failed to find suitable memory type.");
|
||||
}
|
||||
|
||||
private void CopyData(ReadOnlySpan<byte> data)
|
||||
{
|
||||
void* mappedData;
|
||||
var result = _context.Vk.MapMemory(_context.Device, Memory, 0, Size, MemoryMapFlags.None, &mappedData);
|
||||
if (result != Result.Success)
|
||||
throw new InvalidOperationException($"vkMapMemory failed: {result}");
|
||||
|
||||
fixed (byte* src = data)
|
||||
{
|
||||
global::System.Buffer.MemoryCopy(src, mappedData, (long)Size, data.Length);
|
||||
}
|
||||
|
||||
_context.Vk.UnmapMemory(_context.Device, Memory);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_context.Vk.DeviceWaitIdle(_context.Device);
|
||||
_context.Vk.DestroyBuffer(_context.Device, Buffer, null);
|
||||
_context.Vk.FreeMemory(_context.Device, Memory, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
namespace Engine.Graphics.Loaders;
|
||||
|
||||
/// <summary>
|
||||
/// glTF/glTF binary loader using SharpGLTF.Core.
|
||||
/// Loads the first primitive of the first mesh and converts it to a colored Mesh component.
|
||||
/// </summary>
|
||||
public static class GltfLoader
|
||||
{
|
||||
public static Engine.Core.Components.Mesh Load(string path, Vector3? defaultColor = null)
|
||||
{
|
||||
var color = defaultColor ?? new Vector3(0.7f, 0.7f, 0.7f);
|
||||
|
||||
var model = ModelRoot.Load(path);
|
||||
if (model.LogicalMeshes.Count == 0)
|
||||
throw new InvalidOperationException($"glTF file has no meshes: {path}");
|
||||
|
||||
var mesh = model.LogicalMeshes[0];
|
||||
if (mesh.Primitives.Count == 0)
|
||||
throw new InvalidOperationException($"glTF mesh has no primitives: {path}");
|
||||
|
||||
var primitive = mesh.Primitives[0];
|
||||
|
||||
if (!primitive.VertexAccessors.TryGetValue("POSITION", out var positionAccessor))
|
||||
throw new InvalidOperationException($"glTF primitive has no POSITION accessor: {path}");
|
||||
|
||||
var positions = positionAccessor.AsVector3Array();
|
||||
var vertices = new Vertex[positions.Count];
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
{
|
||||
vertices[i] = new Vertex(positions[i], color);
|
||||
}
|
||||
|
||||
uint[] indices;
|
||||
if (primitive.IndexAccessor != null)
|
||||
{
|
||||
var idx = primitive.IndexAccessor.AsIndexArray();
|
||||
indices = new uint[idx.Count];
|
||||
for (var i = 0; i < idx.Count; i++)
|
||||
indices[i] = idx[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-indexed primitive
|
||||
indices = new uint[positions.Count];
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
indices[i] = (uint)i;
|
||||
}
|
||||
|
||||
return new Engine.Core.Components.Mesh(vertices, indices);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
|
||||
namespace Engine.Graphics.Loaders;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal .obj loader.
|
||||
/// Supports vertices (v) and faces (f). Ignores normals/UVs for now.
|
||||
/// Produces a colored Mesh component.
|
||||
/// </summary>
|
||||
public static class ObjLoader
|
||||
{
|
||||
public static Mesh Load(string path, Vector3? defaultColor = null)
|
||||
{
|
||||
var color = defaultColor ?? new Vector3(0.7f, 0.7f, 0.7f);
|
||||
|
||||
var positions = new List<Vector3>();
|
||||
var indices = new List<uint>();
|
||||
|
||||
foreach (var rawLine in File.ReadLines(path))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (string.IsNullOrEmpty(line) || line.StartsWith("#"))
|
||||
continue;
|
||||
|
||||
var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
continue;
|
||||
|
||||
switch (parts[0])
|
||||
{
|
||||
case "v" when parts.Length >= 4:
|
||||
positions.Add(new Vector3(
|
||||
float.Parse(parts[1]),
|
||||
float.Parse(parts[2]),
|
||||
float.Parse(parts[3])));
|
||||
break;
|
||||
|
||||
case "f" when parts.Length >= 4:
|
||||
// Triangulate the face as a fan. Only the position index is used.
|
||||
var baseIndex = ParseFaceIndex(parts[1]);
|
||||
for (var i = 2; i < parts.Length - 1; i++)
|
||||
{
|
||||
indices.Add(baseIndex);
|
||||
indices.Add(ParseFaceIndex(parts[i]));
|
||||
indices.Add(ParseFaceIndex(parts[i + 1]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (positions.Count == 0)
|
||||
throw new InvalidOperationException($"OBJ file has no vertices: {path}");
|
||||
|
||||
var vertices = new Vertex[positions.Count];
|
||||
for (var i = 0; i < positions.Count; i++)
|
||||
{
|
||||
vertices[i] = new Vertex(positions[i], color);
|
||||
}
|
||||
|
||||
return new Mesh(vertices, indices.ToArray());
|
||||
}
|
||||
|
||||
private static uint ParseFaceIndex(string part)
|
||||
{
|
||||
// Formats: v, v/vt, v/vt/vn, v//vn
|
||||
var slashIndex = part.IndexOf('/');
|
||||
var indexStr = slashIndex == -1 ? part : part.Substring(0, slashIndex);
|
||||
var index = int.Parse(indexStr);
|
||||
return (uint)(index - 1); // OBJ indices are 1-based
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Flecs.NET.Core;
|
||||
using Silk.NET.Core;
|
||||
using Silk.NET.Vulkan;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a colored triangle using a vertex buffer and a simple graphics pipeline.
|
||||
/// Uses Silk.NET.Vulkan and reads entity transforms from the ECS world.
|
||||
/// Renders indexed meshes attached to ECS entities.
|
||||
/// Uses Silk.NET.Vulkan and reads Mesh + Transform components from the ECS world.
|
||||
/// </summary>
|
||||
public sealed unsafe class TriangleRenderer : IDisposable
|
||||
public sealed unsafe class MeshRenderer : IDisposable
|
||||
{
|
||||
private readonly VulkanContext _context;
|
||||
private readonly Swapchain _swapchain;
|
||||
private readonly VulkanPipeline _pipeline;
|
||||
private readonly VertexBuffer _vertexBuffer;
|
||||
private readonly Dictionary<Entity, MeshBuffers> _buffers = new();
|
||||
private CommandPool _commandPool;
|
||||
private CommandBuffer[] _commandBuffers = null!;
|
||||
private Silk.NET.Vulkan.Semaphore[] _imageAvailableSemaphores = null!;
|
||||
@@ -24,37 +26,35 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
private Silk.NET.Vulkan.Fence[] _inFlightFences = null!;
|
||||
private int _currentFrame;
|
||||
|
||||
public TriangleRenderer(VulkanContext context, Swapchain swapchain)
|
||||
private sealed class MeshBuffers : IDisposable
|
||||
{
|
||||
public VertexBuffer VertexBuffer;
|
||||
public IndexBuffer IndexBuffer;
|
||||
|
||||
public MeshBuffers(VertexBuffer vertexBuffer, IndexBuffer indexBuffer)
|
||||
{
|
||||
VertexBuffer = vertexBuffer;
|
||||
IndexBuffer = indexBuffer;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
VertexBuffer.Dispose();
|
||||
IndexBuffer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public MeshRenderer(VulkanContext context, Swapchain swapchain)
|
||||
{
|
||||
_context = context;
|
||||
_swapchain = swapchain;
|
||||
|
||||
_pipeline = new VulkanPipeline(context, swapchain);
|
||||
_vertexBuffer = CreateTriangleBuffer();
|
||||
CreateCommandPool();
|
||||
CreateCommandBuffers();
|
||||
CreateSyncObjects();
|
||||
}
|
||||
|
||||
private VertexBuffer CreateTriangleBuffer()
|
||||
{
|
||||
var vertices = new[]
|
||||
{
|
||||
0.0f, -0.5f, 1.0f, 0.0f, 0.0f,
|
||||
0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
|
||||
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f
|
||||
};
|
||||
|
||||
var bytes = new byte[vertices.Length * sizeof(float)];
|
||||
fixed (byte* p = bytes)
|
||||
fixed (float* v = vertices)
|
||||
{
|
||||
global::System.Buffer.MemoryCopy(v, p, bytes.Length, vertices.Length * sizeof(float));
|
||||
}
|
||||
|
||||
return new VertexBuffer(_context, bytes);
|
||||
}
|
||||
|
||||
private void CreateCommandPool()
|
||||
{
|
||||
var createInfo = new CommandPoolCreateInfo
|
||||
@@ -160,16 +160,23 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
_context.Vk.CmdSetViewport(cmd, 0, 1, &viewport);
|
||||
_context.Vk.CmdSetScissor(cmd, 0, 1, &scissor);
|
||||
|
||||
var vertexBuffer = _vertexBuffer.Buffer;
|
||||
var offset = 0ul;
|
||||
_context.Vk.CmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
|
||||
|
||||
var drawCmd = cmd;
|
||||
world.Each((Entity e, ref Transform transform) =>
|
||||
world.Each((Entity e, ref Mesh mesh, ref Transform transform) =>
|
||||
{
|
||||
var bytes = BuildTriangleVertices(transform);
|
||||
_vertexBuffer.Update(bytes);
|
||||
_context.Vk.CmdDraw(drawCmd, 3, 1, 0, 0);
|
||||
if (!_buffers.TryGetValue(e, out var buffers))
|
||||
{
|
||||
buffers = CreateMeshBuffers(mesh);
|
||||
_buffers[e] = buffers;
|
||||
}
|
||||
|
||||
var bytes = BuildMeshVertices(mesh, transform);
|
||||
buffers.VertexBuffer.Update(bytes);
|
||||
|
||||
var vertexBuffer = buffers.VertexBuffer.Buffer;
|
||||
var offset = 0ul;
|
||||
_context.Vk.CmdBindVertexBuffers(drawCmd, 0, 1, &vertexBuffer, &offset);
|
||||
_context.Vk.CmdBindIndexBuffer(drawCmd, buffers.IndexBuffer.Buffer, 0, IndexType.Uint32);
|
||||
_context.Vk.CmdDrawIndexed(drawCmd, buffers.IndexBuffer.Count, 1, 0, 0, 0);
|
||||
});
|
||||
|
||||
_context.Vk.CmdEndRenderPass(cmd);
|
||||
@@ -207,34 +214,52 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
_currentFrame++;
|
||||
}
|
||||
|
||||
private byte[] BuildTriangleVertices(Transform transform)
|
||||
private MeshBuffers CreateMeshBuffers(Mesh mesh)
|
||||
{
|
||||
var vertexBytes = new byte[mesh.Vertices.Length * 6 * sizeof(float)];
|
||||
fixed (byte* p = vertexBytes)
|
||||
{
|
||||
var dst = (float*)p;
|
||||
for (var i = 0; i < mesh.Vertices.Length; i++)
|
||||
{
|
||||
var v = mesh.Vertices[i];
|
||||
dst[i * 6 + 0] = v.Position.X;
|
||||
dst[i * 6 + 1] = v.Position.Y;
|
||||
dst[i * 6 + 2] = v.Position.Z;
|
||||
dst[i * 6 + 3] = v.Color.X;
|
||||
dst[i * 6 + 4] = v.Color.Y;
|
||||
dst[i * 6 + 5] = v.Color.Z;
|
||||
}
|
||||
}
|
||||
|
||||
var indexBytes = new byte[mesh.Indices.Length * sizeof(uint)];
|
||||
fixed (byte* p = indexBytes)
|
||||
fixed (uint* src = mesh.Indices)
|
||||
{
|
||||
global::System.Buffer.MemoryCopy(src, p, indexBytes.Length, mesh.Indices.Length * sizeof(uint));
|
||||
}
|
||||
|
||||
return new MeshBuffers(
|
||||
new VertexBuffer(_context, vertexBytes),
|
||||
new IndexBuffer(_context, indexBytes, (uint)mesh.Indices.Length));
|
||||
}
|
||||
|
||||
private byte[] BuildMeshVertices(Mesh mesh, Transform transform)
|
||||
{
|
||||
var matrix = transform.GetMatrix();
|
||||
var positions = new Vector3[]
|
||||
{
|
||||
new Vector3(0.0f, -0.5f, 0.0f),
|
||||
new Vector3(0.5f, 0.5f, 0.0f),
|
||||
new Vector3(-0.5f, 0.5f, 0.0f)
|
||||
};
|
||||
var colors = new[]
|
||||
{
|
||||
new Vector3(1.0f, 0.0f, 0.0f),
|
||||
new Vector3(0.0f, 1.0f, 0.0f),
|
||||
new Vector3(0.0f, 0.0f, 1.0f)
|
||||
};
|
||||
|
||||
var bytes = new byte[3 * 5 * sizeof(float)];
|
||||
var bytes = new byte[mesh.Vertices.Length * 6 * sizeof(float)];
|
||||
fixed (byte* p = bytes)
|
||||
{
|
||||
var dst = (float*)p;
|
||||
for (var i = 0; i < 3; i++)
|
||||
for (var i = 0; i < mesh.Vertices.Length; i++)
|
||||
{
|
||||
var transformed = Vector3.Transform(positions[i], matrix);
|
||||
dst[i * 5 + 0] = transformed.X;
|
||||
dst[i * 5 + 1] = transformed.Y;
|
||||
dst[i * 5 + 2] = colors[i].X;
|
||||
dst[i * 5 + 3] = colors[i].Y;
|
||||
dst[i * 5 + 4] = colors[i].Z;
|
||||
var transformed = Vector3.Transform(mesh.Vertices[i].Position, matrix);
|
||||
dst[i * 6 + 0] = transformed.X;
|
||||
dst[i * 6 + 1] = transformed.Y;
|
||||
dst[i * 6 + 2] = transformed.Z;
|
||||
dst[i * 6 + 3] = mesh.Vertices[i].Color.X;
|
||||
dst[i * 6 + 4] = mesh.Vertices[i].Color.Y;
|
||||
dst[i * 6 + 5] = mesh.Vertices[i].Color.Z;
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
@@ -244,6 +269,10 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
{
|
||||
_context.Vk.DeviceWaitIdle(_context.Device);
|
||||
|
||||
foreach (var buffers in _buffers.Values)
|
||||
buffers.Dispose();
|
||||
_buffers.Clear();
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
_context.Vk.DestroySemaphore(_context.Device, _renderFinishedSemaphores[i], null);
|
||||
@@ -253,7 +282,6 @@ public sealed unsafe class TriangleRenderer : IDisposable
|
||||
|
||||
_context.Vk.DestroyCommandPool(_context.Device, _commandPool, null);
|
||||
|
||||
_vertexBuffer.Dispose();
|
||||
_pipeline.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec3 fragColor;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
outColor = vec4(fragColor, 1.0);
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,12 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec3 inPosition;
|
||||
layout(location = 1) in vec3 inColor;
|
||||
|
||||
layout(location = 0) out vec3 fragColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = vec4(inPosition, 1.0);
|
||||
fragColor = inColor;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Silk.NET.Vulkan;
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Simple graphics pipeline for a triangle with vec2 position + vec3 color.
|
||||
/// Simple graphics pipeline for indexed meshes with vec3 position + vec3 color.
|
||||
/// Uses Silk.NET.Vulkan.
|
||||
/// </summary>
|
||||
public sealed unsafe class VulkanPipeline : IDisposable
|
||||
@@ -93,7 +93,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
var bindingDescription = new VertexInputBindingDescription
|
||||
{
|
||||
Binding = 0,
|
||||
Stride = (uint)(5 * sizeof(float)),
|
||||
Stride = (uint)(6 * sizeof(float)),
|
||||
InputRate = VertexInputRate.Vertex
|
||||
};
|
||||
|
||||
@@ -103,7 +103,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
{
|
||||
Binding = 0,
|
||||
Location = 0,
|
||||
Format = Format.R32G32Sfloat,
|
||||
Format = Format.R32G32B32Sfloat,
|
||||
Offset = 0
|
||||
},
|
||||
new VertexInputAttributeDescription
|
||||
@@ -111,7 +111,7 @@ public sealed unsafe class VulkanPipeline : IDisposable
|
||||
Binding = 0,
|
||||
Location = 1,
|
||||
Format = Format.R32G32B32Sfloat,
|
||||
Offset = (uint)(2 * sizeof(float))
|
||||
Offset = (uint)(3 * sizeof(float))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user