diag(render/physics): flap root-caused to physics rest µm-jitter; refute prior diagnoses
Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell; the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields. No functional fix shipped. Next: higher-precision physics rest trace -> port retail kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d0b65c4170
commit
d6aa526dd3
6 changed files with 300 additions and 1 deletions
119
docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md
Normal file
119
docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# Handoff — the indoor FLAP traced to a physics rest µm-jitter; prior diagnoses REFUTED — 2026-06-08
|
||||
|
||||
> **CANONICAL PICKUP for the indoor render flap.** This session refuted the 2026-06-07 cutover-flip
|
||||
> diagnosis AND an enqueue-once attempt, confirmed the real mechanism with primary evidence, and traced
|
||||
> the root all the way down to a **physics resting-position µm jitter**. The fix is in physics
|
||||
> (rest-stability), is teed up, and needs one more **higher-precision** trace to pin the exact cause
|
||||
> before porting. Spec: `docs/superpowers/specs/2026-06-08-portal-flood-membership-stability-design.md`
|
||||
> (its §4 enqueue-once design is REFUTED — see §3 here; its §6 physics contingency is now the active
|
||||
> direction).
|
||||
|
||||
---
|
||||
|
||||
## 1. What the flap IS (confirmed, primary evidence)
|
||||
|
||||
At the Holtburg cottage **doorway threshold**, the portal-visibility flood set oscillates frame-to-frame
|
||||
(`ids=[0170,0171,0172,0173,0174,0175]` ↔ `[0170,0171]`, i.e. 6↔2 cells) **from a stable viewer cell**
|
||||
(`root=0xA9B40170`, `outRoot=n`). The deep `0172-0175` cluster pops in/out → textures "battle."
|
||||
|
||||
- It is **NOT** see-through walls from outside (standing outside with the door closed is **stable** —
|
||||
user visual gate), **NOT** the outdoor node, **NOT** a root toggle, **NOT** nondeterminism.
|
||||
- `PortalVisibilityBuilder.Build` is a **pure deterministic** function (proved by
|
||||
`PortalVisibilityBuilderTests.Build_IsDeterministic_*`, passes). So the flip requires a **varying
|
||||
input**.
|
||||
- The high-precision `[pv-input]` probe (6 dp) shows the camera eye AND the **player `RenderPosition`**
|
||||
carry perpetual **~1–8 µm** float jitter at rest (e.g. player Z `94.000000 ↔ 94.000008`). At the
|
||||
threshold a grazing portal's clip is so knife-edge that this µm jitter flips its empty/non-empty
|
||||
result → the flood membership flips → the flap.
|
||||
|
||||
**Mechanism chain:**
|
||||
`physics resting position blips ~µm → ComputeRenderPosition Lerp surfaces it as µm eye jitter → the
|
||||
portal-flood clip (clip-non-empty membership) is µm-sensitive at the grazing threshold portal → flips →
|
||||
flap.` Retail is flap-free because its authoritative local position is bit-stable at rest (so its same
|
||||
clip-non-empty membership never crosses the boundary).
|
||||
|
||||
## 2. REFUTED — the 2026-06-07 cutover-flip diagnosis (do NOT act on its F1/F2)
|
||||
|
||||
`docs/research/2026-06-07-cutover-flip-render-residuals-diagnosis-handoff.md` is wrong on its
|
||||
load-bearing claims (primary evidence in this session):
|
||||
|
||||
- "See-through from outside" — **not reproduced** (outside, door closed, is stable).
|
||||
- "Walls ARE the EnvCell shells; ModelId is a partial frame" — **refuted**: the cottage ModelId GfxObj
|
||||
`0x01000A2B` is a full closed exterior (76 render polys, bbox 20×18×10.4 m, 46 outward-facing walls +
|
||||
roof — `tools/A8CellAudit gfxobj 0x01000A2B`). EnvCell shells are interior-facing. **F2 (EnvCell
|
||||
back-faces) targets the wrong geometry.**
|
||||
- "Oscillation = outdoor-node flood (1↔13)" — **corrected**: it is the *indoor* flood, stable root,
|
||||
2↔6. F1 targeted the wrong root.
|
||||
- "branch=RetailPViewInside every frame proves the flap is gone" — **tautological** (post-flip
|
||||
`clipRoot = viewerRoot ?? _outdoorNode` is ~never null, so `branch` can't report `OutdoorRoot`).
|
||||
|
||||
## 3. REFUTED — enqueue-once traversal (TDD caught it)
|
||||
|
||||
Hypothesis: the flap is acdream's `MaxReprocessPerCell` re-enqueue drift; restore retail's enqueue-once
|
||||
(first-discovery only, no re-enqueue). **Refuted:** retail does NOT stop at first discovery — its
|
||||
`AddViewToPortals` growth branch calls **`AddToCell`** (decomp :433494), so a cell's later-grown view
|
||||
IS propagated (late slices reach exit portals). The existing test
|
||||
`PortalVisibilityBuilderTests.Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit` encodes exactly
|
||||
this retail behavior; enqueue-once broke it. The change + its test were reverted (tree clean, 27 portal
|
||||
tests green). **The divergence is the re-clip DRIFT, not the propagation — and underneath, the flap is
|
||||
the µm input jitter (which removing the drift would only *reduce*, not eliminate; `Build` is
|
||||
deterministic so only a bit-stable INPUT guarantees no flap).**
|
||||
|
||||
## 4. The root — physics resting position not bit-stable
|
||||
|
||||
`PlayerMovementController.ComputeRenderPosition` (line 810): `Vector3.Lerp(_prevPhysicsPos,
|
||||
_currPhysicsPos, alpha)`. `Lerp(a, a, t) == a` exactly, so the µm `RenderPosition` jitter means
|
||||
**`_prev != _curr`** — the physics body's resting position blips ~µm between ticks. Retail's
|
||||
`kill_velocity` (`OBJECTINFO::kill_velocity` = `set_velocity(0)`, decomp :274467) is called by
|
||||
`validate_transition` (:272567) on **every** grounded collision/slide with a valid contact plane,
|
||||
keeping rest bit-stable.
|
||||
|
||||
acdream rest path:
|
||||
- `calc_acceleration` (PhysicsBody.cs:191) zeroes gravity only when **`Contact && OnWalkable &&
|
||||
!Sledding`**.
|
||||
- `UpdatePhysicsInternal` (PhysicsBody.cs:352) skips position integration when `velocity <= 0`.
|
||||
- Player flags set per tick in `PlayerMovementController` (1271-1301): `Contact|OnWalkable` only when
|
||||
`resolveResult.IsOnGround && Velocity.Z <= 0`; else cleared → gravity.
|
||||
- acdream's `kill_velocity` (PhysicsEngine.cs:837) is **narrower than retail's** — fires only on
|
||||
`ObjectInfo.VelocityKilled` (the airborne steep-roof/wall reset), NOT on every grounded contact.
|
||||
|
||||
So at a *clean* rest the position is bit-stable; the blip is an **intermittent** failure (a stray
|
||||
gravity tick / µm velocity residual / contact-plane not re-established). The `[resolve]` probe (3 dp)
|
||||
shows the body stable to **mm** at spawn rest (`94.000` repeated) — confirming the blip is **sub-mm**,
|
||||
below that probe's precision — and shows `groundedIn=True` but `walkable=False cp=none` (no contact
|
||||
plane established at rest), a lead toward the Contact/contact-plane path.
|
||||
|
||||
## 5. NEXT STEPS (the physics rest-stability fix)
|
||||
|
||||
1. **Higher-precision physics rest trace (REQUIRED before fixing).** The 3-dp `[resolve]` probe is too
|
||||
coarse. Add a 6-dp per-tick probe of the resting body: `_body.Position`, `Velocity`, `Acceleration`,
|
||||
`TransientState` (Contact/OnWalkable), `resolveResult.IsOnGround`, contact-plane valid. Launch, let
|
||||
the character sit at spawn (no input needed — autonomous), capture ~10 s, and find the tick where the
|
||||
position blips µm and which condition failed (gravity applied? velocity residual? resolve re-snap?
|
||||
Contact cleared?).
|
||||
2. **Port the retail-faithful rest-stability fix** for the pinned cause — most likely one of:
|
||||
(a) broaden `kill_velocity` to match retail's `validate_transition` (zero velocity on every grounded
|
||||
contact with a valid contact plane, :272567); (b) ensure the `Contact` flag / contact plane is
|
||||
re-established on the zero-distance rest sweep so `calc_acceleration` keeps gravity off; (c) a
|
||||
retail-faithful "supported body at rest is frozen" (skip integration/resolve when grounded + zero
|
||||
velocity + no movement input). TDD: a test asserting the resting body position is **bit-stable across
|
||||
N ticks** with no input.
|
||||
3. **Visual gate** at the cottage doorway threshold: hold still — the 2↔6 oscillation is gone (re-run
|
||||
`[pv-input]`/`[render-sig]`, flood `ids=` constant at rest).
|
||||
|
||||
**DO NOT RETRY:** the overlap-predicate render band-aid (rejected by user — not retail); enqueue-once
|
||||
(refuted, §3); any render-side debounce/grace (forbidden).
|
||||
|
||||
## 6. Apparatus (committed this session) + state
|
||||
|
||||
- **Keep (real regression value):** `PortalVisibilityBuilderTests.Build_IsDeterministic_*` (proves Build
|
||||
deterministic); `tools/A8CellAudit` `gfxobj` mode (dumps render geometry — used to refute the ModelId
|
||||
claim).
|
||||
- **Diagnostic probes (env-gated, inert off; KEEP for the physics trace + flap visual gate, strip after
|
||||
the fix ships):** `[pv-input]` (`ACDREAM_PROBE_PVINPUT`, 6-dp Build inputs + flood count,
|
||||
RenderingDiagnostics + GameWindow); the `outRoot=`/`bshell=` fields on `[render-sig]`;
|
||||
`launch-pvinput.ps1`, `launch-bshell-probe.ps1`, `launch-resolve.ps1`.
|
||||
- Tree: PortalVisibilityBuilder.cs reverted to the re-enqueue (no functional change shipped). Build
|
||||
green; App.Tests green (portal-visibility 27/27).
|
||||
- Memory to update: `project_indoor_flap_rootcause` (root is the physics rest µm-jitter, not the render
|
||||
diagnosis or enqueue-once).
|
||||
|
|
@ -2,7 +2,12 @@
|
|||
|
||||
**Date:** 2026-06-08
|
||||
**Branch:** `claude/thirsty-goldberg-51bb9b`
|
||||
**Status:** design approved (user, 2026-06-08); TDD implementation pending behind a visual gate.
|
||||
**Status:** ⚠️ **§4 (enqueue-once) REFUTED 2026-06-08** — retail propagates late slices via `AddToCell`
|
||||
(decomp :433494); the existing `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit` test encodes
|
||||
that and enqueue-once broke it (reverted). The flap's confirmed root is the **physics resting position
|
||||
µm-jitter** (§6 contingency, now the active direction). **CANONICAL PICKUP:**
|
||||
`docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md`. Keep §1–§3 (mechanism + retail
|
||||
grounding) as accurate diagnosis; treat §4–§5 as a refuted approach.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -7560,6 +7560,19 @@ public sealed class GameWindow : IDisposable
|
|||
clipAssembly = pviewResult.ClipAssembly;
|
||||
envCellShellFilter = pviewResult.DrawableCells;
|
||||
_interiorPartition = pviewResult.Partition;
|
||||
|
||||
// Flap root-cause apparatus (2026-06-07): per-frame, the EXACT Build inputs at 6 dp +
|
||||
// the resulting flood count. The live flap shows flood flipping 2↔6 at an eye/player
|
||||
// identical to cm; this answers whether the inputs differ sub-cm (jitter) or are
|
||||
// byte-identical (nondeterminism). See RenderingDiagnostics.ProbePvInputEnabled.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbePvInputEnabled && pvFrame is not null)
|
||||
{
|
||||
var vp = envCellViewProj;
|
||||
char pvOutRoot = ReferenceEquals(clipRoot, _outdoorNode) ? 'Y' : 'n';
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[pv-input] outRoot={pvOutRoot} flood={pvFrame.OrderedVisibleCells.Count} eye=({camPos.X:F6},{camPos.Y:F6},{camPos.Z:F6}) player=({playerViewPos.X:F6},{playerViewPos.Y:F6},{playerViewPos.Z:F6}) vp=[{vp.M11:F6} {vp.M13:F6} {vp.M22:F6} {vp.M31:F6} {vp.M33:F6} {vp.M41:F6} {vp.M42:F6} {vp.M43:F6}]"));
|
||||
}
|
||||
|
||||
sigPvFrame = pviewResult.PortalFrame;
|
||||
sigClipAssembly = pviewResult.ClipAssembly;
|
||||
sigDrawableCells = pviewResult.DrawableCells;
|
||||
|
|
@ -9190,6 +9203,22 @@ public sealed class GameWindow : IDisposable
|
|||
sb.Append(" outdoorRootObjs=").Append(outdoorRootObjectCount);
|
||||
sb.Append(" liveDynDraw=").Append(liveDynamicDrawnCount);
|
||||
|
||||
// Diagnosis 2026-06-07: draw-vs-occlude probe for the see-through residual.
|
||||
// outRoot=Y means clipRoot is the synthetic outdoor node (eye outdoors). bshell=total/withMesh
|
||||
// counts the building ModelId exterior shells queued in partition.Outdoor for this frame — the
|
||||
// GfxObj exteriors that SHOULD draw the solid walls. Correlate with ids= (the flooded interior
|
||||
// cells): if bshell=N/N and ids=[node only] but the wall is still see-through, the exterior is
|
||||
// failing to rasterize (draw/clip bug, not EnvCell sidedness); if ids includes interior cells,
|
||||
// the outdoor flood is drawing interiors over the exterior.
|
||||
sb.Append(" outRoot=").Append(ReferenceEquals(clipRoot, _outdoorNode) ? 'Y' : 'n');
|
||||
if (partition is not null)
|
||||
{
|
||||
int shellTotal = 0, shellMesh = 0;
|
||||
foreach (var e in partition.Outdoor)
|
||||
if (e.IsBuildingShell) { shellTotal++; if (e.MeshRefs.Count > 0) shellMesh++; }
|
||||
sb.Append(" bshell=").Append(shellTotal).Append('/').Append(shellMesh);
|
||||
}
|
||||
|
||||
if (outdoorPortalDrawn || exteriorPvFrame is not null || exteriorClipAssembly is not null)
|
||||
{
|
||||
sb.Append(" extPortal=").Append(outdoorPortalDrawn ? 'Y' : 'n');
|
||||
|
|
|
|||
|
|
@ -129,6 +129,20 @@ public static class RenderingDiagnostics
|
|||
public static bool ProbeShellEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_SHELL") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// Flap root-cause apparatus (2026-06-07). When true, the indoor render path emits ONE
|
||||
/// <c>[pv-input]</c> line per frame with the EXACT PortalVisibilityBuilder.Build inputs at HIGH
|
||||
/// precision (camera eye + player position to 6 dp, plus orientation-sensitive view-projection
|
||||
/// elements) alongside the resulting flood cell count. The live flap shows the flood set flipping
|
||||
/// 2↔6 at an eye/player that is identical to cm; this probe answers whether the Build INPUTS differ
|
||||
/// below cm precision (sub-cm view jitter → robustness fix) or are byte-identical while the output
|
||||
/// still flips (nondeterminism → surgical bug). Runs WITHOUT the heavy <c>[flap]</c>/<c>[render-sig]</c>
|
||||
/// spam so the log stays diffable. Throwaway apparatus — strip once the jitter source is pinned.
|
||||
/// Initial state from <c>ACDREAM_PROBE_PVINPUT=1</c>.
|
||||
/// </summary>
|
||||
public static bool ProbePvInputEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_PVINPUT") == "1";
|
||||
|
||||
// Cell-change gate for EmitVis. The probe fires once per distinct root cell
|
||||
// so launch.log stays readable under motion (the per-frame call is a no-op
|
||||
// when the root is unchanged). Sentinel 0 = "no root yet" — the first real
|
||||
|
|
|
|||
|
|
@ -58,6 +58,38 @@ public class PortalVisibilityBuilderTests
|
|||
$"OutsideView width {outsideWidth} should be a sliver, far less than full window {windowOnlyWidth}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_IsDeterministic_IdenticalInputsGiveIdenticalVisibleSet()
|
||||
{
|
||||
// Flap root-cause apparatus (2026-06-07): the live threshold flap shows OrderedVisibleCells
|
||||
// flipping 2<->6 at an eye+player identical to cm. Build is a pure static function with
|
||||
// all-fresh per-call state, so identical inputs MUST yield an identical visible set. If this
|
||||
// FAILS, the flap is a determinism bug INSIDE Build; if it PASSES (expected), the live flip is
|
||||
// sub-cm INPUT jitter and the fix must make membership robust, not Build deterministic.
|
||||
// Exercises the re-enqueue fixpoint via a diamond: 0x0004 is reached from BOTH 0x0002 and 0x0003.
|
||||
var cam = Cell(0x0001,
|
||||
new CellPortalInfo(0x0002, 0, 0, 0),
|
||||
new CellPortalInfo(0x0003, 1, 0, 0));
|
||||
cam.PortalPolygons.Add(QuadX(-0.6f, -0.05f, -3f)); // left -> 0002
|
||||
cam.PortalPolygons.Add(QuadX(0.05f, 0.6f, -3f)); // right -> 0003
|
||||
var left = Cell(0x0002, new CellPortalInfo(0x0004, 0, 0, 0));
|
||||
left.PortalPolygons.Add(QuadX(-0.6f, -0.05f, -6f));
|
||||
var right = Cell(0x0003, new CellPortalInfo(0x0004, 0, 0, 0));
|
||||
right.PortalPolygons.Add(QuadX(0.05f, 0.6f, -6f));
|
||||
var back = Cell(0x0004, new CellPortalInfo(0xFFFF, 0, 0, 0));
|
||||
back.PortalPolygons.Add(QuadX(-0.6f, 0.6f, -9f));
|
||||
var all = new Dictionary<uint, LoadedCell>
|
||||
{ [0x0001] = cam, [0x0002] = left, [0x0003] = right, [0x0004] = back };
|
||||
|
||||
var a = Build(cam, all);
|
||||
var b = Build(cam, all);
|
||||
|
||||
Assert.Equal(a.OrderedVisibleCells, b.OrderedVisibleCells);
|
||||
Assert.Equal(
|
||||
a.CellViews.Keys.OrderBy(k => k).ToArray(),
|
||||
b.CellViews.Keys.OrderBy(k => k).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_EyeStandingInInteriorPortal_FloodsNeighbour()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,6 +30,14 @@ else if (args.Length > 0 && string.Equals(args[0], "portals", StringComparison.O
|
|||
foreach (var envCellId in ids)
|
||||
DumpCellPortals(dats, envCellId);
|
||||
}
|
||||
else if (args.Length > 0 && string.Equals(args[0], "gfxobj", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var ids = args.Length == 1
|
||||
? new uint[] { 0x01000A2Bu }
|
||||
: args.Skip(1).Select(ParseHex).ToArray();
|
||||
foreach (var gfxObjId in ids)
|
||||
DumpGfxObj(dats, gfxObjId);
|
||||
}
|
||||
else
|
||||
{
|
||||
var ids = args.Length == 0
|
||||
|
|
@ -365,6 +373,98 @@ static (int RegistryBuildings, int ShellEntities) DumpLandblockBuildings(LandBlo
|
|||
return (registryBuildingId - 1, shellEntities);
|
||||
}
|
||||
|
||||
static void DumpGfxObj(DatCollection dats, uint gfxObjId)
|
||||
{
|
||||
Console.WriteLine($"=== GfxObj 0x{gfxObjId:X8} (RENDER polygons) ===");
|
||||
var g = dats.Get<GfxObj>(gfxObjId);
|
||||
if (g is null)
|
||||
{
|
||||
Console.WriteLine("missing GfxObj");
|
||||
return;
|
||||
}
|
||||
|
||||
var verts = g.VertexArray.Vertices;
|
||||
var min = new Vector3(float.MaxValue);
|
||||
var max = new Vector3(float.MinValue);
|
||||
foreach (var v in verts.Values)
|
||||
{
|
||||
min = Vector3.Min(min, v.Origin);
|
||||
max = Vector3.Max(max, v.Origin);
|
||||
}
|
||||
var centroid = (min + max) * 0.5f;
|
||||
|
||||
int walls = 0, floors = 0, ceilings = 0, slopes = 0;
|
||||
int outwardWalls = 0, inwardWalls = 0;
|
||||
int emitPos = 0, emitNeg = 0, skipped = 0;
|
||||
|
||||
foreach (var (polyId, poly) in g.Polygons.OrderBy(p => p.Key))
|
||||
{
|
||||
if (poly.VertexIds.Count < 3) continue;
|
||||
|
||||
bool hasPos = !poly.Stippling.HasFlag(StipplingType.NoPos);
|
||||
bool hasNeg = poly.Stippling.HasFlag(StipplingType.Negative)
|
||||
|| poly.Stippling.HasFlag(StipplingType.Both)
|
||||
|| (!poly.Stippling.HasFlag(StipplingType.NoNeg) && poly.SidesType == CullMode.Clockwise);
|
||||
if (hasPos) emitPos++;
|
||||
if (hasNeg) emitNeg++;
|
||||
if (!hasPos && !hasNeg) skipped++;
|
||||
|
||||
var n = ComputeNormalG(g, poly);
|
||||
bool isWall = Math.Abs(n.Z) <= 0.15f;
|
||||
bool isFloor = n.Z > 0.9f;
|
||||
bool isCeiling = n.Z < -0.9f;
|
||||
if (isFloor) floors++;
|
||||
else if (isCeiling) ceilings++;
|
||||
else if (isWall) walls++;
|
||||
else slopes++;
|
||||
|
||||
if (isWall)
|
||||
{
|
||||
var pc = PolyCentroidG(g, poly);
|
||||
var toFace = pc - centroid;
|
||||
float outward = Vector3.Dot(n, toFace); // >0 => front face points away from center (exterior)
|
||||
if (outward > 0) outwardWalls++; else inwardWalls++;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
$"verts={verts.Count} renderPolys={g.Polygons.Count} hasPhysics={(g.PhysicsPolygons?.Count ?? 0)} " +
|
||||
$"emitPos={emitPos} emitNeg={emitNeg} skipped={skipped}");
|
||||
Console.WriteLine(
|
||||
$"bbox min=({min.X:F2},{min.Y:F2},{min.Z:F2}) max=({max.X:F2},{max.Y:F2},{max.Z:F2}) " +
|
||||
$"size=({max.X - min.X:F2},{max.Y - min.Y:F2},{max.Z - min.Z:F2})");
|
||||
Console.WriteLine(
|
||||
$"classify: walls={walls} (outwardFacing={outwardWalls} inwardFacing={inwardWalls}) " +
|
||||
$"floors={floors} ceilings={ceilings} slopes={slopes}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
static Vector3 ComputeNormalG(GfxObj g, DatReaderWriter.Types.Polygon poly)
|
||||
{
|
||||
if (poly.VertexIds.Count < 3) return Vector3.Zero;
|
||||
if (!g.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var a) ||
|
||||
!g.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var b) ||
|
||||
!g.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var c))
|
||||
{
|
||||
return Vector3.Zero;
|
||||
}
|
||||
var n = Vector3.Cross(b.Origin - a.Origin, c.Origin - a.Origin);
|
||||
return n.LengthSquared() > 0f ? Vector3.Normalize(n) : Vector3.Zero;
|
||||
}
|
||||
|
||||
static Vector3 PolyCentroidG(GfxObj g, DatReaderWriter.Types.Polygon poly)
|
||||
{
|
||||
var sum = Vector3.Zero;
|
||||
int count = 0;
|
||||
foreach (var vid in poly.VertexIds)
|
||||
if (g.VertexArray.Vertices.TryGetValue((ushort)vid, out var v))
|
||||
{
|
||||
sum += v.Origin;
|
||||
count++;
|
||||
}
|
||||
return count > 0 ? sum / count : Vector3.Zero;
|
||||
}
|
||||
|
||||
static bool IsSupported(uint id)
|
||||
{
|
||||
uint type = id & 0xFF000000u;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue