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:
Erik 2026-05-21 15:13:26 +02:00
parent 8c073e0c4c
commit 16bc10c99d
9 changed files with 785 additions and 5 deletions

View 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
}
}

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

View 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}";
}
}
}

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

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

View file

@ -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;

View file

@ -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;

View file

@ -1,5 +1,5 @@
using AcDream.Core.Rendering.Wb;
using DatReaderWriter.DBObjs;
using WorldBuilder.Shared.Models;
namespace AcDream.Core.World;

View file

@ -1,4 +1,4 @@
using Chorizite.OpenGLSDLBackend.Lib;
using AcDream.Core.Rendering.Wb;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;