using System.Numerics; using NResUI.Rendering.Viewport.Meshes; using NResUI.Rendering.Viewport.OpenGL; using Silk.NET.OpenGL; using Silk.NET.Windowing; namespace NResUI.Rendering.Viewport; public sealed class ViewportRenderer { private readonly GL _gl; private readonly IWindow _window; private readonly MsaaFramebuffer _framebuffer; public GL Gl => _gl; private ShaderProgram? _meshShader; private ShaderProgram? _outlineShader; private GpuMesh? _unitWireBoxMesh; private GpuMesh? _axesMesh; private int _modelLocation; private int _mvpLocation; private int _lightDirectionLocation; private int _useTextureLocation; private int _texture0Location; private int _outlineMvpLocation; private int _outlineColorLocation; private bool _initialized; public ViewportRenderer(GL gl, IWindow window) { _gl = gl; _window = window; _framebuffer = new MsaaFramebuffer(gl, samples: 4); } public uint Render(int width, int height, ViewportScene scene, ViewportCamera camera) { EnsureInitialized(); width = Math.Max(1, width); height = Math.Max(1, height); _framebuffer.EnsureSize(width, height); _framebuffer.BindForRender(); _gl.Enable(EnableCap.DepthTest); _gl.Disable(EnableCap.CullFace); _gl.ClearColor(0.12f, 0.13f, 0.15f, 1.0f); _gl.ClearStencil(0); _gl.Clear((uint)( ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit)); var aspect = width / (float)Math.Max(1, height); var sceneRotation = camera.GetSceneRotationMatrix(); var view = camera.GetViewMatrix(); var projection = camera.GetProjectionMatrix(aspect); DrawGrid(scene, sceneRotation, view, projection); DrawScene(scene, sceneRotation, view, projection); DrawDebugOverlays(scene, sceneRotation, view, projection); _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _framebuffer.Resolve(); _gl.Disable(EnableCap.DepthTest); _gl.Disable(EnableCap.StencilTest); _gl.UseProgram(0); _gl.BindVertexArray(0); _gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0); var framebufferSize = _window.FramebufferSize; _gl.Viewport(0, 0, (uint)framebufferSize.X, (uint)framebufferSize.Y); return _framebuffer.ColorTexture; } private void DrawGrid( ViewportScene scene, Matrix4x4 sceneRotation, Matrix4x4 view, Matrix4x4 projection) { if (_meshShader == null) throw new InvalidOperationException("Viewport mesh shader is not initialized."); var grid = scene.Grid; if (grid == null || !grid.IsVisible) return; var model = grid.LocalTransform * sceneRotation; _gl.Disable(EnableCap.StencilTest); _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); DrawMesh(grid.Mesh, model, view, projection); } private void DrawScene( ViewportScene scene, Matrix4x4 sceneRotation, Matrix4x4 view, Matrix4x4 projection) { if (_meshShader == null || _outlineShader == null) throw new InvalidOperationException("Viewport renderer is not initialized."); _gl.Disable(EnableCap.StencilTest); _meshShader.Use(); _gl.PolygonMode( TriangleFace.FrontAndBack, scene.Debug.Wireframe ? PolygonMode.Line : PolygonMode.Fill); foreach (var piece in scene.Pieces) { if (piece.Id == scene.SelectedPieceId) continue; DrawPiece(piece, sceneRotation, view, projection); } var selectedPiece = scene.SelectedPiece; if (selectedPiece == null) { _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); return; } _gl.Enable(EnableCap.StencilTest); _gl.StencilMask(0xFF); _gl.StencilFunc(StencilFunction.Always, 1, 0xFF); _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); _meshShader.Use(); DrawPiece(selectedPiece, sceneRotation, view, projection); _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _gl.StencilFunc(StencilFunction.Notequal, 1, 0xFF); _gl.StencilMask(0x00); _gl.Disable(EnableCap.DepthTest); DrawPieceOutline(selectedPiece, sceneRotation, view, projection); _gl.Enable(EnableCap.DepthTest); _gl.StencilMask(0xFF); _gl.Disable(EnableCap.StencilTest); } private void DrawDebugOverlays( ViewportScene scene, Matrix4x4 sceneRotation, Matrix4x4 view, Matrix4x4 projection) { if (_meshShader == null || _unitWireBoxMesh == null || _axesMesh == null) throw new InvalidOperationException("Viewport debug resources are not initialized."); _gl.Disable(EnableCap.StencilTest); _gl.Disable(EnableCap.DepthTest); _gl.PolygonMode(TriangleFace.FrontAndBack, PolygonMode.Fill); _gl.LineWidth(2.0f); _meshShader.Use(); if (scene.Debug.ShowOriginAxes) DrawMesh(_axesMesh, Matrix4x4.CreateScale(1.25f) * sceneRotation, view, projection); if (scene.Debug.ShowPieceOrigins) { foreach (var piece in scene.Pieces) { var axesModel = Matrix4x4.CreateScale(0.45f) * piece.LocalTransform * sceneRotation; DrawMesh(_axesMesh, axesModel, view, projection); } } if (scene.Debug.ShowSelectedBounds && scene.SelectedPiece != null) { var selectedPiece = scene.SelectedPiece; var boundsModel = BuildLocalBoundsModel(selectedPiece.BoundsMin, selectedPiece.BoundsMax) * selectedPiece.LocalTransform * sceneRotation; DrawMesh(_unitWireBoxMesh, boundsModel, view, projection); } if (scene.Debug.ShowSceneBounds && scene.TryGetSceneWorldBounds(out var sceneBounds)) { var sceneBoundsModel = BuildLocalBoundsModel(sceneBounds.Min, sceneBounds.Max) * sceneRotation; DrawMesh(_unitWireBoxMesh, sceneBoundsModel, view, projection); } _gl.LineWidth(1.0f); _gl.Enable(EnableCap.DepthTest); } private void DrawPiece( ViewportPiece piece, Matrix4x4 sceneRotation, Matrix4x4 view, Matrix4x4 projection) { var model = piece.LocalTransform * sceneRotation; foreach (var mesh in piece.Meshes) DrawMesh(mesh, model, view, projection); } private void DrawPieceOutline( ViewportPiece piece, Matrix4x4 sceneRotation, Matrix4x4 view, Matrix4x4 projection) { if (_outlineShader == null) throw new InvalidOperationException("Viewport outline shader is not initialized."); const float outlineScale = 1.06f; var model = Matrix4x4.CreateScale(outlineScale) * piece.LocalTransform * sceneRotation; var mvp = model * view * projection; _outlineShader.Use(); _outlineShader.SetMatrix4(_outlineMvpLocation, mvp); _outlineShader.SetVector4(_outlineColorLocation, new Vector4(1.0f, 0.82f, 0.15f, 1.0f)); foreach (var mesh in piece.Meshes) mesh.Draw(); } private void DrawMesh( GpuMesh mesh, Matrix4x4 model, Matrix4x4 view, Matrix4x4 projection) { if (_meshShader == null) throw new InvalidOperationException("Viewport mesh shader is not initialized."); var mvp = model * view * projection; var lightDirection = Vector3.Normalize(new Vector3(-0.35f, -0.75f, -0.55f)); _meshShader.Use(); _meshShader.SetMatrix4(_mvpLocation, mvp); _meshShader.SetMatrix4(_modelLocation, model); _meshShader.SetVector3(_lightDirectionLocation, lightDirection); if (mesh.Material.HasTexture) { _gl.ActiveTexture(TextureUnit.Texture0); _gl.BindTexture(TextureTarget.Texture2D, mesh.Material.TextureHandle); _meshShader.SetInt(_texture0Location, 0); _meshShader.SetInt(_useTextureLocation, 1); } else { _gl.BindTexture(TextureTarget.Texture2D, 0); _meshShader.SetInt(_useTextureLocation, 0); } mesh.Draw(); } private static Matrix4x4 BuildLocalBoundsModel(Vector3 boundsMin, Vector3 boundsMax) { var center = (boundsMin + boundsMax) * 0.5f; var size = boundsMax - boundsMin; return Matrix4x4.CreateScale(size * 0.5f) * Matrix4x4.CreateTranslation(center); } private void EnsureInitialized() { if (_initialized) return; CreateShaders(); _unitWireBoxMesh = PrimitiveMeshes.CreateUnitWireBox(_gl); _axesMesh = PrimitiveMeshes.CreateAxes(_gl); _initialized = true; } private void CreateShaders() { const string meshVertexShaderSource = """ #version 330 core layout (location = 0) in vec3 aPosition; layout (location = 1) in vec3 aColor; layout (location = 2) in vec3 aNormal; layout (location = 3) in vec2 aTexCoord; uniform mat4 uModel; uniform mat4 uMvp; out vec3 vColor; out vec3 vNormalWorld; out vec2 vTexCoord; void main() { vColor = aColor; vNormalWorld = mat3(transpose(inverse(uModel))) * aNormal; vTexCoord = aTexCoord; gl_Position = uMvp * vec4(aPosition, 1.0); } """; const string meshFragmentShaderSource = """ #version 330 core in vec3 vColor; in vec3 vNormalWorld; in vec2 vTexCoord; uniform vec3 uLightDirectionWorld; uniform bool uUseTexture; uniform sampler2D uTexture0; out vec4 FragColor; void main() { vec3 normal = normalize(vNormalWorld); // Useful when backface display is enabled. if (!gl_FrontFacing) normal = -normal; vec3 lightDir = normalize(-uLightDirectionWorld); float diffuse = max(dot(normal, lightDir), 0.0); float lighting = 0.35 + diffuse * 0.65; vec4 texel = uUseTexture ? texture(uTexture0, vTexCoord) : vec4(1.0); if (texel.a < 0.05) discard; FragColor = vec4(vColor * texel.rgb * lighting, texel.a); } """; const string outlineVertexShaderSource = """ #version 330 core layout (location = 0) in vec3 aPosition; uniform mat4 uMvp; void main() { gl_Position = uMvp * vec4(aPosition, 1.0); } """; const string outlineFragmentShaderSource = """ #version 330 core uniform vec4 uColor; out vec4 FragColor; void main() { FragColor = uColor; } """; _meshShader = new ShaderProgram(_gl, meshVertexShaderSource, meshFragmentShaderSource, "Viewport mesh"); _outlineShader = new ShaderProgram(_gl, outlineVertexShaderSource, outlineFragmentShaderSource, "Viewport outline"); _modelLocation = _meshShader.GetUniformLocation("uModel"); _mvpLocation = _meshShader.GetUniformLocation("uMvp"); _lightDirectionLocation = _meshShader.GetUniformLocation("uLightDirectionWorld"); _useTextureLocation = _meshShader.GetUniformLocation("uUseTexture"); _texture0Location = _meshShader.GetUniformLocation("uTexture0"); _outlineMvpLocation = _outlineShader.GetUniformLocation("uMvp"); _outlineColorLocation = _outlineShader.GetUniformLocation("uColor"); } }