Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
80 changes: 75 additions & 5 deletions source/LibRender2/BaseRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,59 @@ public Vector2 ScaleFactor
public bool OptionLighting = true;

/// <summary>Whether normals rendering is enabled in the debug options</summary>
public bool OptionNormals = false;
private bool optionNormals = false;

/// <summary>Whether normals rendering is enabled in the debug options</summary>
/// <remarks>Disposing the normals VAO when disabled frees the duplicate vertex buffer held in RAM.</remarks>
public bool OptionNormals
{
get => optionNormals;
set
{
if (value == optionNormals)
{
return;
}

optionNormals = value;
if (!value)
{
DisposeNormalsVAOs();
// Force the GC so the freed normals buffers are gone from RAM immediately.
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}

private void DisposeNormalsVAOs()
{
if (StaticObjectStates != null)
{
foreach (var state in StaticObjectStates)
{
DisposeNormalsVAO(state);
}
}

if (DynamicObjectStates != null)
{
foreach (var state in DynamicObjectStates)
{
DisposeNormalsVAO(state);
}
}
}

private static void DisposeNormalsVAO(ObjectState state)
{
if (state?.Prototype?.Mesh?.NormalsVAO is VertexArrayObject normalsVao)
{
normalsVao.UnBind();
normalsVao.Dispose();
state.Prototype.Mesh.NormalsVAO = null;
}
}

/// <summary>Whether back face culling is enabled</summary>
public bool OptionBackFaceCulling = true;
Expand Down Expand Up @@ -1429,10 +1481,28 @@ public void RenderFace(Shader shader, ObjectState state, MeshFace face, bool deb
shader.DisableTexturing();
shader.SetBrightness(1.0f);
shader.SetOpacity(1.0f);
VertexArrayObject normalsVao = (VertexArrayObject)state.Prototype.Mesh.NormalsVAO;
normalsVao.Bind();
lastVAO = normalsVao.handle;
normalsVao.Draw(PrimitiveType.Lines, face.NormalsIboStartIndex, face.Vertices.Length * 2);
Mesh normalsMesh = state.Prototype.Mesh;
if (normalsMesh.NormalsVAO == null)
{
// Build the normals VAO lazily on first use to avoid holding a duplicate vertex buffer in RAM
VAOExtensions.CreateNormalsVAO(normalsMesh, state.Prototype.Dynamic, DefaultShader.VertexLayout, this);
}
VertexArrayObject normalsVao = (VertexArrayObject)normalsMesh.NormalsVAO;
if (normalsVao != null && face.IboStartIndex == 0)
{
// Draw all normals for this mesh in a single call (the normals VAO is a contiguous list of line pairs).
// Solid purple overlay: disable lighting and force a white, emissive material so the shader
// outputs the baked per-vertex purple colour unchanged (finalColor *= oLightResult == white).
shader.SetIsLight(false);
shader.SetMaterialAmbient(Color32.White);
shader.SetMaterialFlags(MaterialFlags.Emissive);
normalsVao.Bind();
lastVAO = normalsVao.handle;
normalsVao.DrawArrays(PrimitiveType.Lines, 0, normalsMesh.Vertices.Length > 0 ? normalsMesh.Faces.Sum(f => f.Vertices.Length) * 2 : 0);
shader.SetIsLight(OptionLighting);
shader.SetMaterialFlags(material.Flags);
shader.SetMaterialAmbient(material.Color);
}
}

// finalize
Expand Down
10 changes: 10 additions & 0 deletions source/LibRender2/openGL/Vertex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ public LibRenderVertex(Vector3f position)
MatrixChain = new Vector3i(-1, 0, 0);
}

/// <summary>Creates a LibRenderVertex from a position, normal and a solid color (used by the debug normals overlay)</summary>
public LibRenderVertex(Vector3 position, Vector3 normal, Color128 color)
{
Position = new Vector3f(position.X, position.Y, position.Z);
Normal = new Vector3f(normal.X, normal.Y, normal.Z);
UV = Vector2f.Null;
Color = color;
MatrixChain = new Vector3i(-1, 0, 0);
}

/// <summary>Creates a LibRenderVertex from a template vertex and a normal</summary>
public LibRenderVertex(VertexTemplate template, Vector3f normal)
{
Expand Down
203 changes: 154 additions & 49 deletions source/LibRender2/openGL/VertexArrayObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ public void Draw(PrimitiveType DrawMode, int Start, int Count)
ibo.Draw(DrawMode, Start, Count);
}

/// <summary>
/// Draws the VAO using non-indexed rendering (no IBO required).</summary>
/// <param name="DrawMode">Specifies the primitive or primitives that will be created from vertices.</param>
/// <param name="Start">Start position of the first vertex.</param>
/// <param name="Count">Number of vertices to render.</param>
/// <remarks>
/// Used by the debug normals overlay, where the vertex buffer is already laid out as contiguous line pairs.
/// Avoids holding a duplicate index buffer in RAM.
/// </remarks>
public void DrawArrays(PrimitiveType DrawMode, int Start, int Count)
{
GL.DrawArrays(DrawMode, Start, Count);
}

/// <summary>
/// Unbinds the VAO deactivating the VAO from use
/// </summary>
Expand Down Expand Up @@ -201,72 +215,163 @@ private static void createVAO(Mesh mesh, bool isDynamic, VertexLayout vertexLayo
* This marginally helps loading very large objects (as default C# list starts at a capacity of 4 then doubles exponentially)
*/

var vertexData = new List<LibRenderVertex>(mesh.Vertices.Length);
var indexData = new List<uint>();
int totalFaceVertices = 0;
for (int i = 0; i < mesh.Faces.Length; i++)
{
totalFaceVertices += mesh.Faces[i].Vertices.Length;
}

var vertexData = new List<LibRenderVertex>(totalFaceVertices);
var indexData = new List<uint>(totalFaceVertices);

var normalsVertexData = new List<LibRenderVertex>(mesh.Vertices.Length * 2);
var normalsIndexData = new List<uint>();
for (int i = 0; i < mesh.Faces.Length; i++)
{
mesh.Faces[i].IboStartIndex = indexData.Count;

for (int i = 0; i < mesh.Faces.Length; i++)
foreach (var vertex in mesh.Faces[i].Vertices)
{
mesh.Faces[i].IboStartIndex = indexData.Count;
mesh.Faces[i].NormalsIboStartIndex = normalsIndexData.Count;
vertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index], vertex.Normal));
}

foreach (var vertex in mesh.Faces[i].Vertices)
{
vertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index], vertex.Normal));
normalsVertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index].Coordinates));
normalsVertexData.Add(new LibRenderVertex(mesh.Vertices[vertex.Index].Coordinates + vertex.Normal));
}
indexData.AddRange(Enumerable.Range(mesh.Faces[i].IboStartIndex, mesh.Faces[i].Vertices.Length).Select(x => (uint)x));
}

indexData.AddRange(Enumerable.Range(mesh.Faces[i].IboStartIndex, mesh.Faces[i].Vertices.Length).Select(x => (uint)x));
normalsIndexData.AddRange(Enumerable.Range(mesh.Faces[i].NormalsIboStartIndex, mesh.Faces[i].Vertices.Length * 2).Select(x => (uint)x));
}
VertexArrayObject VAO = (VertexArrayObject)mesh.VAO;
VAO?.UnBind();
VAO?.Dispose();

VertexArrayObject VAO = (VertexArrayObject)mesh.VAO;
VAO?.UnBind();
VAO?.Dispose();
VAO = new VertexArrayObject();
VAO.Bind();
VAO.SetVBO(new VertexBufferObject(vertexData.ToArray(), hint));
if (indexData.Count > 65530)
{
//Marginal headroom, although it probably doesn't matter
VAO.SetIBO(new IndexBufferObjectUI(indexData.ToArray(), hint));
}
else
{
VAO.SetIBO(new IndexBufferObjectUS(indexData.Select(x => (ushort)x).ToArray(), hint));
}

VAO = new VertexArrayObject();
VAO.Bind();
VAO.SetVBO(new VertexBufferObject(vertexData.ToArray(), hint));
if (indexData.Count > 65530)
{
//Marginal headroom, although it probably doesn't matter
VAO.SetIBO(new IndexBufferObjectUI(indexData.ToArray(), hint));
}
else
VAO.SetAttributes(vertexLayout);
VAO.UnBind();
mesh.VAO = VAO;

/*
* The normals VAO is only used for the debug normals overlay (OptionNormals).
* It is built lazily via CreateNormalsVAO to avoid holding a second large
* vertex buffer in RAM for every loaded mesh.
*/
VertexArrayObject NormalsVAO = (VertexArrayObject)mesh.NormalsVAO;
NormalsVAO?.UnBind();
NormalsVAO?.Dispose();
mesh.NormalsVAO = null;
}
catch (Exception e)
{
renderer.currentHost.AddMessage(MessageType.Error, false, $"Creating VAO failed with the following error: {e}");
}
}

/// <summary>Creates the OpenGL/OpenTK VAO for the normals overlay of a mesh.</summary>
/// <remarks>This is built lazily on first use of the normals debug view, as it duplicates the vertex data.</remarks>
public static void CreateNormalsVAO(Mesh mesh, bool isDynamic, VertexLayout vertexLayout, BaseRenderer renderer)
{
if (mesh == null || mesh.NormalsVAO is VertexArrayObject)
{
return;
}
if (!renderer.GameWindow.Context.IsCurrent)
{
renderer.RunInRenderThread(() =>
{
VAO.SetIBO(new IndexBufferObjectUS(indexData.Select(x => (ushort)x).ToArray(), hint));
}
createNormalsVAO(mesh, isDynamic, vertexLayout, renderer);
}, 2000);
}
else
{
createNormalsVAO(mesh, isDynamic, vertexLayout, renderer);
}
}

VAO.SetAttributes(vertexLayout);
VAO.UnBind();
mesh.VAO = VAO;
VertexArrayObject NormalsVAO = (VertexArrayObject)mesh.NormalsVAO;
NormalsVAO?.UnBind();
NormalsVAO?.Dispose();

NormalsVAO = new VertexArrayObject();
NormalsVAO.Bind();
NormalsVAO.SetVBO(new VertexBufferObject(normalsVertexData.ToArray(), hint));
if (normalsIndexData.Count > 65530)
private static void createNormalsVAO(Mesh mesh, bool isDynamic, VertexLayout vertexLayout, BaseRenderer renderer)
{
if (mesh == null)
{
return;
}
try
{
var hint = isDynamic ? BufferUsageHint.DynamicDraw : BufferUsageHint.StaticDraw;

int totalFaceVertices = 0;
for (int i = 0; i < mesh.Faces.Length; i++)
{
totalFaceVertices += mesh.Faces[i].Vertices.Length;
}

// Debug normals overlay: a contiguous list of line pairs (vertex -> vertex + normal).
// Rendered with DrawArrays (no IBO), so we do not hold a duplicate index buffer in RAM.
// Colour is baked per-vertex (purple) because the shader multiplies the uniform colour by the
// (zeroed) vertex colour attribute when colour is disabled, which would otherwise render black.
var normalsVertexData = new List<LibRenderVertex>(totalFaceVertices * 2);
var normalColor = new Color128(0.6f, 0.2f, 1.0f, 1.0f); // purple

// Scale normal lines relative to the mesh bounding box so they stay a sensible on-screen size.
// A fixed 1-unit length blows up into huge overdraw when the camera is close to a small mesh.
double scale = 0.01;
if (mesh.Vertices.Length > 0)
{
Vector3 min = mesh.Vertices[0].Coordinates;
Vector3 max = mesh.Vertices[0].Coordinates;
for (int v = 1; v < mesh.Vertices.Length; v++)
{
//Marginal headroom, although it probably doesn't matter
NormalsVAO.SetIBO(new IndexBufferObjectUI(normalsIndexData.ToArray(), hint));
Vector3 c = mesh.Vertices[v].Coordinates;
min.X = Math.Min(min.X, c.X); min.Y = Math.Min(min.Y, c.Y); min.Z = Math.Min(min.Z, c.Z);
max.X = Math.Max(max.X, c.X); max.Y = Math.Max(max.Y, c.Y); max.Z = Math.Max(max.Z, c.Z);
}
else
double size = Math.Max(max.X - min.X, Math.Max(max.Y - min.Y, max.Z - min.Z));
if (size > 0.0)
{
NormalsVAO.SetIBO(new IndexBufferObjectUS(normalsIndexData.Select(x => (ushort)x).ToArray(), hint));
scale = size * 0.01;
}
}

NormalsVAO.SetAttributes(vertexLayout);
NormalsVAO.UnBind();
mesh.NormalsVAO = NormalsVAO;
for (int i = 0; i < mesh.Faces.Length; i++)
{
foreach (var vertex in mesh.Faces[i].Vertices)
{
Vector3 coordinates = mesh.Vertices[vertex.Index].Coordinates;
Vector3 tip = coordinates + vertex.Normal * scale;
// Pass the normal so the shader's lighting produces a visible (non-black) colour.
// UV/MatrixChain are omitted so the shader cannot sample any texture.
normalsVertexData.Add(new LibRenderVertex(coordinates, vertex.Normal, normalColor));
normalsVertexData.Add(new LibRenderVertex(tip, vertex.Normal, normalColor));
}
}

VertexArrayObject NormalsVAO = (VertexArrayObject)mesh.NormalsVAO;
NormalsVAO?.UnBind();
NormalsVAO?.Dispose();

NormalsVAO = new VertexArrayObject();
NormalsVAO.Bind();
NormalsVAO.SetVBO(new VertexBufferObject(normalsVertexData.ToArray(), hint));
// Position + Normal + Colour: UV/matrix omitted so the shader cannot sample any texture,
// but the normal is supplied so lighting yields a visible (non-black) colour.
var normalsLayout = new VertexLayout
{
Position = vertexLayout.Position,
Normal = vertexLayout.Normal,
Color = vertexLayout.Color
};
NormalsVAO.SetAttributes(normalsLayout);
NormalsVAO.UnBind();
mesh.NormalsVAO = NormalsVAO;
}
catch (Exception e)
{
renderer.currentHost.AddMessage(MessageType.Error, false, $"Creating VAO failed with the following error: {e}");
renderer.currentHost.AddMessage(MessageType.Error, false, $"Creating normals VAO failed with the following error: {e}");
}
}

Expand Down