From e4a5b8a51a7f2970a3b4c7a81ca4b6f30f2bca37 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 11 Apr 2026 18:06:52 +0200 Subject: [PATCH] fix(app): scale order for live entities + FlyCamera speed tune (Phase 5c-fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-reported visuals after Phase 5c ObjScale landed: - Nullified Statue is bigger but "distorted" and half-sunken into the foundry - WASD/Space/Ctrl camera speed too fast Fixes: 1. Scale multiplication order. Phase 5c used `scaleMat * PartTransform`, which in C# row-vector Matrix4x4 semantics means "scale first (in mesh-local space), then apply PartTransform." For multi-part meshes where each part has an attachment translation, this scales the mesh but leaves the attachment offset un-scaled — so child parts drift relative to each other and the base anchor ends up below the ground. Offline scenery hydration has always used the opposite order: `PartTransform * scaleMat`, meaning "transform first, then scale the resulting position." Matching that order fixes both distortion and sinking in one change, and makes live entities consistent with scenery's proven path. 2. FlyCamera.MoveSpeed 100 → 35. 100 world units/sec is ~half a landblock per second at AC scale — fine for terrain-scouting but too aggressive for inspecting specific entities like the foundry statue. 35 is a little faster than walk pace and feels right for visual iteration. --- src/AcDream.App/Rendering/FlyCamera.cs | 2 +- src/AcDream.App/Rendering/GameWindow.cs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/AcDream.App/Rendering/FlyCamera.cs b/src/AcDream.App/Rendering/FlyCamera.cs index 9dcd2a5..eaeb65f 100644 --- a/src/AcDream.App/Rendering/FlyCamera.cs +++ b/src/AcDream.App/Rendering/FlyCamera.cs @@ -11,7 +11,7 @@ public sealed class FlyCamera : ICamera public float FovY { get; set; } = MathF.PI / 3f; public float Aspect { get; set; } = 16f / 9f; - public float MoveSpeed { get; set; } = 100f; // world units per second + public float MoveSpeed { get; set; } = 35f; // world units per second (AC cell size = 24) public float MouseSensitivity { get; set; } = 0.003f; private const float PitchLimit = 1.5533f; // ~89 degrees diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 733c026..a49f78d 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -705,9 +705,16 @@ public sealed class GameWindow : IDisposable if (resolvedOverridesByPart is not null && resolvedOverridesByPart.TryGetValue(partIdx, out var partOverrides)) surfaceOverrides = partOverrides; - // Pre-multiply scale so it happens FIRST (local mesh coords) before - // the part-local transform and the entity-root transform. - var transform = scale == 1.0f ? mr.PartTransform : scaleMat * mr.PartTransform; + // Multiplication order matches offline scenery hydration: + // `PartTransform * scaleMat`. In row-vector semantics this means + // "apply PartTransform first (which includes the part-attachment + // translation), then scale in the resulting space." Using the + // opposite order (`scaleMat * PartTransform`) scales in mesh-local + // space first, which leaves the part-attachment offset unscaled — + // for multi-part entities like the Nullified Statue that causes + // the parts to drift relative to each other ("distorted") and the + // base anchor to end up below the ground ("sinks into foundry"). + var transform = scale == 1.0f ? mr.PartTransform : mr.PartTransform * scaleMat; meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, transform) {