feat(sky): load Setup-backed (0x020xxx) sky objects via SetupMesh.Flatten
Independent code review by an external agent (2026-04-27) flagged
that SkyRenderer.EnsureMeshUploaded only ever called
_dats.Get<GfxObj>(...) — every 0x020xxx Setup ID returned null and
got cached as an empty submesh list, silently dropping every
Setup-backed sky object across the Dereth Region. In Rainy DG3
alone that's 6 dropped SkyObjects (0x02000714, 0x02000BA6 ×2,
0x02000588 ×4, 0x02000589 ×3 across various time-of-day windows).
Verbatim from retail's CelestialPosition struct at acclient.h:35451:
struct CelestialPosition {
IDClass<...> gfx_id;
IDClass<...> pes_id; // particle scheduler
float heading; float rotation;
Vector3 tex_velocity;
float transparent; float luminosity; float max_bright;
unsigned int properties;
};
Per the named retail decomp, CPhysicsObj::InitPartArrayObject (decomp
~280484) dispatches gfx_id by type prefix: type 6 → direct GfxObj,
type 7 → Setup via CPartArray::CreateSetup (decomp ~287490) which
walks Setup.Parts. Mirror that here: detect 0x020xxxxx in
EnsureMeshUploaded, route to a new EnsureSetupUploaded helper that
flattens via SetupMesh.Flatten (existing Phase-2 utility) and bakes
each part's transform into the vertex positions before upload.
Sky setups don't animate in any way that affects the static-mesh
visual we render here.
Probe extension: also added the Diffuse column to RainMeshProbe's
sky-surface audit so the (Type, Translucency, Luminosity, Diffuse)
quadruple is visible on every flag-bit row.
Visual impact at verification launch: not observable. The Setup
objects in Rainy DGs appear to be tiny placeholder meshes existing
mainly to anchor PES emitters. The dynamic "aurora-like" sheen the
user observes in retail comes from the PES particle layer, which
remains unimplemented (issue #28). Keeping this fix because the
geometry path is now decomp-correct and provides foundation for
the eventual PES wiring.
Issue #29 filed for the residual cloud-density gap. 1227 tests pass.
This commit is contained in:
parent
375065ba94
commit
646ccca85e
2 changed files with 108 additions and 4 deletions
|
|
@ -439,14 +439,53 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lazy GfxObj build — reuses <see cref="GfxObjMesh"/> so the
|
||||
/// pos/neg polygon splitting logic stays consistent with the main
|
||||
/// static-mesh pipeline. Most sky meshes are single-surface.
|
||||
/// Lazy mesh build for a sky object. Handles two cases:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>
|
||||
/// <c>0x010xxxxx</c> — direct <see cref="GfxObj"/>. Reuses
|
||||
/// <see cref="GfxObjMesh.Build"/> so the pos/neg polygon
|
||||
/// splitting logic stays consistent with the main static-mesh
|
||||
/// pipeline. Most sky meshes are single-surface.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// <c>0x020xxxxx</c> — <see cref="Setup"/>. The agent at
|
||||
/// 2026-04-27 found these Setup-backed sky objects (e.g.
|
||||
/// <c>0x02000588</c>, <c>0x02000589</c>, <c>0x02000714</c>,
|
||||
/// <c>0x02000BA6</c>) were silently dropped: every cache miss
|
||||
/// fell into the GfxObj branch, returned null, and got cached
|
||||
/// as an empty submesh list. Per the named retail decomp
|
||||
/// <c>CPhysicsObj::InitPartArrayObject</c> at <c>0x0050ed40</c>
|
||||
/// dispatches type 7 to <c>CPartArray::CreateSetup</c>
|
||||
/// (decomp 280484) which loads the Setup and walks its parts.
|
||||
/// We mirror that here: <see cref="SetupMesh.Flatten"/> walks
|
||||
/// <c>Setup.Parts</c> at the default placement frame and
|
||||
/// <see cref="GfxObjMesh.Build"/> produces submeshes for each
|
||||
/// part. Per-part transforms are baked into vertex positions
|
||||
/// (sky setups are static — no animation needed for the static
|
||||
/// mesh half of the visual).
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Even with this fix the visible aurora-style sheen most retail
|
||||
/// rainy/cloudy setups produce comes from the <c>pes_id</c> field
|
||||
/// on each <see cref="DatReaderWriter.Types.SkyObject"/> (a Particle
|
||||
/// Effect Schedule) — that's a separate Phase-level feature.
|
||||
/// Rendering the Setup's static parts here is the geometry half;
|
||||
/// the dynamic particle half is deferred.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void EnsureMeshUploaded(uint gfxObjId)
|
||||
{
|
||||
if (_gpuByGfxObj.ContainsKey(gfxObjId)) return;
|
||||
|
||||
// Setup-backed sky object: walk Setup.Parts and bake per-part
|
||||
// transforms into the per-vertex positions. See doc comment above.
|
||||
if ((gfxObjId & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
EnsureSetupUploaded(gfxObjId);
|
||||
return;
|
||||
}
|
||||
|
||||
// DatCollection isn't thread-safe and the streaming loader can be
|
||||
// actively reading a shared DatBinReader buffer; sky meshes are
|
||||
// loaded on the render thread but GfxObj.Unpack can race with the
|
||||
|
|
@ -487,6 +526,71 @@ public sealed unsafe class SkyRenderer : IDisposable
|
|||
_gpuByGfxObj[gfxObjId] = gpuList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup-backed sky object loader. Walks <see cref="Setup.Parts"/> at
|
||||
/// the default placement frame, builds submeshes via
|
||||
/// <see cref="GfxObjMesh.Build"/>, and bakes the per-part transform
|
||||
/// into the vertex positions before upload. Static-pose only — sky
|
||||
/// setups don't animate in any meaningful way for the visual we care
|
||||
/// about (the dynamic look comes from <c>pes_id</c> particles, not
|
||||
/// the underlying mesh).
|
||||
/// <para>
|
||||
/// Mirrors retail's <see cref="CPhysicsObj.InitPartArrayObject"/> at
|
||||
/// decomp <c>280484</c> dispatching type 7 → <c>CPartArray::CreateSetup</c>
|
||||
/// → <c>CSetup::SetSetupID</c>, which loads the setup and instantiates
|
||||
/// each part as a separate <c>CPhysicsObj</c> child. We collapse the
|
||||
/// children into a flat submesh list because the sky pass renders
|
||||
/// without per-part transforms anyway.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void EnsureSetupUploaded(uint setupId)
|
||||
{
|
||||
Setup? setup = null;
|
||||
try { setup = _dats.Get<Setup>(setupId); }
|
||||
catch { setup = null; }
|
||||
|
||||
if (setup is null)
|
||||
{
|
||||
_gpuByGfxObj[setupId] = new List<SubMeshGpu>();
|
||||
return;
|
||||
}
|
||||
|
||||
var parts = SetupMesh.Flatten(setup);
|
||||
var allSubs = new List<SubMeshGpu>(parts.Count);
|
||||
foreach (var partRef in parts)
|
||||
{
|
||||
GfxObj? partGfx = null;
|
||||
try { partGfx = _dats.Get<GfxObj>(partRef.GfxObjId); }
|
||||
catch { partGfx = null; }
|
||||
if (partGfx is null) continue;
|
||||
|
||||
System.Collections.Generic.IReadOnlyList<GfxObjSubMesh>? partSubs = null;
|
||||
try { partSubs = GfxObjMesh.Build(partGfx, _dats); }
|
||||
catch { partSubs = null; }
|
||||
if (partSubs is null) continue;
|
||||
|
||||
// Bake the part's local transform into the vertices. For sky
|
||||
// setups we don't expect non-uniform scale, so transforming
|
||||
// normals as directions is fine; if a future sky setup ever
|
||||
// breaks that assumption we'd need an inverse-transpose here.
|
||||
var partTx = partRef.PartTransform;
|
||||
foreach (var sub in partSubs)
|
||||
{
|
||||
var transformed = new Vertex[sub.Vertices.Length];
|
||||
for (int i = 0; i < sub.Vertices.Length; i++)
|
||||
{
|
||||
var v = sub.Vertices[i];
|
||||
var p = Vector3.Transform(v.Position, partTx);
|
||||
var n = Vector3.Normalize(Vector3.TransformNormal(v.Normal, partTx));
|
||||
transformed[i] = v with { Position = p, Normal = n };
|
||||
}
|
||||
var rebuilt = sub with { Vertices = transformed };
|
||||
allSubs.Add(UploadSubMesh(rebuilt));
|
||||
}
|
||||
}
|
||||
_gpuByGfxObj[setupId] = allSubs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log each surface's raw flag bits and the derived
|
||||
/// <see cref="TranslucencyKind"/>. Called once per GfxObj when
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue