feat(app): render 3x3 neighbor landblocks with texture atlas
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
347a7e92ff
commit
560100e5b6
4 changed files with 109 additions and 79 deletions
|
|
@ -1,6 +1,4 @@
|
|||
using AcDream.Core.Terrain;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.Maths;
|
||||
|
|
@ -98,75 +96,67 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
_dats = new DatCollection(_datDir, DatAccessType.Read);
|
||||
|
||||
// Find ANY landblock ending in 0xFFFF. Holtburg 0xA9B4FFFF is a
|
||||
// good default; fall back to the first one we find. Using Get<T>
|
||||
// (returns null on miss) rather than TryGet to sidestep
|
||||
// [MaybeNullWhen(false)] nullable-generic analysis under
|
||||
// TreatWarningsAsErrors.
|
||||
uint landblockId = 0xA9B4FFFFu;
|
||||
var block = _dats.Get<LandBlock>(landblockId);
|
||||
if (block is null)
|
||||
{
|
||||
foreach (var file in _dats.Cell.Tree)
|
||||
{
|
||||
if ((file.Id & 0xFFFFu) == 0xFFFFu)
|
||||
{
|
||||
landblockId = file.Id;
|
||||
block = _dats.Get<LandBlock>(landblockId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint centerLandblockId = 0xA9B4FFFFu;
|
||||
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
|
||||
|
||||
if (block is null)
|
||||
throw new InvalidOperationException("no landblock found in cell dat");
|
||||
|
||||
Console.WriteLine($"loaded landblock 0x{landblockId:X8}");
|
||||
|
||||
// Load the non-linear LandHeightTable from the Region dat. AC encodes
|
||||
// per-vertex heights as byte indices into this 256-entry float table,
|
||||
// not as a simple * 2.0 ramp — building placements depend on the real
|
||||
// table, so terrain rendered with the simplified scale would leave
|
||||
// buildings floating or buried.
|
||||
var region = _dats.Get<DatReaderWriter.DBObjs.Region>(0x13000000u);
|
||||
var heightTable = region?.LandDefs.LandHeightTable;
|
||||
if (heightTable is null || heightTable.Length < 256)
|
||||
throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated");
|
||||
|
||||
var meshData = LandblockMesh.Build(block, heightTable, new Dictionary<uint, uint>());
|
||||
_terrain = new TerrainRenderer(_gl, meshData, _shader);
|
||||
// Build the terrain atlas once from the Region dat.
|
||||
var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats);
|
||||
|
||||
_terrain = new TerrainRenderer(_gl, _shader, terrainAtlas);
|
||||
|
||||
// Load the 3x3 neighbor grid.
|
||||
var worldView = AcDream.Core.World.WorldView.Load(_dats, centerLandblockId);
|
||||
Console.WriteLine($"loaded {worldView.Landblocks.Count} landblocks in 3x3 grid");
|
||||
|
||||
int centerX = (int)((centerLandblockId >> 24) & 0xFFu);
|
||||
int centerY = (int)((centerLandblockId >> 16) & 0xFFu);
|
||||
|
||||
foreach (var lb in worldView.Landblocks)
|
||||
{
|
||||
var meshData = AcDream.Core.Terrain.LandblockMesh.Build(
|
||||
lb.Heightmap, heightTable, terrainAtlas.TerrainTypeToLayer);
|
||||
|
||||
// Compute world origin for this landblock relative to the center.
|
||||
int lbX = (int)((lb.LandblockId >> 24) & 0xFFu);
|
||||
int lbY = (int)((lb.LandblockId >> 16) & 0xFFu);
|
||||
var origin = new System.Numerics.Vector3(
|
||||
(lbX - centerX) * 192f,
|
||||
(lbY - centerY) * 192f,
|
||||
0f);
|
||||
|
||||
_terrain.AddLandblock(meshData, origin);
|
||||
}
|
||||
|
||||
_textureCache = new TextureCache(_gl, _dats);
|
||||
_staticMesh = new StaticMeshRenderer(_gl, _meshShader, _textureCache);
|
||||
|
||||
// Load LandBlockInfo for Holtburg, hydrate entities.
|
||||
var info = _dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>((landblockId & 0xFFFF0000u) | 0xFFFEu);
|
||||
var entities = info is not null
|
||||
? AcDream.Core.World.LandblockLoader.BuildEntitiesFromInfo(info)
|
||||
: Array.Empty<AcDream.Core.World.WorldEntity>();
|
||||
// Hydrate entities from ALL loaded landblocks, not just the center.
|
||||
var allEntities = worldView.AllEntities.ToList();
|
||||
Console.WriteLine($"hydrating {allEntities.Count} entities across {worldView.Landblocks.Count} landblocks");
|
||||
|
||||
// Populate MeshRefs for each entity by resolving its source id to GfxObj or Setup
|
||||
// and extracting sub-meshes. Store back onto the entity. Since WorldEntity is
|
||||
// `required init`, we rebuild the entity here.
|
||||
var hydratedEntities = new List<AcDream.Core.World.WorldEntity>(entities.Count);
|
||||
foreach (var e in entities)
|
||||
var hydratedEntities = new List<AcDream.Core.World.WorldEntity>(allEntities.Count);
|
||||
foreach (var e in allEntities)
|
||||
{
|
||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
||||
|
||||
if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x01000000u)
|
||||
{
|
||||
// GfxObj: one mesh ref with identity transform.
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
||||
if (gfx is not null)
|
||||
{
|
||||
var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx);
|
||||
_staticMesh.EnsureUploaded(e.SourceGfxObjOrSetupId, subMeshes);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity));
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
||||
e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity));
|
||||
}
|
||||
}
|
||||
else if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
// Setup: flatten into parts, upload each part's GfxObj.
|
||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
{
|
||||
|
|
@ -184,11 +174,21 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
if (meshRefs.Count > 0)
|
||||
{
|
||||
// Add the landblock origin to the entity's position so the static
|
||||
// mesh renderer draws it at the correct world location.
|
||||
var sourceLandblock = worldView.Landblocks.First(lb => lb.Entities.Contains(e));
|
||||
int lbX = (int)((sourceLandblock.LandblockId >> 24) & 0xFFu);
|
||||
int lbY = (int)((sourceLandblock.LandblockId >> 16) & 0xFFu);
|
||||
var worldOffset = new System.Numerics.Vector3(
|
||||
(lbX - centerX) * 192f,
|
||||
(lbY - centerY) * 192f,
|
||||
0f);
|
||||
|
||||
hydratedEntities.Add(new AcDream.Core.World.WorldEntity
|
||||
{
|
||||
Id = e.Id,
|
||||
SourceGfxObjOrSetupId = e.SourceGfxObjOrSetupId,
|
||||
Position = e.Position,
|
||||
Position = e.Position + worldOffset,
|
||||
Rotation = e.Rotation,
|
||||
MeshRefs = meshRefs,
|
||||
});
|
||||
|
|
@ -196,7 +196,7 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
_entities = hydratedEntities;
|
||||
Console.WriteLine($"hydrated {_entities.Count} entities on landblock 0x{landblockId:X8}");
|
||||
Console.WriteLine($"hydrated {_entities.Count} entities");
|
||||
}
|
||||
|
||||
private void OnRender(double deltaSeconds)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
#version 430 core
|
||||
in float vHeight;
|
||||
in vec2 vTex;
|
||||
in flat uint vLayer;
|
||||
out vec4 fragColor;
|
||||
|
||||
uniform sampler2DArray uAtlas;
|
||||
|
||||
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);
|
||||
fragColor = texture(uAtlas, vec3(vTex, float(vLayer)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@
|
|||
layout(location = 0) in vec3 aPos;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTex;
|
||||
layout(location = 3) in uint aTerrainLayer;
|
||||
|
||||
uniform mat4 uModel;
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
out float vHeight;
|
||||
out vec2 vTex;
|
||||
out flat uint vLayer;
|
||||
|
||||
void main() {
|
||||
vHeight = aPos.z;
|
||||
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
||||
vTex = aTex;
|
||||
vLayer = aTerrainLayer;
|
||||
gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Terrain;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
|
|
@ -7,33 +8,39 @@ 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;
|
||||
private readonly TerrainAtlas _atlas;
|
||||
private readonly List<LandblockGpu> _landblocks = new();
|
||||
|
||||
public TerrainRenderer(GL gl, LandblockMeshData meshData, Shader shader)
|
||||
public TerrainRenderer(GL gl, Shader shader, TerrainAtlas atlas)
|
||||
{
|
||||
_gl = gl;
|
||||
_shader = shader;
|
||||
_indexCount = meshData.Indices.Length;
|
||||
_atlas = atlas;
|
||||
}
|
||||
|
||||
_vao = _gl.GenVertexArray();
|
||||
_gl.BindVertexArray(_vao);
|
||||
public void AddLandblock(LandblockMeshData meshData, Vector3 worldOrigin)
|
||||
{
|
||||
var gpu = new LandblockGpu
|
||||
{
|
||||
Vao = _gl.GenVertexArray(),
|
||||
WorldOrigin = worldOrigin,
|
||||
IndexCount = meshData.Indices.Length,
|
||||
};
|
||||
|
||||
_vbo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
||||
_gl.BindVertexArray(gpu.Vao);
|
||||
|
||||
gpu.Vbo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, gpu.Vbo);
|
||||
fixed (void* p = meshData.Vertices)
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(meshData.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw);
|
||||
|
||||
_ebo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ebo);
|
||||
gpu.Ebo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, gpu.Ebo);
|
||||
fixed (void* p = meshData.Indices)
|
||||
_gl.BufferData(BufferTargetARB.ElementArrayBuffer,
|
||||
(nuint)(meshData.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw);
|
||||
|
||||
// vertex layout: position(3f), normal(3f), texcoord(2f) = 8 floats stride
|
||||
uint stride = (uint)sizeof(Vertex);
|
||||
_gl.EnableVertexAttribArray(0);
|
||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
||||
|
|
@ -45,22 +52,45 @@ public sealed unsafe class TerrainRenderer : IDisposable
|
|||
_gl.VertexAttribIPointer(3, 1, VertexAttribIType.UnsignedInt, stride, (void*)(8 * sizeof(float)));
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
_landblocks.Add(gpu);
|
||||
}
|
||||
|
||||
public void Draw(OrbitCamera camera)
|
||||
public void Draw(OrbitCamera camera) // ICamera in Task 5
|
||||
{
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uView", camera.View);
|
||||
_shader.SetMatrix4("uProjection", camera.Projection);
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawElements(PrimitiveType.Triangles, (uint)_indexCount, DrawElementsType.UnsignedInt, (void*)0);
|
||||
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, _atlas.GlTexture);
|
||||
|
||||
foreach (var lb in _landblocks)
|
||||
{
|
||||
var model = Matrix4x4.CreateTranslation(lb.WorldOrigin);
|
||||
_shader.SetMatrix4("uModel", model);
|
||||
_gl.BindVertexArray(lb.Vao);
|
||||
_gl.DrawElements(PrimitiveType.Triangles, (uint)lb.IndexCount, DrawElementsType.UnsignedInt, (void*)0);
|
||||
}
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteBuffer(_vbo);
|
||||
_gl.DeleteBuffer(_ebo);
|
||||
_gl.DeleteVertexArray(_vao);
|
||||
foreach (var lb in _landblocks)
|
||||
{
|
||||
_gl.DeleteBuffer(lb.Vbo);
|
||||
_gl.DeleteBuffer(lb.Ebo);
|
||||
_gl.DeleteVertexArray(lb.Vao);
|
||||
}
|
||||
_landblocks.Clear();
|
||||
}
|
||||
|
||||
private sealed class LandblockGpu
|
||||
{
|
||||
public uint Vao;
|
||||
public uint Vbo;
|
||||
public uint Ebo;
|
||||
public int IndexCount;
|
||||
public Vector3 WorldOrigin;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue