Commit Graph
137 Commits
Author SHA1 Message Date
atdunbg 5d88ed2057 Improve PBR quality: mip chains, anisotropic filtering, baseColorFactor
Root causes behind blurry/unphysical materials (4K pocket watch render
mushy, glass fully opaque, watch hands unreadable):

- Textures loaded via stbi had a single mip level: 4096x4096 albedo/MR
  maps sampled with no LOD -> minification aliasing turned fine detail
  (watch hands) into noise. Generate a full blit-based mip chain with
  per-level layout transitions.
- samplerAnisotropy was never enabled on the device and every sampler
  was created with anisotropy disabled: grazing-angle surfaces (watch
  face, ground) collapsed into a blur. Enable the device feature and
  16x anisotropic filtering on material texture samplers.
- glTF baseColorFactor was ignored: the glass material (alphaMode
  BLEND, no baseColor texture, baseColorFactor.a = 0.3) fell back to
  the default white texture with alpha = 1 and rendered fully opaque.
  MaterialParams gains a baseColor vec4 (rgb tints albedo, a scales
  the blend alpha) threaded through both PBR shaders.
- alphaMode MASK was treated as opaque: add a factors.w switch that
  performs an alpha cutout test in the opaque shader.
- Fix material cache key dropping factors.z (separate-roughness
  switch); it now encodes the full factors and baseColor vectors.
2026-10-05 12:01:07 +08:00
atdunbg a3037cd401 Input facade: add cursor capture, mouse delta and scroll delta
SetCursorCaptured / IsCursorCaptured / GetMouseDelta / ConsumeScrollDelta
now route through the Input facade, so the editor camera no longer takes
a Window& at all: Camera::Update and EditorCamera::Update lose their
window parameter and SandboxApp calls Update(dt) directly. App layer
now has zero Window/GLFW usage for input - everything goes through
Engine::Input.
2026-10-05 11:45:57 +08:00
atdunbg 641b255cb2 Unify input behind Engine::Input facade; fix Ctrl+P and Pause button
New Engine/Core/Input.h/.cpp: static input facade with a Key/MouseButton
enum (values map to GLFW key codes internally) so scripts and game logic
no longer touch GLFW directly - window backend becomes swappable and the
facade is the surface to expose when embedding scripting later. Wired up
in Application's constructor; Camera (editor camera WASD/RMB/MMB), the
WASDMove script and SandboxApp key handling now query Input only.

Ctrl+P hotkey fix: the !m_Playing guard made the hotkey dead exactly
when it was needed - after pressing Play. Now toggles both ways.

Toolbar Pause button now reads "Resume" while paused (tooltip follows).
2026-10-05 11:34:27 +08:00
atdunbg c083ee5cb7 Fix PIE deadlock when playing a scene without a primary camera
BuildFrame returned false (skip frame) when the game view had no
primary camera, skipping the WaitFence/ResetFence/submit chain - the
frame-slot fence never got signaled, so the next WaitFence on that
slot blocked forever. Reproduced instantly by playing an empty scene
with embedded PIE; latent in manual fullscreen preview too.

Game view now falls back to the editor camera instead of skipping the
frame, keeping the per-slot fence cadence intact. Viewport warning
text updated to mention the editor-view fallback.
2026-10-05 11:21:50 +08:00
atdunbg 1a278d5945 PIE: viewport-embedded playback with UE-style main toolbar
Play no longer switches to fullscreen direct rendering: PIE now keeps
the editor UI alive and renders the primary camera into the Viewport
panel (new SceneRenderer::SetGameView flag; kDisplayDirect stays a
manual fullscreen preview for later release form). Editor camera stops
being driven during PIE, and viewport click-pick is gated off while
playing (clicks are game input now).

The Play/Stop buttons move out of the menu bar into a dedicated main
toolbar row below it (centered, UE Main Toolbar semantics): green Play
when idle; Pause (yellow, highlighted while paused) + Stop (orange)
while playing. Ctrl+P hotkey kept. Stats "Pause Simulation" merged
into the toolbar Pause; viewport overlay shows PAUSED accordingly.
2026-10-05 11:14:24 +08:00
atdunbg 5ca5ac2a1e Editor: group Details parameters and reorganize Stats panel
Details: multi-parameter components (RigidBody/Joint/CharacterController/
Animation) now use SeparatorText category headers in UE Details style;
all parameter labels normalized to Pascal Case.

Stats: rearranged into View / Scene / Rendering / Physics / Performance
collapsing groups. Clear Color moved to Scene, Debug View to Rendering,
GPU Timings + Frame merged into Performance; bare top-level widgets and
the now-obsolete Environment group removed.
2026-10-05 01:20:00 +08:00
atdunbg 432089747f Environment settings become scene entities (UE Skybox/SkyLight semantics)
Skybox/SkyLight are now Actor-carried components instead of resident
global settings, matching UE where SkyAtmosphere/SkyLight/fog are
Actors added from Place Actors and configured in Details.

- New SkyboxComponent (active + envPath asset slot) and SkyLightComponent
  (active + ambientColor); first-active entity wins per frame
- SceneRenderer collects env from components into FrameSceneData
  (DrawSkybox/envPath/ambient); Renderer performs delta sync - env
  reloads only when the resolved path changes, neutral fallback when
  empty or unloadable
- App-side env plumbing removed: kEnvPaths table and getEnvIndex/
  setEnvIndex callbacks deleted; StatsPanel keeps ClearColor + debug
  view, env params move to Details
- Serializer: scene-level Settings stores ClearColor only; env
  components serialize per entity; legacy v1 scenes (Environment header
  + Settings.AmbientColor/DrawSkybox) auto-migrate into an Environment
  entity on load for visual parity
- Inspector: Skybox/Sky Light sections with .hdr asset slot dropdown
  and Add Component entries; Outliner "+ Create" popup offers
  Empty/Light/Camera/Environment
2026-10-05 01:02:21 +08:00
atdunbg b7d6f53591 Editor: rework default layout to UE5 arrangement
Panels renamed to UE semantics (docking identity change): Hierarchy ->
Outliner, Inspector -> Details, Asset Browser -> Content Browser.

Default DockBuilder layout now matches UE5 factory layout: Details
docked right (full height), Content Browser docked bottom spanning
left+center, Outliner top-left, Viewport center with Stats as a
side tab.

Legacy imgui.ini (old window names, no new names) is removed once at
Init so the new default rebuilds cleanly; fresh or new-style ini is
untouched.
2026-10-05 00:47:33 +08:00
atdunbg 42757bab36 AssetManager: drop immediate SaveGUIDRegistry on registration
2268fa5 added the dirty-flag deferred flush but left the immediate
save in place, so new GUID registrations wrote the registry twice
(once inline, once at the next FlushGUIDRegistry). Remove the inline
save so the deferred path is the only writer.
2026-10-05 00:39:48 +08:00
atdunbg dec171cd1e Fix selection outline never showing for the first entity
entt entity 0's integral bits are 0, colliding with the "no selection"
sentinel: u32 selected == 0 meant "clear selection", so the scene's
first-created entity could never be outlined. Replaced with
std::optional<u32> + FrameSceneData::selectedEntityValid.

Also restore per-batch instance contiguity broken by the batchMats
flattening (e038cb3): entities sharing a mesh scatter across the flat
instance array in collection order. ViewScratch now tracks per-batch
singly-linked lists and appendView aggregates each batch into a
contiguous range, preserving the flat-array performance intent.

Hierarchy click selection now routes through SelectEntity so outline
stays in sync when picking from the panel.
2026-10-05 00:39:48 +08:00
atdunbg e038cb39fa SceneRenderer 实例收集扁平化:batchMats 从 vector<vector> 改为单 vector 按 (first,count) 切片,优化大场景下批次实例存储与 cache 命中率 2026-10-04 21:26:48 +08:00
atdunbg 3e85285e41 修改SceneRenderer 的存储的世界矩阵存储方式 2026-10-04 20:10:49 +08:00
atdunbg 2268fa5f76 资产改为保存场景或者关闭软件时候更新guid文件,vulkan/DescriptorManager 创建DescriptorPool时候注释eFreeDescriptorSet,PhysicsWorld初始化时候改为推荐的inNumBodyMutexes 2026-10-04 19:55:49 +08:00
atdunbg c1854453e5 修改场景实体查询为unordered_set存储,提升查询效率, 修改Physics的SyncScene也为unordered_set 2026-10-04 19:12:53 +08:00
atdunbg b78d9fb4c9 修改错误的TryBitsByBodyId返回值 2026-10-04 18:49:18 +08:00
atdunbg c8a1cd90bf 修复physics物理数据计算不正确的问题,同时为物理添加map索引 2026-10-04 18:42:23 +08:00
atdunbg ee5ab8e94e feat: parent-child transform hierarchy (UE Actor attach semantics)
- TransformComponent TRS = local (relative to parent); cached world SRT
  composed hierarchically (parent pos/rot/scale, no matrix decomposition
  shear under non-uniform parent scale)
- HierarchyComponent (parent + children, lazily attached); Scene::
  SetParent (keepWorld, cycle-safe) / GetParent / IsDescendantOf /
  SetWorldPose (world->local inverse, used by physics writeback & gizmo)
- UpdateTransforms: DFS from roots, recompose only dirty-or-dirty-
  ancestor subtrees (parents before children)
- DestroyEntity: descendants buried with parent (UE cascade)
- Serialization v2: optional Parent uuid key, two-pass relink on load
- Hierarchy panel: tree view, drag-drop reparent / drop-to-root detach,
  Create Child / Detach context menu; Inspector shows parent name
- Consumers moved to world-space reads: lights, camera pos, physics
  bodies/characters/joints/collider baking, gizmo, F-focus, AlignTo
2026-10-04 15:22:25 +08:00
atdunbg 97408e506c fix: ImGui hover jumps while cursor captured (RMB look / MMB orbit)
With GLFW_CURSOR_DISABLED the official GLFW backend keeps feeding
virtual unbounded cursor coords to ImGui each frame, so hover state
wanders across hidden panels while the physical cursor is invisible.
Append a "mouse left the window" pos event before ImGui::NewFrame
whenever the cursor is captured - ImGui treats the frame as mouseless.
2026-10-04 15:00:29 +08:00
atdunbg 230df67db7 fix: editor camera draw distance 100m -> 10km (UE semantics)
UE's far clip is effectively unlimited and shadow distance is an
independent parameter. Split kCameraFar (main projection, 10km) from
kShadowDistance (cascade splits, unchanged 100m) so distant geometry
is no longer clipped while shadow quality stays bit-identical.
Main projection now references the shared constants instead of
duplicated literals.
2026-10-04 14:49:27 +08:00
atdunbg 13055cc8ac fix: assimp INCOMPLETE flag no longer rejects animation-only FBX
Assimp flags every mesh-less scene as AI_SCENE_FLAGS_INCOMPLETE
(FBXConverter), so pure animation FBX never reached clip parsing -
the scene was discarded before ParseAnimations ran. Only treat null
scene / missing root node as hard failure; log INCOMPLETE as a
warning and continue. Final validity stays with ParsedModel::Valid().
2026-10-04 09:31:23 +08:00
atdunbg 175ce35def fix: animation-only FBX assets loadable; clip data viewer
Pure-animation FBX (no meshes, skeleton clips only) was rejected at
the cache layer: ParsedModel::Valid() required non-empty meshes, so
Model::FromParsed/LoadModel returned null and the asset never made it
into the cache. Animation assets are now first-class:

- ParsedModel::Valid() = meshes or clips non-empty; parse failures
  (both empty) still rejected. Empty-mesh consumers already guarded
  (rendering skips empty mesh loop, mesh collider falls back to Box)
- ParseFromDisk: fix null-deref when assimp returns a null scene
- AssetCache::Find(path) + AssetManager::GetModelByPath for panels
- Asset Browser hover card shows meshes/bones/clips for loaded models
- Inspector model line flags "animation-only" when meshes == 0
- Animation section: collapsible clip data table (name/duration/
  channel count) to verify bone-name matching of attached clips
2026-10-04 02:31:30 +08:00
atdunbg 2577809ee4 feat: Play-In-Editor (PIE) with in-memory scene snapshot
Editor toolbar gains Play/Stop (Ctrl+P) with UE semantics:

- Play: serialize current scene to an in-memory YAML snapshot via new
  SceneSerializer::SerializeToString, switch display to kDisplayDirect
  (primary camera entity renders), unfreeze time (timeScale 0 -> 1)
- Stop: DeserializeFromString atomically rebuilds the pre-play scene,
  back to kDisplayViewport (editor camera), refreeze time; selection
  cleared and gizmo drag reset since all entity handles dangle
- SceneSerializer refactored: SerializeDoc/ParseRoot shared by file
  and memory variants; file Save/Load behavior unchanged
- Running state disables gizmo + editor hotkeys in the viewport (input
  goes to game scripts), draws yellow PLAYING hint and a no-camera
  warning; File->Save/Load disabled while playing (UE behavior)

Physics bodies/constraints/characters and script instances are
reconciled automatically by the existing per-frame sync on restore.
2026-10-04 02:22:49 +08:00
atdunbg c949e8684b build: strip Jolt debug symbols from Release (55MB -> 11.3MB exe)
Jolt's GENERATE_DEBUG_SYMBOLS option defaults to ON and injects -g in
every config, so the Release binary carried ~44MB of DWARF sections
(.debug_info/.debug_line/...) from Jolt TUs alone -- 80% of the 55MB
exe. Force the option OFF for Release builds only (the vendored
submodule is untouched; Debug/RelWithDebInfo keep symbols).

Measured: 55.0MB -> 11.3MB, .text unchanged at 8.5MB, zero debug
sections left. The remaining ~1.6MB is the ELF symtab, useful for
crash stacks; plain strip takes it to 9.7MB if a distributable is
needed.
2026-10-04 02:10:08 +08:00
atdunbg 90097a756e fix(editor): rotation drag accumulates without wrap (UE session semantics)
Dragging rotation hit a wall at the wrap point: each frame re-read
euler from the quat and wrapped to [0,360), so dragging past 360
snapped the display back to 0 (and backwards dragging wrapped up to
360) -- values "jumped around" instead of accumulating.

Port UE TransformDetails session semantics: the rotation row shows a
local mirror value while the user edits it (accumulating freely in
both directions, never wrapping), and re-syncs from the quat only on
foreign changes (gizmo drag, physics writeback, entity switch).
Detection is exact component-wise quat equality: the mirror quat comes
from the same QuatFromEuler used by SetRotation, so an untouched pair
compares bit-equal, while any foreign writeback -- including sub-0.05
degree gizmo steps that a dot-product threshold would miss -- differs
in at least one float. Edit frames skip the check (mirror leads the
quat by one drag delta), gated on the previous frame's item-active
state.
2026-10-04 02:00:42 +08:00
atdunbg 4498bc7535 feat(editor): rotation display in UE's 0-360 convention
User pointed out the real gap: UE Details shows rotation in [0,360)
(-90 reads 270); our readback exposed [-180,180] with negative zeros.
GetRotation now wraps each axis to [0,360) via fmod -- strictly
equivalent math (each axis has 360-degree period), so matrix-rebuild
consumers like Camera::EulerRotMat are unaffected. The one consumer
needing a continuous domain is the free-look pitch clamp [-89,89] in
Camera::Update, which wraps back to [-180,180) before applying deltas.
2026-10-04 01:46:05 +08:00
atdunbg 4fc48c21ac fix(editor): euler display precision at gimbal lock (90 -> 89.98)
Typing 90 degrees on the pitch axis showed 89.98. The stored quat is
exact (matrices differ by 0.0) -- the noise is display-side: quat's
sin(90) components square to 0.99999994 in float, and asin() diverges
at |sin|->1, amplifying 6e-8 into 0.02 degrees. Yaw/roll use atan2 and
were always exact, matching the user's report being X-axis only.

Two fixes, both aligned with how UE gets away with it (internally UE
is quat + equally pathological, its Details panel just shows one
decimal):
- GetRotation gimbal-lock threshold 1.0 -> 1-1e-6: pitch near +-90
  now takes the composite-angle atan2 branch (ratio-stable) instead
  of asin, so typing 90.0 reads back exactly 90.0. Probed: X=90 and
  X=-90 now round-trip to 0 error; generic poses stay ~1e-6 deg.
- Rotation row shows one decimal ("%.1f", UE Details default), masking
  the residual asin noise for angles near (not at) the pole; Ctrl+Click
  input stays full precision. Negative zero from atan2 cleaned up.

Remaining asin noise for e.g. 89.9 (~9e-4 deg) is below the 1-decimal
display and unreachable in practice.
2026-10-04 01:37:49 +08:00
atdunbg 90bf63e641 feat(animation): attach external animation files to a model
Animation-only FBX files (no mesh, clips only) could not be used:
clips are keyed by node index into the parsed model's node table, so
there was no path to get them onto a different model's skeleton.

Add AssetManager::AttachAnimationClips: parses the container with the
CPU-only path (no GPU upload -- empty meshes cannot build a Model),
remaps each clip's channels to the target model's node indices by
bone name, and appends to its clip list. Channels whose bone name is
absent are skipped and counted; clips with zero matching channels are
dropped. Same-source attach is idempotent via a new
AnimationClip::sourcePath marker; name clashes get the file stem
prefixed.

Inspector Animation block gains an attach slot (UE Details semantics)
listing model-container files plus a bullet list of attached sources;
already-attached paths are replayed idempotently when the entity is
selected, so clips survive restarts. The paths persist in the scene
(Animation.Attached); the clips themselves stay runtime state.

Limitations (documented, by design): same-skeleton retargeting only
-- bone names must match between source and target (same DCC export
or Mixamo-style per-animation downloads); cross-skeleton retargeting
is out of scope.
2026-10-04 01:25:04 +08:00
atdunbg 69671cf4eb feat(editor): manual mesh renderer setup + asset hover info card
Mesh Renderer was missing from the Add Component menu and its model
reference was read-only, so a mesh could only be attached by loading
a saved scene. Add it to the menu (Unity Add Component semantics) and
give the block a UE-Details-style asset slot: a dropdown listing all
model files under assets/, selecting applies LoadModel (cache deduped).

Asset browser files now show a UE Content Browser style hover card:
kind, size, modified time, and loaded state (new AssetCache::Contains
query, read-only -- hovering never triggers loading).

Fix latent FATALs unlocked by manual assignment: AssetCache::Get
aborts on invalid handles, so rendering, animation sampling, and mesh
collider build now guard mr.model.Valid() before calling it.
2026-10-04 01:07:38 +08:00
atdunbg 7d02d344e5 fix(scripts): prevent duplicate script attachment per entity
The per-entity script list allowed picking the same script class
twice; the name-keyed runtime silently ignores duplicates, leaving
the extra rows dead weight with unusable enable bits. Enforce
DisallowMultipleComponent semantics: the dropdown of each row only
offers unattached names (own name kept for echo), "+ Add Script"
disables when every registered script is attached, and scene loading
dedupes as a safety net for hand-edited files.
2026-10-04 00:57:45 +08:00
atdunbg fd2d8f9b34 fix(editor): asset browser directory navigation
The file list was hardwired to the root directory, so it was always
empty (all assets live in subdirectories). Directories are now
selectable from both the tree (click row) and the list (click [D]
row), with a breadcrumb + Root button. Prefix check done manually
(path::starts_with is missing in this libstdc++).
2026-10-04 00:55:19 +08:00
atdunbg ab7a43a686 feat(editor): multi-script entities + asset browser panel
ScriptComponent now holds a list of {name, active} items (Unity
semantics: one entity, many scripts, each with its own enable bit;
component-level active stays as the master switch). ScriptRuntime
reconciles instances by name per entity -- rename/remove destroys
only the affected instances, untouched ones keep ticking. Scenes
persist Script.Items; old single-script key still loads.

AssetBrowserPanel (lightweight Unity Project window): fixed roots
(assets/scenes) as tabs, recursive directory tree, file list with
type filter (models/textures/scenes) and name substring filter,
double-click preloads models/HDRs into the asset caches (path-
deduped). Scene files point to the File menu for now; drag & drop
is a later iteration. Docked with Stats by default in fresh layouts.
2026-10-04 00:43:06 +08:00
atdunbg 74a2044441 chore(scripts): rename script API to GetComponent/TryGetComponent; misc
Clearer accessor naming on ScriptBehavior (Get -> GetComponent,
TryGet -> TryGetComponent), WASDMove follows. Teleport marked const
(JPH Ref does not propagate constness). Spinner: auto-rotation
disabled for the moment, registration switched from the registration
macro to an explicit lambda to verify the mechanism.
2026-10-04 00:33:47 +08:00
atdunbg 134812dae3 feat(physics): debug-draw character controller capsule
CharacterVirtual is not a registered body, so PhysicsSystem::DrawBodies
never renders it (Jolt provides no CharacterVirtual::DebugDraw either).
Add CharacterController::DebugDraw (shape wireframe at foot-origin +
shapeOffset, green) and drive it from CollectDebugData under the
colliders bit.
2026-10-03 19:01:57 +08:00
atdunbg 29c413579d refactor(physics): rename isSensor to isTrigger (Unity/UE terminology)
UE calls it Trigger (Overlap Events / Trigger preset) and Unity's UI
says "Is Trigger"; "sensor" is the physics-backend term (Jolt sensor,
Box2D sensor). Rename the component/record flag and the Inspector
label. Scene files now persist "IsTrigger" with backward-compatible
read of the old "IsSensor" key.
2026-10-03 18:53:38 +08:00
atdunbg b6d0868e15 feat(ecs): per-component enable/disable + inspector rework
Add `active` flag to the 8 behavior components (Unity enabled / UE
SetActive semantics; structural Id/Name/Transform excluded). Consumers
skip disabled components: renderer (mesh/light/anim), primary camera
election, physics reconciliation (body/constraint/character destroyed
and rebuilt on toggle), scripts keep instance but stop ticking.

Inspector: component headers now carry an enable checkbox and a remove
button, Add-Component menu greys out attached entries, and conflict
hints added (RigidBody <-> CharacterController mutual exclusion, Joint
requires RigidBody, Animation requires MeshRenderer, unregistered
script name). Scenes persist Active per component (default true).
2026-10-03 18:45:03 +08:00
atdunbg cad0481bc0 feat(app): WASDMove script (WASD camera-relative movement)
ScriptBehavior gains App() host access (Application injected via
ScriptRuntime), TryGet<T> and EntityBits for physics facade addressing.
WASDMove maps WASD to the editor camera's horizontal heading (UE
gameplay convention): dynamic bodies go through the physics velocity
facade preserving the vertical component (zeroing Y would cancel
gravity mid-fall), bodiless entities translate their transform directly.
Editor-mode key conflict is naturally avoided: RMB-held flight consumes
WASD, release hands keys to the script.
2026-10-03 18:27:12 +08:00
atdunbg 3403822a52 feat(engine): C++ native script system (ScriptBehavior + registry)
ScriptComponent stores only a script name string (POD, serializable);
instances are reconciled by ScriptRuntime each frame (PhysicsWorld
pattern): orphan/rename cleanup with OnDestroy, per-name instantiation
from a file-level self-registering registry (ENGINE_REGISTER_SCRIPT),
OnCreate on bind, OnUpdate after physics step on scaled dt. Engine
framerate loop only sees the base interface, so future Lua/C# behaviors
plug in as foreign ScriptBehavior implementations with zero engine
changes. Ships a Spinner sample script and Inspector script picker.
2026-10-03 18:02:49 +08:00
atdunbg ea6beee593 fix(physics): editor drag teleports dynamic bodies
SyncScene only reconciled static/kinematic transforms into Jolt bodies;
dragging a dynamic entity (esp. while paused, where Writeback never runs)
moved the visual but left the body -- and the collider debug wireframe --
at the old pose. Dynamic bodies now get the same drag reconciliation
(teleport + zero velocity + activate), and Writeback syncs
lastPos/lastRot to the body pose so physics motion is never misread as
editor dragging (which would teleport the body back).
2026-10-03 17:51:11 +08:00
atdunbg d5a86c170e fix(renderer): mesh collider source positions in render space for skinned meshes
Skinned meshes keep vertices in raw mesh space (bakeTransform=false);
rendering transforms them via bind palette (global(bone) x invBind, folds
FBX unit scale). Physics MeshShape/ConvexHull and the mesh bounding sphere
consumed the raw positions directly -> M1911 hull measured ~10x the visual
size. ParseFromDisk now bakes bind-palette-skinned positions per vertex
(same formula as the skinning shader) into ParsedMesh::cpuPositions; Mesh
uses them for m_CpuPositions and bounds. GPU vertex data and rendering are
untouched.
2026-10-03 17:43:55 +08:00
atdunbg e1cef53158 physics: character controller (Jolt CharacterVirtual)
- CharacterControllerComponent: capsule params (radius/halfHeight, mass,
  max slope, step height); entity origin = feet via shape offset
- CharacterController per-entity wrapper: gravity integration + grounded
  clamp, ExtendedUpdate (sweep/slide + stick-to-floor + walk-stairs),
  SetLinearVelocity/Jump/IsGrounded facade
- PhysicsWorld: SyncCharacters reconcile (create/rebuild on param
  change, editor-drag teleport), UpdateCharacters after physics Update
  (Jolt recommended order), writes back to Transform
- Inspector: params + grounded/velocity telemetry + jump debug button
- Serialization: params only, state rebuilt by reconcile
2026-10-03 16:34:20 +08:00
atdunbg 82910c41e8 physics: joint constraints (fixed/hinge/ball)
- JointComponent: type, target UUID, per-body local anchors, hinge axis
  + angle limits (data only, zero Jolt deps)
- PhysicsWorld::SyncConstraints: create/remove/rebuild on snapshot
  change; body rebuild contagion via invalidated-bits list; target
  resolved lazily by UUID each sync (no two-pass load needed)
- Jolt FixedConstraint/HingeConstraint/PointConstraint in
  LocalToBodyCOM space (anchors minus shape COM); fixed frames lock
  creation-time relative pose; hinge axis aligned across bodies
- Inspector: Joint section with target entity name dropdown
- Serialization: Joint map with UUID target, as<T> fallbacks
2026-10-03 16:26:20 +08:00
atdunbg a744b332eb physics: capsule/cylinder/compound collider shapes
- ColliderType + Capsule/Cylinder/Compound; CompoundChild list (Box/
  Sphere/Capsule, local pos/quat, non-recursive)
- CreateBody: CapsuleShape/CylinderShape (ctor param orders differ) +
  StaticCompoundShape; empty/failed compound falls back to Box
- BodyRecord caches radius/halfHeight/compound; SyncScene rebuilds on
  change (per-collider param extraction)
- Inspector: combo entries + shape params + compound child list editor
- Serialization: new fields with as<T> fallback, compound seq round-trip
2026-10-03 16:17:44 +08:00
atdunbg 6c81bfd70d physics: rigid body drive API (force/impulse/torque/velocity)
PhysicsWorld facade methods addressed by entity bits via body records:
AddForce/AddImpulse/AddTorque (dynamic bodies only, auto-wake), Set/
GetLinearVelocity/SetAngularVelocity (dynamic + kinematic). Inspector
RigidBody block gains impulse debug controls for manual response tests.
Transient by design — nothing serialized.
2026-10-03 16:10:22 +08:00
atdunbg fae6614e70 editor camera: dedicated Scene-view camera decoupled from camera entities
EditorCamera (UE FEditorViewportClient semantics) owned by SceneRenderer:
own TransformComponent + reused Camera controller. BuildFrame dispatches
render source by display mode — editor form always renders from the
editor camera (usable in empty scenes), game form renders from the
scene's primary camera entity. AlignTo copies an entity pose into the
editor camera ("Align Viewport to Camera" button in Inspector). F focus
and viewport gizmo now follow the same dispatch. main.cpp stripped of
dangling sandbox camera/spinner state; onSceneLoaded hook removed.
2026-10-03 15:52:28 +08:00
atdunbg e2a9c21e68 fix(serialization): load failure on hardened yaml-cpp + flow-style arrays
yaml-cpp master hardens zombie nodes: operator=, size(), Type() and
chained operator[] all throw InvalidNode instead of silently passing.
Storing per-component nodes via Node::operator= threw for every key
that was absent, so all 7 entities were skipped on load (0 loaded).
Reads are now level-by-level guarded (copy-init + IsDefined), which
only uses the safe surface (as<T>(fallback), begin(), IsDefined).

Save side switched from node-tree streaming to a manual Emitter:
vector/quat fields emit as single-line flow arrays ([0, 1.2, -6])
and model refs as inline maps, with 9-digit float precision for
lossless float32 round-trips.

Verified offline against the previously saved scene: 7/7 entities
parse with correct component counts; emitter output round-trips.
2026-10-03 15:30:36 +08:00
atdunbg 007d175314 serialization: scene save/load (Unity-style YAML + UE-style GUID registry)
Engine-layer SceneSerializer (yaml-cpp submodule): entities/components
to versioned YAML, atomic clear-and-rebuild on load with per-entity
fault tolerance, missing-field defaults, unknown-component skip and
UUID dedup. Asset references are {Guid, Path} pairs resolved through
the new AssetManager GUID registry (assets/asset_registry.yaml) with
path fallback; procedural:// pseudo-paths rebuild from parameters.
Editor File menu gets Save/Load Scene modals; app re-resolves stale
entity handles via onSceneLoaded callback.
2026-10-03 14:47:02 +08:00
atdunbg ffe33e9967 camera: UE Scene-view semantics — RMB fly, MMB focus-orbit, wheel dolly, F focus
Key conflict fix (UE semantics): while RMB is held the keyboard belongs
to the camera, so WASD no longer collides with the gizmo W/E/R tool
hotkeys. Editor mode gates fly movement behind RMB; game mode keeps
always-on WASD. Viewport tool hotkeys (W/E/R/T/F) are suppressed while
RMB is held.

Focus orbit (pivot): MMB drag rotates the camera around a pivot keeping
distance; first orbit picks a pivot 5m ahead on the view ray. Shift+MMB
pans in screen plane (camera and pivot move together). Mouse wheel
dollies toward/away from the pivot with exponential steps (no pivot:
move along view ray). F focuses the selected entity (frame-selection):
pivot set to its position, camera slides along the view ray; orbit
distance persists.

Window gains a polled scroll-delta channel (ConsumeScrollDelta) that
respects ImGui mouse capture; Camera::Update mutates orbit state (no
longer const).
2026-10-03 13:02:55 +08:00
atdunbg 9ff08651f0 editor: revert self-drawn scale gizmo, clip degenerate axes instead
User feedback: the self-drawn scale handles clash visually with the
ImGuizmo translate/rotate rods (different size/style, clearly not one
widget). Keep the library widget for all three ops for visual
consistency and prevent mis-drags by masking degenerate axes via
SetAxisMask:

- translate: axis ~ parallel to camera-to-object line (sin < 0.25)
- scale: axis ~ perpendicular to camera-to-object line (|dot| < 0.1,
  intersection numerator identically zero -> grab fails/values jump)
  OR axis ~ parallel to view direction (|dot| > 0.98, zero screen
  displacement -> value never changes)
- rotate: screen-space math, no degeneracy, no mask

Masked axes are neither drawn nor activatable; a slight view change
restores them. Center uniform-scale square (MT_SCALE_XYZ, pure screen
space) stays available in any view.
2026-10-03 12:46:07 +08:00
atdunbg c9d4eed043 editor: scale gizmo fully self-drawn (screen-space, view-independent)
ImGuizmo scale hover/drag relies on ray-plane intersection with the
axis as plane normal; in front views (axis ⟂ camera-to-object) the
intersection numerator is identically zero, so handles can't be
grabbed and values jump randomly; axes parallel to the view have zero
screen displacement. Submodule is read-only and AxisMask can only hide
degenerate axes, not replace them.

Replace the library scale path entirely with a screen-space gizmo:
- three axis rods (line + end square) + center uniform-scale square,
  constant screen size in any view
- axis drag: mouse delta projected onto the axis' screen direction
- axis pointing at camera: radial mapping (drag away = grow)
- center square: uniform scale (library MT_SCALE_XYZ semantics)
- Ctrl snapping quantizes the result; scale clamped >= 0.01
- pick suppressed while hovering/dragging the self-drawn handles
2026-10-03 12:23:00 +08:00
atdunbg 21b33d7d7f fix(editor): 缩放轴病态判据与平移互补,按 op 分派 AxisMask
缩放独有老问题根因(源码实证):缩放 hover(GetScaleType)与拖拽
(HandleScale ratio 分母)依赖"法线=轴"平面求交,交点距离分子 numer
= -dist·dot(camToObj, 轴)。正对视角相机与物体无横偏时 numer≡0 →
len=0/垃圾 → 杆 hover 不到、ratio 分子分母乱跳(数值随机猛变)。
平移 hover 是纯屏幕距离、平面法线经正交化自适应;旋转是屏幕角数学
——故唯独缩放有此病。SCALE op 下判据换 |dot(camToObj,轴)|<0.1,
ROTATE 不 mask,TRANSLATE 保持 sinθ<0.25。中心块等比缩放
(MT_SCALE_XYZ,屏幕空间数学)不受 mask 恒可用。
2026-10-03 12:11:16 +08:00