Skip to content

Commit

Permalink
🔢 Add instanced rendering (#81)
Browse files Browse the repository at this point in the history
* 🚧 Make renderable construction more flexible

* 🔢 Allow instanced vertex data

* 🐛 Update examples

* 🔒 Enforce index buffer type
  • Loading branch information
paulcscharf authored Aug 5, 2024
1 parent d5acd25 commit 5f3cd72
Show file tree
Hide file tree
Showing 15 changed files with 347 additions and 230 deletions.
5 changes: 4 additions & 1 deletion Bearded.Graphics.Examples/01.Basics/GameWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ protected override void OnLoad()
addTriangle(buffer);

// Create a renderable wrapper for our buffer, interpreting it as a triangle list of vertices.
var renderable = Renderable.ForVertices(buffer, PrimitiveType.Triangles);
var renderable = Renderable.Build(
PrimitiveType.Triangles,
b => b.With(buffer.AsVertexBuffer())
);

// The shader program contains the vertex and fragment shaders. It is assigned to a renderer.
shaderProgram = ShaderProgram.FromShaders(
Expand Down
7 changes: 6 additions & 1 deletion Bearded.Graphics.Examples/02.IndexBuffer/GameWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ protected override void OnLoad()

// When creating our renderable, the PrimitiveType defines how the indices are interpreted and translated
// into geometry.
var renderable = Renderable.ForVerticesAndIndices(vertexBuffer, indexBuffer, PrimitiveType.Triangles);
var renderable = Renderable.Build(
PrimitiveType.Triangles,
b => b
.With(vertexBuffer.AsVertexBuffer())
.With(indexBuffer.AsIndexBuffer())
);

shaderProgram = ShaderProgram.FromShaders(
ShaderFactory.Vertex.FromFile("geometry.vs"), ShaderFactory.Fragment.FromFile("geometry.fs"));
Expand Down
25 changes: 25 additions & 0 deletions Bearded.Graphics/Core/Rendering/BufferExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Bearded.Graphics.Vertices;

namespace Bearded.Graphics.Rendering;

public static class BufferExtensions
{
public static IVertexBuffer AsVertexBuffer<TVertex>(this Buffer<TVertex> buffer)
where TVertex : struct, IVertexData
{
return VertexBuffer.From(buffer);
}

public static IVertexBuffer AsVertexBuffer<TVertex>(this BufferStream<TVertex> buffer)
where TVertex : struct, IVertexData
{
return VertexBuffer.From(buffer);
}

public static IIndexBuffer AsIndexBuffer(this Buffer<byte> buffer) => IndexBuffer.From(buffer);
public static IIndexBuffer AsIndexBuffer(this Buffer<ushort> buffer) => IndexBuffer.From(buffer);
public static IIndexBuffer AsIndexBuffer(this Buffer<uint> buffer) => IndexBuffer.From(buffer);
public static IIndexBuffer AsIndexBuffer(this BufferStream<byte> buffer) => IndexBuffer.From(buffer);
public static IIndexBuffer AsIndexBuffer(this BufferStream<ushort> buffer) => IndexBuffer.From(buffer);
public static IIndexBuffer AsIndexBuffer(this BufferStream<uint> buffer) => IndexBuffer.From(buffer);
}
6 changes: 6 additions & 0 deletions Bearded.Graphics/Core/Rendering/IFlushableBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Bearded.Graphics.Rendering;

public interface IFlushableBuffer
{
void FlushIfNeeded();
}
55 changes: 55 additions & 0 deletions Bearded.Graphics/Core/Rendering/IndexBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using OpenTK.Graphics.OpenGL;

namespace Bearded.Graphics.Rendering;

public interface IIndexBuffer
{
int Count { get; }
DrawElementsType ElementType { get; }
void ConfigureBoundVertexArray();
}

public static class IndexBuffer
{
public static IIndexBuffer From(Buffer<byte> buffer) => new Static<byte>(buffer);
public static IIndexBuffer From(Buffer<ushort> buffer) => new Static<ushort>(buffer);
public static IIndexBuffer From(Buffer<uint> buffer) => new Static<uint>(buffer);

public static IIndexBuffer From(BufferStream<byte> stream) => new Streaming<byte>(stream);
public static IIndexBuffer From(BufferStream<ushort> stream) => new Streaming<ushort>(stream);
public static IIndexBuffer From(BufferStream<uint> stream) => new Streaming<uint>(stream);

private sealed class Static<TIndex>(Buffer<TIndex> buffer) : IIndexBuffer
where TIndex : struct
{
public int Count => buffer.Count;

public DrawElementsType ElementType { get; } = elementType<TIndex>();

public void ConfigureBoundVertexArray() => buffer.Bind(BufferTarget.ElementArrayBuffer);
}

private sealed class Streaming<TIndex>(BufferStream<TIndex> stream) : IIndexBuffer, IFlushableBuffer
where TIndex : struct
{
public int Count => stream.Count;

public DrawElementsType ElementType { get; } = elementType<TIndex>();

public void ConfigureBoundVertexArray() => stream.Buffer.Bind(BufferTarget.ElementArrayBuffer);

public void FlushIfNeeded() => stream.FlushIfDirty();
}

private static DrawElementsType elementType<TIndex>()
{
return default(TIndex) switch
{
byte => DrawElementsType.UnsignedByte,
ushort => DrawElementsType.UnsignedShort,
uint => DrawElementsType.UnsignedInt,
_ => throw new NotSupportedException("Index type must be one of [byte, ushort, uint].")
};
}
}
143 changes: 143 additions & 0 deletions Bearded.Graphics/Core/Rendering/Renderable.Builder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Bearded.Graphics.Shading;
using OpenTK.Graphics.OpenGL;

namespace Bearded.Graphics.Rendering;

public static partial class Renderable
{
public static IRenderable Build(PrimitiveType primitiveType, Action<Builder> configure)
{
var builder = new Builder(primitiveType);
configure(builder);
return builder.Build();
}

public sealed class Builder(PrimitiveType primitiveType)
{
private readonly List<IVertexBuffer> vertexBuffers = [];
private IIndexBuffer? indexBuffer;
private Func<int>? instanceCount;

public Builder With(IVertexBuffer buffer)
{
vertexBuffers.Add(buffer);
return this;
}

public Builder With(params IVertexBuffer[] buffers)
{
vertexBuffers.AddRange(buffers);
return this;
}

public Builder With(ReadOnlySpan<IVertexBuffer> buffers)
{
vertexBuffers.AddRange(buffers);
return this;
}

public Builder With(IEnumerable<IVertexBuffer> buffers)
{
vertexBuffers.AddRange(buffers);
return this;
}

public Builder With(IIndexBuffer buffer)
{
indexBuffer = buffer;
return this;
}

public Builder InstancedWith(Func<int> getInstanceCount)
{
instanceCount = getInstanceCount;
return this;
}

public IRenderable Build()
{
if (vertexBuffers.Count == 0)
throw new InvalidOperationException("Renderable must have at least one vertex buffer.");

return build(primitiveType, [..vertexBuffers], indexBuffer, instanceCount);
}

private static IRenderable build(
PrimitiveType type,
ImmutableArray<IVertexBuffer> vertices,
IIndexBuffer? indices,
Func<int>? instanceCount)
{
var flushables = listFlushableBuffers(vertices, indices);

Action draw = (indices, instanceCount) switch
{
(null, null) => () => GL.DrawArrays(type, 0, vertices[0].Count),
(null, not null) => () => GL.DrawArraysInstanced(type, 0, vertices[0].Count, instanceCount()),
(not null, null) => () => GL.DrawElements(type, indices.Count, indices.ElementType, 0),
(not null, not null) => () => GL.DrawElementsInstanced(type, indices.Count, indices.ElementType, 0, instanceCount()),
};

return new Implementation(configure, flushables.IsDefaultOrEmpty ? draw : flushAndDraw);

void configure(ShaderProgram program)
{
foreach (var buffer in vertices)
{
buffer.ConfigureBoundVertexArray(program);
}

indices?.ConfigureBoundVertexArray();
}

void flushAndDraw()
{
foreach (var flushable in flushables)
{
flushable.FlushIfNeeded();
}

draw();
}

}

private static ImmutableArray<IFlushableBuffer> listFlushableBuffers(
ImmutableArray<IVertexBuffer> vertices, IIndexBuffer? indices)
{
var flushableCount = vertices.Count(b => b is IFlushableBuffer);
flushableCount += indices is IFlushableBuffer ? 1 : 0;

if (flushableCount == 0)
return ImmutableArray<IFlushableBuffer>.Empty;

var builder = ImmutableArray.CreateBuilder<IFlushableBuffer>(flushableCount);

foreach (var buffer in vertices)
{
if (buffer is IFlushableBuffer flushable)
{
builder.Add(flushable);
}
}
if (indices is IFlushableBuffer indexFlushable)
{
builder.Add(indexFlushable);
}

return builder.MoveToImmutable();
}
}

private sealed class Implementation(Action<ShaderProgram> configureBoundVertexArray, Action render) : IRenderable
{
public DrawCall MakeDrawCallFor(ShaderProgram program)
{
return DrawCall.With(() => configureBoundVertexArray(program), render);
}
}
}
47 changes: 27 additions & 20 deletions Bearded.Graphics/Core/Rendering/Renderable.ForBatched.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,76 +11,83 @@ public static partial class Renderable
public static IBatchedRenderable ForBatchedVertices<TV>(Batcher<Buffer<TV>> batcher, PrimitiveType primitiveType)
where TV : struct, IVertexData
{
return new WithBatched<Buffer<TV>>(batcher, primitiveType, ForVertices);
return new WithBatched<Buffer<TV>>(batcher, buffer => Build(primitiveType, b => b.With(buffer.AsVertexBuffer())));
}

public static IBatchedRenderable ForBatchedVertices<TV>(Batcher<BufferStream<TV>> batcher, PrimitiveType primitiveType)
where TV : struct, IVertexData
{
return new WithBatched<BufferStream<TV>>(batcher, primitiveType, ForVertices);
return new WithBatched<BufferStream<TV>>(batcher, stream => Build(primitiveType, b => b.With(stream.AsVertexBuffer())));
}

public static IBatchedRenderable ForBatchedVerticesAndIndices<TV>(
Batcher<(Buffer<TV>, Buffer<ushort>)> batcher, PrimitiveType primitiveType)
where TV : struct, IVertexData
{
return new WithBatched<(Buffer<TV>, Buffer<ushort>)>(batcher, primitiveType,
(buffers, pt) => ForVerticesAndIndices(buffers.Item1, buffers.Item2, pt));
return new WithBatched<(Buffer<TV> Vertices, Buffer<ushort> Indices)>(batcher,
buffers => Build(primitiveType, b => b
.With(buffers.Vertices.AsVertexBuffer())
.With(buffers.Indices.AsIndexBuffer())
));
}

public static IBatchedRenderable ForBatchedVerticesAndIndices<TV, TBatchData>(
Batcher<TBatchData> batcher, Func<TBatchData, (Buffer<TV>, Buffer<ushort>)> bufferSelector,
PrimitiveType primitiveType)
where TV : struct, IVertexData
{
return new WithBatched<TBatchData>(batcher, primitiveType,
(batch, pt) =>
return new WithBatched<TBatchData>(batcher,
batch =>
{
var (vb, ib) = bufferSelector(batch);
return ForVerticesAndIndices(vb, ib, pt);
return Build(primitiveType, b => b
.With(vb.AsVertexBuffer())
.With(ib.AsIndexBuffer()));
});
}

public static IBatchedRenderable ForBatchedVerticesAndIndices<TV>(
Batcher<(BufferStream<TV>, BufferStream<ushort>)> batcher, PrimitiveType primitiveType)
where TV : struct, IVertexData
{
return new WithBatched<(BufferStream<TV>, BufferStream<ushort>)>(batcher, primitiveType,
(buffers, pt) => ForVerticesAndIndices(buffers.Item1, buffers.Item2, pt));
return new WithBatched<(BufferStream<TV>, BufferStream<ushort>)>(batcher,
buffers => Build(primitiveType, b => b
.With(buffers.Item1.AsVertexBuffer())
.With(buffers.Item2.AsIndexBuffer())
));
}

public static IBatchedRenderable ForBatchedVerticesAndIndices<TV, TBatchData>(
Batcher<TBatchData> batcher, Func<TBatchData, (BufferStream<TV>, BufferStream<ushort>)> bufferSelector,
PrimitiveType primitiveType)
where TV : struct, IVertexData
{
return new WithBatched<TBatchData>(batcher, primitiveType,
(batch, pt) =>
return new WithBatched<TBatchData>(batcher,
batch =>
{
var (vb, ib) = bufferSelector(batch);
return ForVerticesAndIndices(vb, ib, pt);
return Build(primitiveType, b => b
.With(vb.AsVertexBuffer())
.With(ib.AsIndexBuffer())
);
});
}

private sealed class WithBatched<TBatchData> : IBatchedRenderable
{
private readonly Batcher<TBatchData> batcher;
private readonly PrimitiveType primitiveType;
private readonly Func<TBatchData, PrimitiveType, IRenderable> createRenderable;
private readonly Func<TBatchData, IRenderable> createRenderable;

private readonly Dictionary<Batcher<TBatchData>.Batch, IRenderable> renderables
= new Dictionary<Batcher<TBatchData>.Batch, IRenderable>();
private readonly Dictionary<Batcher<TBatchData>.Batch, IRenderable> renderables = new();

public event Action<IRenderable>? BatchActivated;
public event Action<IRenderable>? BatchDeactivated;

public WithBatched(
Batcher<TBatchData> batcher,
PrimitiveType primitiveType,
Func<TBatchData, PrimitiveType, IRenderable> createRenderable)
Func<TBatchData, IRenderable> createRenderable)
{
this.batcher = batcher;
this.primitiveType = primitiveType;
this.createRenderable = createRenderable;

batcher.BatchActivated += onBatchActivated;
Expand All @@ -103,7 +110,7 @@ private IRenderable getOrCreateRenderableFor(Batcher<TBatchData>.Batch batch)
{
if (!renderables.TryGetValue(batch, out var renderable))
{
renderable = createRenderable(batch.Data, primitiveType);
renderable = createRenderable(batch.Data);
renderables.Add(batch, renderable);
}

Expand Down
Loading

0 comments on commit 5f3cd72

Please sign in to comment.