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);
}