diff --git a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs index 6b91c35..1aa9550 100644 --- a/src/AcDream.App/Rendering/Sky/SkyRenderer.cs +++ b/src/AcDream.App/Rendering/Sky/SkyRenderer.cs @@ -439,14 +439,53 @@ public sealed unsafe class SkyRenderer : IDisposable } /// - /// Lazy GfxObj build — reuses 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: + /// + /// + /// 0x010xxxxx — direct . Reuses + /// so the pos/neg polygon + /// splitting logic stays consistent with the main static-mesh + /// pipeline. Most sky meshes are single-surface. + /// + /// + /// 0x020xxxxx. The agent at + /// 2026-04-27 found these Setup-backed sky objects (e.g. + /// 0x02000588, 0x02000589, 0x02000714, + /// 0x02000BA6) 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 + /// CPhysicsObj::InitPartArrayObject at 0x0050ed40 + /// dispatches type 7 to CPartArray::CreateSetup + /// (decomp 280484) which loads the Setup and walks its parts. + /// We mirror that here: walks + /// Setup.Parts at the default placement frame and + /// 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). + /// + /// + /// + /// Even with this fix the visible aurora-style sheen most retail + /// rainy/cloudy setups produce comes from the pes_id field + /// on each (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. + /// /// 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; } + /// + /// Setup-backed sky object loader. Walks at + /// the default placement frame, builds submeshes via + /// , 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 pes_id particles, not + /// the underlying mesh). + /// + /// Mirrors retail's at + /// decomp 280484 dispatching type 7 → CPartArray::CreateSetup + /// → CSetup::SetSetupID, which loads the setup and instantiates + /// each part as a separate CPhysicsObj child. We collapse the + /// children into a flat submesh list because the sky pass renders + /// without per-part transforms anyway. + /// + /// + private void EnsureSetupUploaded(uint setupId) + { + Setup? setup = null; + try { setup = _dats.Get(setupId); } + catch { setup = null; } + + if (setup is null) + { + _gpuByGfxObj[setupId] = new List(); + return; + } + + var parts = SetupMesh.Flatten(setup); + var allSubs = new List(parts.Count); + foreach (var partRef in parts) + { + GfxObj? partGfx = null; + try { partGfx = _dats.Get(partRef.GfxObjId); } + catch { partGfx = null; } + if (partGfx is null) continue; + + System.Collections.Generic.IReadOnlyList? 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; + } + /// /// Log each surface's raw flag bits and the derived /// . Called once per GfxObj when diff --git a/tools/RainMeshProbe/Program.cs b/tools/RainMeshProbe/Program.cs index 1eaff70..0839f3d 100644 --- a/tools/RainMeshProbe/Program.cs +++ b/tools/RainMeshProbe/Program.cs @@ -59,7 +59,7 @@ static void ProbeSkySurface(DatCollection dats, uint sid) { Console.WriteLine($" Surface 0x{sid:X8} (NOT FOUND)"); return; } uint t = (uint)s.Type; bool luminous = (t & 0x40u) != 0u; - Console.Write($" Surface 0x{sid:X8} Type=0x{t:X8} Luminous={(luminous ? "YES" : "no ")} Lum={s.Luminosity:F4} Trans={s.Translucency:F4} "); + Console.Write($" Surface 0x{sid:X8} Type=0x{t:X8} Luminous={(luminous ? "YES" : "no ")} Lum={s.Luminosity:F4} Trans={s.Translucency:F4} Diff={s.Diffuse:F4} "); // Decode bits inline. var bits = new (uint mask, string n)[] { (0x01u,"B1Solid"),(0x02u,"B1Image"),(0x04u,"B1ClipMap"),(0x10u,"Translucent"),