feat(render): Phase A8 Wave 1 — WB scaffolding extraction + stencil low-level method
Five tasks shipped together (interdependent at build time): Task 1: WbRenderPass enum — verbatim port of WB RenderPass.cs:1-22 Task 2: WbFrustum + WbBoundingBox + FrustumTestResult — verbatim port of WB Frustum.cs (98 LOC) with namespace + BoundingBox-type adaptations. +7 unit tests. Task 3: EnvCellSceneryInstance + EnvCellLandblock — verbatim port of WB SceneryInstance.cs:1-161, renamed scope-narrow. Dropped editor-only fields (DisqualificationReason, ParticleEmitters, IsQueuedForUpload, InstanceBufferOffset, InstanceCount, MdiCommands, IsTransformOnlyUpdate) + InstanceId narrowed uint (we don't use ObjectId's editor methods). +5 unit tests. Task 4: EnvCellVisibilitySnapshot — direct port of WB VisibilitySnapshot narrowed to BatchedByCell + VisibleLandblocks only. Task 7: IndoorCellStencilPipeline.RenderBuildingStencilMask — new low-level WB-faithful entry mirroring PortalRenderManager:471-484. No surrounding GL state setup (caller's responsibility). Probe fields LastStencilVertexCount / LastStencilWasFarPunch / LastStencilBuildingId for the [stencil] probe emitter in Task 9. Build green, 18 tests pass (7 new Frustum + 5 new SceneryInstance + 6 existing stencil pipeline). Ready for Wave 2 (EnvCellRenderer port). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
95f0d5267b
commit
fc68d6d01f
7 changed files with 595 additions and 0 deletions
|
|
@ -464,6 +464,73 @@ public sealed unsafe class IndoorCellStencilPipeline : IDisposable
|
|||
return idx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-28): low-level building-portal stencil draw. Mirrors WB
|
||||
/// <c>PortalRenderManager.RenderBuildingStencilMask</c> at
|
||||
/// <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs:471-484</c>.
|
||||
///
|
||||
/// <para>Uploads the building's exit-portal mesh to our shared VBO and draws
|
||||
/// it with the portal_stencil shader. <strong>Does NOT set or restore any
|
||||
/// surrounding GL state</strong> — caller is responsible (stencil func,
|
||||
/// depth mask, color mask, cull face, etc.) per WB
|
||||
/// <c>VisibilityManager.RenderInsideOut</c> Steps 1/2/5a/5b/5d
|
||||
/// expectations.</para>
|
||||
///
|
||||
/// <para>Note: enables/disables <c>GL_DEPTH_CLAMP</c> around the draw
|
||||
/// because portal polygons can extend beyond the camera's near/far range.
|
||||
/// This is symmetric — no state leakage.</para>
|
||||
/// </summary>
|
||||
/// <param name="building">The building whose exit portal polygons to draw.</param>
|
||||
/// <param name="viewProjection">Camera view-projection matrix.</param>
|
||||
/// <param name="writeFarDepth">When true, the fragment shader writes
|
||||
/// <c>gl_FragDepth = 1.0</c> (WB Step 2 / Step 5b "punch" semantic).
|
||||
/// When false, default depth is written (WB Step 1 / Step 5a "mark"
|
||||
/// semantic).</param>
|
||||
public void RenderBuildingStencilMask(AcDream.App.Rendering.Wb.Building building, Matrix4x4 viewProjection, bool writeFarDepth)
|
||||
{
|
||||
int vertexCount = UploadBuildingPortalMesh(building);
|
||||
if (vertexCount == 0)
|
||||
{
|
||||
LastStencilVertexCount = 0;
|
||||
LastStencilWasFarPunch = writeFarDepth;
|
||||
LastStencilBuildingId = building.BuildingId;
|
||||
return;
|
||||
}
|
||||
|
||||
_gl.Enable(EnableCap.DepthClamp);
|
||||
|
||||
_shader.Use();
|
||||
var vp = viewProjection;
|
||||
_gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp);
|
||||
_gl.Uniform1(_uWriteFarDepthLoc, writeFarDepth ? 1 : 0);
|
||||
|
||||
_gl.BindVertexArray(_vao);
|
||||
_gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)vertexCount);
|
||||
_gl.BindVertexArray(0);
|
||||
|
||||
_gl.Disable(EnableCap.DepthClamp);
|
||||
|
||||
LastStencilVertexCount = vertexCount;
|
||||
LastStencilWasFarPunch = writeFarDepth;
|
||||
LastStencilBuildingId = building.BuildingId;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Probe data (Phase A8 — read by the [stencil] probe emitter in GameWindow).
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Phase A8 RR9: vertex count of the most recent
|
||||
/// <see cref="RenderBuildingStencilMask"/> draw. 0 if the building had no portals.</summary>
|
||||
public int LastStencilVertexCount { get; private set; }
|
||||
|
||||
/// <summary>Phase A8 RR9: true iff the most recent
|
||||
/// <see cref="RenderBuildingStencilMask"/> draw was a far-depth punch (Step 2).</summary>
|
||||
public bool LastStencilWasFarPunch { get; private set; }
|
||||
|
||||
/// <summary>Phase A8 RR9: building id of the most recent
|
||||
/// <see cref="RenderBuildingStencilMask"/> draw.</summary>
|
||||
public uint LastStencilBuildingId { get; private set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_shader.Dispose();
|
||||
|
|
|
|||
146
src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs
Normal file
146
src/AcDream.App/Rendering/Wb/EnvCellSceneryInstance.cs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs
|
||||
// Phase A8 extraction (2026-05-28). Verbatim port; adaptations:
|
||||
// - SceneryInstance -> EnvCellSceneryInstance (scope-narrow to env-cell rendering)
|
||||
// - ObjectLandblock -> EnvCellLandblock
|
||||
// - Namespace: AcDream.App.Rendering.Wb
|
||||
// - BoundingBox -> WbBoundingBox (defined in WbFrustum.cs)
|
||||
// - DisqualificationReason + SceneryDisqualificationReason dropped (editor-only)
|
||||
// - IsQueuedForUpload, IsTransformOnlyUpdate, ParticleEmitters, InstanceBufferOffset,
|
||||
// InstanceCount, MdiCommands dropped (editor-only / shared-MDI)
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight data for a single placed env-cell scenery object.
|
||||
/// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 11-56)
|
||||
/// </summary>
|
||||
public struct EnvCellSceneryInstance
|
||||
{
|
||||
/// <summary>GfxObj or Setup ID from DAT.</summary>
|
||||
public ulong ObjectId;
|
||||
|
||||
/// <summary>Unique instance index within the landblock (WB used a 128-bit editor ID; we use uint).</summary>
|
||||
public uint InstanceId;
|
||||
|
||||
/// <summary>True for multi-part Setup objects, false for simple GfxObj.</summary>
|
||||
public bool IsSetup;
|
||||
|
||||
/// <summary>True if this instance is a building.</summary>
|
||||
public bool IsBuilding;
|
||||
|
||||
/// <summary>True if this is an interior cell connected directly to the landblock.</summary>
|
||||
public bool IsEntryCell;
|
||||
|
||||
/// <summary>World-space position.</summary>
|
||||
public Vector3 WorldPosition;
|
||||
|
||||
/// <summary>Local-space position (relative to landblock origin).</summary>
|
||||
public Vector3 LocalPosition;
|
||||
|
||||
/// <summary>Rotation quaternion.</summary>
|
||||
public Quaternion Rotation;
|
||||
|
||||
/// <summary>The current cell ID this instance is in (used for previewing moves between cells).</summary>
|
||||
public uint CurrentPreviewCellId;
|
||||
|
||||
/// <summary>Scale (typically uniform).</summary>
|
||||
public Vector3 Scale;
|
||||
|
||||
/// <summary>Pre-computed world transform matrix.</summary>
|
||||
public Matrix4x4 Transform;
|
||||
|
||||
/// <summary>Local-space bounding box.</summary>
|
||||
public WbBoundingBox LocalBoundingBox;
|
||||
|
||||
/// <summary>World-space bounding box.</summary>
|
||||
public WbBoundingBox BoundingBox;
|
||||
|
||||
/// <summary>Rendering flags for this instance.</summary>
|
||||
public uint Flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds all EnvCell instances for a single landblock, ready for rendering.
|
||||
/// Shared by both scenery and static object render managers.
|
||||
/// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/SceneryInstance.cs (lines 62-160)
|
||||
/// </summary>
|
||||
public class EnvCellLandblock
|
||||
{
|
||||
/// <summary>Grid X coordinate of this landblock.</summary>
|
||||
public int GridX { get; set; }
|
||||
|
||||
/// <summary>Grid Y coordinate of this landblock.</summary>
|
||||
public int GridY { get; set; }
|
||||
|
||||
public object Lock { get; } = new();
|
||||
|
||||
public List<EnvCellSceneryInstance> Instances { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Grouped bounding boxes for each EnvCell in this landblock.
|
||||
/// Key: CellID, Value: Composite bounding box of the cell and all its static objects.
|
||||
/// </summary>
|
||||
public Dictionary<uint, WbBoundingBox> EnvCellBounds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Set of EnvCell IDs in this landblock that have the SeenOutside flag.
|
||||
/// </summary>
|
||||
public HashSet<uint> SeenOutsideCells { get; set; } = new();
|
||||
|
||||
public List<EnvCellSceneryInstance>? PendingInstances { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Grouped bounding boxes for each EnvCell in this landblock (pending upload).
|
||||
/// </summary>
|
||||
public Dictionary<uint, WbBoundingBox>? PendingEnvCellBounds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set of EnvCell IDs in this landblock that have the SeenOutside flag (pending upload).
|
||||
/// </summary>
|
||||
public HashSet<uint>? PendingSeenOutsideCells { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Grouped transforms for each GfxObj part for static objects, for efficient instanced rendering.
|
||||
/// Key: GfxObjId, Value: List of transforms
|
||||
/// </summary>
|
||||
public Dictionary<ulong, List<InstanceData>> StaticPartGroups { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Grouped transforms for each GfxObj part for buildings, for efficient instanced rendering.
|
||||
/// Key: GfxObjId, Value: List of transforms
|
||||
/// </summary>
|
||||
public Dictionary<ulong, List<InstanceData>> BuildingPartGroups { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// World-space bounding box of this landblock.
|
||||
/// </summary>
|
||||
public WbBoundingBox BoundingBox { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bounding box covering all EnvCells in this landblock.
|
||||
/// </summary>
|
||||
public WbBoundingBox TotalEnvCellBounds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bounding box covering all EnvCells in this landblock (pending upload).
|
||||
/// </summary>
|
||||
public WbBoundingBox PendingTotalEnvCellBounds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether instances (positions/bounding boxes) have been generated.
|
||||
/// </summary>
|
||||
public bool InstancesReady { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether mesh data for all instances has been prepared (CPU-side).
|
||||
/// </summary>
|
||||
public bool MeshDataReady { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether GPU resources have been uploaded.
|
||||
/// </summary>
|
||||
public bool GpuReady { get; set; }
|
||||
}
|
||||
35
src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs
Normal file
35
src/AcDream.App/Rendering/Wb/EnvCellVisibilitySnapshot.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-28): EnvCell-scoped visibility snapshot. Direct port of
|
||||
/// WB's <c>VisibilitySnapshot</c> at
|
||||
/// <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilitySnapshot.cs:1-36</c>,
|
||||
/// narrowed to the fields <see cref="EnvCellRenderer"/> actually consumes
|
||||
/// (<c>BatchedByCell</c> + <c>VisibleLandblocks</c>). The scenery-side
|
||||
/// <c>VisibleGroups</c> / <c>VisibleGfxObjIds</c> / <c>IntersectingLandblocks</c>
|
||||
/// / <c>PostPreparePoolIndex</c> are dropped — we render scenery through
|
||||
/// <see cref="WbDrawDispatcher"/>, not through this snapshot.
|
||||
///
|
||||
/// <para>Used as an immutable snapshot atomically swapped under the
|
||||
/// renderer's render lock so PrepareRenderBatches (worker-driven) and
|
||||
/// Render (render-thread-driven) can't race on a half-populated dict.</para>
|
||||
/// </summary>
|
||||
public sealed class EnvCellVisibilitySnapshot
|
||||
{
|
||||
/// <summary>Landblocks fully or partially inside the frustum at prepare time.</summary>
|
||||
public List<EnvCellLandblock> VisibleLandblocks { get; init; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Grouped instance data by full 32-bit cell id.
|
||||
/// Outer key: <c>CellId</c>. Inner key: <c>GfxObjId</c> (ulong; bit 33 set for
|
||||
/// deduplicated cell geometry per <see cref="EnvCellRenderer.GetEnvCellGeomId"/>).
|
||||
/// Value: list of per-instance transforms (one per cell or per static object
|
||||
/// inside that cell).
|
||||
/// </summary>
|
||||
public Dictionary<uint, Dictionary<ulong, List<InstanceData>>> BatchedByCell { get; init; } = new();
|
||||
|
||||
/// <summary>True when no visible cells were produced this prepare cycle.</summary>
|
||||
public bool IsEmpty => VisibleLandblocks.Count == 0 && BatchedByCell.Count == 0;
|
||||
}
|
||||
143
src/AcDream.App/Rendering/Wb/WbFrustum.cs
Normal file
143
src/AcDream.App/Rendering/Wb/WbFrustum.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// Ported from references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs
|
||||
// Phase A8 extraction (2026-05-28). Verbatim algorithm; adaptations:
|
||||
// - Namespace: AcDream.App.Rendering.Wb
|
||||
// - Class renamed Frustum -> WbFrustum
|
||||
// - BoundingBox -> WbBoundingBox (inlined below; no new project dep)
|
||||
// - FrustumTestResult kept as-is (inlined below)
|
||||
// - WbPlane inlined (was an inner type of Frustum.cs in WB)
|
||||
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
public struct WbBoundingBox
|
||||
{
|
||||
public Vector3 Min;
|
||||
public Vector3 Max;
|
||||
|
||||
public WbBoundingBox(Vector3 min, Vector3 max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public static WbBoundingBox Union(WbBoundingBox a, WbBoundingBox b) =>
|
||||
new WbBoundingBox(
|
||||
Vector3.Min(a.Min, b.Min),
|
||||
Vector3.Max(a.Max, b.Max));
|
||||
}
|
||||
|
||||
public enum FrustumTestResult
|
||||
{
|
||||
Outside,
|
||||
Inside,
|
||||
Intersecting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// View-frustum helper extracted from WorldBuilder's Frustum class.
|
||||
/// Source: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Frustum.cs
|
||||
/// Phase A8 extraction (2026-05-28).
|
||||
/// </summary>
|
||||
public sealed class WbFrustum
|
||||
{
|
||||
private struct Plane
|
||||
{
|
||||
public Vector3 Normal;
|
||||
public float D;
|
||||
|
||||
public Plane(float a, float b, float c, float d)
|
||||
{
|
||||
Normal = new Vector3(a, b, c);
|
||||
float length = Normal.Length();
|
||||
Normal /= length;
|
||||
D = d / length;
|
||||
}
|
||||
|
||||
public float Dot(Vector3 point) => Vector3.Dot(Normal, point) + D;
|
||||
}
|
||||
|
||||
private readonly Plane[] _planes = new Plane[6];
|
||||
private readonly object _lock = new();
|
||||
|
||||
public void Update(Matrix4x4 matrix)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// Left plane
|
||||
_planes[0] = new Plane(matrix.M14 + matrix.M11, matrix.M24 + matrix.M21, matrix.M34 + matrix.M31, matrix.M44 + matrix.M41);
|
||||
// Right plane
|
||||
_planes[1] = new Plane(matrix.M14 - matrix.M11, matrix.M24 - matrix.M21, matrix.M34 - matrix.M31, matrix.M44 - matrix.M41);
|
||||
// Bottom plane
|
||||
_planes[2] = new Plane(matrix.M14 + matrix.M12, matrix.M24 + matrix.M22, matrix.M34 + matrix.M32, matrix.M44 + matrix.M42);
|
||||
// Top plane
|
||||
_planes[3] = new Plane(matrix.M14 - matrix.M12, matrix.M24 - matrix.M22, matrix.M34 - matrix.M32, matrix.M44 - matrix.M42);
|
||||
// Near plane
|
||||
_planes[4] = new Plane(matrix.M14 + matrix.M13, matrix.M24 + matrix.M23, matrix.M34 + matrix.M33, matrix.M44 + matrix.M43);
|
||||
// Far plane
|
||||
_planes[5] = new Plane(matrix.M14 - matrix.M13, matrix.M24 - matrix.M23, matrix.M34 - matrix.M33, matrix.M44 - matrix.M43);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Intersects(WbBoundingBox box, bool ignoreNearPlane = false)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (ignoreNearPlane && i == 4) continue;
|
||||
|
||||
Vector3 positive = box.Min;
|
||||
if (_planes[i].Normal.X >= 0) positive.X = box.Max.X;
|
||||
if (_planes[i].Normal.Y >= 0) positive.Y = box.Max.Y;
|
||||
if (_planes[i].Normal.Z >= 0) positive.Z = box.Max.Z;
|
||||
|
||||
if (_planes[i].Dot(positive) < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public FrustumTestResult TestBox(WbBoundingBox box, bool ignoreNearPlane = false)
|
||||
{
|
||||
var result = FrustumTestResult.Inside;
|
||||
lock (_lock)
|
||||
{
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
if (ignoreNearPlane && i == 4) continue;
|
||||
|
||||
Vector3 positive = box.Min;
|
||||
Vector3 negative = box.Max;
|
||||
if (_planes[i].Normal.X >= 0)
|
||||
{
|
||||
positive.X = box.Max.X;
|
||||
negative.X = box.Min.X;
|
||||
}
|
||||
if (_planes[i].Normal.Y >= 0)
|
||||
{
|
||||
positive.Y = box.Max.Y;
|
||||
negative.Y = box.Min.Y;
|
||||
}
|
||||
if (_planes[i].Normal.Z >= 0)
|
||||
{
|
||||
positive.Z = box.Max.Z;
|
||||
negative.Z = box.Min.Z;
|
||||
}
|
||||
|
||||
if (_planes[i].Dot(positive) < 0)
|
||||
{
|
||||
return FrustumTestResult.Outside;
|
||||
}
|
||||
if (_planes[i].Dot(negative) < 0)
|
||||
{
|
||||
result = FrustumTestResult.Intersecting;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
31
src/AcDream.App/Rendering/Wb/WbRenderPass.cs
Normal file
31
src/AcDream.App/Rendering/Wb/WbRenderPass.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-28): WB's RenderPass enum, extracted verbatim from
|
||||
/// <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/RenderPass.cs:1-22</c>.
|
||||
///
|
||||
/// Renamed to <c>WbRenderPass</c> to keep the WB-faithful name distinct
|
||||
/// from any future acdream-side RenderPass with different semantics.
|
||||
/// Consumed by <see cref="EnvCellRenderer"/> and matches the
|
||||
/// <c>uRenderPass</c> uniform in the modern mesh shaders.
|
||||
/// </summary>
|
||||
public enum WbRenderPass
|
||||
{
|
||||
/// <summary>
|
||||
/// The opaque pass. Only non-transparent objects are rendered.
|
||||
/// </summary>
|
||||
Opaque = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The transparent pass. Only transparent objects are rendered,
|
||||
/// usually after the opaque pass.
|
||||
/// </summary>
|
||||
Transparent = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A single-pass render that includes both opaque and (sometimes)
|
||||
/// transparent objects, or for special cases like skyboxes and
|
||||
/// certain UI elements.
|
||||
/// </summary>
|
||||
SinglePass = 2,
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
public class EnvCellSceneryInstanceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Instance_DefaultConstruct_HasZeroFields()
|
||||
{
|
||||
var s = new EnvCellSceneryInstance();
|
||||
Assert.Equal(0UL, s.ObjectId);
|
||||
Assert.False(s.IsBuilding);
|
||||
Assert.False(s.IsSetup);
|
||||
Assert.False(s.IsEntryCell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Instance_AssignFields_RoundTrip()
|
||||
{
|
||||
var t = Matrix4x4.CreateTranslation(1, 2, 3);
|
||||
var s = new EnvCellSceneryInstance
|
||||
{
|
||||
ObjectId = 0x01000123,
|
||||
IsBuilding = true,
|
||||
IsSetup = false,
|
||||
IsEntryCell = true,
|
||||
WorldPosition = new Vector3(1, 2, 3),
|
||||
Rotation = Quaternion.Identity,
|
||||
Scale = Vector3.One,
|
||||
Transform = t,
|
||||
};
|
||||
Assert.Equal(0x01000123UL, s.ObjectId);
|
||||
Assert.True(s.IsBuilding);
|
||||
Assert.True(s.IsEntryCell);
|
||||
Assert.Equal(new Vector3(1, 2, 3), s.WorldPosition);
|
||||
Assert.Equal(t, s.Transform);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Landblock_Construct_StartsEmpty()
|
||||
{
|
||||
var lb = new EnvCellLandblock { GridX = 0xA9, GridY = 0xB4 };
|
||||
Assert.Empty(lb.StaticPartGroups);
|
||||
Assert.Empty(lb.BuildingPartGroups);
|
||||
Assert.Empty(lb.Instances);
|
||||
Assert.Empty(lb.EnvCellBounds);
|
||||
Assert.False(lb.InstancesReady);
|
||||
Assert.False(lb.GpuReady);
|
||||
Assert.False(lb.MeshDataReady);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Landblock_AddInstanceToBuildingPartGroups_PreservesOrder()
|
||||
{
|
||||
var lb = new EnvCellLandblock();
|
||||
if (!lb.BuildingPartGroups.TryGetValue(0x01000001UL, out var list))
|
||||
{
|
||||
list = new List<InstanceData>();
|
||||
lb.BuildingPartGroups[0x01000001UL] = list;
|
||||
}
|
||||
list.Add(default);
|
||||
list.Add(default);
|
||||
Assert.Single(lb.BuildingPartGroups);
|
||||
Assert.Equal(2, lb.BuildingPartGroups[0x01000001UL].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Landblock_PendingInstancesNullByDefault()
|
||||
{
|
||||
var lb = new EnvCellLandblock();
|
||||
Assert.Null(lb.PendingInstances);
|
||||
Assert.Null(lb.PendingEnvCellBounds);
|
||||
Assert.Null(lb.PendingSeenOutsideCells);
|
||||
}
|
||||
}
|
||||
95
tests/AcDream.App.Tests/Rendering/Wb/WbFrustumTests.cs
Normal file
95
tests/AcDream.App.Tests/Rendering/Wb/WbFrustumTests.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
public class WbFrustumTests
|
||||
{
|
||||
private static Matrix4x4 PerspectiveVp(Vector3 eye, Vector3 lookAt) =>
|
||||
Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitY)
|
||||
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 4f, 1.0f, 0.1f, 100.0f);
|
||||
|
||||
[Fact]
|
||||
public void TestBox_BoxInFrontOfCamera_ReturnsInsideOrIntersecting()
|
||||
{
|
||||
var f = new WbFrustum();
|
||||
f.Update(PerspectiveVp(Vector3.Zero, new Vector3(0, 0, -1)));
|
||||
var box = new WbBoundingBox(new Vector3(-1, -1, -10), new Vector3(1, 1, -2));
|
||||
var res = f.TestBox(box);
|
||||
Assert.NotEqual(FrustumTestResult.Outside, res);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBox_BoxBehindCamera_ReturnsOutside()
|
||||
{
|
||||
var f = new WbFrustum();
|
||||
f.Update(PerspectiveVp(Vector3.Zero, new Vector3(0, 0, -1)));
|
||||
var box = new WbBoundingBox(new Vector3(-1, -1, 2), new Vector3(1, 1, 10));
|
||||
Assert.Equal(FrustumTestResult.Outside, f.TestBox(box));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Intersects_BoxInFrontOfCamera_ReturnsTrue()
|
||||
{
|
||||
var f = new WbFrustum();
|
||||
f.Update(PerspectiveVp(Vector3.Zero, new Vector3(0, 0, -1)));
|
||||
var box = new WbBoundingBox(new Vector3(-1, -1, -10), new Vector3(1, 1, -2));
|
||||
Assert.True(f.Intersects(box));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsIdempotent()
|
||||
{
|
||||
var f = new WbFrustum();
|
||||
var vp = PerspectiveVp(Vector3.Zero, new Vector3(0, 0, -1));
|
||||
f.Update(vp);
|
||||
f.Update(vp);
|
||||
var box = new WbBoundingBox(new Vector3(-1, -1, -10), new Vector3(1, 1, -2));
|
||||
Assert.NotEqual(FrustumTestResult.Outside, f.TestBox(box));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WbBoundingBox_Union_ExtendsToCoverBoth()
|
||||
{
|
||||
var a = new WbBoundingBox(new Vector3(0, 0, 0), new Vector3(1, 1, 1));
|
||||
var b = new WbBoundingBox(new Vector3(2, 2, 2), new Vector3(3, 3, 3));
|
||||
var u = WbBoundingBox.Union(a, b);
|
||||
Assert.Equal(new Vector3(0, 0, 0), u.Min);
|
||||
Assert.Equal(new Vector3(3, 3, 3), u.Max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Intersects_BoxBehindCamera_ReturnsFalse()
|
||||
{
|
||||
var f = new WbFrustum();
|
||||
f.Update(PerspectiveVp(Vector3.Zero, new Vector3(0, 0, -1)));
|
||||
var box = new WbBoundingBox(new Vector3(-1, -1, 2), new Vector3(1, 1, 10));
|
||||
Assert.False(f.Intersects(box));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBox_IgnoreNearPlane_DoesNotReturnOutsideForNearOverlap()
|
||||
{
|
||||
// Box straddles the near plane (z from -0.05 to 5) — with near plane ignored,
|
||||
// the box should not be culled as Outside.
|
||||
var f = new WbFrustum();
|
||||
f.Update(PerspectiveVp(Vector3.Zero, new Vector3(0, 0, -1)));
|
||||
var box = new WbBoundingBox(new Vector3(-0.5f, -0.5f, -5f), new Vector3(0.5f, 0.5f, -0.05f));
|
||||
var withNear = f.TestBox(box, ignoreNearPlane: false);
|
||||
var withoutNear = f.TestBox(box, ignoreNearPlane: true);
|
||||
// Both should be non-Outside for a box clearly in front
|
||||
Assert.NotEqual(FrustumTestResult.Outside, withoutNear);
|
||||
_ = withNear; // result varies by near clip; just verify no exception
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WbBoundingBox_Union_WithOverlapping_CoversAll()
|
||||
{
|
||||
var a = new WbBoundingBox(new Vector3(-1, -1, -1), new Vector3(2, 2, 2));
|
||||
var b = new WbBoundingBox(new Vector3(0, 0, 0), new Vector3(3, 3, 3));
|
||||
var u = WbBoundingBox.Union(a, b);
|
||||
Assert.Equal(new Vector3(-1, -1, -1), u.Min);
|
||||
Assert.Equal(new Vector3(3, 3, 3), u.Max);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue