diff --git a/source/LibRender2/BaseRenderer.cs b/source/LibRender2/BaseRenderer.cs index 1055f9da7..8f9a404c7 100644 --- a/source/LibRender2/BaseRenderer.cs +++ b/source/LibRender2/BaseRenderer.cs @@ -170,7 +170,59 @@ public Vector2 ScaleFactor public bool OptionLighting = true; /// Whether normals rendering is enabled in the debug options - public bool OptionNormals = false; + private bool optionNormals = false; + + /// Whether normals rendering is enabled in the debug options + /// Disposing the normals VAO when disabled frees the duplicate vertex buffer held in RAM. + 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; + } + } /// Whether back face culling is enabled public bool OptionBackFaceCulling = true; @@ -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 diff --git a/source/LibRender2/openGL/Vertex.cs b/source/LibRender2/openGL/Vertex.cs index fc25cf67c..82f791897 100644 --- a/source/LibRender2/openGL/Vertex.cs +++ b/source/LibRender2/openGL/Vertex.cs @@ -39,6 +39,16 @@ public LibRenderVertex(Vector3f position) MatrixChain = new Vector3i(-1, 0, 0); } + /// Creates a LibRenderVertex from a position, normal and a solid color (used by the debug normals overlay) + 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); + } + /// Creates a LibRenderVertex from a template vertex and a normal public LibRenderVertex(VertexTemplate template, Vector3f normal) { diff --git a/source/LibRender2/openGL/VertexArrayObject.cs b/source/LibRender2/openGL/VertexArrayObject.cs index 6d20cbae3..879537952 100644 --- a/source/LibRender2/openGL/VertexArrayObject.cs +++ b/source/LibRender2/openGL/VertexArrayObject.cs @@ -118,6 +118,20 @@ public void Draw(PrimitiveType DrawMode, int Start, int Count) ibo.Draw(DrawMode, Start, Count); } + /// + /// Draws the VAO using non-indexed rendering (no IBO required). + /// Specifies the primitive or primitives that will be created from vertices. + /// Start position of the first vertex. + /// Number of vertices to render. + /// + /// 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. + /// + public void DrawArrays(PrimitiveType DrawMode, int Start, int Count) + { + GL.DrawArrays(DrawMode, Start, Count); + } + /// /// Unbinds the VAO deactivating the VAO from use /// @@ -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(mesh.Vertices.Length); - var indexData = new List(); + int totalFaceVertices = 0; + for (int i = 0; i < mesh.Faces.Length; i++) + { + totalFaceVertices += mesh.Faces[i].Vertices.Length; + } + + var vertexData = new List(totalFaceVertices); + var indexData = new List(totalFaceVertices); - var normalsVertexData = new List(mesh.Vertices.Length * 2); - var normalsIndexData = new List(); + 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}"); + } + } + + /// Creates the OpenGL/OpenTK VAO for the normals overlay of a mesh. + /// This is built lazily on first use of the normals debug view, as it duplicates the vertex data. + 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(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}"); } }