From 87c45c70acb3042eb304bab4a8ee4a09c9cc76fa Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 16:44:08 +0200 Subject: [PATCH] feat(app): render landblock with height-ramp shader + orbit camera MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 2-stage GLSL shader (vertex + fragment), a Shader helper that compiles/links and exposes SetMatrix4 for uniforms, and an OrbitCamera with yaw/pitch/distance and a 192-unit-centered target for a single landblock. TerrainRenderer now takes a Shader and issues an actual DrawElements call with uView + uProjection uniforms. GameWindow owns the Shader and Camera, routes mouse drag to camera yaw/pitch, and scroll wheel to camera distance. The fragment shader maps world Z to a green-brown-white ramp so lowlands read green, midlands brown, and peaks white — no textures yet, but enough to visually confirm the terrain shape. Shaders are copied to the output dir via a item group. Smoke verified against real dats: process stays alive with no GL errors, no shader compile/link failures, and no exception trail. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.App/AcDream.App.csproj | 5 ++ src/AcDream.App/Rendering/GameWindow.cs | 37 +++++++++++++- src/AcDream.App/Rendering/OrbitCamera.cs | 28 +++++++++++ src/AcDream.App/Rendering/Shader.cs | 50 +++++++++++++++++++ .../Rendering/Shaders/terrain.frag | 14 ++++++ .../Rendering/Shaders/terrain.vert | 14 ++++++ src/AcDream.App/Rendering/TerrainRenderer.cs | 13 +++-- 7 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 src/AcDream.App/Rendering/OrbitCamera.cs create mode 100644 src/AcDream.App/Rendering/Shader.cs create mode 100644 src/AcDream.App/Rendering/Shaders/terrain.frag create mode 100644 src/AcDream.App/Rendering/Shaders/terrain.vert diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj index a1140d6..b1e7d62 100644 --- a/src/AcDream.App/AcDream.App.csproj +++ b/src/AcDream.App/AcDream.App.csproj @@ -19,4 +19,9 @@ + + + PreserveNewest + + diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 5569b70..1c395c9 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -16,7 +16,11 @@ public sealed class GameWindow : IDisposable private GL? _gl; private IInputContext? _input; private TerrainRenderer? _terrain; + private Shader? _shader; + private OrbitCamera? _camera; private DatCollection? _dats; + private float _lastMouseX; + private float _lastMouseY; public GameWindow(string datDir) => _datDir = datDir; @@ -53,9 +57,37 @@ public sealed class GameWindow : IDisposable _window!.Close(); }; + foreach (var mouse in _input.Mice) + { + mouse.MouseMove += (m, pos) => + { + if (m.IsButtonPressed(MouseButton.Left)) + { + _camera!.Yaw -= (pos.X - _lastMouseX) * 0.005f; + _camera!.Pitch = Math.Clamp( + _camera.Pitch + (pos.Y - _lastMouseY) * 0.005f, + 0.1f, 1.5f); + } + _lastMouseX = pos.X; + _lastMouseY = pos.Y; + }; + mouse.Scroll += (_, scroll) => + _camera!.Distance = Math.Clamp(_camera.Distance - scroll.Y * 20f, 50f, 2000f); + } + _gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f); _gl.Enable(EnableCap.DepthTest); + string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders"); + _shader = new Shader(_gl, + Path.Combine(shadersDir, "terrain.vert"), + Path.Combine(shadersDir, "terrain.frag")); + + _camera = new OrbitCamera + { + Aspect = _window!.Size.X / (float)_window.Size.Y, + }; + _dats = new DatCollection(_datDir, DatAccessType.Read); // Find ANY landblock ending in 0xFFFF. Holtburg 0xA9B4FFFF is a @@ -81,18 +113,19 @@ public sealed class GameWindow : IDisposable Console.WriteLine($"loaded landblock 0x{landblockId:X8}"); var meshData = LandblockMesh.Build(block); - _terrain = new TerrainRenderer(_gl, meshData); + _terrain = new TerrainRenderer(_gl, meshData, _shader); } private void OnRender(double deltaSeconds) { _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - _terrain?.Draw(); + _terrain?.Draw(_camera!); } private void OnClosing() { _terrain?.Dispose(); + _shader?.Dispose(); _dats?.Dispose(); _input?.Dispose(); _gl?.Dispose(); diff --git a/src/AcDream.App/Rendering/OrbitCamera.cs b/src/AcDream.App/Rendering/OrbitCamera.cs new file mode 100644 index 0000000..95f9977 --- /dev/null +++ b/src/AcDream.App/Rendering/OrbitCamera.cs @@ -0,0 +1,28 @@ +using System.Numerics; + +namespace AcDream.App.Rendering; + +public sealed class OrbitCamera +{ + public Vector3 Target { get; set; } = new(96, 96, 0); // center of a 192x192 landblock + public float Distance { get; set; } = 300f; + public float Yaw { get; set; } = MathF.PI / 4f; + public float Pitch { get; set; } = MathF.PI / 6f; + public float FovY { get; set; } = MathF.PI / 3f; + public float Aspect { get; set; } = 16f / 9f; + + public Matrix4x4 View + { + get + { + var eye = Target + new Vector3( + Distance * MathF.Cos(Pitch) * MathF.Cos(Yaw), + Distance * MathF.Cos(Pitch) * MathF.Sin(Yaw), + Distance * MathF.Sin(Pitch)); + return Matrix4x4.CreateLookAt(eye, Target, Vector3.UnitZ); + } + } + + public Matrix4x4 Projection + => Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 1f, 5000f); +} diff --git a/src/AcDream.App/Rendering/Shader.cs b/src/AcDream.App/Rendering/Shader.cs new file mode 100644 index 0000000..4c97c23 --- /dev/null +++ b/src/AcDream.App/Rendering/Shader.cs @@ -0,0 +1,50 @@ +using System.Numerics; +using Silk.NET.OpenGL; + +namespace AcDream.App.Rendering; + +public sealed class Shader : IDisposable +{ + private readonly GL _gl; + public uint Program { get; } + + public Shader(GL gl, string vertexPath, string fragmentPath) + { + _gl = gl; + uint vert = Compile(File.ReadAllText(vertexPath), ShaderType.VertexShader); + uint frag = Compile(File.ReadAllText(fragmentPath), ShaderType.FragmentShader); + + Program = _gl.CreateProgram(); + _gl.AttachShader(Program, vert); + _gl.AttachShader(Program, frag); + _gl.LinkProgram(Program); + _gl.GetProgram(Program, ProgramPropertyARB.LinkStatus, out int linked); + if (linked == 0) + throw new Exception("program link failed: " + _gl.GetProgramInfoLog(Program)); + _gl.DetachShader(Program, vert); + _gl.DetachShader(Program, frag); + _gl.DeleteShader(vert); + _gl.DeleteShader(frag); + } + + private uint Compile(string source, ShaderType type) + { + uint id = _gl.CreateShader(type); + _gl.ShaderSource(id, source); + _gl.CompileShader(id); + _gl.GetShader(id, ShaderParameterName.CompileStatus, out int ok); + if (ok == 0) + throw new Exception($"{type} compile failed: " + _gl.GetShaderInfoLog(id)); + return id; + } + + public void Use() => _gl.UseProgram(Program); + + public unsafe void SetMatrix4(string name, Matrix4x4 m) + { + int loc = _gl.GetUniformLocation(Program, name); + _gl.UniformMatrix4(loc, 1, false, (float*)&m); + } + + public void Dispose() => _gl.DeleteProgram(Program); +} diff --git a/src/AcDream.App/Rendering/Shaders/terrain.frag b/src/AcDream.App/Rendering/Shaders/terrain.frag new file mode 100644 index 0000000..d6e747b --- /dev/null +++ b/src/AcDream.App/Rendering/Shaders/terrain.frag @@ -0,0 +1,14 @@ +#version 430 core +in float vHeight; +out vec4 fragColor; + +void main() { + float t = clamp(vHeight / 200.0, 0.0, 1.0); + vec3 low = vec3(0.10, 0.35, 0.15); // green lowland + vec3 mid = vec3(0.55, 0.45, 0.25); // brown mid + vec3 high = vec3(0.90, 0.90, 0.95); // snowy peak + vec3 color = t < 0.5 + ? mix(low, mid, t * 2.0) + : mix(mid, high, (t - 0.5) * 2.0); + fragColor = vec4(color, 1.0); +} diff --git a/src/AcDream.App/Rendering/Shaders/terrain.vert b/src/AcDream.App/Rendering/Shaders/terrain.vert new file mode 100644 index 0000000..1f3b04f --- /dev/null +++ b/src/AcDream.App/Rendering/Shaders/terrain.vert @@ -0,0 +1,14 @@ +#version 430 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aTex; + +uniform mat4 uView; +uniform mat4 uProjection; + +out float vHeight; + +void main() { + vHeight = aPos.z; + gl_Position = uProjection * uView * vec4(aPos, 1.0); +} diff --git a/src/AcDream.App/Rendering/TerrainRenderer.cs b/src/AcDream.App/Rendering/TerrainRenderer.cs index 2c393ec..e61c301 100644 --- a/src/AcDream.App/Rendering/TerrainRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainRenderer.cs @@ -6,14 +6,16 @@ namespace AcDream.App.Rendering; public sealed unsafe class TerrainRenderer : IDisposable { private readonly GL _gl; + private readonly Shader _shader; private readonly uint _vao; private readonly uint _vbo; private readonly uint _ebo; private readonly int _indexCount; - public TerrainRenderer(GL gl, LandblockMeshData meshData) + public TerrainRenderer(GL gl, LandblockMeshData meshData, Shader shader) { _gl = gl; + _shader = shader; _indexCount = meshData.Indices.Length; _vao = _gl.GenVertexArray(); @@ -43,12 +45,13 @@ public sealed unsafe class TerrainRenderer : IDisposable _gl.BindVertexArray(0); } - public void Draw() + public void Draw(OrbitCamera camera) { - // Shader binding + draw call come in Task 9. For this task, binding the - // VAO is enough to prove the buffers uploaded without GL errors. + _shader.Use(); + _shader.SetMatrix4("uView", camera.View); + _shader.SetMatrix4("uProjection", camera.Projection); _gl.BindVertexArray(_vao); - // intentionally no draw call yet + _gl.DrawElements(PrimitiveType.Triangles, (uint)_indexCount, DrawElementsType.UnsignedInt, (void*)0); _gl.BindVertexArray(0); }