feat(O-T2): extract pure stateless helpers to AcDream.Core.Rendering.Wb
Verbatim copy of 5 WorldBuilder files into src/AcDream.Core/Rendering/Wb/: - TextureHelpers.cs (pixel-format decoders, Chorizite Lib) - SceneryHelpers.cs (scenery transforms, Chorizite Lib) - TerrainUtils.cs, TerrainEntry.cs, CellSplitDirection.cs (WB.Shared Landscape) Namespace migrated from WorldBuilder.* / Chorizite.OpenGLSDLBackend.Lib to AcDream.Core.Rendering.Wb per O-D11. [MemoryPackable] stripped from TerrainEntry per O-D10 (we don't serialize the struct). Updated 3 source files + 1 test file to import from the new namespace. Verbatim discipline (O-D1): only namespace + MemoryPack attribute changed. All algorithm bodies byte-identical to upstream. Note: TextureHelpers omits IsAlphaFormat() and GetCompressedLayerSize() because those reference Chorizite.Core.Render.Enums.TextureFormat, a type that has no path into AcDream.Core without adding an unwanted NuGet dep. Neither method is called from Core or the test suite; the omission is safe. Verified on main checkout: dotnet build green (0 errors), dotnet test green — Failed: 8, Passed: 1147, Skipped: 0, Total: 1155 (baseline maintained). TextureDecodeConformanceTests (9/9) pass byte-for-byte after namespace swap. AcDream.Core project alone builds green in this worktree (App-layer failures are pre-existing, blocked by empty WB submodule, addressed in Tasks 3+4). Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8c073e0c4c
commit
16bc10c99d
9 changed files with 785 additions and 5 deletions
18
src/AcDream.Core/Rendering/Wb/CellSplitDirection.cs
Normal file
18
src/AcDream.Core/Rendering/Wb/CellSplitDirection.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace AcDream.Core.Rendering.Wb
|
||||
{
|
||||
/// <summary>
|
||||
/// The direction a terrain cell is split into triangles.
|
||||
/// </summary>
|
||||
public enum CellSplitDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// South-West to North-East
|
||||
/// </summary>
|
||||
SWtoNE,
|
||||
|
||||
/// <summary>
|
||||
/// South-East to North-West
|
||||
/// </summary>
|
||||
SEtoNW
|
||||
}
|
||||
}
|
||||
107
src/AcDream.Core/Rendering/Wb/SceneryHelpers.cs
Normal file
107
src/AcDream.Core/Rendering/Wb/SceneryHelpers.cs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
using DatReaderWriter.Types;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Stateless utility methods for deterministic scenery placement.
|
||||
/// </summary>
|
||||
public static class SceneryHelpers {
|
||||
/// <summary>
|
||||
/// Displaces a scenery object into a pseudo-randomized location
|
||||
/// </summary>
|
||||
public static Vector3 Displace(ObjectDesc obj, uint ix, uint iy, uint iq) {
|
||||
float x;
|
||||
float y;
|
||||
float z = obj.BaseLoc.Origin.Z;
|
||||
var loc = obj.BaseLoc;
|
||||
|
||||
if (obj.DisplaceX <= 0)
|
||||
x = loc.Origin.X;
|
||||
else
|
||||
x = (float)(unchecked((uint)(1813693831u * iy - (iq + 45773u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
|
||||
* 2.3283064e-10 * obj.DisplaceX + loc.Origin.X);
|
||||
|
||||
if (obj.DisplaceY <= 0)
|
||||
y = loc.Origin.Y;
|
||||
else
|
||||
y = (float)(unchecked((uint)(1813693831u * iy - (iq + 72719u) * (1360117743u * iy * ix + 1888038839u) - 1109124029u * ix))
|
||||
* 2.3283064e-10 * obj.DisplaceY + loc.Origin.Y);
|
||||
|
||||
var quadrantVal = unchecked((uint)(1813693831u * iy - ix * (1870387557u * iy + 1109124029u) - 402451965u)) * 2.3283064e-10f;
|
||||
|
||||
if (quadrantVal >= 0.75) return new Vector3(y, -x, z);
|
||||
if (quadrantVal >= 0.5) return new Vector3(-x, -y, z);
|
||||
if (quadrantVal >= 0.25) return new Vector3(-y, x, z);
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the scale for a scenery object
|
||||
/// </summary>
|
||||
public static float ScaleObj(ObjectDesc obj, uint x, uint y, uint k) {
|
||||
var minScale = obj.MinScale;
|
||||
var maxScale = obj.MaxScale;
|
||||
|
||||
if (minScale == maxScale)
|
||||
return maxScale;
|
||||
|
||||
return (float)(Math.Pow(maxScale / minScale,
|
||||
unchecked((uint)(1813693831u * y - (k + 32593u) * (1360117743u * y * x + 1888038839u) - 1109124029u * x)) * 2.3283064e-10) * minScale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the rotation for a scenery object as a Quaternion.
|
||||
/// </summary>
|
||||
public static Quaternion RotateObj(ObjectDesc obj, uint x, uint y, uint k, Vector3 loc) {
|
||||
var baseOrientation = new Quaternion(obj.BaseLoc.Orientation.X, obj.BaseLoc.Orientation.Y, obj.BaseLoc.Orientation.Z, obj.BaseLoc.Orientation.W);
|
||||
|
||||
if (obj.MaxRotation <= 0.0f)
|
||||
return baseOrientation;
|
||||
|
||||
var degrees = (float)(unchecked((uint)(1813693831u * y - (k + 63127u) * (1360117743u * y * x + 1888038839u) - 1109124029u * x))
|
||||
* 2.3283064e-10 * obj.MaxRotation);
|
||||
|
||||
return SetHeading(baseOrientation, degrees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aligns an object to a plane normal, returning Quaternion.
|
||||
/// </summary>
|
||||
public static Quaternion ObjAlign(ObjectDesc obj, Vector3 normal, float z, Vector3 loc) {
|
||||
var baseOrientation = new Quaternion(obj.BaseLoc.Orientation.X, obj.BaseLoc.Orientation.Y, obj.BaseLoc.Orientation.Z, obj.BaseLoc.Orientation.W);
|
||||
|
||||
var negNormal = -normal;
|
||||
var headingDeg = (450.0f - (MathF.Atan2(negNormal.Y, negNormal.X) * 180f / MathF.PI)) % 360f;
|
||||
|
||||
return SetHeading(baseOrientation, headingDeg);
|
||||
}
|
||||
|
||||
public static Quaternion SetHeading(Quaternion orientation, float degrees) {
|
||||
var rads = degrees * MathF.PI / 180f;
|
||||
|
||||
var matrix = Matrix4x4.CreateFromQuaternion(orientation);
|
||||
var heading = new Vector3(MathF.Sin(rads), MathF.Cos(rads), matrix.M23 + matrix.M13);
|
||||
|
||||
var normal = Vector3.Normalize(heading);
|
||||
|
||||
// Avoid attempting to rotate if normal is too small
|
||||
if (normal.LengthSquared() < 0.0001f)
|
||||
return orientation;
|
||||
|
||||
var headingDeg = MathF.Atan2(normal.Y, normal.X) * 180f / MathF.PI;
|
||||
var zDeg = 450.0f - headingDeg;
|
||||
var zRot = -(zDeg % 360.0f) * MathF.PI / 180f;
|
||||
|
||||
var xRot = MathF.Asin(normal.Z);
|
||||
return Quaternion.CreateFromYawPitchRoll(xRot, 0, zRot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if floor slope is within bounds for this object
|
||||
/// </summary>
|
||||
public static bool CheckSlope(ObjectDesc obj, float zNormal) {
|
||||
return zNormal >= obj.MinSlope && zNormal <= obj.MaxSlope;
|
||||
}
|
||||
}
|
||||
}
|
||||
239
src/AcDream.Core/Rendering/Wb/TerrainEntry.cs
Normal file
239
src/AcDream.Core/Rendering/Wb/TerrainEntry.cs
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.Core.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Flags that indicate which fields are present in TerrainEntry
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum TerrainEntryFlags : byte {
|
||||
None = 0,
|
||||
Height = 1 << 0,
|
||||
Texture = 1 << 1,
|
||||
Scenery = 1 << 2,
|
||||
Road = 1 << 3,
|
||||
Encounters = 1 << 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a terrain entry with optional height, texture, scenery, road, encounters, and flags.
|
||||
/// Flags are automatically synchronized with nullable fields.
|
||||
/// Packed format: [Height:8][Texture:5][Scenery:5][Encounters:4][Road:3][Unused:2][Flags:5]
|
||||
/// </summary>
|
||||
public struct TerrainEntry {
|
||||
private const int HEIGHT_SHIFT = 24;
|
||||
private const uint HEIGHT_MASK = 0xFF000000;
|
||||
|
||||
private const int TEXTURE_SHIFT = 19;
|
||||
private const uint TEXTURE_MASK = 0x00F80000;
|
||||
|
||||
private const int SCENERY_SHIFT = 14;
|
||||
private const uint SCENERY_MASK = 0x0007C000;
|
||||
|
||||
private const int ENCOUNTERS_SHIFT = 10;
|
||||
private const uint ENCOUNTERS_MASK = 0x00003C00;
|
||||
|
||||
private const int ROAD_SHIFT = 7;
|
||||
private const uint ROAD_MASK = 0x00000380;
|
||||
|
||||
private const int FLAGS_SHIFT = 0;
|
||||
private const uint FLAGS_MASK = 0x0000001F;
|
||||
|
||||
private uint _data;
|
||||
|
||||
/// <summary>Gets the flags for this terrain entry, indicating which fields are set.</summary>
|
||||
public TerrainEntryFlags Flags {
|
||||
get => (TerrainEntryFlags)((_data & FLAGS_MASK) >> FLAGS_SHIFT);
|
||||
private set => _data = (_data & ~FLAGS_MASK) | ((uint)value << FLAGS_SHIFT);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the height of the terrain.</summary>
|
||||
public byte? Height {
|
||||
get => (_data & (uint)TerrainEntryFlags.Height) != 0
|
||||
? (byte)((_data & HEIGHT_MASK) >> HEIGHT_SHIFT)
|
||||
: null;
|
||||
set {
|
||||
if (value.HasValue) {
|
||||
_data = (_data & ~HEIGHT_MASK) | ((uint)value.Value << HEIGHT_SHIFT);
|
||||
Flags |= TerrainEntryFlags.Height;
|
||||
}
|
||||
else {
|
||||
_data &= ~HEIGHT_MASK;
|
||||
Flags &= ~TerrainEntryFlags.Height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the texture type of the terrain.</summary>
|
||||
public byte? Type {
|
||||
get => (_data & (uint)TerrainEntryFlags.Texture) != 0
|
||||
? (byte)((_data & TEXTURE_MASK) >> TEXTURE_SHIFT)
|
||||
: null;
|
||||
set {
|
||||
if (value.HasValue) {
|
||||
if (value.Value > 31)
|
||||
throw new ArgumentOutOfRangeException(nameof(Type), "Texture must be 0-31");
|
||||
_data = (_data & ~TEXTURE_MASK) | ((uint)value.Value << TEXTURE_SHIFT);
|
||||
Flags |= TerrainEntryFlags.Texture;
|
||||
}
|
||||
else {
|
||||
_data &= ~TEXTURE_MASK;
|
||||
Flags &= ~TerrainEntryFlags.Texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the scenery index of the terrain.</summary>
|
||||
public byte? Scenery {
|
||||
get => (_data & (uint)TerrainEntryFlags.Scenery) != 0
|
||||
? (byte)((_data & SCENERY_MASK) >> SCENERY_SHIFT)
|
||||
: null;
|
||||
set {
|
||||
if (value.HasValue) {
|
||||
if (value.Value > 31)
|
||||
throw new ArgumentOutOfRangeException(nameof(Scenery), "Scenery must be 0-31");
|
||||
_data = (_data & ~SCENERY_MASK) | ((uint)value.Value << SCENERY_SHIFT);
|
||||
Flags |= TerrainEntryFlags.Scenery;
|
||||
}
|
||||
else {
|
||||
_data &= ~SCENERY_MASK;
|
||||
Flags &= ~TerrainEntryFlags.Scenery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the road index of the terrain.</summary>
|
||||
public byte? Road {
|
||||
get => (_data & (uint)TerrainEntryFlags.Road) != 0
|
||||
? (byte)((_data & ROAD_MASK) >> ROAD_SHIFT)
|
||||
: null;
|
||||
set {
|
||||
if (value.HasValue) {
|
||||
if (value.Value > 7)
|
||||
throw new ArgumentOutOfRangeException(nameof(Road), "Road must be 0-7");
|
||||
_data = (_data & ~ROAD_MASK) | ((uint)value.Value << ROAD_SHIFT);
|
||||
Flags |= TerrainEntryFlags.Road;
|
||||
}
|
||||
else {
|
||||
_data &= ~ROAD_MASK;
|
||||
Flags &= ~TerrainEntryFlags.Road;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the encounters index of the terrain.</summary>
|
||||
public byte? Encounters {
|
||||
get => (_data & (uint)TerrainEntryFlags.Encounters) != 0
|
||||
? (byte)((_data & ENCOUNTERS_MASK) >> ENCOUNTERS_SHIFT)
|
||||
: null;
|
||||
set {
|
||||
if (value.HasValue) {
|
||||
if (value.Value > 15)
|
||||
throw new ArgumentOutOfRangeException(nameof(Encounters), "Encounters must be 0-15");
|
||||
_data = (_data & ~ENCOUNTERS_MASK) | ((uint)value.Value << ENCOUNTERS_SHIFT);
|
||||
Flags |= TerrainEntryFlags.Encounters;
|
||||
}
|
||||
else {
|
||||
_data &= ~ENCOUNTERS_MASK;
|
||||
Flags &= ~TerrainEntryFlags.Encounters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="TerrainEntry"/> struct.</summary>
|
||||
public TerrainEntry() {
|
||||
_data = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TerrainEntry"/> struct with specified components.
|
||||
/// </summary>
|
||||
/// <param name="height">The terrain height.</param>
|
||||
/// <param name="texture">The texture type.</param>
|
||||
/// <param name="scenery">The scenery index.</param>
|
||||
/// <param name="road">The road index.</param>
|
||||
/// <param name="encounters">The encounters index.</param>
|
||||
public TerrainEntry(byte? height, byte? texture, byte? scenery, byte? road, byte? encounters) {
|
||||
_data = 0;
|
||||
|
||||
if (texture.HasValue && texture.Value > 31)
|
||||
throw new ArgumentOutOfRangeException(nameof(texture), "Texture must be 0-31");
|
||||
if (scenery.HasValue && scenery.Value > 31)
|
||||
throw new ArgumentOutOfRangeException(nameof(scenery), "Scenery must be 0-31");
|
||||
if (road.HasValue && road.Value > 7)
|
||||
throw new ArgumentOutOfRangeException(nameof(road), "Road must be 0-7");
|
||||
if (encounters.HasValue && encounters.Value > 15)
|
||||
throw new ArgumentOutOfRangeException(nameof(encounters), "Encounters must be 0-15");
|
||||
|
||||
Height = height;
|
||||
Type = texture;
|
||||
Scenery = scenery;
|
||||
Road = road;
|
||||
Encounters = encounters;
|
||||
}
|
||||
|
||||
/// <summary>Creates a <see cref="TerrainEntry"/> with only height data.</summary>
|
||||
/// <param name="height">The height.</param>
|
||||
/// <returns>A new <see cref="TerrainEntry"/>.</returns>
|
||||
public static TerrainEntry FromHeight(byte height) => new(height, null, null, null, null);
|
||||
|
||||
/// <summary>Creates a <see cref="TerrainEntry"/> with only texture data.</summary>
|
||||
/// <param name="texture">The texture type.</param>
|
||||
/// <returns>A new <see cref="TerrainEntry"/>.</returns>
|
||||
public static TerrainEntry FromTexture(byte texture) => new(null, texture, null, null, null);
|
||||
|
||||
/// <summary>Creates a <see cref="TerrainEntry"/> with only scenery data.</summary>
|
||||
/// <param name="scenery">The scenery index.</param>
|
||||
/// <returns>A new <see cref="TerrainEntry"/>.</returns>
|
||||
public static TerrainEntry FromScenery(byte scenery) => new(null, null, scenery, null, null);
|
||||
|
||||
/// <summary>Creates a <see cref="TerrainEntry"/> with only road data.</summary>
|
||||
/// <param name="road">The road index.</param>
|
||||
/// <returns>A new <see cref="TerrainEntry"/>.</returns>
|
||||
public static TerrainEntry FromRoad(byte road) => new(null, null, null, road, null);
|
||||
|
||||
/// <summary>Creates a <see cref="TerrainEntry"/> with only encounters data.</summary>
|
||||
/// <param name="encounters">The encounters index.</param>
|
||||
/// <returns>A new <see cref="TerrainEntry"/>.</returns>
|
||||
public static TerrainEntry FromEncounters(byte encounters) => new(null, null, null, null, encounters);
|
||||
|
||||
/// <summary>Creates a <see cref="TerrainEntry"/> with texture and scenery data.</summary>
|
||||
/// <param name="texture">The texture type.</param>
|
||||
/// <param name="scenery">The scenery index.</param>
|
||||
/// <returns>A new <see cref="TerrainEntry"/>.</returns>
|
||||
public static TerrainEntry FromTextureScenery(byte texture, byte scenery) => new(null, texture, scenery, null, null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the packed 32-bit representation
|
||||
/// </summary>
|
||||
public uint Pack() => _data;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TerrainEntry from a packed 32-bit value
|
||||
/// </summary>
|
||||
public static TerrainEntry Unpack(uint packed) {
|
||||
return new TerrainEntry { _data = packed };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges another entry into this one. Null values are ignored.
|
||||
/// </summary>
|
||||
public void Merge(TerrainEntry value) {
|
||||
if (value.Height.HasValue) Height = value.Height;
|
||||
if (value.Type.HasValue) Type = value.Type;
|
||||
if (value.Scenery.HasValue) Scenery = value.Scenery;
|
||||
if (value.Road.HasValue) Road = value.Road;
|
||||
if (value.Encounters.HasValue) Encounters = value.Encounters;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() {
|
||||
string height = Height?.ToString() ?? "null";
|
||||
string texture = Type?.ToString() ?? "null";
|
||||
string scenery = Scenery?.ToString() ?? "null";
|
||||
string road = Road?.ToString() ?? "null";
|
||||
string encounters = Encounters?.ToString() ?? "null";
|
||||
|
||||
return $"Height:{height}, Texture:{texture}, Scenery:{scenery}, Road:{road}, Encounters:{encounters}, Flags:{Flags}";
|
||||
}
|
||||
}
|
||||
}
|
||||
256
src/AcDream.Core/Rendering/Wb/TerrainUtils.cs
Normal file
256
src/AcDream.Core/Rendering/Wb/TerrainUtils.cs
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AcDream.Core.Rendering.Wb
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for terrain calculations.
|
||||
/// </summary>
|
||||
public static class TerrainUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// The width of a road in units.
|
||||
/// </summary>
|
||||
public const float RoadWidth = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum Z component of a surface normal for it to be considered walkable.
|
||||
/// </summary>
|
||||
public const float FloorZ = 0.66417414618662751f;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a surface with the given normal is walkable.
|
||||
/// </summary>
|
||||
/// <param name="normal">The surface normal.</param>
|
||||
/// <returns>True if the surface is walkable, false otherwise.</returns>
|
||||
public static bool IsValidWalkable(Vector3 normal)
|
||||
{
|
||||
return normal.Z >= FloorZ;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="landblockX">The global landblock X coordinate.</param>
|
||||
/// <param name="cellX">The local cell X coordinate (0-7).</param>
|
||||
/// <param name="landblockY">The global landblock Y coordinate.</param>
|
||||
/// <param name="cellY">The local cell Y coordinate (0-7).</param>
|
||||
/// <returns>The split direction for the cell.</returns>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the interpolated terrain height at a local position within a landblock.
|
||||
/// Uses barycentric interpolation on the cell's triangle pair.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terrain surface normal at a local position within a landblock.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a local position within a landblock is on a road.
|
||||
/// Uses per-vertex road flags and proximity testing.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the road value for a specific vertex in the 9x9 terrain entry grid.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/AcDream.Core/Rendering/Wb/TextureHelpers.cs
Normal file
161
src/AcDream.Core/Rendering/Wb/TextureHelpers.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.Core.Rendering.Wb {
|
||||
public static class TextureHelpers {
|
||||
public static byte[] CreateSolidColorTexture(DatReaderWriter.Types.ColorARGB color, int width, int height) {
|
||||
var bytes = new byte[width * height * 4];
|
||||
for (int i = 0; i < width * height; i++) {
|
||||
bytes[i * 4 + 0] = color.Red;
|
||||
bytes[i * 4 + 1] = color.Green;
|
||||
bytes[i * 4 + 2] = color.Blue;
|
||||
bytes[i * 4 + 3] = color.Alpha;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static void FillIndex16(byte[] src, Palette palette, Span<byte> dst, int width, int height, bool isClipMap = false) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var srcIdx = (y * width + x) * 2;
|
||||
var palIdx = (ushort)(src[srcIdx] | (src[srcIdx + 1] << 8));
|
||||
var color = palette.Colors[palIdx];
|
||||
var dstIdx = (y * width + x) * 4;
|
||||
|
||||
if (isClipMap && palIdx < 8) {
|
||||
dst[dstIdx + 0] = 0;
|
||||
dst[dstIdx + 1] = 0;
|
||||
dst[dstIdx + 2] = 0;
|
||||
dst[dstIdx + 3] = 0;
|
||||
}
|
||||
else {
|
||||
dst[dstIdx + 0] = color.Red;
|
||||
dst[dstIdx + 1] = color.Green;
|
||||
dst[dstIdx + 2] = color.Blue;
|
||||
dst[dstIdx + 3] = color.Alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillP8(byte[] src, Palette palette, Span<byte> dst, int width, int height, bool isClipMap = false) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var srcIdx = (y * width + x);
|
||||
var palIdx = src[srcIdx];
|
||||
var color = palette.Colors[palIdx];
|
||||
var dstIdx = (y * width + x) * 4;
|
||||
|
||||
if (isClipMap && palIdx < 8) {
|
||||
dst[dstIdx + 0] = 0;
|
||||
dst[dstIdx + 1] = 0;
|
||||
dst[dstIdx + 2] = 0;
|
||||
dst[dstIdx + 3] = 0;
|
||||
}
|
||||
else {
|
||||
dst[dstIdx + 0] = color.Red;
|
||||
dst[dstIdx + 1] = color.Green;
|
||||
dst[dstIdx + 2] = color.Blue;
|
||||
dst[dstIdx + 3] = color.Alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillR5G6B5(byte[] src, Span<byte> dst, int width, int height) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var srcIdx = (y * width + x) * 2;
|
||||
var val = (ushort)(src[srcIdx] | (src[srcIdx + 1] << 8));
|
||||
var dstIdx = (y * width + x) * 4;
|
||||
|
||||
dst[dstIdx + 0] = (byte)(((val & 0xF800) >> 11) << 3);
|
||||
dst[dstIdx + 1] = (byte)(((val & 0x7E0) >> 5) << 2);
|
||||
dst[dstIdx + 2] = (byte)((val & 0x1F) << 3);
|
||||
dst[dstIdx + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillA4R4G4B4(byte[] src, Span<byte> dst, int width, int height) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var srcIdx = (y * width + x) * 2;
|
||||
var val = (ushort)(src[srcIdx] | (src[srcIdx + 1] << 8));
|
||||
var dstIdx = (y * width + x) * 4;
|
||||
|
||||
dst[dstIdx + 0] = (byte)(((val >> 8) & 0x0F) * 17);
|
||||
dst[dstIdx + 1] = (byte)(((val >> 4) & 0x0F) * 17);
|
||||
dst[dstIdx + 2] = (byte)((val & 0x0F) * 17);
|
||||
var alpha = (byte)(((val >> 12) & 0x0F) * 17);
|
||||
dst[dstIdx + 3] = alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillA8R8G8B8(byte[] src, Span<byte> dst, int width, int height) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var srcIdx = (y * width + x) * 4;
|
||||
var dstIdx = (y * width + x) * 4;
|
||||
|
||||
dst[dstIdx + 0] = src[srcIdx + 2]; // R
|
||||
dst[dstIdx + 1] = src[srcIdx + 1]; // G
|
||||
dst[dstIdx + 2] = src[srcIdx + 0]; // B
|
||||
|
||||
var alpha = src[srcIdx + 3];
|
||||
dst[dstIdx + 3] = alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillR8G8B8(byte[] src, Span<byte> dst, int width, int height) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
var srcIdx = (y * width + x) * 3;
|
||||
var dstIdx = (y * width + x) * 4;
|
||||
|
||||
dst[dstIdx + 0] = src[srcIdx + 2]; // R
|
||||
dst[dstIdx + 1] = src[srcIdx + 1]; // G
|
||||
dst[dstIdx + 2] = src[srcIdx + 0]; // B
|
||||
dst[dstIdx + 3] = 255; // A
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillA8(byte[] src, Span<byte> dst, int width, int height) {
|
||||
for (int i = 0; i < width * height; i++) {
|
||||
byte val = src[i];
|
||||
dst[i * 4] = 255;
|
||||
dst[i * 4 + 1] = 255;
|
||||
dst[i * 4 + 2] = 255;
|
||||
dst[i * 4 + 3] = val;
|
||||
}
|
||||
}
|
||||
|
||||
public static void FillA8Additive(byte[] src, Span<byte> dst, int width, int height) {
|
||||
for (int i = 0; i < width * height; i++) {
|
||||
byte val = src[i];
|
||||
dst[i * 4] = val;
|
||||
dst[i * 4 + 1] = val;
|
||||
dst[i * 4 + 2] = val;
|
||||
dst[i * 4 + 3] = val;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a pixel format is compressed
|
||||
/// </summary>
|
||||
public static bool IsCompressedFormat(DatReaderWriter.Enums.PixelFormat format) {
|
||||
return format == DatReaderWriter.Enums.PixelFormat.PFID_DXT1 ||
|
||||
format == DatReaderWriter.Enums.PixelFormat.PFID_DXT3 ||
|
||||
format == DatReaderWriter.Enums.PixelFormat.PFID_DXT5;
|
||||
}
|
||||
|
||||
public static byte[] Color565ToRgba(ushort color565) {
|
||||
int r = (color565 >> 11) & 31;
|
||||
int g = (color565 >> 5) & 63;
|
||||
int b = color565 & 31;
|
||||
return new byte[] { (byte)(r * 255 / 31), (byte)(g * 255 / 63), (byte)(b * 255 / 31), 255 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using AcDream.Core.Rendering.Wb;
|
||||
using BCnEncoder.Decoder;
|
||||
using BCnEncoder.Shared;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
using System.Numerics;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
using AcDream.Core.Rendering.Wb;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using WorldBuilder.Shared.Modules.Landscape.Lib;
|
||||
|
||||
namespace AcDream.Core.World;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using AcDream.Core.Rendering.Wb;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using WorldBuilder.Shared.Models;
|
||||
|
||||
namespace AcDream.Core.World;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
using AcDream.Core.Rendering.Wb;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue