Logs SweepEye input (desiredEye) vs output (eye) at micrometre precision. Used to prove the indoor flap is NOT the camera: the eye is smooth (clean one-way pass = 3/18 direction-changes over 25.7k frames) and ~1um stable at rest, yet the visible-cell count oscillates 414x with 648 clip=0 near edge-on doorways. The flap is the flood/clip's edge-on behaviour, not the eye. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
6.5 KiB
C#
112 lines
6.5 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>
|
|
/// <see cref="ICameraCollisionProbe"/> backed by the player's swept-sphere
|
|
/// engine. Ports retail's <c>SmartBox::update_viewer</c> (0x00453ce0): sweep
|
|
/// the 0.3 m <c>viewer_sphere</c> from the head-pivot to the desired eye via a
|
|
/// <c>CTransition</c> and use the stopped position. Reusing
|
|
/// <see cref="PhysicsEngine.ResolveWithTransition"/> collides against indoor
|
|
/// cell walls (<c>FindEnvCollisions</c>) AND outdoor/baked GfxObj shells
|
|
/// (<c>FindObjCollisions</c>) in one faithful path.
|
|
/// </summary>
|
|
public sealed class PhysicsCameraCollisionProbe : ICameraCollisionProbe
|
|
{
|
|
/// <summary>Retail <c>viewer_sphere</c> radius (acclient :93314).</summary>
|
|
public const float ViewerSphereRadius = 0.3f;
|
|
|
|
private readonly PhysicsEngine _physics;
|
|
|
|
public PhysicsCameraCollisionProbe(PhysicsEngine physics) => _physics = physics;
|
|
|
|
public CameraSweepResult SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId, Vector3 playerPos)
|
|
{
|
|
// update_viewer: player->cell == 0 → set_viewer(player_pos, 1), viewer_cell = null
|
|
// (acclient_2013_pseudo_c.txt:92775). No cell to sweep against → snap to the player.
|
|
if (cellId == 0) return new CameraSweepResult(playerPos, 0u);
|
|
|
|
// === Start cell (pc:92824-92844) ===
|
|
// Indoor (objcell_id >= 0x100): seat the sweep's start cell at the head-PIVOT via
|
|
// CPhysicsObj::AdjustPosition (pc:92832) — the head can sit in a different cell than
|
|
// the feet (the cellar lip: feet in the low connector, head up at floor level). On
|
|
// failure retail falls back to player->cell. Outdoor: cell = player->cell (no AdjustPosition).
|
|
uint startCell = cellId;
|
|
if ((cellId & 0xFFFFu) >= 0x0100u)
|
|
{
|
|
var (pivotCell, found) = _physics.AdjustPosition(cellId, pivot);
|
|
if (found) startCell = pivotCell;
|
|
}
|
|
|
|
// === Sweep the viewer_sphere pivot → sought-eye from the start cell (pc:92860-92868) ===
|
|
// SpherePath.InitPath puts sphere0's center at pathPos + (0,0,radius) (the player
|
|
// foot-capsule convention). Retail's viewer_sphere center is (0,0,0), so shift the
|
|
// path DOWN by the radius to make the SPHERE CENTER travel pivot→eye, then add it back.
|
|
Vector3 begin = ToSpherePath(pivot, ViewerSphereRadius);
|
|
Vector3 end = ToSpherePath(desiredEye, ViewerSphereRadius);
|
|
|
|
var r = _physics.ResolveWithTransition(
|
|
currentPos: begin,
|
|
targetPos: end,
|
|
cellId: startCell,
|
|
sphereRadius: ViewerSphereRadius,
|
|
sphereHeight: 0f, // single sphere (no head sphere)
|
|
stepUpHeight: 0f, // no step-up for a camera
|
|
stepDownHeight: 0f, // no step-down / ground snap
|
|
isOnGround: false, // no contact-plane / walkable semantics
|
|
body: null, // no cross-frame persistence
|
|
// Retail SmartBox::update_viewer calls init_object(player, 0x5c) =
|
|
// IsViewer | PathClipped | FreeRotate | PerfectClip (acclient
|
|
// pseudo-C :92864; enum TransitionTypes.cs:24-33). PathClipped makes
|
|
// the sweep HARD-STOP at first contact (TransitionTypes.cs:811) — the
|
|
// spring-arm pull-in, not the player's edge-slide. IsViewer lets the
|
|
// eye pass through creatures, colliding only with world geometry
|
|
// (CollisionExemption.cs:83-85). FreeRotate/PerfectClip are no-ops in
|
|
// acdream today but set to match retail's exact value. NOT IsPlayer
|
|
// (0x100), so camera sweeps stay out of the #98 capture filter.
|
|
moverFlags: ObjectInfoState.IsViewer | ObjectInfoState.PathClipped
|
|
| ObjectInfoState.FreeRotate | ObjectInfoState.PerfectClip,
|
|
movingEntityId: selfEntityId); // skip the player's own ShadowEntry
|
|
|
|
Vector3 eye = FromSpherePath(r.Position, ViewerSphereRadius);
|
|
|
|
// [flap-sweep] camera-collision probe (ACDREAM_PROBE_FLAP), paired with the
|
|
// builder's [flap]/[flap-cam]. start = the pivot-seated start cell (vs cell = the
|
|
// player feet cell); ok = the sweep found a valid position (find_valid_position != 0).
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled)
|
|
{
|
|
var cp = _physics.DataCache?.GetCellStruct(startCell);
|
|
string bsp = cp?.BSP is null ? "nobsp" : (cp.BSP.Root is null ? "noroot" : "ok");
|
|
float desiredBack = Vector3.Distance(pivot, desiredEye);
|
|
float eyeBack = Vector3.Distance(pivot, eye);
|
|
System.Console.WriteLine(
|
|
$"[flap-sweep] cell=0x{cellId:X8} start=0x{startCell:X8} ok={r.Ok} resolved={(cp is not null ? "Y" : "n")} bsp={bsp} " +
|
|
$"desiredBack={desiredBack:F2} eyeBack={eyeBack:F2} pulledIn={desiredBack - eyeBack:F2} " +
|
|
$"in=({desiredEye.X:F6},{desiredEye.Y:F6},{desiredEye.Z:F6}) out=({eye.X:F6},{eye.Y:F6},{eye.Z:F6}) " +
|
|
$"viewerCell=0x{r.CellId:X8} collNormValid={r.CollisionNormalValid}");
|
|
}
|
|
|
|
// success: set_viewer(curr_pos, 0); viewer_cell = sphere_path.curr_cell (pc:92870-92871).
|
|
// Graph-tracked, no AABB/grace.
|
|
if (r.Ok) return new CameraSweepResult(eye, r.CellId);
|
|
|
|
// === Fallback 1 (pc:92878-92883): AdjustPosition at the sought eye ===
|
|
// The sweep found no valid position; try to seat the eye at its own cell.
|
|
// (Seed with the player cell — acdream's camera doesn't track the sought-eye's
|
|
// cell separately; the eye is near the player so its stab-list is the right one.)
|
|
var (eyeCell, eyeFound) = _physics.AdjustPosition(cellId, desiredEye);
|
|
if (eyeFound) return new CameraSweepResult(desiredEye, eyeCell);
|
|
|
|
// === Fallback 2 (pc:92886-92887): set_viewer(player_pos), viewer_cell = null ===
|
|
return new CameraSweepResult(playerPos, 0u);
|
|
}
|
|
|
|
/// <summary>Eye/pivot point → InitPath path point (subtract the sphere-center offset).</summary>
|
|
internal static Vector3 ToSpherePath(Vector3 spherePoint, float radius)
|
|
=> spherePoint - new Vector3(0f, 0f, radius);
|
|
|
|
/// <summary>InitPath path point → eye point (add the sphere-center offset back).</summary>
|
|
internal static Vector3 FromSpherePath(Vector3 pathPoint, float radius)
|
|
=> pathPoint + new Vector3(0f, 0f, radius);
|
|
}
|