Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add instanced rendering #81

Merged
merged 4 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
30 changes: 30 additions & 0 deletions Bearded.Graphics/Core/Rendering/BufferExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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<TIndex>(this Buffer<TIndex> buffer)
where TIndex : struct
{
return IndexBuffer.From(buffer);
}

public static IIndexBuffer AsIndexBuffer<TIndex>(this BufferStream<TIndex> buffer)
where TIndex : struct
{
return 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();
}
59 changes: 59 additions & 0 deletions Bearded.Graphics/Core/Rendering/IndexBuffer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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<TIndex>(Buffer<TIndex> buffer)
where TIndex : struct
{
return new Static<TIndex>(buffer);
}

public static IIndexBuffer From<TIndex>(BufferStream<TIndex> stream)
where TIndex : struct
{
return new Streaming<TIndex>(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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably be more explicit here. With for records replaces, here it appends. This gets especially confusing since the index buffer, which also uses With, indeed does replace. So I would consider calling this AddBuffer (or even AddVertexBuffer) or AppendBuffer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree. But all the name changes I tried I ended up not being happy with. Perhaps the builder itself isn't quite ideal. I'll leave it like it is for now, and will see if I can come up with a better interface in the future.

{
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
Loading