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