From ff81a9a657a5a3fcb52459187e9eaf07c5b99285 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Thu, 22 Oct 2026 07:58:19 +0300 Subject: [PATCH] Fixed some glass material woes making characters look bals and faceless --- src/features/editScene/AGENTS.md | 12 +++- src/features/editScene/EditorApp.cpp | 4 +- .../editScene/demos/demo-taxi/PLAN.md | 30 ++++++++- .../editScene/demos/demo-taxi/demo_main.cpp | 64 ++----------------- .../editScene/systems/CharacterSlotSystem.cpp | 62 +++++++++++++++--- 5 files changed, 96 insertions(+), 76 deletions(-) diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index dc49a4c..c1f867a 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -403,7 +403,13 @@ EDITSCENE_PROFILE=1 ./demoTaxi --headless --benchmark # space) selects paint/bloom/glass behavior per fragment; the # `vehicleColor` shader param is the hook for future per-car colors. # Two variants swap by camera distance (`` alpha-blended glass -# within 10 m, `-opaque` beyond). gen_vehicle.py bakes the ID +# within 10 m, `-opaque` beyond). The near variant carries +# `transparent_sorting force`: `alpha_blend` + `depth_write on` +# otherwise lands in OGRE's SOLID pass-group collection (ordered by +# Pass* address, arbitrary), and the window glass depth-blocked the +# driver's face/hair behind it (2026-10-21); forcing transparent +# sorting keeps `depth_write on` for car self-occlusion while +# rendering the car after opaque characters. gen_vehicle.py bakes the ID # map (warning on palette texels shared between materials — fix the # blend), sorts glass faces last per mesh and writes the `.material` # itself (the palette atlas is sampled with mipmaps OFF — `linear @@ -795,9 +801,9 @@ position reproducibility at a revisited hour). `NpcDetailSystem` (EditorApp-owned singleton `getInstance()`, game mode only, `systems/NpcDetailSystem.hpp/.cpp`) is the high-detail tier over the symbolic motion layer: population NPCs within `spawnDistanceSq` (80 m — -the taxi demo tunes it to 55 m in demo_main.cpp for batch budget) of +the taxi demo tunes it to 65 m in demo_main.cpp for batch budget) of the camera become real walking entities and drop back to symbolic beyond -`despawnDistanceSq` (120 m — demo: 85 m) — hysteresis plus a `minDwellSeconds` (3 s) +`despawnDistanceSq` (120 m — demo: 95 m) — hysteresis plus a `minDwellSeconds` (3 s) dwell and a `spawnBudgetPerFrame` (2) cap, all public config fields. Spawn eligibility (phase 4.9.1) gates the distance rule — `isSpawnEligible(id)`: an NPC AT an INDOOR location (`TownLocations::isIndoorKind` — Home, Work, diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 05cd9cd..747811a 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -2667,7 +2667,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) stepProfile("buoyancy"); /* --- Hair physics root sync (before physics step) --- */ - if (m_hairPhysicsSystem) { + if (m_hairPhysicsSystem && !getenv("EDITSCENE_NO_HAIRPHYS")) { m_hairPhysicsSystem->prePhysicsUpdate(); } @@ -2720,7 +2720,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt) } /* --- Hair physics pose read-back (after physics step) --- */ - if (m_hairPhysicsSystem) { + if (m_hairPhysicsSystem && !getenv("EDITSCENE_NO_HAIRPHYS")) { m_hairPhysicsSystem->postPhysicsUpdate(); } stepProfile("postPhysicsPins"); diff --git a/src/features/editScene/demos/demo-taxi/PLAN.md b/src/features/editScene/demos/demo-taxi/PLAN.md index 05e4f26..dac7625 100644 --- a/src/features/editScene/demos/demo-taxi/PLAN.md +++ b/src/features/editScene/demos/demo-taxi/PLAN.md @@ -2950,6 +2950,32 @@ taxi's lights should look distinct. (p50 4.86, p95 9.24, p99 11.8), physics spikes >8 ms down from x460 to x1, find_objs cost collapsed (0.34 ms avg) — the previous per-material `find` hot spots (male_face-atlas etc.) are gone. +- **Seated player's face/hair invisible through the window (fixed + 2026-10-21).** The near vehicle material combines `scene_blend + alpha_blend` with `depth_write on`; in this OGRE version a + transparent technique only reaches the transparent render-queue + collection when depth write/check or colour write is off, so the + whole car landed in the SOLIDS collection, which (default + pass-group organisation, no depth sort) iterates a + `std::map` — i.e. arbitrary Pass-address order. The car + pass happened to sort before the face/hair RTSS passes, the window + glass wrote closer depth first, and the character fragments behind + it failed the depth test (HW vs SW skinning differed only because + the generated passes' addresses shifted). Fix: `transparent_sorting + force` in the near variant's pass in `gen_vehicle.py`'s + IDMAP_MATERIAL_TEMPLATE — routes the car to the transparent + collection (after opaque characters, back-to-front) while keeping + `depth_write on` for per-pixel car self-occlusion. The `-opaque` + far variant is unchanged (stays solid by design). A proper + glass-submesh split remains the structural alternative if this ever + regresses. + Post-fix benchmark-drive (llvmpipe): PASS, find split avg objs + 0.31 ms (was 13.7), physics split avg step 0.96 ms with zero + physics spikes >8 ms. Remaining spikes for phase 11.5: `water` + x93 frames >8 ms (worst ~24-45 ms) and `proceduralMesh` x7, + including a ~724 ms burst at run start (initial town generation) + and a ~150 ms mid-run burst — candidates for threaded/async + generation once streaming safety is designed. ### Phase 5.9 — town improvements before foot traffic - In building DSL we need to identify house types, as a list of tag keywords. @@ -2961,6 +2987,8 @@ taxi's lights should look distinct. ### Phase 6 — Foot traffic NPCs +This part concerns asset pipeline in assets/blender/characters in addition to the code. + - Character asset system changes: - Need a system to use separate body parts for truly nude top and bottom keeping existing ones for body part composition with clothes (with fallback to current nude body parts). - Need to implement LOD body parts to reduce number of batches. First candidate is face. @@ -2968,7 +2996,7 @@ taxi's lights should look distinct. - There is an idea to use Ogre's manual LODs as the following: - at default LOD0 distance is < 10m, body is modular, highly detailed, materials a bit optimized, but not at cost of quality. - LOD1, distance >= 10m and < 20m body is modular and highly detailed, 1 material per body part (the same style as with cars, material is the same for all body parts), uses texture atlas of 2048x2048 with separate textures, the face is replaced by face mask which has single material from the same atlas, shape keys removed. - - LOD2, distance >= 20m and < 60m, like with LOD1 but combinations of possible body parts and shapes (thin/fat) are generated and used. No shape keys. For some combinations it is easier to show nothing at this LOD or show something closer/generic. Hair physics disabled. + - LOD2, distance >= 20m and < 60m, like with LOD1 but combinations of possible body parts and shapes (thin/fat) are generated and used. No shape keys. For some combinations it is easier to show nothing at this LOD or show something closer/generic. Hair physics disabled (make sure no accumulated momentum and no motion because of physics manipulation). - LOD3. distance >= 60m and < 80m, Like LOD2, but from combined meshes, decimated ones are generated with recommended triangles limit of 400 and hard limit of 400, more close/generic ones at this stage. - Because of lower NPC counts in interior environments it is enough to have LOD0 and LOD1 in them, but it should be benchmarked and checked. - character prefabs should have "character." prefix in file names to not mix with other prefabs. It should be supported with prefab save/load in editor to easily add prefix. diff --git a/src/features/editScene/demos/demo-taxi/demo_main.cpp b/src/features/editScene/demos/demo-taxi/demo_main.cpp index 6ffb720..ed0f21d 100644 --- a/src/features/editScene/demos/demo-taxi/demo_main.cpp +++ b/src/features/editScene/demos/demo-taxi/demo_main.cpp @@ -31,6 +31,7 @@ #include "components/RigidBody.hpp" #include "components/Vehicle.hpp" #include "components/Character.hpp" +#include "components/CharacterIdentity.hpp" #include "components/PathFollowing.hpp" #include "components/PlayerController.hpp" #include "components/Terrain.hpp" @@ -199,63 +200,6 @@ struct DebugCaptureListener : public Ogre::FrameListener, enterSent = true; } } - /* One-shot pose probe after --debug-enter-vehicle: character - * node and key bone positions (model space) to check the seat - * pin against the actual rendered pose. */ - if (enterSent && frame > 120 && frame % 60 == 0) { - flecs::entity player = app->getPlayerCharacterEntity(); - if (player.is_alive() && - player.has()) { - const TransformComponent &t = - player.get(); - if (t.node) { - std::cout << "[probe] char node " - << t.node->_getDerivedPosition() - << std::endl; - for (Ogre::MovableObject *m : - t.node->getAttachedObjects()) { - if (m->getMovableType() != "Entity") - continue; - Ogre::Entity *ent = - static_cast(m); - if (!ent->hasSkeleton()) - continue; - Ogre::SkeletonInstance *sk = - ent->getSkeleton(); - for (const char *bn : - {"Root", "mixamorig:Hips", - "mixamorig:Head"}) { - if (!sk->hasBone(bn)) - continue; - std::cout - << "[probe] bone " - << bn << " " - << sk->getBone(bn) - ->_getDerivedPosition() - << std::endl; - } - Ogre::AnimationStateSet *aset = - ent->getAllAnimationStates(); - if (!aset) - continue; - Ogre::ConstEnabledAnimationStateIterator ait = - aset->getEnabledAnimationStateIterator(); - while (ait.hasMoreElements()) { - Ogre::AnimationState *as2 = - ait.getNext(); - std::cout - << "[probe] anim " - << as2->getAnimationName() - << " w " - << as2->getWeight() - << " t " - << as2->getTimePosition() - << std::endl; - } - } - } - } - } if (teleportEnabled) { flecs::entity player = app->getPlayerCharacterEntity(); if (player.is_alive() && @@ -6776,13 +6720,13 @@ int main(int argc, char *argv[]) /* Phase 5.5 follow-up gameplay tuning: pull the NPC and * traffic spawn radii in to keep the in-town batch count - * near the phase target. NPCs beyond ~55 m and cars beyond + * near the phase target. NPCs beyond ~65 m and cars beyond * ~110 m are barely visible but each costs ~10/11 batches. * Test listeners override these at frame time when they * need deterministic full spawns, so this stays safe. */ if (NpcDetailSystem *detail = NpcDetailSystem::getInstance()) { - detail->spawnDistanceSq = 55.0f * 55.0f; - detail->despawnDistanceSq = 85.0f * 85.0f; + detail->spawnDistanceSq = 65.0f * 65.0f; + detail->despawnDistanceSq = 95.0f * 95.0f; } if (NpcTrafficSystem *traffic = NpcTrafficSystem::getInstance()) { diff --git a/src/features/editScene/systems/CharacterSlotSystem.cpp b/src/features/editScene/systems/CharacterSlotSystem.cpp index 68edb77..120cfbf 100644 --- a/src/features/editScene/systems/CharacterSlotSystem.cpp +++ b/src/features/editScene/systems/CharacterSlotSystem.cpp @@ -935,6 +935,12 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e) m_entities[e.id()].parts[masterSlot] = masterEnt; m_entities[e.id()].masterEntity = masterEnt; finalizeHardwareSkinning(masterEnt); + /* HW skinning keeps the bind-pose bounding box, so a + * part animated far from bind (a seated character's + * face/hair stay at standing head height) gets + * frustum-culled while its vertices render fine. + * Derive the bounds from the skeleton instead. */ + masterEnt->setUpdateBoundingBoxFromSkeleton(true); /* Setup pose animation for shape keys */ const nlohmann::json *entry = @@ -993,22 +999,55 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e) "attach_to_bone", attachBone); } + /* Auto-detect an incompatible skeleton: the + * part mesh links its own armature (hair + * exports) that differs from the master's + * skeleton. shareSkeletonInstanceWith only + * asserts in debug builds; in release it + * silently binds the part to the wrong bones, + * which looks fine near bind pose (idle, + * walking) but crumples the part into the + * body on any larger pose (sitting). Treat + * the part as own-skeleton and attach it to + * the head bone instead. */ + if (!ownSkeleton && partMesh->getSkeleton() && + masterEnt->getMesh()->getSkeleton() && + partMesh->getSkeleton() != + masterEnt->getMesh()->getSkeleton()) + ownSkeleton = true; + if (ownSkeleton) { /* Hair with its own skeleton: attach * to the master entity's Head bone * so it follows head movement while * being animated by its own skeleton. */ - masterEnt->attachObjectToBone( - attachBone, partEnt); - Ogre::LogManager::getSingleton() - .logMessage( - "[CharacterSlotSystem] " - "Attached hair '" + - slot + - "' with own skeleton to " - "bone '" + - attachBone + "'"); + if (masterEnt->getSkeleton()->hasBone( + attachBone)) { + masterEnt->attachObjectToBone( + attachBone, partEnt); + Ogre::LogManager::getSingleton() + .logMessage( + "[CharacterSlotSystem] " + "Attached hair '" + + slot + + "' with own skeleton to " + "bone '" + + attachBone + "'"); + } else { + Ogre::LogManager::getSingleton() + .logMessage( + "[CharacterSlotSystem] " + "attach bone '" + + attachBone + + "' not in master " + "skeleton, attaching '" + + slot + + "' to the character " + "node"); + transform.node->attachObject( + partEnt); + } } else { partEnt->shareSkeletonInstanceWith( masterEnt); @@ -1016,6 +1055,9 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e) } m_entities[e.id()].parts[slot] = partEnt; finalizeHardwareSkinning(partEnt); + /* See masterEnt above: skeleton-derived bounds for + * HW-skinned parts. */ + partEnt->setUpdateBoundingBoxFromSkeleton(true); applyShapeKeys(e, partEnt, entry);