feat(app): render landblock with height-ramp shader + orbit camera
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 <None Update> 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) <noreply@anthropic.com>
This commit is contained in:
parent
8356fe65a0
commit
87c45c70ac
7 changed files with 154 additions and 7 deletions
|
|
@ -19,4 +19,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
<ProjectReference Include="..\AcDream.Core\AcDream.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Rendering\Shaders\*.*">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,11 @@ public sealed class GameWindow : IDisposable
|
||||||
private GL? _gl;
|
private GL? _gl;
|
||||||
private IInputContext? _input;
|
private IInputContext? _input;
|
||||||
private TerrainRenderer? _terrain;
|
private TerrainRenderer? _terrain;
|
||||||
|
private Shader? _shader;
|
||||||
|
private OrbitCamera? _camera;
|
||||||
private DatCollection? _dats;
|
private DatCollection? _dats;
|
||||||
|
private float _lastMouseX;
|
||||||
|
private float _lastMouseY;
|
||||||
|
|
||||||
public GameWindow(string datDir) => _datDir = datDir;
|
public GameWindow(string datDir) => _datDir = datDir;
|
||||||
|
|
||||||
|
|
@ -53,9 +57,37 @@ public sealed class GameWindow : IDisposable
|
||||||
_window!.Close();
|
_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.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||||
_gl.Enable(EnableCap.DepthTest);
|
_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);
|
_dats = new DatCollection(_datDir, DatAccessType.Read);
|
||||||
|
|
||||||
// Find ANY landblock ending in 0xFFFF. Holtburg 0xA9B4FFFF is a
|
// 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}");
|
Console.WriteLine($"loaded landblock 0x{landblockId:X8}");
|
||||||
|
|
||||||
var meshData = LandblockMesh.Build(block);
|
var meshData = LandblockMesh.Build(block);
|
||||||
_terrain = new TerrainRenderer(_gl, meshData);
|
_terrain = new TerrainRenderer(_gl, meshData, _shader);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRender(double deltaSeconds)
|
private void OnRender(double deltaSeconds)
|
||||||
{
|
{
|
||||||
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||||
_terrain?.Draw();
|
_terrain?.Draw(_camera!);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClosing()
|
private void OnClosing()
|
||||||
{
|
{
|
||||||
_terrain?.Dispose();
|
_terrain?.Dispose();
|
||||||
|
_shader?.Dispose();
|
||||||
_dats?.Dispose();
|
_dats?.Dispose();
|
||||||
_input?.Dispose();
|
_input?.Dispose();
|
||||||
_gl?.Dispose();
|
_gl?.Dispose();
|
||||||
|
|
|
||||||
28
src/AcDream.App/Rendering/OrbitCamera.cs
Normal file
28
src/AcDream.App/Rendering/OrbitCamera.cs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
50
src/AcDream.App/Rendering/Shader.cs
Normal file
50
src/AcDream.App/Rendering/Shader.cs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
14
src/AcDream.App/Rendering/Shaders/terrain.frag
Normal file
14
src/AcDream.App/Rendering/Shaders/terrain.frag
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
14
src/AcDream.App/Rendering/Shaders/terrain.vert
Normal file
14
src/AcDream.App/Rendering/Shaders/terrain.vert
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -6,14 +6,16 @@ namespace AcDream.App.Rendering;
|
||||||
public sealed unsafe class TerrainRenderer : IDisposable
|
public sealed unsafe class TerrainRenderer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly GL _gl;
|
private readonly GL _gl;
|
||||||
|
private readonly Shader _shader;
|
||||||
private readonly uint _vao;
|
private readonly uint _vao;
|
||||||
private readonly uint _vbo;
|
private readonly uint _vbo;
|
||||||
private readonly uint _ebo;
|
private readonly uint _ebo;
|
||||||
private readonly int _indexCount;
|
private readonly int _indexCount;
|
||||||
|
|
||||||
public TerrainRenderer(GL gl, LandblockMeshData meshData)
|
public TerrainRenderer(GL gl, LandblockMeshData meshData, Shader shader)
|
||||||
{
|
{
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
|
_shader = shader;
|
||||||
_indexCount = meshData.Indices.Length;
|
_indexCount = meshData.Indices.Length;
|
||||||
|
|
||||||
_vao = _gl.GenVertexArray();
|
_vao = _gl.GenVertexArray();
|
||||||
|
|
@ -43,12 +45,13 @@ public sealed unsafe class TerrainRenderer : IDisposable
|
||||||
_gl.BindVertexArray(0);
|
_gl.BindVertexArray(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw()
|
public void Draw(OrbitCamera camera)
|
||||||
{
|
{
|
||||||
// Shader binding + draw call come in Task 9. For this task, binding the
|
_shader.Use();
|
||||||
// VAO is enough to prove the buffers uploaded without GL errors.
|
_shader.SetMatrix4("uView", camera.View);
|
||||||
|
_shader.SetMatrix4("uProjection", camera.Projection);
|
||||||
_gl.BindVertexArray(_vao);
|
_gl.BindVertexArray(_vao);
|
||||||
// intentionally no draw call yet
|
_gl.DrawElements(PrimitiveType.Triangles, (uint)_indexCount, DrawElementsType.UnsignedInt, (void*)0);
|
||||||
_gl.BindVertexArray(0);
|
_gl.BindVertexArray(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue