using System; using System.Numerics; using System.Runtime.CompilerServices; namespace AcDream.Core.Rendering.Wb { /// /// Utility methods for terrain calculations. /// public static class TerrainUtils { /// /// The width of a road in units. /// public const float RoadWidth = 5f; /// /// The minimum Z component of a surface normal for it to be considered walkable. /// public const float FloorZ = 0.66417414618662751f; /// /// Determines if a surface with the given normal is walkable. /// /// The surface normal. /// True if the surface is walkable, false otherwise. public static bool IsValidWalkable(Vector3 normal) { return normal.Z >= FloorZ; } /// /// Calculates the split direction for a terrain cell based on its coordinates. /// This is deterministic and used to ensure consistency between the renderer and physics/logic. /// /// The global landblock X coordinate. /// The local cell X coordinate (0-7). /// The global landblock Y coordinate. /// The local cell Y coordinate (0-7). /// The split direction for the cell. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static CellSplitDirection CalculateSplitDirection(uint landblockX, uint cellX, uint landblockY, uint cellY) { uint seedA = (landblockX * 8 + cellX) * 214614067u; uint seedB = (landblockY * 8 + cellY) * 1109124029u; uint magicA = seedA + 1813693831u; uint magicB = seedB; float splitDir = magicA - magicB - 1369149221u; return splitDir * 2.3283064e-10f >= 0.5f ? CellSplitDirection.SEtoNW : CellSplitDirection.SWtoNE; } /// /// Gets the interpolated terrain height at a local position within a landblock. /// Uses barycentric interpolation on the cell's triangle pair. /// public static float GetHeight(DatReaderWriter.DBObjs.Region region, TerrainEntry[] lbTerrainEntries, uint landblockX, uint landblockY, Vector3 localPos) { uint cellX = (uint)(localPos.X / 24f); uint cellY = (uint)(localPos.Y / 24f); if (cellX >= 8 || cellY >= 8) return 0f; var splitDirection = CalculateSplitDirection(landblockX, cellX, landblockY, cellY); var bottomLeft = GetTerrainEntryForCell(lbTerrainEntries, cellX, cellY); var bottomRight = GetTerrainEntryForCell(lbTerrainEntries, cellX + 1, cellY); var topRight = GetTerrainEntryForCell(lbTerrainEntries, cellX + 1, cellY + 1); var topLeft = GetTerrainEntryForCell(lbTerrainEntries, cellX, cellY + 1); float h0 = region.LandDefs.LandHeightTable[bottomLeft.Height ?? 0]; float h1 = region.LandDefs.LandHeightTable[bottomRight.Height ?? 0]; float h2 = region.LandDefs.LandHeightTable[topRight.Height ?? 0]; float h3 = region.LandDefs.LandHeightTable[topLeft.Height ?? 0]; float lx = localPos.X - cellX * 24f; float ly = localPos.Y - cellY * 24f; float s = lx / 24f; float t = ly / 24f; if (splitDirection == CellSplitDirection.SWtoNE) { if (s + t <= 1f) { return h0 * (1f - s - t) + h1 * s + h3 * t; } else { float u = s + t - 1f; float v = 1f - s; float w = 1f - u - v; return h1 * w + h2 * u + h3 * v; } } else { if (s >= t) { return h0 * (1f - s) + h1 * (s - t) + h2 * t; } else { return h0 * (1f - t) + h2 * s + h3 * (t - s); } } } /// /// Gets the terrain surface normal at a local position within a landblock. /// public static Vector3 GetNormal(DatReaderWriter.DBObjs.Region region, TerrainEntry[] lbTerrainEntries, uint landblockX, uint landblockY, Vector3 localPos) { uint cellX = (uint)(localPos.X / 24f); uint cellY = (uint)(localPos.Y / 24f); if (cellX >= 8 || cellY >= 8) return new Vector3(0, 0, 1); var splitDirection = CalculateSplitDirection(landblockX, cellX, landblockY, cellY); var bottomLeft = GetTerrainEntryForCell(lbTerrainEntries, cellX, cellY); var bottomRight = GetTerrainEntryForCell(lbTerrainEntries, cellX + 1, cellY); var topRight = GetTerrainEntryForCell(lbTerrainEntries, cellX + 1, cellY + 1); var topLeft = GetTerrainEntryForCell(lbTerrainEntries, cellX, cellY + 1); float h0 = region.LandDefs.LandHeightTable[bottomLeft.Height ?? 0]; float h1 = region.LandDefs.LandHeightTable[bottomRight.Height ?? 0]; float h2 = region.LandDefs.LandHeightTable[topRight.Height ?? 0]; float h3 = region.LandDefs.LandHeightTable[topLeft.Height ?? 0]; float lx = localPos.X - cellX * 24f; float ly = localPos.Y - cellY * 24f; Vector3 p0 = new Vector3(0, 0, h0); Vector3 p1 = new Vector3(24, 0, h1); Vector3 p2 = new Vector3(24, 24, h2); Vector3 p3 = new Vector3(0, 24, h3); if (splitDirection == CellSplitDirection.SWtoNE) { Vector3 normal1 = Vector3.Normalize(Vector3.Cross(p1 - p0, p3 - p0)); Vector3 normal2 = Vector3.Normalize(Vector3.Cross(p2 - p1, p3 - p1)); bool inTri1 = (lx + ly <= 24f); return inTri1 ? normal1 : normal2; } else { Vector3 normal1 = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0)); Vector3 normal2 = Vector3.Normalize(Vector3.Cross(p2 - p0, p3 - p0)); bool inTri1 = (lx >= ly); return inTri1 ? normal1 : normal2; } } /// /// Checks if a local position within a landblock is on a road. /// Uses per-vertex road flags and proximity testing. /// public static bool OnRoad(Vector3 obj, TerrainEntry[] entries) { int x = (int)(obj.X / 24f); int y = (int)(obj.Y / 24f); float rMin = RoadWidth; float rMax = 24f - RoadWidth; uint r0 = GetRoad(entries, x, y); uint r1 = GetRoad(entries, x, y + 1); uint r2 = GetRoad(entries, x + 1, y); uint r3 = GetRoad(entries, x + 1, y + 1); if (r0 == 0 && r1 == 0 && r2 == 0 && r3 == 0) return false; float dx = obj.X - x * 24f; float dy = obj.Y - y * 24f; if (r0 > 0) { if (r1 > 0) { if (r2 > 0) { if (r3 > 0) return true; else return (dx < rMin || dy < rMin); } else { if (r3 > 0) return (dx < rMin || dy > rMax); else return (dx < rMin); } } else { if (r2 > 0) { if (r3 > 0) return (dx > rMax || dy < rMin); else return (dy < rMin); } else { if (r3 > 0) return (Math.Abs(dx - dy) < rMin); else return (dx + dy < rMin); } } } else { if (r1 > 0) { if (r2 > 0) { if (r3 > 0) return (dx > rMax || dy > rMax); else return (Math.Abs(dx + dy - 24f) < rMin); } else { if (r3 > 0) return (dy > rMax); else return (24f + dx - dy < rMin); } } else { if (r2 > 0) { if (r3 > 0) return (dx > rMax); else return (24f - dx + dy < rMin); } else { if (r3 > 0) return (24f * 2f - dx - dy < rMin); else return false; } } } } /// /// Gets the road value for a specific vertex in the 9x9 terrain entry grid. /// public static uint GetRoad(TerrainEntry[] entries, int x, int y) { if (x < 0 || y < 0 || x >= 9 || y >= 9) return 0; var idx = x * 9 + y; if (idx >= entries.Length) return 0; var road = entries[idx].Road ?? 0; return (uint)(road & 0x3); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static TerrainEntry GetTerrainEntryForCell(TerrainEntry[] data, uint cellX, uint cellY) { var idx = (int)(cellX * 9 + cellY); return data != null && idx < data.Length ? data[idx] : new TerrainEntry(); } } }