diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md
index 02ed75b..dc49a4c 100644
--- a/src/features/editScene/AGENTS.md
+++ b/src/features/editScene/AGENTS.md
@@ -338,7 +338,47 @@ cd demos/demo-taxi
# BaseWhite (no-material fallback) offenders.
EDITSCENE_PROFILE=1 ./demoTaxi --headless --benchmark
# env var: dump per-step ms/frame averages of
-# EditorApp::frameRenderingQueued every 600 frames.
+# EditorApp::frameRenderingQueued every 600 frames, plus the full
+# frame split: renderPass (frameStarted -> frameRenderingQueued =
+# scene graph update + cull + draw submission), systems (the
+# profiled steps), swapWait (frameRenderingQueued end -> frameEnded
+# = the vsync/GPU-bound wait) and OGRE tris/batches averages.
+./demoTaxi --headless --benchmark-drive [--drive-speed mps]
+ [--drive-frames N] [--drive-csv path]
+# (phase 5.5) Continuous road-tour probe: builds an edge-coverage
+# tour of the town road graph (DFS over edges, RoadPath::findPath
+# reconnects at dead ends), pins the player camera on it at 2.2 m
+# eye height AND teleports the player character along (the NPC
+# spawn tiers measure against the player camera, which
+# PlayerControllerSystem re-derives from the player before the NPC
+# systems run — pinning only the camera leaves the tiers anchored
+# at the spawn point), and sets the game clock to noon so NPCs are
+# outdoors-eligible (the default ~2 AM start makes almost everyone
+# sleep indoors). The cursor advances at the drive speed (default
+# 13 m/s), one fixed 1/60 s step per frame in BOTH headless and
+# windowed runs so the ground covered per frame is identical
+# everywhere (default cap 6000 recorded frames). Forces the
+# exterior scene like the other town probes (the demo otherwise
+# starts in the interior). Every frame appends a CSV row
+# (default benchmark_drive.csv): wall ms, the frame split
+# (renderPass / systems / swapWait), the render-pass sub-split
+# (viewport = draw submission, cull = scene graph update + cull),
+# the viewport sub-sub-split (find = findVisibleObjects cull/queue
+# fill, queueMain/queueOverlay = per-queue-group render execution,
+# renderables = visible renderables queued — SceneManager::Listener
+# + RenderQueueListener + RenderQueue::RenderableListener, only
+# registered in --benchmark-drive mode), the systems spike
+# attribution (spike_step_ms/spike_step = slowest profiled
+# EditorApp step of the frame),
+# batches, tris, terrain pages loaded, high-detail NPCs and active
+# traffic cars — FPS drops correlate with the recorded state. The
+# summary prints wall-ms percentiles, span averages, a histogram of
+# the systems-spike steps (>8 ms frames) and the top-10 worst
+# frames with full state, then
+# "[benchmark-drive] PASS" (wireable into the test battery once
+# baseline thresholds are known). Headless wall times are
+# llvmpipe-relative; run windowed on real hardware for absolute
+# numbers.
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door /
# sit in the seat of the sedan door you used (driver = front-left,
# only the driver seat drives), hold E = exit the vehicle, Space =
@@ -845,6 +885,42 @@ symbolic restore; the snapshot settles two frames first because the
population seeds `rec->position` with the home doorstep and the
first pure evaluation overwrites it).
+Character rendering performance (phase 5.5, 2026-10-18): two OGRE
+vertex-animation traps made every visible NPC cost ~1.1-1.5 ms of CPU
+in the find window before the fix — (1) exported meshes carry Blender
+shape keys, and an always-created "ShapeKeys" pose animation marked
+them VAT_POSE, which OGRE cannot do in hardware with RTSS materials,
+forcing per-frame CPU morph blending; `buildCharacter` now strips
+vertex animations + poses from meshes of characters whose registry
+record has no non-zero `inlineShapeKeyWeights` (meshes are shared;
+if a shape-keyed character ever needs an already stripped mesh it
+must be reloaded from disk), and (2) the blender2ogre materials never
+requested RTSS hardware skinning, so every skeletal entity was
+CPU-skinned every frame; `buildCharacter` now injects the
+`SGX_HardwareSkinning` sub-render state into each character mesh's
+materials (linear, weight_count 4, max_bone_count = largest compact
+palette in the mesh) and invalidates the material. Note: RTSS HW
+skinning silently disables itself on glsles without GLSL 300 es.
+Follow-up (2026-10-21, "floating head" regression): the generated
+shader reads `blendWeights` as vec4 (weight_count 4) while OGRE sizes
+the mesh's blend-weight element by actual influences (VET_FLOAT1 for
+single-bone meshes like the face); GL pads missing vec4 attribute
+components with (0,0,1), adding a phantom second bone-matrix term
+that double-transformed every sub-4-weight part (face/hair rendered
+at ~2x bind height above the body). `ensureHardwareSkinning` now
+repacks every character mesh's blend-weight element to a full
+zero-padded float4 at load (`normalizeVertexDataBlendWeights`), and
+`finalizeHardwareSkinning` force-generates the RTSS technique +
+clears the entity's stale software/hardware vertex-processing cache
+(the RTSS HS sub-render state never marks the generated program
+skeletal). `EDITSCENE_NO_HWSKIN=1` restores full software skinning.
+`CharacterSlotSystem::prewarmShaders()` compiles all catalog skinning
+shaders at startup (windowed runs only, gated on `isHeadless()`) so
+the first NPC spawn does not hitch on glCompileShader.
+Details in demo-taxi/PLAN.md phase 5.5 follow-up; per-step find-walk
+attribution is available via the temporary `OGRE_FIND_DEBUG` patch in
+the vendored OGRE repo (see PLAN.md for usage).
+
### NpcTaxiSystem (demo-taxi phase 4.7.7)
`NpcTaxiSystem` (EditorApp-owned singleton `getInstance()`, game mode only,
diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp
index 4579a77..05cd9cd 100644
--- a/src/features/editScene/EditorApp.cpp
+++ b/src/features/editScene/EditorApp.cpp
@@ -523,6 +523,11 @@ void EditorApp::setup()
Ogre::RTShader::ShaderGenerator *shadergen =
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
if (shadergen) {
+ /* Debug aid: EDITSCENE_RTSS_CACHE=dir dumps every
+ * generated shader source to
/ShaderCache. */
+ const char *rtssCache = std::getenv("EDITSCENE_RTSS_CACHE");
+ if (rtssCache && rtssCache[0])
+ shadergen->setShaderCachePath(rtssCache);
shadergen->addSceneManager(m_sceneMgr);
}
@@ -2359,11 +2364,17 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
m_profRenderAccum += m_lastRenderPassMs;
}
m_lastSystemsMs = 0.0f;
+ m_lastSpikeStepMs = 0.0f;
+ m_lastSpikeStep.clear();
auto stepProfile = [&stepTimer, this](const char *name) {
float ms = (float)stepTimer.getMilliseconds();
stepTimer.reset();
m_lastSystemsMs += ms;
+ if (ms > m_lastSpikeStepMs) {
+ m_lastSpikeStepMs = ms;
+ m_lastSpikeStep = name;
+ }
if (ms > 50.0f)
Ogre::LogManager::getSingleton().logMessage(
"FrameProfile: " + Ogre::String(name) +
@@ -2619,6 +2630,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
if (m_characterSystem) {
m_characterSystem->update(fixedEvt.timeSinceLastFrame);
}
+ stepProfile("characterSystem");
/* --- Buoyancy system (before physics so impulse is integrated) --- */
if (m_buoyancySystem) {
@@ -2652,6 +2664,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
}
m_buoyancySystem->update(fixedEvt.timeSinceLastFrame);
}
+ stepProfile("buoyancy");
/* --- Hair physics root sync (before physics step) --- */
if (m_hairPhysicsSystem) {
@@ -2663,12 +2676,14 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
if (m_aiDriverSystem) {
m_aiDriverSystem->update(fixedEvt.timeSinceLastFrame);
}
+ stepProfile("aiDriver");
/* --- F10 vehicle inputs (before the physics step) --- */
if (m_vehicleSystem) {
m_vehicleSystem->prePhysicsUpdate(
fixedEvt.timeSinceLastFrame);
}
+ stepProfile("vehiclePre");
/* --- Main physics step --- */
if (m_physicsSystem) {
diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp
index 303861c..a6932d2 100644
--- a/src/features/editScene/EditorApp.hpp
+++ b/src/features/editScene/EditorApp.hpp
@@ -323,6 +323,16 @@ public:
{
return m_sceneMgr;
}
+
+ /* Per-frame time split (see the m_last*Ms members): the render
+ * pass (scene graph update, cull, submit), the CPU systems in
+ * frameRenderingQueued, and the swap/vsync wait. */
+ float getLastRenderPassMs() const { return m_lastRenderPassMs; }
+ float getLastSystemsMs() const { return m_lastSystemsMs; }
+ float getLastSwapWaitMs() const { return m_lastSwapWaitMs; }
+ /* Slowest profiled step of the last frame (see m_lastSpikeStep*). */
+ float getLastSpikeStepMs() const { return m_lastSpikeStepMs; }
+ const std::string &getLastSpikeStep() const { return m_lastSpikeStep; }
flecs::world *getWorld()
{
return &m_world;
@@ -610,15 +620,16 @@ private:
float m_lastRenderPassMs = 0.0f;
float m_lastSystemsMs = 0.0f;
float m_lastSwapWaitMs = 0.0f;
+ /* Per-frame spike attribution: the slowest profiled step of the
+ * last frameRenderingQueued (benchmark CSVs correlate FPS drops
+ * with the responsible system). */
+ float m_lastSpikeStepMs = 0.0f;
+ std::string m_lastSpikeStep;
double m_profRenderAccum = 0.0;
double m_profSwapAccum = 0.0;
double m_profTrisAccum = 0.0;
double m_profBatchesAccum = 0.0;
- float getLastRenderPassMs() const { return m_lastRenderPassMs; }
- float getLastSystemsMs() const { return m_lastSystemsMs; }
- float getLastSwapWaitMs() const { return m_lastSwapWaitMs; }
-
void processPendingSceneSwitch();
bool performSceneSwitch(const Ogre::String &scenePath,
const SceneSwitchOptions &opts);
diff --git a/src/features/editScene/components/AiDriver.hpp b/src/features/editScene/components/AiDriver.hpp
new file mode 100644
index 0000000..6393151
--- /dev/null
+++ b/src/features/editScene/components/AiDriver.hpp
@@ -0,0 +1,65 @@
+#ifndef EDITSCENE_AIDRIVER_HPP
+#define EDITSCENE_AIDRIVER_HPP
+#pragma once
+
+#include
+
+#include
+
+/**
+ * One waypoint of an AiDriverComponent route (demo-taxi phase 5.1,
+ * junction fields phase 5.3).
+ */
+struct AiRoutePoint {
+ /* Route position (world/render space matching the scene). */
+ Ogre::Vector3 position;
+ /* Road node id of the junction (degree >= 3 road node) this
+ * point passes through, -1 when the point is not a junction.
+ * Junction points make the AI slow, yield to cross traffic and
+ * reserve the junction before crossing. */
+ int junctionNodeId = -1;
+ /* Junction occupancy center — the road node's position (the
+ * route point itself is lane-offset). Only meaningful when
+ * junctionNodeId >= 0. */
+ Ogre::Vector3 junctionCenter = Ogre::Vector3::ZERO;
+};
+
+/**
+ * AI driver component (demo-taxi phase 5.1).
+ *
+ * Turns an entity with a VehicleComponent into an AI-driven vehicle:
+ * AiDriverSystem follows `route` (a world-space polyline, e.g. built
+ * from the road graph by RoadPath::findPath plus a right-lane offset)
+ * with pure-pursuit steering and a throttle/brake speed controller,
+ * writing the same VehicleComponent::inputForward/inputRight/inputBrake
+ * fields the player controller writes — VehicleSystem stays the single
+ * consumer of those fields. A vehicle carrying the player
+ * (PlayerControlledComponent) is never touched.
+ *
+ * The route polyline is XZ-driven: waypoint Y is ignored for steering
+ * and arrival (the physics keeps the car on the ground), it only
+ * carries the road surface height for callers that want it.
+ */
+struct AiDriverComponent {
+ /* Route polyline (world/render space matching the scene). */
+ std::vector route;
+ /* Index of the waypoint the car currently drives toward. */
+ size_t routeIndex = 0;
+ /* true: wrap to route[0] after the last waypoint (circuit);
+ * false: brake to a stop at the last waypoint and set arrived. */
+ bool loop = false;
+ /* Cruise speed in m/s (the per-edge speed limit folds into this
+ * when routes carry limits — phase 5.1 keeps a single value). */
+ float cruiseSpeed = 11.0f;
+ /* Runtime: the car reached the end of a non-looping route. */
+ bool arrived = false;
+ /* Runtime: number of completed route wraps (loop laps) — used by
+ * the headless traffic test. */
+ int lapsCompleted = 0;
+ /* Runtime: junction waypoints were classified against the road
+ * graph (AiDriverSystem::classifyJunctions). Route builders that
+ * flag junctions themselves set this to skip the auto pass. */
+ bool junctionsClassified = false;
+};
+
+#endif /* EDITSCENE_AIDRIVER_HPP */
diff --git a/src/features/editScene/demos/demo-taxi/PLAN.md b/src/features/editScene/demos/demo-taxi/PLAN.md
index 0ff9c5e..05e4f26 100644
--- a/src/features/editScene/demos/demo-taxi/PLAN.md
+++ b/src/features/editScene/demos/demo-taxi/PLAN.md
@@ -2813,11 +2813,150 @@ taxi's lights should look distinct.
for 11.5, unchanged: TownGen per-page static merge, CellGrid
per-building shell merge, road chunk/markings merge, face submesh
merge (10.5), substitute LOD meshes, terrain far-page strategy.
+- **Full-frame profiler split + drive benchmark (added).** The second
+ playtest measured 40 FPS at only ~50 batches when just the road was
+ visible — batch count alone does not explain the frame time, and the
+ 5.5 system profiler only covers `frameRenderingQueued`. EditorApp now
+ records the full frame split every frame (a few timer reads):
+ renderPass (frameStarted -> frameRenderingQueued = scene graph update
+ + cull + draw submission), systems (the profiled steps), swapWait
+ (frameRenderingQueued end -> frameEnded = vsync/GPU-bound wait);
+ `EDITSCENE_PROFILE=1` dumps 600-frame averages of all three plus
+ tris/batches, and per-frame getters feed the new probe.
+ `--benchmark-drive [--drive-speed mps] [--drive-frames N]
+ [--drive-csv path]` drives a camera at 2.2 m eye height along a
+ continuous edge-coverage tour of the road graph (fixed 1/60 s steps
+ in both headless and windowed runs) and logs every frame to CSV
+ (wall ms, frame split, batches/tris, terrain pages, high-detail NPCs,
+ active cars) with a top-10 worst-frames report — FPS drops become
+ correlatable with scene state, and the run doubles as a regression
+ harness once baseline thresholds are chosen. Next step: a windowed
+ on-hardware run to localize the 25 ms frames.
+- **On-hardware drive result + benchmark anchoring fix.** First
+ windowed `--benchmark-drive` run (real GPU): wall avg 18.25 ms
+ (p50 16.8 = vsync period, p95 32.3 = doubled vsync), spans
+ renderPass 10.0 / systems 3.6 / swapWait 2.8 ms, batches avg 61
+ max 132, tris avg 213 K. Conclusions: NOT GPU-bound (swapWait 2.8
+ ms) and NOT batch-count-bound — the render pass's CPU side (scene
+ graph update / cull / submission) is the biggest average share and
+ spikes to 26 ms; systems spikes (~13 ms, synchronous terrain page
+ loads) tip frames over the 16.6 ms vsync boundary into 33 ms. The
+ run also exposed a benchmark artifact: npcs/cars stayed 0 because
+ the NPC spawn tiers measure against the player camera, which
+ PlayerControllerSystem re-derives from the player BEFORE the NPC
+ systems run — the camera pin only won at render time. The
+ benchmark now teleports the player character along the tour (spawn
+ tiers + character physics/animation follow the drive) and sets the
+ clock to noon (the default ~2 AM start makes almost every NPC
+ spawn-ineligible — asleep indoors). CSV gained viewport_ms (draw
+ submission) / cull_ms (scene graph update + cull) columns splitting
+ the render pass.
+- **Per-NPC find-window cost root-caused and fixed (2026-10-18).**
+ The second windowed drive run showed find = 13.7 ms (the whole
+ viewport cost), bimodal on visible NPC count (~1.1-1.5 ms per
+ NPC), and per-material attribution pinned it on character-part
+ renderables. A temporary env-gated instrumentation patch
+ (`OGRE_FIND_DEBUG=1` per-type, `=2` adds per-entity names) in the
+ VENDORED OGRE repo `/media/slapin/library/ogre3/ogre`
+ (OgreFindDbg.h new; `_findVisibleObjects`, `processVisibleObject`,
+ `Entity::updateAnimation`, `calcVertexProcessing` wrapped; run with
+ `LD_LIBRARY_PATH` pointing at a dir containing only the repo-built
+ `libOgreMain.so.14.6` — the repo build lacks the SDK's ImGui
+ component, so do NOT point it at the whole repo `lib/` dir) split
+ the find walk per step: `Entity::_updateRenderQueue` was ~99% of
+ the walk, and inside it `updateAnimation()` — concretely
+ `applyVertexAnimation` (CPU morph blending) and
+ `softwareVertexBlend` (CPU skinning) — at ~250-820 us per skeletal
+ entity per frame (Debug build). Two independent causes, both in
+ `CharacterSlotSystem::buildCharacter`:
+ 1. Exported character meshes carry Blender shape keys, and the slot
+ system always created a "ShapeKeys" pose animation, marking every
+ such mesh VAT_POSE. OGRE only does hardware POSE animation with
+ a custom shader (RTSS has none), so VAT_POSE disabled hardware
+ vertex animation for the whole entity -> per-frame CPU morph
+ blend. No registry record uses non-zero shape-key weights, so
+ this was pure waste: when the character record has no non-zero
+ weights the meshes now get `removeAllAnimations()` +
+ `removeAllPoses()` once at load (meshes are shared resources;
+ idempotent, tracked in a static set). Characters WITH shape keys
+ keep the old path (and still pay CPU morph blending — bake into
+ per-weight-set mesh clones if that ever matters).
+ 2. The blender2ogre materials' `rtshader_system` blocks never
+ requested skinning, so the RTSS-generated vertex programs had
+ `isSkeletalAnimationIncluded()==false` and EVERY skeletal entity
+ (all NPCs + the player) was skinned on the CPU every frame
+ (positions AND normals). Fix: per character-mesh material,
+ inject the `SGX_HardwareSkinning` sub-render state (linear,
+ weight_count 4, max_bone_count = skeleton bone count, factory
+ maxCalculableBoneCount raised to match) into the
+ ShaderGeneratorDefaultScheme render state and invalidate the
+ material so the technique regenerates. hwcheck probe confirms
+ `skel=1` on all character materials afterwards.
+ Result (headless Debug, llvmpipe): find walk 861 -> 282 us;
+ `Entity::updateRQ` 31.8 -> 5.2 us/call; vtxAnim/swBlend gone.
+ Release headless find_objs avg 2.7 -> 0.58 ms. Caveats: on glsles
+ without GLSL 300 es RTSS hardware skinning disables itself
+ (`doBoneCalculations=false`) and silently falls back to software
+ skinning; a 139-bone skeleton needs 417 vec4 uniforms, over the
+ GLES2-era 256-vec4 minimum — if GLES2 becomes the target, prune
+ bones or segment meshes. Part entities already share one
+ `SkeletonInstance` via `shareSkeletonInstanceWith` (skeleton pose
+ evaluated once per NPC per frame, ~35 us Debug); hair with its own
+ armature still pays its own small skeleton. Verified: taxi battery
+ 17/17 PASS. Remaining find cost is now small; the physics step
+ (avg ~3 ms windowed, spikes >8 ms on ~8% of frames near
+ NPCs+cars) is the next target.
+- **"Floating head" regression root-caused and fixed (2026-10-21).**
+ Enabling RTSS hardware skinning made every NPC render a rigid
+ face+hair head hovering ~2x bind height above a correct animated
+ headless body. Per-draw GL/RTSS forensics (temporary env-gated
+ hooks in the vendored OGRE, since removed) proved the uploaded bone
+ palettes, world matrices and generated vertex shaders were all
+ correct — the corruption happened inside the vertex shader:
+ `SGX_HardwareSkinning` is generated with `weight_count 4`, so the
+ shader reads `blendWeights` as **vec4**, but OGRE sizes the mesh's
+ blend-weight element by the actual influences per vertex
+ (`Mesh::compileBoneAssignments`: VET_FLOAT1 for a single-bone mesh
+ like the face, FLOAT3 for the hair). GL pads the missing
+ components of a vec4 attribute read with `(0,0,1)`, so every
+ sub-4-weight vertex gained a **phantom 4th bone-matrix term with
+ weight 1.0** — a second application of the bone transform
+ (bind-pos -> ~2x bind-pos for the rigidly head-bound face, while
+ FLOAT4 body parts were unaffected and the FLOAT2 feet only shifted
+ centimeters, which is why it went unnoticed there). Two earlier
+ "did not move" observations that sent the hunt down wrong paths
+ were measurement errors: the RTSS disk shader cache is
+ content-addressed (`generateHash(source, defines)`) so it cannot
+ serve stale sources, and an SGXLib sink patch DID move the floater
+ (~150 px), confirming it went through `blendBonesMat3x4` after all.
+ Fix in `CharacterSlotSystem.cpp`: `normalizeVertexDataBlendWeights`
+ repacks every character mesh's blend-weight element to a full
+ float4 (zero-padded) once at load in `ensureHardwareSkinning`
+ (shared + dedicated vertex data), so the attribute read matches the
+ shader. `finalizeHardwareSkinning` is kept too: the RTSS HS
+ sub-render state never sets `skeletalAnimationIncluded` on the
+ generated program, and without the forced re-evaluation the entity
+ cached a "software" vertex-processing decision and uploaded no
+ palette (a real second bug, masked by the first). Kill switch:
+ `EDITSCENE_NO_HWSKIN=1` restores full software skinning. Verified:
+ xvfb screenshot (head attached on both characters), taxi battery
+ 17/17 PASS.
+- **Shader prewarm + post-fix benchmark (2026-10-21).**
+ `CharacterSlotSystem::prewarmShaders()` compiles every catalog
+ skinning shader (VS+FS `load()` per material pass) at startup in
+ windowed runs (gated on `EditorApp::isHeadless()` so tests do not
+ pay for it), removing the first-NPC-spawn glCompileShader hitch.
+ benchmark-drive on the fixed build: PASS, wall avg 5.66 ms
+ (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.
### Phase 5.9 — town improvements before foot traffic
- In building DSL we need to identify house types, as a list of tag keywords.
+- The template layouts should define score which should prioritize layout selection if more than one is possible.
- We need to implement template layouts which can be used instead of default layout
- Town area should be able to select preferred types when one is not declared explicitly from templates, using type tag keywords.
+- Weighted/scored Monte-Carlo selection should be used. Low-scored templates still should be able to show up but under lower frequency.
- Should be able to group town areas into single town/city and have multiple towns each potentially having multiple areas. That is needed for flexibility like having industrial, residential, commercial part and town center, for example, with templatelayouts and some unique buildings.
### Phase 6 — Foot traffic NPCs
@@ -2829,7 +2968,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.
+ - 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.
- 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.
@@ -2867,14 +3006,15 @@ taxi's lights should look distinct.
### Phase 6.5 CellGrid furniture placement - before interiors
-- the same as with node version, building layout DSL can define room type and tags/keywords for the furniture
+- all furniture assets should use the same atlas material, which should be generated by Blender's export script (the one which generates jsons too)
+- as with node version, building layout DSL should be able to define room type and tags/keywords for the furniture
- should support 4 layers of furniture:
- thin floor objects not blocking path/no collider (carpets, floor lights, etc.)
- thin wall objects not blocking path (light switches, wall jacks/outlets, picture frames, etc.)
- normal wall and multi-cell objects (multi-cell is new), having colliders
- ceiling objects (lights, panels, etc.)
- each furniture is defined either by json (as it is now) or prefab (new).
-- some room layouts need special furniture placement, that should be supported via Lua events.
+- some room layouts need special furniture placement, that should be supported via Lua events. This is about unique buildings and stores/shops for special room layouts, probably another S-expression based DSL would be very useful here in addition to Lua.
- NPC AI POIs/actuators are either a part of prefab/json declaration (prefab is supported more)
- furniture prefabs should have names starting with "furniture." and work together with furniture json and support the same metadata as furniture json. Probably just be able to create furniture json for the prefab easily. Prefix should be supported in editor's prefab save/load for easy naming.
@@ -2925,6 +3065,20 @@ taxi-specific):
produce identical interiors (determinism check); a multi-storey
building's upper floor is reachable by stairs.
+### Phase 8.5 car effects
+
+- With help of asset pipeline car effects need to be implemented
+ - Doors should be opened before NPC or player enters vehicle
+ - door handle should be used
+ - Smoke should be generated on exhaust
+ - skid marks should be left on intense braking and sidehifting on intense turns, also smoke from under wheels.
+ - stop lights, backwards motion lights, turn indicators should be working automatically depending on driver actions.
+ - it should be possible to turn on lights
+ - implement in-car first person view
+ - hands on steering wheel - implement IK to put arms on stair wheel properly.
+ - engine sounds
+ - horn
+
### Phase 9 — Taxi passengers
- Passenger NPCs waiting at curbs/POIs; hail interaction (E prompt via
@@ -2936,11 +3090,13 @@ taxi-specific):
- Destination assignment: passenger names a POI/curb from the
generated town (any settlement in the world); route distance
computed over the road graph (drives the fare timer).
+- Should be controlled by Lua code.
- Headless: scripted passenger boards, rides, alights at destination;
vehicle carries passenger through a scene-physics run.
### Phase 10 — Fare chain gameplay (the taxi game)
+- Should be implemented as Lua and C++ functionality with system-first principle.
- `lua-scripts/taxi.lua` (pattern of `sokoban.lua`): fare state
machine (awaiting fare -> passenger aboard -> delivered/failed),
price doubling per completed fare (fare 1 = base price, fare 10 =
@@ -2953,6 +3109,10 @@ taxi-specific):
after fare 10.
- Persistence in `GlobalStateStore` under `taxi.*` so the chain
survives scene switches and save/load.
+- Should implement mission-type behaviors:
+ - package delivery
+ - person delivery (normal taxi)
+ - easy to add other missions.
- Headless: scripted 3-fare run asserting prices double, timer scales
with distance, a timeout resets the chain to fare 1, completion
events fire, HUD state updates.
diff --git a/src/features/editScene/demos/demo-taxi/demo_main.cpp b/src/features/editScene/demos/demo-taxi/demo_main.cpp
index c91892e..6ffb720 100644
--- a/src/features/editScene/demos/demo-taxi/demo_main.cpp
+++ b/src/features/editScene/demos/demo-taxi/demo_main.cpp
@@ -1,4 +1,5 @@
#include
+#include
#include
#include "EditorApp.hpp"
#include "ProjectConfig.hpp"
@@ -37,6 +38,7 @@
#include
#include
#include
+#include
#include