- .animctrl state machine asset (params/transitions/blend trees, 1D+2D
directional blending) with Unity Animator-style editor panel: state
list, transitions, params, directional pad preview driving live
CrossFade on the selected entity
- Split animation evaluation out of SceneRenderer into
AnimationSystem/AnimationEvaluator; skin palettes with rest-pose
fallback and blend-weight epsilon filtering
- MaterialAsset (.material) with reload-safe preview tile pool
(DropMaterial clears dirty refs before instance destruction)
- EditorGizmoPass skeleton wires + viewport joint overlay; camera
projection rework (baked Vulkan Y-flip, ortho support)
- Editor/game build split via HELLOVULKAN_EDITOR option
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.
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().
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
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.