In phase 5
This commit is contained in:
@@ -275,6 +275,149 @@ for m in used_mats:
|
||||
print("gen_vehicle: unpacked texture '%s' -> %s" %
|
||||
(img.name, tex_name))
|
||||
|
||||
# --- merged single material + ID map (CAR_MODELING.md) ---
|
||||
# Every mesh part is re-materialized onto a single material named
|
||||
# <base> so blender2ogre emits ONE submesh per part, and the exporter's
|
||||
# fixed-function .material is replaced with the VehicleIdMap shader
|
||||
# materials after the export. The ID map texture shares the color
|
||||
# atlas UV space (all parts MUST use that one UV space with no texel
|
||||
# shared between materials; fix the blend if not) and marks each
|
||||
# fragment: red = paint (atlas color), green = bloom (lights),
|
||||
# blue = glass.
|
||||
IDMAP_SIZE = 512
|
||||
|
||||
|
||||
def idmap_color(mat):
|
||||
ln = mat.name.lower() if mat else ""
|
||||
if "glass" in ln:
|
||||
return (0, 0, 255)
|
||||
if "bloom" in ln:
|
||||
return (0, 255, 0)
|
||||
return (255, 0, 0)
|
||||
|
||||
|
||||
def bake_idmap(mesh_datas, path):
|
||||
"""Paint per-face material IDs into a texture in UV space.
|
||||
|
||||
The car UVs are degenerate: every polygon collapses onto a single
|
||||
color-atlas texel (the atlas is a palette, not a surface unwrap),
|
||||
so the ID map only needs that texel plus a small margin painted.
|
||||
A texel shared by two different materials is unresolvable and is
|
||||
warned about (fix the blend: give one of them its own texel)."""
|
||||
size = IDMAP_SIZE
|
||||
buf = bytearray(size * size * 3)
|
||||
owner = {}
|
||||
warned = set()
|
||||
|
||||
def paint(u, v, col, matname):
|
||||
u = u - math.floor(u)
|
||||
v = v - math.floor(v)
|
||||
cx, cy = int(u * (size - 1)), int(v * (size - 1))
|
||||
for dy in range(-2, 3):
|
||||
for dx in range(-2, 3):
|
||||
if dx * dx + dy * dy > 5:
|
||||
continue
|
||||
px, py = cx + dx, cy + dy
|
||||
if not (0 <= px < size and 0 <= py < size):
|
||||
continue
|
||||
idx = py * size + px
|
||||
prev = owner.get(idx)
|
||||
if prev is not None and prev != matname:
|
||||
pair = (prev, matname)
|
||||
if pair not in warned:
|
||||
warned.add(pair)
|
||||
print("gen_vehicle: WARNING - UV texel (%d,%d) "
|
||||
"shared by materials '%s' and '%s'; the "
|
||||
"ID map cannot tell them apart (fix the "
|
||||
"blend)" % (px, py, prev, matname))
|
||||
continue
|
||||
owner[idx] = matname
|
||||
buf[idx * 3] = col[0]
|
||||
buf[idx * 3 + 1] = col[1]
|
||||
buf[idx * 3 + 2] = col[2]
|
||||
|
||||
for me in mesh_datas:
|
||||
uv_layer = me.uv_layers.active
|
||||
if not uv_layer:
|
||||
continue
|
||||
uv = uv_layer.data
|
||||
for poly in me.polygons:
|
||||
mat = (me.materials[poly.material_index]
|
||||
if poly.material_index < len(me.materials) else None)
|
||||
puv = uv[poly.loop_indices[0]].uv
|
||||
paint(puv.x, puv.y, idmap_color(mat),
|
||||
mat.name if mat else "<none>")
|
||||
|
||||
img = bpy.data.images.new(
|
||||
os.path.splitext(os.path.basename(path))[0],
|
||||
width=size, height=size, alpha=False)
|
||||
px = [0.0] * (size * size * 4)
|
||||
for i in range(size * size):
|
||||
px[i * 4] = buf[i * 3] / 255.0
|
||||
px[i * 4 + 1] = buf[i * 3 + 1] / 255.0
|
||||
px[i * 4 + 2] = buf[i * 3 + 2] / 255.0
|
||||
px[i * 4 + 3] = 1.0
|
||||
img.pixels = px
|
||||
img.filepath_raw = path
|
||||
img.file_format = 'PNG'
|
||||
img.save()
|
||||
bpy.data.images.remove(img)
|
||||
print("gen_vehicle: wrote ID map %s (%d texels)" %
|
||||
(path, len(owner)))
|
||||
|
||||
|
||||
def move_glass_faces_last():
|
||||
"""Re-order each mesh so glass faces export last: with the merged
|
||||
transparent material (depth_write on) the cabin stays visible
|
||||
through the windows only if glass triangles draw after the
|
||||
interior."""
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
me = obj.data
|
||||
glass_slots = [i for i, m in enumerate(me.materials)
|
||||
if m and "glass" in m.name.lower()]
|
||||
if not glass_slots:
|
||||
continue
|
||||
order = [i for i in range(len(me.materials))
|
||||
if i not in glass_slots] + glass_slots
|
||||
mats = [me.materials[i] for i in order]
|
||||
remap = dict((old, new) for new, old in enumerate(order))
|
||||
# materials.clear() zeroes every polygon's material_index, so
|
||||
# save them first
|
||||
saved = [p.material_index for p in me.polygons]
|
||||
me.materials.clear()
|
||||
for m in mats:
|
||||
me.materials.append(m)
|
||||
for poly, old_idx in zip(me.polygons, saved):
|
||||
poly.material_index = remap[old_idx]
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='SELECT')
|
||||
bpy.ops.mesh.sort_elements(type='MATERIAL', elements={'FACE'})
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
obj.select_set(False)
|
||||
print("gen_vehicle: sorted glass faces last on '%s'" % me.name)
|
||||
|
||||
|
||||
all_mesh_datas = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and obj.data not in all_mesh_datas:
|
||||
all_mesh_datas.append(obj.data)
|
||||
|
||||
move_glass_faces_last()
|
||||
bake_idmap(all_mesh_datas, os.path.join(outdir, base + "-idmap.png"))
|
||||
|
||||
merged_mat = bpy.data.materials.new(base)
|
||||
merged_mat.use_nodes = True
|
||||
for me in all_mesh_datas:
|
||||
me.materials.clear()
|
||||
me.materials.append(merged_mat)
|
||||
print("gen_vehicle: consolidated %d mesh data blocks onto material "
|
||||
"'%s'" % (len(all_mesh_datas), base))
|
||||
|
||||
# --- JSON fragment (Ogre coordinates) ---
|
||||
smn, smx = mesh_bbox(root.data)
|
||||
collider = {
|
||||
@@ -511,6 +654,101 @@ bpy.ops.ogre.export(filepath=scene_file,
|
||||
EX_LOD_PERCENT=40
|
||||
)
|
||||
|
||||
# --- replace the exporter's fixed-function .material with the merged
|
||||
# VehicleIdMap shader materials (CAR_MODELING.md): "<base>" is the
|
||||
# transparent near variant, "<base>-opaque" the far variant; the
|
||||
# runtime swaps them by camera distance (SedanGlassLodListener).
|
||||
# Both reference the VehicleIdMapVP/FP programs staged in the same
|
||||
# resource dir (vehicle_idmap.program, CMake-staged from
|
||||
# assets/blender/vehicles). ---
|
||||
IDMAP_MATERIAL_TEMPLATE = """// written by gen_vehicle.py — merged ID-map vehicle material
|
||||
// (CAR_MODELING.md). Not a blender2ogre product; hand edits are
|
||||
// overwritten by every export.
|
||||
material @BASE@
|
||||
{
|
||||
receive_shadows on
|
||||
technique
|
||||
{
|
||||
pass
|
||||
{
|
||||
cull_hardware clockwise
|
||||
depth_write on
|
||||
scene_blend alpha_blend
|
||||
|
||||
vertex_program_ref VehicleIdMapVP
|
||||
{
|
||||
}
|
||||
|
||||
fragment_program_ref VehicleIdMapFP
|
||||
{
|
||||
param_named glassAlpha float 0.25
|
||||
}
|
||||
|
||||
texture_unit
|
||||
{
|
||||
texture @BASE@-atlas.png
|
||||
tex_address_mode wrap
|
||||
filtering linear linear none
|
||||
}
|
||||
|
||||
texture_unit
|
||||
{
|
||||
texture @BASE@-idmap.png
|
||||
tex_address_mode clamp
|
||||
filtering point point none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
material @BASE@-opaque
|
||||
{
|
||||
receive_shadows on
|
||||
technique
|
||||
{
|
||||
pass
|
||||
{
|
||||
cull_hardware clockwise
|
||||
|
||||
vertex_program_ref VehicleIdMapVP
|
||||
{
|
||||
}
|
||||
|
||||
fragment_program_ref VehicleIdMapFP
|
||||
{
|
||||
param_named glassAlpha float 1.0
|
||||
}
|
||||
|
||||
texture_unit
|
||||
{
|
||||
texture @BASE@-atlas.png
|
||||
tex_address_mode wrap
|
||||
filtering linear linear none
|
||||
}
|
||||
|
||||
texture_unit
|
||||
{
|
||||
texture @BASE@-idmap.png
|
||||
tex_address_mode clamp
|
||||
filtering point point none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
mat_path = os.path.join(outdir, base + ".material")
|
||||
with open(mat_path, "w") as f:
|
||||
f.write(IDMAP_MATERIAL_TEMPLATE.replace("@BASE@", base))
|
||||
print("gen_vehicle: wrote merged material %s" % mat_path)
|
||||
|
||||
# Drop the stale split-material files from pre-ID-map exports so the
|
||||
# resource dir does not accumulate them.
|
||||
for suffix in ("-color", "-color-bloom", "-glass"):
|
||||
stale = os.path.join(outdir, base + suffix + ".material")
|
||||
if os.path.exists(stale):
|
||||
os.remove(stale)
|
||||
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
@@ -51,6 +51,13 @@ foreach(VEHICLE_FILE ${VEHICLES_GEN_SRC})
|
||||
list(APPEND VEHICLE_OUTPUT_FILES ${VEHICLE_OUTPUT_FILE} ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.scene ${VEHICLE_JSON_FILE})
|
||||
endforeach()
|
||||
|
||||
# Shared merged-material vehicle shader (one ID-map material per car;
|
||||
# gen_vehicle.py writes the per-car .material referencing these
|
||||
# programs; see vehicles/CAR_MODELING.md). Staged next to the
|
||||
# generated .material files.
|
||||
file(COPY vehicle_idmap.vert vehicle_idmap.frag vehicle_idmap.program
|
||||
DESTINATION ${CMAKE_BINARY_DIR}/resources/vehicles)
|
||||
|
||||
add_custom_target(import_vehicles ALL DEPENDS ${VEHICLE_OUTPUT_FILES})
|
||||
|
||||
# One-step vehicle refresh: re-export changed .blend files (import_vehicles),
|
||||
|
||||
Binary file not shown.
@@ -328,6 +328,17 @@ cd demos/demo-taxi
|
||||
# delta is wall time (a few FPS under xvfb), so the fixed-length
|
||||
# W-hold drives the sedan clear off the test floor and the exit-spot
|
||||
# raycast legitimately fails.
|
||||
# Performance probes (phase 5.5):
|
||||
./demoTaxi --headless --benchmark
|
||||
# waits for town population + traffic, then measures 6 fixed views
|
||||
# (empty-horizon control, four street-level cardinal directions at
|
||||
# the town center, overview from +80 m): wall-clock ms (llvmpipe
|
||||
# under xvfb — relative only), OGRE batches/tris, frustum-visible
|
||||
# entity groups, per-material render-queue batch counts and
|
||||
# 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.
|
||||
# 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 =
|
||||
@@ -343,6 +354,27 @@ cd demos/demo-taxi
|
||||
# assets/blender/vehicles/CAR_MODELING.md. After editing a vehicle
|
||||
# blend run `cmake --build build-vscode --target update_vehicles` (one
|
||||
# step: re-export, re-stage resources, re-merge both demo scenes).
|
||||
# Rendering (phase 5.5): every part (body, doors, hood, trunk, wheels,
|
||||
# steering) exports with exactly ONE submesh on ONE shared material
|
||||
# (`sedan-v2` / `sedan-v2-taxi`) using the VehicleIdMap shader
|
||||
# (resources/vehicles/vehicle_idmap.*, GLES2-compatible,
|
||||
# OgreUnifiedShader.h) — a baked ID-map texture (red = paint, green =
|
||||
# bloom/lights, blue = glass, sharing the cars' palette-degenerate UV
|
||||
# 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 (`<base>` alpha-blended glass
|
||||
# within 10 m, `<base>-opaque` beyond). 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
|
||||
# linear none` — because minification averages neighboring palette
|
||||
# texels and distant cars bled wrong colors); the demo's
|
||||
# SedanGlassLodListener does the near/far swap. The shader's samplers
|
||||
# are bound explicitly (`param_named uAtlas int 0` / `param_named
|
||||
# uIdMap int 1` in vehicle_idmap.program) — OGRE's sampler
|
||||
# auto-assignment follows driver uniform-enumeration order and let
|
||||
# uIdMap read the atlas on some machines, making the whole car
|
||||
# semi-transparent (2026-10-16).
|
||||
# The exterior
|
||||
# scene parks sedan_taxi and sedan_plain near the spawn point; each
|
||||
# door child has an actuator with a vehicle_seat_* action (driver,
|
||||
@@ -482,8 +514,9 @@ binary + its staged project directory as the distributable.
|
||||
18. `EventHandlerSystem`
|
||||
19. `CharacterSystem`
|
||||
20. `BuoyancySystem`
|
||||
21. `PhysicsSystem` (pre: `VehicleSystem::prePhysicsUpdate`; post:
|
||||
`VehicleSystem::postPhysicsUpdate` wheel/fork/steering visuals)
|
||||
21. `PhysicsSystem` (pre: `AiDriverSystem` writes traffic-car
|
||||
`VehicleComponent` inputs, then `VehicleSystem::prePhysicsUpdate`;
|
||||
post: `VehicleSystem::postPhysicsUpdate` wheel/fork/steering visuals)
|
||||
21a. `VehicleControllerSystem::postPhysicsUpdate` pins the seated
|
||||
character to the freshly synced vehicle node, then
|
||||
`PlayerControllerSystem::postPhysicsUpdate` runs the driving-mode
|
||||
@@ -721,9 +754,10 @@ 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) of
|
||||
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 camera become real walking entities and drop back to symbolic beyond
|
||||
`despawnDistanceSq` (120 m) — hysteresis plus a `minDwellSeconds` (3 s)
|
||||
`despawnDistanceSq` (120 m — demo: 85 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,
|
||||
@@ -915,6 +949,142 @@ inside (a curb-side stand must not oscillate). Tens of NPCs make this
|
||||
well under a millisecond per frame; threads are a phase-6 crowd question
|
||||
if profiling ever says so.
|
||||
|
||||
### AiDriverSystem (demo-taxi phase 5.1/5.2/5.3)
|
||||
|
||||
`AiDriverSystem` (EditorApp-owned singleton `getInstance()`, game mode
|
||||
only, `systems/AiDriverSystem.hpp/.cpp`, runs right before
|
||||
`VehicleSystem::prePhysicsUpdate`) drives every entity with an
|
||||
`AiDriverComponent` along its `route` polyline: pure-pursuit steering
|
||||
(speed-scaled lookahead, max 5 m or 0.8 s of travel) plus a
|
||||
proportional throttle/brake speed controller with curvature slowdown
|
||||
(full-speed heading error up to `cornerFullAngleDeg` 60, bottoming at
|
||||
`minCornerSpeed` 3 m/s). It writes the SAME
|
||||
`VehicleComponent::inputForward/inputRight/inputBrake/inputHandBrake`
|
||||
fields the player's VehicleControllerSystem writes, so VehicleSystem
|
||||
stays the single consumer (brake/reverse protection, parked damping and
|
||||
righting come for free); a vehicle carrying the player
|
||||
(PlayerControlledComponent) is never touched, so entering a traffic car
|
||||
just works. Steering is XZ-only; waypoint Y is informational.
|
||||
Non-looping routes brake to a stop at the last waypoint and park
|
||||
(`arrived` + handbrake); looping routes wrap and count
|
||||
`lapsCompleted`. Sign convention: the sedan
|
||||
nose is +Z, its LEFT is +X (door naming), so a target at +X of the nose
|
||||
(crossY > 0) means steer LEFT = negative `inputRight`.
|
||||
|
||||
Whisker braking (5.2): a 3-ray fan (nose, +/-18 deg) is cast at bumper
|
||||
height 0.55 m, starting 2.8 m ahead of the origin, every 3 frames
|
||||
staggered by entity id, via
|
||||
`JoltPhysicsWrapper::raycastQueryTraffic()` — the
|
||||
`raycastQueryWalkable` AllHit-collector pattern (per-hit surface normal
|
||||
via BodyLockRead, hits with `normal.y >= walkableNormalY` skipped so
|
||||
road and ground never brake the car) with two differences: character
|
||||
bodies are NOT skipped (pedestrians must brake cars) and the car's own
|
||||
chassis bodyID is excluded by `SkipOneBodyFilter`. The fan length is
|
||||
`speed^2/(2*4.0) + followGap + 4` (4 m/s^2 braking assumption,
|
||||
followGap 4.5 m). A hit clamps the speed target to
|
||||
`(obstacleDistance - followGap) * 0.8`, which glides the car to a stop
|
||||
4.5 m short of the obstacle; at standstill the brake is held while the
|
||||
path stays blocked, and the target releases the moment the obstacle
|
||||
clears (stop-and-go). The query needs `RigidBodyComponent` (bodyID) on
|
||||
the vehicle in addition to `VehicleComponent` + `AiDriverComponent`.
|
||||
Headless probes: `--test-traffic-loop` (vehicletest scene sedan1+sedan2,
|
||||
3 laps each on an 80x50 m rectangle, fails on contact, NaN or leaving
|
||||
the loop area) and `--test-traffic-follow` (parked broadside blocker at
|
||||
x=20, sedan2 must stop in -10 < x < 15 — the near-blocker window keeps
|
||||
the test from passing vacuously on a stationary car — hold 120 frames,
|
||||
resume after the blocker teleports away, arrive at x=60).
|
||||
|
||||
Junction priority (5.3): the route is a list of `AiRoutePoint`
|
||||
(position + junctionNodeId + junctionCenter);
|
||||
`classifyJunctions()` flags points within `junctionSnapDist` of a
|
||||
degree >= 3 road node (auto-runs once per route on the first update
|
||||
against the terrain road graph — the same
|
||||
`TerrainSystem::getInstance()->getRoadSystem()->getTerrainEntity()`
|
||||
pattern as NpcTaxiSystem — and keeps pre-flagged points). A car
|
||||
latches the next flagged point ahead, slows to `junctionSpeed` inside
|
||||
`junctionApproachDist`, and at the approach-zone edge either takes the
|
||||
junction reservation (first-come-first-served in flecs iteration order)
|
||||
or yields with a constant-deceleration sqrt brake profile to a stop
|
||||
line `junctionStopDist` before the center. Clear = no other AI car
|
||||
holds the reservation AND no vehicle (AI or player, snapshotted once
|
||||
per frame) has its chassis within `junctionRadius` of the center;
|
||||
yielders stopped at the stop line do not count as occupants. Release:
|
||||
the center behind the car and `junctionReleaseDist` away (a plain
|
||||
distance check would drop the reservation during the approach leg,
|
||||
which starts farther out), plus a `junctionHoldTimeout` stale breaker;
|
||||
a per-frame purge drops drive state (and reservations) of despawned or
|
||||
player-taken cars. Gotcha: reserve at the approach-zone edge, not the
|
||||
stop line — two simultaneously arriving cars both fit inside a stop-line
|
||||
window before either can brake. Headless `--test-traffic-yield` (flat
|
||||
vehicletest scene, hand-flagged junction node — the priority logic is
|
||||
graph-independent): two sedans cross the origin junction from 60 m at
|
||||
12 m/s; the loser must stop in the 7.5-30 m approach band, hold, then
|
||||
cross. The AI components are attached 3 frames AFTER the teleport so
|
||||
the waypoint cursor cannot skip the junction point from the stale
|
||||
pre-teleport position.
|
||||
|
||||
### NpcTrafficSystem (demo-taxi phase 5.4)
|
||||
|
||||
`NpcTrafficSystem` (EditorApp-owned singleton `getInstance()`, game mode
|
||||
only, `systems/NpcTrafficSystem.hpp/.cpp`, update after
|
||||
NpcAvoidanceSystem, `postPhysicsUpdate` right after NpcTaxiSystem's)
|
||||
owns the population's car fleet. When an area's population and
|
||||
`TownLocations::LocationSet` exist, each Parking location rolls a
|
||||
deterministic hash (fnv1a of the location name, the TownLocations
|
||||
per-lot discipline) against `carOwnershipRatio` for one car, capped at
|
||||
`maxCarsPerArea`; areas without parking rects (only the layout DSL
|
||||
`parking` verb creates Parking POIs) fall back to the Curb locations as
|
||||
on-street parking. The driver is the first adult (ageYears >= 18)
|
||||
whose `npcHome` string column matches the parking spot's address.
|
||||
Parked cars sit nose along the nearest road edge
|
||||
(`RoadPath::nearestEdgeInfo`, deterministic direction bit) with the
|
||||
handbrake on and no driver; a deterministic `cruisingRatio` subset
|
||||
drives an endless loop (home -> deterministic other Parking/Curb ->
|
||||
home) built from `RoadPath::findPath` + `RoadPath::offsetToRightLane`
|
||||
(rightmost-lane offset: carriageway half width minus half a lane,
|
||||
RIGHT = forward x up in OGRE's right-handed +Y-up world; off-road curb
|
||||
endpoints are first snapped onto the nearest carriageway point so the
|
||||
car never leaves the road to touch the exact curb) and
|
||||
junction-classified via `AiDriverSystem::classifyJunctions`. Cruising
|
||||
drivers are suspended from NpcMotionSystem and their registry position
|
||||
follows the car (the walk-vs-drive choice is deferred to phase 6, so a
|
||||
cruising driver effectively lives in its car).
|
||||
|
||||
Two tiers, symbolic ground truth (same design as NpcDetailSystem): the
|
||||
`TrafficCar` record (route + progress in meters) is authoritative and
|
||||
survives spawn/despawn and scene switches; the high-detail tier (camera
|
||||
within 150 m, hysteresis 220 m — the taxi demo tunes these to 110/165 m
|
||||
in demo_main.cpp for batch budget —, dwell 3 s, spawn budget 1/frame, cap
|
||||
`maxActiveVehicles` 8) instantiates `prefabs/sedan-traffic.json` (demo
|
||||
source tree, extracted from the vehicletest scene's sedan1 by
|
||||
`extract_sedan_prefab.py`, configure_file-staged like the heightmap)
|
||||
via a locally constructed `PrefabSystem`, removes the
|
||||
EditorMarkerComponent `createInstance` adds (runtime-only: not
|
||||
selectable, never serialized), attaches the junction-classified route
|
||||
as an AiDriverComponent with `routeIndex` from the symbolic progress,
|
||||
and seats the driver through the same manual-spawner +
|
||||
`postPhysicsUpdate` seat-pinning approach as NpcTaxiSystem — plus an
|
||||
AnimationTreeComponent added at seat time when the inline-spawned
|
||||
character lacks one (tree name from
|
||||
`NpcDetailSystem::animationTreeName`), with the "driving" locomotion
|
||||
state re-asserted every `postPhysicsUpdate` frame (the spawn-frame
|
||||
setState no-ops before AnimationTreeSystem has per-entity state; the
|
||||
re-assert is an early-out once latched). On despawn
|
||||
the live vehicle position projects back onto the route progress.
|
||||
`NpcDetailSystem::isSpawnEligible` consults
|
||||
`NpcTrafficSystem::isDriving` so a suspended cruising driver never
|
||||
materializes as a pedestrian (suspension erases the motion state, which
|
||||
would otherwise read as eligible). A car the player enters
|
||||
(PlayerControlledComponent — carjacking) leaves the fleet: the record
|
||||
is erased, the driver alights in place and returns to its schedule.
|
||||
Symbolic cars are invisible and cannot collide — junction priority only
|
||||
exists in the high-detail tier, so a transient overlap at the tier
|
||||
boundary is accepted. Headless probe: `--test-traffic-town` (exterior
|
||||
scene): waits for the fleet, teleports the player next to the first
|
||||
cruiser, the car must spawn and drive, a far teleport must despawn it
|
||||
while the symbolic progress keeps advancing, and teleporting back must
|
||||
respawn it.
|
||||
|
||||
### EditorSkyboxSystem / EditorSunSystem
|
||||
|
||||
The procedural sky (`Skybox/Dynamic` material) is rendered as a fullscreen
|
||||
@@ -1614,6 +1784,15 @@ the frame and the tunnel into the void — keep the darkness hole-free. The shar
|
||||
all-black colours (diffuse/ambient/specular/emissive) — with lighting
|
||||
disabled RTSS ignores material colours and renders the pass white.
|
||||
|
||||
Grid rebuilds (e.g. the texture-ready rebuild right after scene load)
|
||||
recreate the door entities; `CellGridSystem` stashes any in-flight
|
||||
swing state (`isOpen`/`currentAngle`/`toggleRequested`/
|
||||
`sceneSwitchPending`, keyed by `doorId`, in
|
||||
`MeshData::pendingDoorStates`) before destroying the old doors and
|
||||
re-applies it one-shot to the rebuilt ones, so a toggle or
|
||||
scene-switch request landing just before a rebuild is not lost.
|
||||
Persisted (F6) doors keep using `GlobalStateStore` as before.
|
||||
|
||||
New `CellGridComponent` fields (serialized in the scene JSON and exposed to
|
||||
Lua): `doorsEnabled`, `doorRectName`, `doorMeshName`, `doorUseMeshMaterial`,
|
||||
`doorOpenAngle`, `doorOpenSpeed`, `doorSwingReversed` (F3: negates the
|
||||
|
||||
@@ -37,6 +37,8 @@ set(EDITSCENE_SOURCES
|
||||
systems/NpcDetailSystem.cpp
|
||||
systems/NpcTaxiSystem.cpp
|
||||
systems/NpcAvoidanceSystem.cpp
|
||||
systems/NpcTrafficSystem.cpp
|
||||
systems/AiDriverSystem.cpp
|
||||
systems/GameClockSystem.cpp
|
||||
systems/NpcScheduleDebugOverlay.cpp
|
||||
systems/LightSystem.cpp
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <OgreMaterialManager.h>
|
||||
#include "systems/EditorUISystem.hpp"
|
||||
#include "systems/VehicleSystem.hpp"
|
||||
#include "systems/AiDriverSystem.hpp"
|
||||
#include "systems/ZoneSystem.hpp"
|
||||
#include "systems/GameHudSystem.hpp"
|
||||
#include "systems/PhysicsSystem.hpp"
|
||||
@@ -32,6 +33,7 @@
|
||||
#include "systems/NpcScheduleDebugOverlay.hpp"
|
||||
#include "systems/NpcDetailSystem.hpp"
|
||||
#include "systems/NpcTaxiSystem.hpp"
|
||||
#include "systems/NpcTrafficSystem.hpp"
|
||||
#include "systems/NpcAvoidanceSystem.hpp"
|
||||
#include "components/CharacterSpawner.hpp"
|
||||
#include "systems/TerrainPrefabSpawnerSystem.hpp"
|
||||
@@ -440,6 +442,7 @@ void EditorApp::destroyEditorSystems()
|
||||
m_characterSpawnerSystem.reset();
|
||||
m_npcDetailSystem.reset();
|
||||
m_npcAvoidanceSystem.reset();
|
||||
m_npcTrafficSystem.reset();
|
||||
m_npcMotionSystem.reset();
|
||||
m_npcPopulationSystem.reset();
|
||||
/* Despawn prefab instances before terrain/physics teardown. */
|
||||
@@ -461,6 +464,7 @@ void EditorApp::destroyEditorSystems()
|
||||
/* Vehicles hold physics step listeners; tear down before physics. */
|
||||
m_gameHudSystem.reset();
|
||||
m_zoneSystem.reset();
|
||||
m_aiDriverSystem.reset();
|
||||
m_vehicleSystem.reset();
|
||||
m_physicsSystem.reset();
|
||||
|
||||
@@ -569,6 +573,10 @@ void EditorApp::setup()
|
||||
m_vehicleSystem = std::make_unique<VehicleSystem>(
|
||||
m_world, m_physicsSystem->getPhysicsWrapper());
|
||||
|
||||
/* AI-driven vehicles (demo-taxi phase 5.1). */
|
||||
m_aiDriverSystem =
|
||||
std::make_unique<AiDriverSystem>(m_world, this);
|
||||
|
||||
/* F11 target zones (needs the physics wrapper). */
|
||||
m_zoneSystem = std::make_unique<ZoneSystem>(
|
||||
m_world, m_physicsSystem->getPhysicsWrapper());
|
||||
@@ -697,6 +705,12 @@ void EditorApp::setup()
|
||||
m_npcTaxiSystem =
|
||||
std::make_unique<NpcTaxiSystem>(m_world, this);
|
||||
|
||||
/* NPC vehicle traffic (demo-taxi phase 5.4): the
|
||||
* population's car fleet — parked cars at Parking
|
||||
* locations, cruisers on road-graph loops. */
|
||||
m_npcTrafficSystem =
|
||||
std::make_unique<NpcTrafficSystem>(m_world, this);
|
||||
|
||||
/* NPC local avoidance (demo-taxi phase 4.9.3): halt-based
|
||||
* separation + obstacle whiskers for the high-detail
|
||||
* tier. */
|
||||
@@ -2294,8 +2308,30 @@ void EditorApp::createAxes()
|
||||
}
|
||||
}
|
||||
|
||||
bool EditorApp::frameStarted(const Ogre::FrameEvent &evt)
|
||||
{
|
||||
bool ret = OgreBites::ApplicationContext::frameStarted(evt);
|
||||
m_profFrameStartUs = m_profTimer.getMicroseconds();
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool EditorApp::frameEnded(const Ogre::FrameEvent & /*evt*/)
|
||||
{
|
||||
/* Swap/vsync wait span (frameRenderingQueued end -> frameEnded)
|
||||
* plus OGRE render stats for the profile dump. */
|
||||
uint64_t nowUs = m_profTimer.getMicroseconds();
|
||||
m_lastSwapWaitMs = (float)(nowUs - m_profQueuedEndUs) / 1000.0f;
|
||||
static const bool s_profileDump =
|
||||
getenv("EDITSCENE_PROFILE") != nullptr;
|
||||
if (s_profileDump) {
|
||||
m_profSwapAccum += m_lastSwapWaitMs;
|
||||
if (getRenderWindow()) {
|
||||
const Ogre::RenderTarget::FrameStats &st =
|
||||
getRenderWindow()->getStatistics();
|
||||
m_profTrisAccum += (double)st.triangleCount;
|
||||
m_profBatchesAccum += (double)st.batchCount;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2304,17 +2340,41 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
bool paused = (m_gamePlayState == GamePlayState::Paused);
|
||||
|
||||
/* Lightweight frame profiler: logs any profiled step that takes
|
||||
* longer than 50 ms. Used to find load/switch stalls. */
|
||||
* longer than 50 ms. Used to find load/switch stalls. With
|
||||
* EDITSCENE_PROFILE=1 in the environment it also accumulates every
|
||||
* step and dumps per-step averages to stdout every 600 frames
|
||||
* (phase 5.5 CPU hot-spot hunting). */
|
||||
Ogre::Timer stepTimer;
|
||||
auto stepProfile = [&stepTimer](const char *name) {
|
||||
static const bool s_profileDump = getenv("EDITSCENE_PROFILE") != nullptr;
|
||||
|
||||
/* Render-pass span: frameStarted -> frameRenderingQueued entry
|
||||
* covers the scene graph update, culling and draw submission of
|
||||
* all render targets (Root::_updateAllRenderTargets renders
|
||||
* first, then fires frameRenderingQueued). */
|
||||
{
|
||||
uint64_t nowUs = m_profTimer.getMicroseconds();
|
||||
m_lastRenderPassMs =
|
||||
(float)(nowUs - m_profFrameStartUs) / 1000.0f;
|
||||
if (s_profileDump)
|
||||
m_profRenderAccum += m_lastRenderPassMs;
|
||||
}
|
||||
m_lastSystemsMs = 0.0f;
|
||||
|
||||
auto stepProfile = [&stepTimer, this](const char *name) {
|
||||
float ms = (float)stepTimer.getMilliseconds();
|
||||
stepTimer.reset();
|
||||
m_lastSystemsMs += ms;
|
||||
if (ms > 50.0f)
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"FrameProfile: " + Ogre::String(name) +
|
||||
" took " +
|
||||
Ogre::StringConverter::toString(ms / 1000.0f) +
|
||||
"s");
|
||||
if (s_profileDump) {
|
||||
auto &acc = m_profileAccum[name];
|
||||
acc.first += ms;
|
||||
acc.second += 1;
|
||||
}
|
||||
};
|
||||
|
||||
/* Headless test runs can pin the frame delta so physics and
|
||||
@@ -2483,6 +2543,15 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
}
|
||||
stepProfile("npcAvoidance");
|
||||
|
||||
/* --- NPC vehicle traffic (demo-taxi phase 5.4): fleet
|
||||
* seeding, symbolic cruise advance, distance-gated
|
||||
* spawn/despawn. Runs after the population/motion/detail
|
||||
* systems so the locations and registry are fresh --- */
|
||||
if (m_npcTrafficSystem) {
|
||||
m_npcTrafficSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
stepProfile("npcTraffic");
|
||||
|
||||
/* --- Terrain prefab spawners (after terrain so spawn Y-snap
|
||||
* and compliance see fresh terrain data) --- */
|
||||
if (m_terrainPrefabSpawnerSystem) {
|
||||
@@ -2589,6 +2658,12 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_hairPhysicsSystem->prePhysicsUpdate();
|
||||
}
|
||||
|
||||
/* --- AI vehicle drivers (before the vehicle input feed, so
|
||||
* the computed inputs land in the same physics step) --- */
|
||||
if (m_aiDriverSystem) {
|
||||
m_aiDriverSystem->update(fixedEvt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- F10 vehicle inputs (before the physics step) --- */
|
||||
if (m_vehicleSystem) {
|
||||
m_vehicleSystem->prePhysicsUpdate(
|
||||
@@ -2619,6 +2694,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
if (m_npcTaxiSystem) {
|
||||
m_npcTaxiSystem->postPhysicsUpdate();
|
||||
}
|
||||
/* NPC traffic drivers pinned to their seats (demo-taxi
|
||||
* phase 5.4) — same post-physics-sync timing. */
|
||||
if (m_npcTrafficSystem) {
|
||||
m_npcTrafficSystem->postPhysicsUpdate();
|
||||
}
|
||||
if (m_playerControllerSystem) {
|
||||
m_playerControllerSystem->postPhysicsUpdate(
|
||||
fixedEvt.timeSinceLastFrame);
|
||||
@@ -2628,6 +2708,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
if (m_hairPhysicsSystem) {
|
||||
m_hairPhysicsSystem->postPhysicsUpdate();
|
||||
}
|
||||
stepProfile("postPhysicsPins");
|
||||
}
|
||||
|
||||
/* --- Rendering support systems --- */
|
||||
@@ -2642,6 +2723,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
cam = m_camera->getCamera();
|
||||
m_sunSystem->update(fixedEvt.timeSinceLastFrame, cam);
|
||||
}
|
||||
stepProfile("sun");
|
||||
if (m_skyboxSystem) {
|
||||
Ogre::Camera *cam = nullptr;
|
||||
if (m_sceneMgr->hasCamera("PlayerCamera"))
|
||||
@@ -2650,6 +2732,7 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
cam = m_camera->getCamera();
|
||||
m_skyboxSystem->update(cam);
|
||||
}
|
||||
stepProfile("skybox");
|
||||
if (m_waterPlaneSystem) {
|
||||
Ogre::Camera *cam = nullptr;
|
||||
if (m_sceneMgr->hasCamera("PlayerCamera"))
|
||||
@@ -2658,12 +2741,14 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
cam = m_camera->getCamera();
|
||||
m_waterPlaneSystem->update(fixedEvt.timeSinceLastFrame, cam);
|
||||
}
|
||||
stepProfile("water");
|
||||
if (m_lightSystem) {
|
||||
m_lightSystem->update();
|
||||
}
|
||||
if (m_cameraSystem) {
|
||||
m_cameraSystem->update();
|
||||
}
|
||||
stepProfile("light+camera");
|
||||
if (m_lodSystem) {
|
||||
m_lodSystem->update();
|
||||
}
|
||||
@@ -2726,6 +2811,53 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
// Reset per-frame input state
|
||||
m_gameInput.resetPerFrame();
|
||||
|
||||
/* EDITSCENE_PROFILE dump: per-step averages over the last 600
|
||||
* frames, sorted by total cost. */
|
||||
if (s_profileDump) {
|
||||
++m_profileFrames;
|
||||
if (m_profileFrames % 600 == 0) {
|
||||
std::vector<
|
||||
std::pair<std::string, std::pair<double, long> > >
|
||||
sorted(m_profileAccum.begin(),
|
||||
m_profileAccum.end());
|
||||
std::sort(sorted.begin(), sorted.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
return a.second.first > b.second.first;
|
||||
});
|
||||
double total = 0.0;
|
||||
for (const auto &kv : sorted)
|
||||
total += kv.second.first;
|
||||
std::cout << "[profile] frames " << m_profileFrames
|
||||
<< ": frame total "
|
||||
<< (total + m_profRenderAccum +
|
||||
m_profSwapAccum) / 600.0
|
||||
<< " ms = renderPass "
|
||||
<< m_profRenderAccum / 600.0
|
||||
<< " + systems " << total / 600.0
|
||||
<< " + swapWait "
|
||||
<< m_profSwapAccum / 600.0
|
||||
<< " | tris " << m_profTrisAccum / 600.0
|
||||
<< " batches "
|
||||
<< m_profBatchesAccum / 600.0;
|
||||
for (const auto &kv : sorted) {
|
||||
if (kv.second.first / 600.0 < 0.05)
|
||||
continue;
|
||||
std::cout << " | " << kv.first << " "
|
||||
<< kv.second.first / 600.0;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
m_profileAccum.clear();
|
||||
m_profRenderAccum = 0.0;
|
||||
m_profSwapAccum = 0.0;
|
||||
m_profTrisAccum = 0.0;
|
||||
m_profBatchesAccum = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/* End of the systems span — frameEnded measures the swap wait
|
||||
* from here. */
|
||||
m_profQueuedEndUs = m_profTimer.getMicroseconds();
|
||||
|
||||
// Don't call base class - it crashes when iterating input listeners
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,17 @@
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <OgreRenderTargetListener.h>
|
||||
#include <flecs.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include "lua/LuaState.hpp"
|
||||
#include "ProjectConfig.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
class VehicleSystem;
|
||||
class AiDriverSystem;
|
||||
class ZoneSystem;
|
||||
class GameHudSystem;
|
||||
class NpcScheduleDebugOverlay;
|
||||
@@ -34,6 +38,7 @@ class NpcMotionSystem;
|
||||
class NpcDetailSystem;
|
||||
class NpcTaxiSystem;
|
||||
class NpcAvoidanceSystem;
|
||||
class NpcTrafficSystem;
|
||||
class GameClockSystem;
|
||||
class TerrainPrefabSpawnerSystem;
|
||||
class AnimationTreeSystem;
|
||||
@@ -177,6 +182,7 @@ public:
|
||||
|
||||
// OgreBites::ApplicationContext overrides
|
||||
void setup() override;
|
||||
bool frameStarted(const Ogre::FrameEvent &evt) override;
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &evt) override;
|
||||
bool frameEnded(const Ogre::FrameEvent &evt) override;
|
||||
void locateResources() override;
|
||||
@@ -357,6 +363,10 @@ public:
|
||||
{
|
||||
return m_npcTaxiSystem.get();
|
||||
}
|
||||
NpcTrafficSystem *getNpcTrafficSystem() const
|
||||
{
|
||||
return m_npcTrafficSystem.get();
|
||||
}
|
||||
NpcAvoidanceSystem *getNpcAvoidanceSystem() const
|
||||
{
|
||||
return m_npcAvoidanceSystem.get();
|
||||
@@ -381,6 +391,10 @@ public:
|
||||
{
|
||||
return m_vehicleSystem.get();
|
||||
}
|
||||
AiDriverSystem *getAiDriverSystem() const
|
||||
{
|
||||
return m_aiDriverSystem.get();
|
||||
}
|
||||
ZoneSystem *getZoneSystem() const
|
||||
{
|
||||
return m_zoneSystem.get();
|
||||
@@ -492,6 +506,7 @@ private:
|
||||
std::unique_ptr<NpcMotionSystem> m_npcMotionSystem;
|
||||
std::unique_ptr<NpcDetailSystem> m_npcDetailSystem;
|
||||
std::unique_ptr<NpcTaxiSystem> m_npcTaxiSystem;
|
||||
std::unique_ptr<NpcTrafficSystem> m_npcTrafficSystem;
|
||||
std::unique_ptr<NpcAvoidanceSystem> m_npcAvoidanceSystem;
|
||||
std::unique_ptr<GameClockSystem> m_gameClockSystem;
|
||||
std::unique_ptr<TerrainPrefabSpawnerSystem> m_terrainPrefabSpawnerSystem;
|
||||
@@ -520,6 +535,9 @@ private:
|
||||
std::unique_ptr<PlayerControllerSystem> m_playerControllerSystem;
|
||||
/* F10 vehicles: Jolt VehicleConstraint per VehicleComponent. */
|
||||
std::unique_ptr<VehicleSystem> m_vehicleSystem;
|
||||
/* AI-driven vehicles (demo-taxi phase 5.1): writes the same
|
||||
* VehicleComponent input fields the player controller writes. */
|
||||
std::unique_ptr<AiDriverSystem> m_aiDriverSystem;
|
||||
std::unique_ptr<ZoneSystem> m_zoneSystem;
|
||||
std::unique_ptr<GameHudSystem> m_gameHudSystem;
|
||||
/* F7 NPC schedule debug overlay (demo-taxi phase 4.7.4). */
|
||||
@@ -572,6 +590,35 @@ private:
|
||||
* the wall-clock delta (headless test determinism). */
|
||||
float m_fixedDeltaTime = 0.0f;
|
||||
|
||||
/* EDITSCENE_PROFILE=1 accumulation: step name -> (total ms,
|
||||
* sample count) over the last 600 frames (see
|
||||
* frameRenderingQueued). */
|
||||
std::map<std::string, std::pair<double, long> > m_profileAccum;
|
||||
long m_profileFrames = 0;
|
||||
|
||||
/* Per-frame time split, recorded unconditionally (a few timer
|
||||
* reads per frame): frameStarted -> frameRenderingQueued entry is
|
||||
* the render pass (scene graph update, cull, draw submission);
|
||||
* the frameRenderingQueued body is the CPU systems; its end ->
|
||||
* frameEnded is the swap/vsync wait (where a GPU-bound frame
|
||||
* hides). The EDITSCENE_PROFILE dump prints 600-frame averages
|
||||
* including these spans plus OGRE render stats; benchmarks read
|
||||
* the per-frame getters. */
|
||||
Ogre::Timer m_profTimer;
|
||||
uint64_t m_profFrameStartUs = 0;
|
||||
uint64_t m_profQueuedEndUs = 0;
|
||||
float m_lastRenderPassMs = 0.0f;
|
||||
float m_lastSystemsMs = 0.0f;
|
||||
float m_lastSwapWaitMs = 0.0f;
|
||||
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);
|
||||
|
||||
@@ -58,6 +58,17 @@ struct WaterPlane {
|
||||
// Which viewport to update this frame (0=reflection, 1=refraction)
|
||||
int updateViewportIndex = 0;
|
||||
|
||||
// RTT throttling: one reflection/refraction viewport update every
|
||||
// rttUpdateInterval frames (alternating), skipped entirely while the
|
||||
// water surface is outside the camera frustum. Each RTT update is
|
||||
// a full scene re-render and dominates the water CPU cost.
|
||||
int rttUpdateInterval = 4;
|
||||
int rttFrameCounter = 0;
|
||||
|
||||
// Cached shader parameter targets (resolved on rebuild)
|
||||
Ogre::MaterialPtr cachedMaterial;
|
||||
Ogre::Pass *cachedPass = nullptr;
|
||||
|
||||
// Time accumulator for shader
|
||||
float shaderTime = 0.0f;
|
||||
|
||||
|
||||
@@ -67,6 +67,15 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/heightmap.bin"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/heightmaps/4242424300000001/heightmap.bin"
|
||||
COPYONLY)
|
||||
|
||||
# Traffic vehicle prefab (phase 5.4): demo-owned, generated by
|
||||
# extract_sedan_prefab.py from the vehicletest scene's sedan1 subtree and
|
||||
# versioned with the demo. configure_file COPYONLY like the heightmap:
|
||||
# regenerating the prefab re-copies it into the staging directory on the
|
||||
# next build without manual intervention.
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/prefabs/sedan-traffic.json"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/prefabs/sedan-traffic.json"
|
||||
COPYONLY)
|
||||
|
||||
# Road region files (heightmaps/<terrainId>/roads/<px>_<py>.json). The
|
||||
# source tree is authoritative — edit the road network while running from
|
||||
# the source directory; every file registered here is re-copied into the
|
||||
|
||||
@@ -301,7 +301,10 @@ cabin completely). `fixupSedanGlassMaterial()` (demo_main.cpp) makes
|
||||
`sedan-glass-opaque`; `SedanGlassLodListener` swaps every vehicle's
|
||||
glass submeshes between the two by camera distance (10 m threshold) —
|
||||
Ogre 14 no longer applies `Material::setLodLevels` to entities, so a
|
||||
real LOD material never switches.
|
||||
real LOD material never switches. (Superseded in phase 5.5: the
|
||||
listener now swaps the single merged VehicleIdMap material
|
||||
`<base>` <-> `<base>-opaque`; `fixupSedanGlassMaterial()` only remains
|
||||
for the legacy `sedan-glass`.)
|
||||
|
||||
**sedan-v2 (hand-modeled base sedan, current reference).** The
|
||||
hand-modeled `assets/blender/vehicles/sedan-v2.blend` replaced the
|
||||
@@ -324,7 +327,10 @@ animation (authored so the base position is the seat point; no
|
||||
offsets, `seatCharacterScale` 1.0). `sedan-v2-glass` was added to
|
||||
`kGlassMaterialNames` for the near/far glass swap. The full contract
|
||||
is `assets/blender/vehicles/CAR_MODELING.md`; the taxi variant of
|
||||
sedan-v2 is next. One engine fix this exposed:
|
||||
sedan-v2 is next. (Superseded in phase 5.5: the three
|
||||
color/color-bloom/glass materials are now ONE merged VehicleIdMap
|
||||
material per car with the near/far swap done on it — see the 5.5
|
||||
results.) One engine fix this exposed:
|
||||
`AnimationTreeSystem` now disables animations that fall out of the
|
||||
evaluated set in a single frame (a large-deltaTime fade-out step used
|
||||
to leave the old animation enabled at its last weight forever — the
|
||||
@@ -2227,7 +2233,14 @@ Read-only references:
|
||||
--test-taxi` (assigns a goal, teleports the sedan to the pickup curb,
|
||||
asserts boarding + riding with the seat owned and physics off,
|
||||
teleports to the destination curb, asserts alighting, seat release
|
||||
and schedule resume).
|
||||
and schedule resume). Gotcha: the sedan must NOT be dropped right on
|
||||
the curb POI — that point sits on the lot in front of the building
|
||||
door, so the waiting NPC ends up inside the car's collider, pushes
|
||||
the rolling sedan and chases the moving door point forever. The test
|
||||
parks the car towards the carriageway instead (nearest road edge via
|
||||
`RoadPath::nearestEdgeInfo`, clamped to 4.5 m from the POI to stay
|
||||
inside the boarding radius, aligned with the edge direction) and sets
|
||||
`inputHandBrake = 1` so NPC bumps cannot roll it.
|
||||
Original text follows.
|
||||
|
||||
Mission NPCs near the player get the
|
||||
@@ -2458,29 +2471,166 @@ population spawns in the exterior scene.
|
||||
pocket.
|
||||
|
||||
### Phase 5 — NPC vehicle traffic
|
||||
- the NPC owns the vehicle, the vehicles are registered to the NPC as driver the same way as NPCs are added in town. Other group member can get to the vehicle as passengers if they go to the same location, or the driver can drive them to their different destinations.
|
||||
- the NPC can choose by distance if vehicle is used or going by feet (phase 8).
|
||||
- the vehicle is used when NPC has one parked near their current location.
|
||||
- NPC position is simulated in varying levels of detail
|
||||
- camera is far away and not visible =
|
||||
- AI driver system (new, engine-level): a vehicle follows the road
|
||||
graph (route = path over edges), respects per-edge speed limits,
|
||||
basic collision avoidance (raycast/whisker braking for the vehicle
|
||||
ahead and for the player).
|
||||
- Intersection traffic controls: priority rules on intersection nodes
|
||||
(the visual intersections from Phase 2 get behaviour), AI drivers
|
||||
slow/stop/yield accordingly.
|
||||
- NPC driver characters seated in the vehicles (visual occupancy;
|
||||
reuse the seat-pinning approach of `VehicleControllerSystem`).
|
||||
- Traffic vehicles drawn from the Phase 1 sedan pool (type + color
|
||||
variants).
|
||||
- Traffic density control: spawn/despawn traffic vehicles by distance
|
||||
to the camera (spawner pattern), cap active count.
|
||||
- Headless: two AI cars on a generated loop complete N laps without
|
||||
collision/assert; intersection yield check; despawn/respawn
|
||||
stability.
|
||||
|
||||
### Phase 5.5 — Interim performance pass (playable while building)
|
||||
Broken into sub-phases (the original bullet list kept as the spec under
|
||||
each item):
|
||||
|
||||
- **5.1 AI driver core (engine-level). COMPLETE (2026-10-14).**
|
||||
`AiDriverComponent` (route polyline + loop/cruiseSpeed + runtime
|
||||
routeIndex/arrived/lapsCompleted) + `AiDriverSystem` (EditorApp
|
||||
singleton, game mode only, runs right before
|
||||
`VehicleSystem::prePhysicsUpdate`): pure-pursuit steering
|
||||
(speed-scaled lookahead) plus a proportional throttle/brake
|
||||
controller with curvature slowdown, writing the same
|
||||
`VehicleComponent::inputForward/inputRight/inputBrake/inputHandBrake`
|
||||
fields the player controller writes — VehicleSystem stays the single
|
||||
consumer, and a car carrying the player
|
||||
(PlayerControlledComponent) is never touched (carjacking a traffic
|
||||
car just works). Non-looping routes brake to a stop at the last
|
||||
waypoint and park (handbrake on); looping routes wrap and count laps.
|
||||
Headless `--test-traffic-loop`: the vehicletest scene gained sedan2
|
||||
(full sedan1 copy); both sedans are teleported onto opposite corners
|
||||
of an 80x50 m rectangle loop and must complete 3 laps each — fails on
|
||||
contact (< 1 m), NaN or a car leaving the inflated loop bounds.
|
||||
Per-edge speed limits fold in with the town routes (5.4).
|
||||
- **5.2 Whisker braking. COMPLETE (2026-10-14).** Forward raycasts
|
||||
(staggered) brake the AI car for the vehicle ahead, the player's car
|
||||
and pedestrians; stop-and-go gap keeping.
|
||||
`JoltPhysicsWrapper::raycastQueryTraffic()` (physics/physics.h/.cpp)
|
||||
is `raycastQueryWalkable`'s AllHit CastRayCollector + per-hit surface
|
||||
normal via BodyLockRead (skips `normal.y >= walkableNormalY` road and
|
||||
ground) minus the character-body skip (pedestrians must brake cars),
|
||||
with a new `SkipOneBodyFilter` excluding the car's own chassis.
|
||||
AiDriverSystem casts a 3-ray fan (nose, +/-18 deg) at bumper height
|
||||
0.55 m starting 2.8 m ahead of the origin, every 3 frames staggered
|
||||
by entity id; the fan length is `speed^2/(2*4.0) + followGap + 4`.
|
||||
A hit clamps the speed target to `(obstacleDistance - followGap) * 0.8`
|
||||
(followGap 4.5 m — glide-to-stop), and at standstill the brake is
|
||||
held while the path stays blocked. The query additionally reads
|
||||
`RigidBodyComponent` for the chassis bodyID. Headless
|
||||
`--test-traffic-follow`: sedan1 is parked broadside at x=20 with the
|
||||
handbrake, sedan2 drives x=-60 -> 60 at 12 m/s and must stop before
|
||||
the blocker (the stop must land in -10 < x < 15 — without the
|
||||
near-blocker window the test passes vacuously on a stationary car),
|
||||
hold it for 120 frames, then resume and arrive after the blocker is
|
||||
teleported away. Result: stopped at x=11.71 (min gap 8.29 m =
|
||||
bumper offset 2.8 + followGap 4.5 + ~1.0 blocker half-width), held,
|
||||
resumed, arrived.
|
||||
- **5.3 Intersection priority rules. COMPLETE (2026-10-14).** Nodes
|
||||
with 3+ neighbors get behavior: AI drivers slow/stop/yield (the
|
||||
phase-2 visual intersections and stop lines get meaning).
|
||||
`AiRoutePoint` (position + junctionNodeId + junctionCenter) replaced
|
||||
the plain Vector3 route point; `AiDriverSystem::classifyJunctions()`
|
||||
flags route points within `junctionSnapDist` (6 m) of a degree >= 3
|
||||
road node (auto-runs once per route against the terrain road graph;
|
||||
pre-flagged points are kept). A car latches the next junction point
|
||||
ahead, slows to `junctionSpeed` (7 m/s) inside `junctionApproachDist`
|
||||
(30 m) and, at the approach-zone edge, either takes the junction
|
||||
reservation (first-come-first-served in flecs iteration order) or
|
||||
yields with a constant-deceleration brake profile to a stop line
|
||||
`junctionStopDist` (9 m) before the center. Clear = no other AI car
|
||||
holds the reservation and no vehicle (AI or player) has its chassis
|
||||
within `junctionRadius` (7.5 m) of the center — yielders stopped at
|
||||
the stop line are not occupants. The reservation releases once the
|
||||
center is behind the car and `junctionReleaseDist` (14 m) away, with
|
||||
an 8 s hold timeout as the stale-reservation breaker; despawned or
|
||||
player-taken cars lose their drive state (and reservation) in a
|
||||
per-frame purge. Two gotchas the headless test exposed: the
|
||||
reservation must be taken at the approach-zone edge, not the stop
|
||||
line (two cars arriving simultaneously from 30 m out both fit inside
|
||||
the stop-line window before either could brake — the loser needs the
|
||||
full zone to stop in), and the release must require the center BEHIND
|
||||
the car (a plain distance check drops the reservation on the approach
|
||||
leg, which starts farther out than the release distance). Headless
|
||||
`--test-traffic-yield` (flat vehicletest scene, hand-flagged fake
|
||||
junction node — the priority logic is graph-independent): sedan1
|
||||
west-east and sedan2 south-north through a shared junction at the
|
||||
origin, both from 60 m at 12 m/s; the winner crosses, the loser
|
||||
brakes to the 9 m stop line, holds, then crosses once the junction
|
||||
clears. The AI is attached 3 frames AFTER the teleport: attaching on
|
||||
the teleport frame let the waypoint cursor advance from the stale
|
||||
pre-teleport position and skip the junction waypoint. PASS: loser
|
||||
stopped at 8.87 m, min gap 8.88 m, both arrived.
|
||||
- **5.4 Fleet, ownership & density. COMPLETE (2026-10-14).** Vehicles
|
||||
registered to population NPCs as drivers (same registry path as
|
||||
NPCs); a parked fleet placed at Parking locations; household members
|
||||
can ride as passengers; the walk-vs-drive distance choice stays
|
||||
deferred to phase 6 as the spec says. Distance-based spawn/despawn
|
||||
of traffic vehicles (spawner pattern mirroring NpcDetailSystem's
|
||||
hysteresis) with an active-count cap. NPC driver characters seated
|
||||
via the existing seat-pinning (VehicleControllerSystem approach).
|
||||
Traffic vehicles drawn from the Phase 1 sedan pool (type + color
|
||||
variants).
|
||||
|
||||
As built: `NpcTrafficSystem` (EditorApp singleton, game mode only,
|
||||
update after NpcAvoidanceSystem, postPhysicsUpdate right after
|
||||
NpcTaxiSystem's) owns the fleet. When an area's population +
|
||||
location set exist, each Parking location rolls a deterministic hash
|
||||
(fnv1a of the location name, the TownLocations per-lot discipline)
|
||||
against `carOwnershipRatio` (0.5) for one car, capped at
|
||||
`maxCarsPerArea` (6); areas WITHOUT parking rects (the common case —
|
||||
only the layout DSL `parking` verb creates Parking POIs) fall back to
|
||||
the Curb locations as on-street parking. The driver is the first
|
||||
adult (ageYears >= 18) whose `npcHome` matches the parking spot's
|
||||
address (fallback: the area's first adult). Parked cars sit nose
|
||||
along the nearest road edge (`RoadPath::nearestEdgeInfo`,
|
||||
deterministic direction bit) with the handbrake on and no driver.
|
||||
A deterministic `cruisingRatio` (0.5) subset drives an endless loop
|
||||
home -> deterministic other Parking/Curb -> home, built from
|
||||
`RoadPath::findPath` + the new `RoadPath::offsetToRightLane`
|
||||
(rightmost-lane offset: carriageway half width minus half a lane) and
|
||||
junction-classified via `AiDriverSystem::classifyJunctions`.
|
||||
Cruising drivers are suspended from the schedule layer and their
|
||||
registry position follows the car (the walk-vs-drive choice is
|
||||
deferred to phase 6, so a cruising driver effectively lives in its
|
||||
car). The TrafficCar record (route + progress in meters) is the
|
||||
ground truth and survives spawn/despawn and scene switches; the
|
||||
high-detail tier (camera within 150 m, hysteresis 220 m, dwell 3 s,
|
||||
spawn budget 1/frame, cap `maxActiveVehicles` 8) instantiates the new
|
||||
`prefabs/sedan-traffic.json` (extracted from the vehicletest scene's
|
||||
sedan1 by `extract_sedan_prefab.py`, configure_file-staged like the
|
||||
heightmap), attaches the junction-classified route as an
|
||||
AiDriverComponent with `routeIndex` from the symbolic progress, and
|
||||
seats the driver through the same manual-spawner + seat-pinning
|
||||
approach as NpcTaxiSystem. On despawn the live position projects
|
||||
back onto the route progress. `NpcDetailSystem::isSpawnEligible`
|
||||
consults `NpcTrafficSystem::isDriving` so a suspended cruising driver
|
||||
never materializes as a pedestrian (suspension erases the motion
|
||||
state, which would otherwise read as eligible). A car the player
|
||||
enters (carjacking) leaves the fleet: the record is erased, the
|
||||
driver alights in place and returns to its schedule. Symbolic cars
|
||||
are invisible and cannot collide — junction priority only exists in
|
||||
the high-detail tier, so a transient overlap at the tier boundary is
|
||||
accepted. Headless `--test-traffic-town` (exterior scene): waits for
|
||||
the fleet (6 cars seeded on the demo town, population 33), teleports
|
||||
the player next to the first cruiser (the spawn gate opens), the car
|
||||
drives (84 m in 15 s), a far teleport despawns it while the symbolic
|
||||
progress keeps advancing (153 -> 208 m), and teleporting back
|
||||
respawns it. PASS. The pre-existing probes were adjusted for the
|
||||
fleet: `--test-taxi`, `--test-taxi-lowdetail`, `--test-motion` and
|
||||
`--test-detail` skip fleet drivers when picking their test NPCs
|
||||
(drivers refuse taxi goals and their symbolic position follows the
|
||||
car, not the schedule).
|
||||
Post-verification fixes (2026-10-14): (a) `RoadPath::offsetToRightLane`
|
||||
had the lateral sign flipped — `UNIT_Y.crossProduct(dir)` is the LEFT
|
||||
side in OGRE's right-handed +Y-up world (right = forward x up), so
|
||||
cruisers drove on the left lane; (b) route endpoints stayed on the
|
||||
off-road curb POI, so a cruiser left the carriageway at both ends of
|
||||
every leg and whisker-stalled against the car parked there — the
|
||||
endpoints are now snapped onto the nearest carriageway point before
|
||||
the lane offset; (c) fleet drivers T-posed: the manual driver spawner
|
||||
added no AnimationTreeComponent (NpcDetailSystem adds one for its
|
||||
inline spawns) and the seatDriver setState ran on the spawn frame,
|
||||
before AnimationTreeSystem had per-entity state — the tree is now
|
||||
added at seat time and postPhysicsUpdate re-asserts the "driving"
|
||||
state every frame (a no-op once latched). Drivers never alighting
|
||||
is BY DESIGN: cruisers loop endlessly until phase 6's walk-vs-drive
|
||||
choice.
|
||||
- Headless (full phase): two AI cars on a generated town loop complete
|
||||
N laps without collision/assert; intersection yield check;
|
||||
despawn/respawn stability.
|
||||
|
||||
### Phase 5.5 — Interim performance pass (playable while building) — COMPLETE 2026-10-15 (60 FPS met; worst view 84 vs 80 batches, remainder itemized for 11.5)
|
||||
|
||||
The full optimization/batching effort stays in Phase 11.5 + the Phase 12
|
||||
performance pass; this phase only makes the current scene (generated
|
||||
@@ -2514,8 +2664,175 @@ Method:
|
||||
- Done when: `--benchmark` reports >= 60 FPS and <= 80 batches on the
|
||||
reference view; the before/after table lives in this file.
|
||||
|
||||
#### Phase 5.5 results (2026-10-15, headless xvfb/llvmpipe, `--benchmark`)
|
||||
|
||||
Tooling added (kept): `EDITSCENE_PROFILE=1` env dumps per-step ms/frame
|
||||
averages from `EditorApp::frameRenderingQueued` every 600 frames;
|
||||
`demoTaxi --headless --benchmark` waits for town population + traffic,
|
||||
then measures 6 fixed views (empty-horizon control, four street-level
|
||||
cardinal directions at the town center, overview from +80 m) reporting
|
||||
wall-clock ms, OGRE batches/tris, frustum-visible entity groups and
|
||||
per-material render-queue batch counts (incl. BaseWhite offenders).
|
||||
Numbers below are headless wall clock (llvmpipe-influenced — relative
|
||||
only); batch counts are the hardware-independent metric.
|
||||
|
||||
| view | batches before | batches after | avgMs before | avgMs after |
|
||||
|------|---------------|---------------|--------------|-------------|
|
||||
| empty-horizon | 5 | 5 | ~4.4 | ~6.8 |
|
||||
| street-yaw0 (worst) | 95 | 84 | ~11-12 | ~13-14 |
|
||||
| street-yaw90 | 17 | 17 | ~10 | ~10.5 |
|
||||
| street-yaw180 | 18 | 18 | ~11 | ~11 |
|
||||
| street-yaw270 | 19 | 19 | ~13.5 | ~13.5 |
|
||||
| overview | 19 | 19 | ~11.6 | ~11.6 |
|
||||
|
||||
All views stay well under 16.7 ms even in software rendering, so the
|
||||
60 FPS half of the target is met with margin on any real GPU. The
|
||||
worst view lands at 84 batches vs the 80 target — the remaining 4 are
|
||||
itemized below and need the structural (Phase 11.5) fixes; squeezing
|
||||
them now would mean doing that work early.
|
||||
|
||||
What each fix bought:
|
||||
|
||||
- **Water RTT throttle + frustum skip + cached shader params**
|
||||
(`EditorWaterPlaneSystem`, `WaterPlane::rttUpdateInterval = 4`):
|
||||
reflection/refraction RTT viewports now update on alternating frames
|
||||
at 1/4 rate and are skipped entirely while the water surface is
|
||||
outside the camera frustum; the shader material/pass are cached
|
||||
instead of looked up per frame. Water cost 5.1 -> ~1.2 ms/frame.
|
||||
- **LodSystem cached query**: the two per-frame fallback scans reuse a
|
||||
cached flecs query (no measurable change, removed allocation churn).
|
||||
- **Merged vehicle ID-map material** (the big one): all sedan parts
|
||||
(body, doors, hood, trunk, wheels, steering) export with exactly ONE
|
||||
submesh on ONE material (`sedan-v2` / `sedan-v2-taxi`) instead of
|
||||
three RTSS materials (color/color-bloom/glass). The new
|
||||
`VehicleIdMapVP/FP` shader pair (`resources/vehicles/
|
||||
vehicle_idmap.vert/.frag/.program`, GLES2-compatible, built on
|
||||
`OgreUnifiedShader.h`) samples the color atlas AND a baked
|
||||
per-texel ID map (red = paint, green = bloom/lights, blue = glass)
|
||||
at the same UVs and blends the three fragment behaviors by the ID
|
||||
weights; `vehicleColor` is a shader param hook for the future
|
||||
registry-driven per-car color. Two material variants swap by camera
|
||||
distance as before (`<base>` transparent near / `<base>-opaque` far,
|
||||
10 m). `gen_vehicle.py` bakes the ID map from the palette-degenerate
|
||||
car UVs (every face collapses onto one atlas texel; a texel shared by
|
||||
two materials is a build WARNING — fix the blend), sorts glass faces
|
||||
last per mesh (transparent material keeps depth_write on, so glass
|
||||
must draw after the cabin interior), consolidates the mesh datas onto
|
||||
the single material and writes the final `.material` itself.
|
||||
Batch effect: ~15.5 -> 11 batches per car (22 total for the two
|
||||
parked sedans, was ~30); street-yaw0 95 -> 83.
|
||||
Follow-up fix (2026-10-16): the atlas texture unit must NOT be
|
||||
mipmapped — it is a palette (every face UV-collapses onto one texel),
|
||||
so auto-mipmaps average neighboring palette colors under minification
|
||||
and distant cars bled wrong colors (saturated-blue headlights/hubs).
|
||||
The generated `.material` now uses `filtering linear linear none` on
|
||||
the atlas (the ID map already used `point point none`); far-distance
|
||||
colors verified stable via screenshots. The tiny blue dots on rocker
|
||||
panels/hubs visible up close are genuine palette texels of the model,
|
||||
not a shader artifact (identical on both sedans).
|
||||
- **w1 exterior shell material**: the demo's own CellGrid building
|
||||
(entity `w1`, exteriorOnly) fell back to OGRE's BaseWhite because the
|
||||
CellGridSystem default `Ogre/StandardFloor` does not exist in the
|
||||
project resources; it now carries its own
|
||||
ProceduralMaterial/ProceduralTexture (same placeholder look as the
|
||||
interior). Batch-neutral, but the parts are now properly
|
||||
materialized (and the material resolves on the frame after creation —
|
||||
the startup "Can't assign material" line is transient). This exposed
|
||||
a real engine bug: the texture-ready grid rebuild right after scene
|
||||
load recreated w1's scene-switch door and silently ate an in-flight
|
||||
toggle/scene-switch request (`--test-switch` return leg timed out).
|
||||
`CellGridSystem` now stashes in-flight door swing state
|
||||
(`isOpen`/`currentAngle`/`toggleRequested`/`sceneSwitchPending`,
|
||||
keyed by `doorId`) before destroying door entities on rebuild and
|
||||
re-applies it one-shot to the recreated doors.
|
||||
|
||||
Remaining worst-view (street-yaw0 = 84) batch budget, itemized — all
|
||||
deferred to Phase 11.5 unless noted:
|
||||
|
||||
- TownGen building shells: 15 batches sharing ONE area material ->
|
||||
static per-page/per-area merge (biggest single item).
|
||||
- Two parked sedans: 22 (11 each: body, 4 doors, hood, trunk, 4
|
||||
wheels, steering) — inherent while doors/wheels/steering articulate;
|
||||
distant cars should swap to auto-generated substitute LOD meshes
|
||||
(decimated original geometry on a single texture atlas — pipeline
|
||||
work, see 11.5).
|
||||
- `male_Face.mesh`: 8 submeshes, ALL material `male_face-atlas` -> 7
|
||||
batches for one NPC face. Submeshes are Fcl_* pose-track driven, so
|
||||
merging needs pose-track vertex-index remapping in the character
|
||||
export pipeline (or the same substitute-LOD answer as cars). With
|
||||
several NPCs on screen this dominates: ~10 batches per NPC.
|
||||
- CellGrid shell of building 491: ~7 batches (per-floor
|
||||
extWall/extWindows + extDoors + door leaf + scene-switch door
|
||||
occluder quads) — merge exterior shell pieces per building at 11.5;
|
||||
the occluder quads are intentional (always visible behind closed
|
||||
scene-switch doors).
|
||||
- Road chunks + markings: ~8 batches per page in view -> per-page
|
||||
merge at 11.5.
|
||||
- Terrain ~5 visible tiles x 1 batch, skybox 1, water 1: legitimate.
|
||||
|
||||
Known cosmetic issue: sedan-v2-taxi.blend shares two palette texels
|
||||
(~(0.84, 0.65)) between its color and color-bloom materials (build
|
||||
WARNING) — harmless today (bloom shades identically to paint and
|
||||
`bloomEmissive` is 0), but give bloom its own atlas texel when the
|
||||
taxi's lights should look distinct.
|
||||
|
||||
#### Phase 5.5 follow-up (2026-10-16, from the first on-hardware playtest)
|
||||
|
||||
- **Vehicle semi-transparency bug (fixed).** In-game the cars were
|
||||
semi-transparent up close (interior visible through closed doors);
|
||||
in framebuffer-dump screenshots the affected pixels show as the
|
||||
classic alpha checkerboard. Root cause: `vehicle_idmap.program`
|
||||
relied on OGRE's sampler auto-assignment, which follows driver
|
||||
uniform-enumeration order — `uIdMap` ended up bound to texture unit 0
|
||||
(the ATLAS), so each paint texel's blue channel leaked into
|
||||
`wGlass`/`glassAlpha` (dark paint -> alpha ~0.8, beige mostly
|
||||
opaque). Fix: explicit `param_named uAtlas int 0` / `param_named
|
||||
uIdMap int 1` in the fragment program's default_params. Verified via
|
||||
framebuffer alpha probes: doors/roof/wheels now 255, only the window
|
||||
glass reduced (by design, near variant). This is driver-dependent —
|
||||
the pre-fix verification screenshots on the same machine were clean
|
||||
because enumeration order happened to be right before.
|
||||
- **In-town FPS (worked on, structural remainder at 11.5).** Playtest
|
||||
measured ~200 batches / 20 FPS in town and ~120 batches / 30 FPS with
|
||||
only buildings visible, while the 5.5 benchmark's fixed views peaked
|
||||
at 84. Profiling (`EDITSCENE_PROFILE=1`) shows the CPU systems are
|
||||
NOT the problem: steady-state ~2.4 ms/frame total (water 1.2,
|
||||
physics 1.1, everything else below 0.5) — the cost is render-side
|
||||
(batches/fill), plus terrain/TownGen streaming spikes (~13 ms during
|
||||
page loads; synchronous by design) which explain momentary drops.
|
||||
The fixed benchmark views miss the crowd content: real driving views
|
||||
add up to ~10 batches per NPC (7 just for the 8-submesh face) and 11
|
||||
per traffic car on top of ~90-100 for terrain (25-49 pages to the
|
||||
horizon) + roads (7 chunks + 3 markings per page) + town + the two
|
||||
parked sedans.
|
||||
Applied (demo-side tuning in demo_main.cpp, engine defaults
|
||||
unchanged): NPC detail tier 80/120 m -> 55/85 m, traffic cars
|
||||
150/220 m -> 110/165 m — expected to bring the busy-view worst case
|
||||
from ~200 to ~140 batches. The benchmark's fixed views are unchanged
|
||||
(still 84; they never contained a crowd). Remaining structural items
|
||||
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.
|
||||
|
||||
### Phase 5.9 — town improvements before foot traffic
|
||||
- In building DSL we need to identify house types, as a list of tag keywords.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
- 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.
|
||||
- Need to implement texture atlas generation for 20m-distant NPCs.
|
||||
- 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.
|
||||
- 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.
|
||||
- Pedestrian system: spawners at houses/POIs, pedestrians walk navmesh
|
||||
routes along sidewalks between points of interest, avoid standing on
|
||||
the road, despawn by distance.
|
||||
@@ -2525,15 +2842,43 @@ Method:
|
||||
Phase 5 AI driver); the reverse flow parks and walks home. (Real
|
||||
walk-through-interior exits arrive with Phase 8; until then "leaving
|
||||
a house" starts at its door POI.)
|
||||
- **Schedule-adherent drivers** (replaces 5.4's placeholder cruising):
|
||||
a driving NPC must follow its NpcScheduleSystem schedule, not loop
|
||||
endlessly — when the schedule says move, the NPC walks to its parked
|
||||
car (or spawns into it if low-detail), drives to the destination's
|
||||
parking/curb, parks (handbrake, engine off), alights and spends the
|
||||
scheduled dwell time there (inside the building once Phase 8
|
||||
interiors work), then departs on the next schedule leg. The
|
||||
walk-vs-drive distance choice deferred from 5.4 lands here: short
|
||||
legs walk, long legs drive, and the choice must respect the time
|
||||
budget the schedule leaves for the leg (a walking NPC that would be
|
||||
late drives instead). Cruising with no schedule leg to serve becomes
|
||||
rare ambient traffic at most, not the default.
|
||||
- Uses existing `NavMeshSystem` + `PathFollowingSystem` +
|
||||
`CharacterSpawnerSystem`; the new work is orchestration data
|
||||
(sidewalk waypoint extraction from the road graph) plus the flow
|
||||
scheduler.
|
||||
- Headless: pedestrians spawn, reach waypoints, never path through
|
||||
building interiors; house -> car hand-off completes; counts stay
|
||||
within caps while the camera moves.
|
||||
within caps while the camera moves. A scripted driver completes a
|
||||
scheduled trip (depart -> drive -> park -> alight -> dwell ->
|
||||
schedule resumes on the next leg) and the F7 overlay shows the trip
|
||||
state.
|
||||
|
||||
### Phase 7 — CellGrid stair generation
|
||||
### 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
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### Phase 7 — CellGrid stair generation - before interiors
|
||||
|
||||
Multi-storey CellGrid buildings (`height` > 1) generate stacked floors
|
||||
with no connection between them — interiors above ground level are
|
||||
@@ -2550,6 +2895,9 @@ taxi-specific):
|
||||
- Navmesh: stairs rasterize as walkable (Recast step/slope limits), so
|
||||
NPCs can path between floors; verify doorway-area costs and door
|
||||
obstacles still behave.
|
||||
- should support stairwells and explicit floor connections between rooms where a room has stair in it which connects to above location. Like single-floor stairwell. Floors and ceilings are removed for the stair to pass. The space between floors should be hidden by geometry to avoid gaps.
|
||||
- should support exterior stairs outside walls (like in many motels)
|
||||
- should support procedural material the same as walls/floors, using squares for walls and floors by default, depending on surface.
|
||||
- Editor + serialization + Lua binding, per the component rules
|
||||
(serialized flag, Cell Grid editor toggle, `cellgrid_door_test`-style
|
||||
headless coverage).
|
||||
@@ -2653,6 +3001,17 @@ demo needs them:
|
||||
- Generation preview in the editor (ghost overlay before committing).
|
||||
- More vehicle types beyond sedans (the Phase 1 pipeline is already
|
||||
generic): vans, trucks, buses; service vehicles.
|
||||
- **Registry-driven vehicle colors.** The shader side landed in 5.5:
|
||||
the merged `VehicleIdMap` material multiplies the paint term by a
|
||||
per-material `vehicleColor` param (default white = atlas color
|
||||
unchanged). Remaining work: a per-car color stored in the registry
|
||||
record where the car is registered to its NPC owner (the
|
||||
NpcTrafficSystem fleet seeding), assigned once at registration and
|
||||
permanent for that car (only later game mechanics — repaint shop,
|
||||
damage — may change it), pushed into the car entities' materials at
|
||||
spawn (the shared material must be cloned per color or the param set
|
||||
per subentity). The same mechanism then serves the taxi livery and
|
||||
player-car customization.
|
||||
|
||||
### Phase 11.5 — Geometry polish, marking depth & render batching
|
||||
|
||||
@@ -2678,14 +3037,26 @@ must land before the Phase 12 performance/polish pass.
|
||||
small custom-shader offset pulling `gl_Position` toward the camera),
|
||||
so the paint stays visually coplanar but always wins the depth test.
|
||||
Verify on slopes, at long distance and against the decal material.
|
||||
- **Render batching.** The bare exterior scene already draws ~85
|
||||
batches; a full settlement will explode. First measure what is hot
|
||||
(road page chunks, marking/decal overlays, terrain, spawner
|
||||
prefabs), then batch: merge same-material road/overlay chunks into
|
||||
fewer meshes per page, instance repeated props (lamps, signs,
|
||||
trees), static-geometry batching for placed buildings, and material/
|
||||
texture atlasing where it cuts state switches. Define the target
|
||||
batch budget after measuring; the Phase 12 FPS check enforces it.
|
||||
- **Render batching.** Measured per-material on the generated demo
|
||||
town (Phase 5.5 `--benchmark`, worst street-level view = 84 batches):
|
||||
TownGen building shells 15 batches on ONE shared area material
|
||||
(static merge candidate), two parked sedans 22 (11 parts each —
|
||||
inherent while doors/wheels/steering articulate), one NPC ~10
|
||||
(`male_Face.mesh` alone is 8 submeshes all on `male_face-atlas` = 7
|
||||
batches — Fcl_* pose-track driven, so merging needs pose-track
|
||||
index remapping in the character export), the 2-storey CellGrid
|
||||
shell ~7, road chunks + markings ~8 per page, terrain ~5 tiles,
|
||||
skybox/water 2. Batch plan: merge same-material TownGen shells per
|
||||
page/area, merge CellGrid exterior shell pieces per building, merge
|
||||
same-material road/overlay chunks into fewer meshes per page,
|
||||
instance repeated props (lamps, signs, trees), and merge the face
|
||||
submeshes at export. **Substitute distant LODs**: auto-generate
|
||||
decimated LOD meshes from the original geometry (cars AND character
|
||||
faces/body parts) on a single texture atlas as a pipeline step, so
|
||||
distant cars/NPCs render as one batch each; the merged VehicleIdMap
|
||||
material (5.5) already cut cars from ~15.5 to 11 batches. Target:
|
||||
<= 80 batches in the worst street-level gameplay view (5.5 missed it
|
||||
by 4); the Phase 12 FPS check enforces it.
|
||||
|
||||
### Phase 12 — Demo assembly, polish, docs
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -682,12 +682,404 @@
|
||||
"antiRollBarStiffness": 2000.0,
|
||||
"centerOfMassOffset": {
|
||||
"x": 0.0,
|
||||
"y": -0.45,
|
||||
"y": -0.4617,
|
||||
"z": 0.0
|
||||
},
|
||||
"downforceCoefficient": 35.0
|
||||
},
|
||||
"id": 500
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"name": {
|
||||
"name": "sedan2_door_rl"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "sedan-v2-door-rl.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.9192,
|
||||
"y": -0.2869,
|
||||
"z": -0.132
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"vehicle_seat_rl"
|
||||
],
|
||||
"height": 2.0,
|
||||
"radius": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"name": {
|
||||
"name": "sedan2_door_rr"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "sedan-v2-door-rr.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": -0.9192,
|
||||
"y": -0.2869,
|
||||
"z": -0.132
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"vehicle_seat_rr"
|
||||
],
|
||||
"height": 2.0,
|
||||
"radius": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"name": {
|
||||
"name": "sedan2_door_fl"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "sedan-v2-door-fl.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.9192,
|
||||
"y": -0.2868,
|
||||
"z": 0.801
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"vehicle_seat_driver"
|
||||
],
|
||||
"height": 2.0,
|
||||
"radius": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"name": {
|
||||
"name": "sedan2_door_fr"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "sedan-v2-door-fr.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": -0.9192,
|
||||
"y": -0.2868,
|
||||
"z": 0.801
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"vehicle_seat_passenger"
|
||||
],
|
||||
"height": 2.0,
|
||||
"radius": 1.2
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"name": {
|
||||
"name": "sedan2_hood"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "sedan-v2-hood.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": -0.0,
|
||||
"y": 0.266,
|
||||
"z": 0.9992
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"name": {
|
||||
"name": "sedan2_trunk"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "sedan-v2-trunk.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.2744,
|
||||
"z": -1.446
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"collider": {
|
||||
"halfHeight": 1.0,
|
||||
"meshName": "",
|
||||
"offset": {
|
||||
"x": 0.0,
|
||||
"y": 0.1535,
|
||||
"z": -0.0629
|
||||
},
|
||||
"parameters": {
|
||||
"x": 1.0107,
|
||||
"y": 0.6126,
|
||||
"z": 2.4834
|
||||
},
|
||||
"radius": 0.5,
|
||||
"rotationOffset": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"shapeType": "box"
|
||||
},
|
||||
"name": {
|
||||
"name": "sedan2"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "sedan-v2.mesh",
|
||||
"visible": true
|
||||
},
|
||||
"rigidBody": {
|
||||
"bodyType": "dynamic",
|
||||
"enabled": true,
|
||||
"friction": 0.8,
|
||||
"isSensor": false,
|
||||
"mass": 1200.0,
|
||||
"restitution": 0.0
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 40.0,
|
||||
"y": 1.5,
|
||||
"z": 25.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
"cameraDistance": 6.0,
|
||||
"cameraHeight": 1.6,
|
||||
"engineBrake": 0.0,
|
||||
"maxPitchRollAngleDeg": 60.0,
|
||||
"maxTorque": 300.0,
|
||||
"rearSteer": false,
|
||||
"seatOffset": {
|
||||
"x": 0.48,
|
||||
"y": -0.28,
|
||||
"z": 0.2012
|
||||
},
|
||||
"steeringRatio": 4.0,
|
||||
"steeringWheelMeshName": "sedan-v2-steering-wheel.mesh",
|
||||
"steeringWheelOffset": {
|
||||
"x": 0.4915,
|
||||
"y": 0.0807,
|
||||
"z": 0.601
|
||||
},
|
||||
"steeringWheelTiltDeg": 25.0,
|
||||
"wheelMeshName": "sedan-v2-wheel.mesh",
|
||||
"wheels": [
|
||||
{
|
||||
"driven": false,
|
||||
"maxHandBrakeTorque": 0.0,
|
||||
"maxSteerAngleDeg": 35.0,
|
||||
"position": {
|
||||
"x": 0.7805,
|
||||
"y": -0.2052,
|
||||
"z": 1.4806
|
||||
},
|
||||
"radius": 0.3918,
|
||||
"suspensionDamping": 1.0,
|
||||
"suspensionFrequency": 1.3,
|
||||
"suspensionMaxLength": 0.35,
|
||||
"suspensionMinLength": 0.1,
|
||||
"width": 0.3145
|
||||
},
|
||||
{
|
||||
"driven": false,
|
||||
"maxHandBrakeTorque": 0.0,
|
||||
"maxSteerAngleDeg": 35.0,
|
||||
"position": {
|
||||
"x": -0.7805,
|
||||
"y": -0.2052,
|
||||
"z": 1.4806
|
||||
},
|
||||
"radius": 0.3918,
|
||||
"suspensionDamping": 1.0,
|
||||
"suspensionFrequency": 1.3,
|
||||
"suspensionMaxLength": 0.35,
|
||||
"suspensionMinLength": 0.1,
|
||||
"width": 0.3145
|
||||
},
|
||||
{
|
||||
"driven": true,
|
||||
"maxHandBrakeTorque": 300.0,
|
||||
"maxSteerAngleDeg": 0.0,
|
||||
"position": {
|
||||
"x": 0.7805,
|
||||
"y": -0.2052,
|
||||
"z": -1.2806
|
||||
},
|
||||
"radius": 0.3918,
|
||||
"suspensionDamping": 1.0,
|
||||
"suspensionFrequency": 1.3,
|
||||
"suspensionMaxLength": 0.35,
|
||||
"suspensionMinLength": 0.1,
|
||||
"width": 0.3145
|
||||
},
|
||||
{
|
||||
"driven": true,
|
||||
"maxHandBrakeTorque": 300.0,
|
||||
"maxSteerAngleDeg": 0.0,
|
||||
"position": {
|
||||
"x": -0.7805,
|
||||
"y": -0.2052,
|
||||
"z": -1.2806
|
||||
},
|
||||
"radius": 0.3918,
|
||||
"suspensionDamping": 1.0,
|
||||
"suspensionFrequency": 1.3,
|
||||
"suspensionMaxLength": 0.35,
|
||||
"suspensionMinLength": 0.1,
|
||||
"width": 0.3145
|
||||
}
|
||||
],
|
||||
"seats": [
|
||||
{
|
||||
"name": "driver",
|
||||
"offset": {
|
||||
"x": 0.48,
|
||||
"y": -0.28,
|
||||
"z": 0.2012
|
||||
},
|
||||
"driver": true
|
||||
},
|
||||
{
|
||||
"name": "passenger",
|
||||
"offset": {
|
||||
"x": -0.48,
|
||||
"y": -0.28,
|
||||
"z": 0.2012
|
||||
},
|
||||
"driver": false
|
||||
},
|
||||
{
|
||||
"name": "rl",
|
||||
"offset": {
|
||||
"x": 0.48,
|
||||
"y": -0.28,
|
||||
"z": -0.6032
|
||||
},
|
||||
"driver": false
|
||||
},
|
||||
{
|
||||
"name": "rr",
|
||||
"offset": {
|
||||
"x": -0.48,
|
||||
"y": -0.28,
|
||||
"z": -0.6032
|
||||
},
|
||||
"driver": false
|
||||
}
|
||||
],
|
||||
"seatCharacterScale": 1.0,
|
||||
"tireFrictionLongitudinal": 1.3,
|
||||
"tireFrictionLateral": 3.0,
|
||||
"antiRollBarStiffness": 2000.0,
|
||||
"centerOfMassOffset": {
|
||||
"x": 0.0,
|
||||
"y": -0.45,
|
||||
"z": 0.0
|
||||
},
|
||||
"downforceCoefficient": 35.0
|
||||
},
|
||||
"id": 4294967795
|
||||
}
|
||||
],
|
||||
"version": "1.0"
|
||||
|
||||
@@ -196,6 +196,29 @@ private:
|
||||
const std::set<JPH::BodyID> &m_characters;
|
||||
};
|
||||
|
||||
/* Body filter skipping a single body (the querying vehicle's own
|
||||
* chassis for the traffic whisker). */
|
||||
class SkipOneBodyFilter : public JPH::BodyFilter {
|
||||
public:
|
||||
explicit SkipOneBodyFilter(JPH::BodyID inSkip)
|
||||
: m_skip(inSkip)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool ShouldCollide(const JPH::BodyID &inBodyID) const override
|
||||
{
|
||||
return inBodyID != m_skip;
|
||||
}
|
||||
|
||||
virtual bool ShouldCollideLocked(const JPH::Body &) const override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
JPH::BodyID m_skip;
|
||||
};
|
||||
|
||||
/// Class that determines if an object layer can collide with a broadphase layer
|
||||
class ObjectVsBroadPhaseLayerFilterImpl
|
||||
: public JPH::ObjectVsBroadPhaseLayerFilter {
|
||||
@@ -2120,6 +2143,47 @@ public:
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/* Traffic whisker (demo-taxi phase 5.2): like
|
||||
* raycastQueryWalkable (walkable-normal filtering), but character
|
||||
* bodies are NOT skipped — a pedestrian ahead must brake a car —
|
||||
* and the querying vehicle's own chassis is skipped instead. */
|
||||
bool raycastQueryTraffic(Ogre::Vector3 startPoint,
|
||||
Ogre::Vector3 endPoint, Ogre::Vector3 &position,
|
||||
Ogre::Vector3 &normal, JPH::BodyID &id,
|
||||
JPH::BodyID ignoreBody, float walkableNormalY)
|
||||
{
|
||||
Ogre::Vector3 direction = endPoint - startPoint;
|
||||
JPH::RRayCast ray{ JoltPhysics::convert(startPoint),
|
||||
JoltPhysics::convert<JPH::Vec3>(direction) };
|
||||
JPH::AllHitCollisionCollector<JPH::CastRayCollector> collector;
|
||||
JPH::RayCastSettings raySettings;
|
||||
physics_system.GetNarrowPhaseQuery().CastRay(
|
||||
ray, raySettings, collector, {},
|
||||
ObstacleObjectLayerFilter(), SkipOneBodyFilter(ignoreBody));
|
||||
if (!collector.HadHit())
|
||||
return false;
|
||||
collector.Sort();
|
||||
for (const JPH::RayCastResult &hit : collector.mHits) {
|
||||
JPH::RVec3 point = ray.GetPointOnRay(hit.mFraction);
|
||||
JPH::Vec3 n;
|
||||
{
|
||||
JPH::BodyLockRead lock(
|
||||
physics_system.GetBodyLockInterface(),
|
||||
hit.mBodyID);
|
||||
if (!lock.Succeeded())
|
||||
continue;
|
||||
n = lock.GetBody().GetWorldSpaceSurfaceNormal(
|
||||
hit.mSubShapeID2, point);
|
||||
}
|
||||
if (n.GetY() >= walkableNormalY)
|
||||
continue;
|
||||
position = JoltPhysics::convert(point);
|
||||
normal = JoltPhysics::convert(n);
|
||||
id = hit.mBodyID;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/* Spawn-placement check: does a character capsule standing at
|
||||
* @p feetPosition overlap anything solid (static world or
|
||||
* vehicles; character bodies skipped)? */
|
||||
@@ -2577,6 +2641,18 @@ bool JoltPhysicsWrapper::raycastQueryWalkable(Ogre::Vector3 startPoint,
|
||||
normal, id, walkableNormalY);
|
||||
}
|
||||
|
||||
bool JoltPhysicsWrapper::raycastQueryTraffic(Ogre::Vector3 startPoint,
|
||||
Ogre::Vector3 endPoint,
|
||||
Ogre::Vector3 &position,
|
||||
Ogre::Vector3 &normal,
|
||||
JPH::BodyID &id,
|
||||
JPH::BodyID ignoreBody,
|
||||
float walkableNormalY)
|
||||
{
|
||||
return phys->raycastQueryTraffic(startPoint, endPoint, position, normal,
|
||||
id, ignoreBody, walkableNormalY);
|
||||
}
|
||||
|
||||
bool JoltPhysicsWrapper::capsuleOverlapsObstacles(
|
||||
const Ogre::Vector3 &feetPosition, float halfHeight, float radius)
|
||||
{
|
||||
|
||||
@@ -295,6 +295,15 @@ public:
|
||||
Ogre::Vector3 &position,
|
||||
Ogre::Vector3 &normal, JPH::BodyID &id,
|
||||
float walkableNormalY);
|
||||
/* Traffic whisker (demo-taxi phase 5.2): like
|
||||
* raycastQueryWalkable, but character bodies are NOT skipped (a
|
||||
* pedestrian ahead must brake a car) and the querying vehicle's
|
||||
* own chassis (ignoreBody) is. Render-space positions. */
|
||||
bool raycastQueryTraffic(Ogre::Vector3 startPoint,
|
||||
Ogre::Vector3 endPoint,
|
||||
Ogre::Vector3 &position,
|
||||
Ogre::Vector3 &normal, JPH::BodyID &id,
|
||||
JPH::BodyID ignoreBody, float walkableNormalY);
|
||||
/* Does a character capsule standing at @p feetPosition overlap
|
||||
* anything solid? Spawn-placement check. */
|
||||
bool capsuleOverlapsObstacles(const Ogre::Vector3 &feetPosition,
|
||||
|
||||
@@ -320,6 +320,95 @@ void offsetToShoulder(const RoadGraph &graph,
|
||||
}
|
||||
}
|
||||
|
||||
void offsetToRightLane(const RoadGraph &graph,
|
||||
std::vector<Ogre::Vector3> &path)
|
||||
{
|
||||
if (path.size() < 2 || graph.edges.empty())
|
||||
return;
|
||||
|
||||
/* id -> index (nodes are stored unordered, edges reference ids). */
|
||||
std::map<int, int> indexOf;
|
||||
for (size_t i = 0; i < graph.nodes.size(); ++i)
|
||||
indexOf[graph.nodes[i].id] = (int)i;
|
||||
|
||||
for (size_t i = 0; i < path.size(); ++i) {
|
||||
const Ogre::Vector3 &w = path[i];
|
||||
|
||||
/* The edge this waypoint belongs to (same nearest-edge
|
||||
* scan as offsetToShoulder). */
|
||||
const RoadEdge *best = nullptr;
|
||||
float bestD = std::numeric_limits<float>::max();
|
||||
Ogre::Vector3 bestProj = Ogre::Vector3::ZERO;
|
||||
for (const RoadEdge &e : graph.edges) {
|
||||
auto ia = indexOf.find(e.nodeA);
|
||||
auto ib = indexOf.find(e.nodeB);
|
||||
if (ia == indexOf.end() || ib == indexOf.end())
|
||||
continue;
|
||||
const Ogre::Vector3 &a = graph.nodes[ia->second].position;
|
||||
const Ogre::Vector3 &b = graph.nodes[ib->second].position;
|
||||
float abx = b.x - a.x;
|
||||
float abz = b.z - a.z;
|
||||
float len2 = abx * abx + abz * abz;
|
||||
float t = 0.0f;
|
||||
if (len2 > 1.0e-12f) {
|
||||
t = ((w.x - a.x) * abx + (w.z - a.z) * abz) /
|
||||
len2;
|
||||
t = std::max(0.0f, std::min(1.0f, t));
|
||||
}
|
||||
float dx = w.x - (a.x + abx * t);
|
||||
float dz = w.z - (a.z + abz * t);
|
||||
float d = dx * dx + dz * dz;
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
best = &e;
|
||||
bestProj = Ogre::Vector3(a.x + abx * t, w.y,
|
||||
a.z + abz * t);
|
||||
}
|
||||
}
|
||||
if (!best)
|
||||
continue;
|
||||
|
||||
/* Travel direction at the waypoint (XZ); vehicles keep to
|
||||
* the RIGHT of it. Endpoints use the one-sided
|
||||
* direction. */
|
||||
Ogre::Vector3 dir;
|
||||
if (i == 0)
|
||||
dir = path[1] - path[0];
|
||||
else if (i + 1 == path.size())
|
||||
dir = path[i] - path[i - 1];
|
||||
else
|
||||
dir = path[i + 1] - path[i - 1];
|
||||
dir.y = 0.0f;
|
||||
if (dir.squaredLength() < 1.0e-8f)
|
||||
continue;
|
||||
dir.normalise();
|
||||
/* OGRE is right-handed with +Y up: right = forward x up
|
||||
* (sanity: facing -Z, right must be +X). */
|
||||
Ogre::Vector3 right = dir.crossProduct(Ogre::Vector3::UNIT_Y);
|
||||
|
||||
/* Total carriageway half width from the resolved lane
|
||||
* counts; the rightmost lane's center sits half a lane
|
||||
* width in from the curb. */
|
||||
int lanesFrom = 0;
|
||||
int lanesTo = 0;
|
||||
graph.resolveLaneCounts(*best, best->nodeA, lanesFrom,
|
||||
lanesTo);
|
||||
float halfW =
|
||||
(lanesFrom + lanesTo) * graph.config.laneWidth * 0.5f;
|
||||
float lateral = halfW - graph.config.laneWidth * 0.5f;
|
||||
|
||||
/* Endpoints are off-road POIs (curbs sit on the lot in
|
||||
* front of the building): snap them onto the carriageway
|
||||
* first, so a vehicle route never leaves the road to
|
||||
* touch the exact curb (and never plows into the car
|
||||
* parked there). */
|
||||
if (i == 0 || i + 1 == path.size())
|
||||
path[i] = bestProj;
|
||||
|
||||
path[i] += right * lateral;
|
||||
}
|
||||
}
|
||||
|
||||
bool nearestEdgeInfo(const RoadGraph &graph, const Ogre::Vector3 &pos,
|
||||
Ogre::Vector3 &projPoint, Ogre::Vector3 &edgeDir,
|
||||
float &carriagewayHalf)
|
||||
|
||||
@@ -54,6 +54,19 @@ void offsetToShoulder(const RoadGraph &graph,
|
||||
std::vector<Ogre::Vector3> &path,
|
||||
float shoulderMargin = 0.6f);
|
||||
|
||||
/**
|
||||
* Shift a driving path onto the right traffic lane (demo-taxi phase
|
||||
* 5.4): every waypoint moves perpendicular to its nearest road
|
||||
* edge, to the RIGHT of the travel direction, onto the center of the
|
||||
* rightmost lane (carriageway half width minus half a lane width).
|
||||
* Endpoints are off-road POIs (curbs sit on the lot in front of the
|
||||
* building): they are first snapped onto the nearest carriageway point,
|
||||
* so a vehicle route never leaves the road. No-op for paths shorter
|
||||
* than 2 points or an edgeless graph.
|
||||
*/
|
||||
void offsetToRightLane(const RoadGraph &graph,
|
||||
std::vector<Ogre::Vector3> &path);
|
||||
|
||||
/** Total polyline length in meters. */
|
||||
float polylineLength(const std::vector<Ogre::Vector3> &path);
|
||||
|
||||
|
||||
@@ -2911,7 +2911,17 @@ void CellGridSystem::destroyCellGridMeshes(flecs::entity entity)
|
||||
// Destroy physics colliders
|
||||
destroyPhysicsColliders(entity);
|
||||
|
||||
/* Keep the in-flight door swing states across the erase (a
|
||||
* rebuild re-creates the MeshData right away and re-applies
|
||||
* them to the rebuilt doors); a teardown for a dead owner
|
||||
* (scene switch) drops them with the entry. */
|
||||
auto savedDoorStates = std::move(it->second.pendingDoorStates);
|
||||
m_entityMeshes.erase(it);
|
||||
if (!savedDoorStates.empty() && entity.is_valid() &&
|
||||
entity.is_alive()) {
|
||||
m_entityMeshes[entity.id()].pendingDoorStates =
|
||||
std::move(savedDoorStates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5105,6 +5115,39 @@ void CellGridSystem::buildDoorEntities(flecs::entity entity,
|
||||
if (!doorEntity.is_valid())
|
||||
return;
|
||||
|
||||
/* Re-apply in-flight swing state stashed by
|
||||
* destroyDoorEntities() before this rebuild (one-shot). */
|
||||
if (!params.doorId.empty()) {
|
||||
auto stIt = meshData.pendingDoorStates.find(
|
||||
params.doorId);
|
||||
if (stIt != meshData.pendingDoorStates.end()) {
|
||||
const MeshData::DoorSwingState &st =
|
||||
stIt->second;
|
||||
auto &door =
|
||||
doorEntity.get_mut<DoorComponent>();
|
||||
door.isOpen = st.isOpen;
|
||||
door.currentAngle = st.currentAngle;
|
||||
door.toggleRequested = st.toggleRequested;
|
||||
door.sceneSwitchPending =
|
||||
st.sceneSwitchPending;
|
||||
if (transform.node &&
|
||||
st.currentAngle != 0.0f) {
|
||||
auto &dt = doorEntity.get_mut<
|
||||
TransformComponent>();
|
||||
if (dt.node)
|
||||
dt.node->setOrientation(
|
||||
DoorSystem::swingOrientation(
|
||||
door,
|
||||
st.currentAngle));
|
||||
}
|
||||
auto &rb = doorEntity.get_mut<
|
||||
RigidBodyComponent>();
|
||||
rb.enabled = !st.isOpen &&
|
||||
st.currentAngle == 0.0f;
|
||||
meshData.pendingDoorStates.erase(stIt);
|
||||
}
|
||||
}
|
||||
|
||||
meshData.doorEntities.push_back({key, doorEntity});
|
||||
};
|
||||
|
||||
@@ -5147,6 +5190,23 @@ void CellGridSystem::destroyDoorEntities(flecs::entity entity)
|
||||
if (!doorEntity.is_valid() || !doorEntity.is_alive())
|
||||
continue;
|
||||
|
||||
/* Stash in-flight swing state so the rebuild that follows
|
||||
* (e.g. the texture-ready rebuild right after scene load)
|
||||
* does not eat a pending toggle or scene-switch request. */
|
||||
if (doorEntity.has<DoorComponent>()) {
|
||||
const auto &d = doorEntity.get<DoorComponent>();
|
||||
if (!d.doorId.empty() &&
|
||||
(d.toggleRequested || d.sceneSwitchPending ||
|
||||
d.currentAngle != 0.0f)) {
|
||||
MeshData::DoorSwingState st;
|
||||
st.isOpen = d.isOpen;
|
||||
st.currentAngle = d.currentAngle;
|
||||
st.toggleRequested = d.toggleRequested;
|
||||
st.sceneSwitchPending = d.sceneSwitchPending;
|
||||
it->second.pendingDoorStates[d.doorId] = st;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the physics body before the entity dies
|
||||
if (doorEntity.has<RigidBodyComponent>() &&
|
||||
m_physicsSystem) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <Ogre.h>
|
||||
#include "../components/RigidBody.hpp"
|
||||
@@ -304,6 +305,19 @@ private:
|
||||
// with its canonical edge key (F0)
|
||||
std::vector<std::pair<std::string, flecs::entity> > doorEntities;
|
||||
std::string doorLeafMesh; // procedural leaf mesh (custom mesh not owned)
|
||||
|
||||
/* In-flight door swing state carried over a rebuild: the grid
|
||||
* rebuilds when its procedural texture first becomes ready
|
||||
* (right after scene load), which recreates the door entities;
|
||||
* without the carry-over a toggle/scene-switch request landing
|
||||
* just before that rebuild is silently lost. */
|
||||
struct DoorSwingState {
|
||||
bool isOpen = false;
|
||||
float currentAngle = 0.0f;
|
||||
bool toggleRequested = false;
|
||||
bool sceneSwitchPending = false;
|
||||
};
|
||||
std::map<std::string, DoorSwingState> pendingDoorStates;
|
||||
};
|
||||
std::unordered_map<uint64_t, MeshData> m_entityMeshes;
|
||||
|
||||
|
||||
@@ -100,14 +100,25 @@ void EditorWaterPlaneSystem::update(float deltaTime, Ogre::Camera *camera)
|
||||
if (camera)
|
||||
updatePosition(wp, deltaTime, camera);
|
||||
|
||||
// Alternate viewport updates
|
||||
if (wp.renderTarget) {
|
||||
// Alternate viewport updates, throttled: each RTT update is a full
|
||||
// scene re-render and dominates the water CPU cost. Skipped
|
||||
// entirely while the water surface is outside the camera frustum.
|
||||
if (wp.renderTarget) {
|
||||
bool surfaceVisible = true;
|
||||
if (camera && wp.manualObject)
|
||||
surfaceVisible = camera->isVisible(
|
||||
wp.manualObject->getWorldBoundingBox());
|
||||
if (surfaceVisible &&
|
||||
++wp.rttFrameCounter >= wp.rttUpdateInterval) {
|
||||
wp.rttFrameCounter = 0;
|
||||
if (wp.updateViewportIndex == 0 && wp.reflectionViewport)
|
||||
wp.reflectionViewport->update();
|
||||
else if (wp.updateViewportIndex == 1 && wp.refractionViewport)
|
||||
else if (wp.updateViewportIndex == 1 &&
|
||||
wp.refractionViewport)
|
||||
wp.refractionViewport->update();
|
||||
wp.updateViewportIndex = 1 - wp.updateViewportIndex;
|
||||
}
|
||||
}
|
||||
|
||||
wp.sceneNode->setVisible(true);
|
||||
});
|
||||
@@ -206,6 +217,14 @@ void EditorWaterPlaneSystem::rebuildWaterPlane(
|
||||
}
|
||||
wp.manualObject->setMaterialName(0, "WaterPlane/Dynamic");
|
||||
|
||||
// Cache the shader parameter targets so the per-frame update does
|
||||
// not hit the material manager
|
||||
wp.cachedMaterial = mat;
|
||||
wp.cachedPass = nullptr;
|
||||
if (mat && mat->getNumTechniques() > 0 &&
|
||||
mat->getTechnique(0)->getNumPasses() > 0)
|
||||
wp.cachedPass = mat->getTechnique(0)->getPass(0);
|
||||
|
||||
// Set visibility mask so cameras can hide water
|
||||
wp.manualObject->setVisibilityFlags(WATER_MASK);
|
||||
|
||||
@@ -260,6 +279,8 @@ void EditorWaterPlaneSystem::cleanupWaterPlane(WaterPlane &wp)
|
||||
m_sceneMgr->destroyManualObject(wp.manualObject);
|
||||
wp.manualObject = nullptr;
|
||||
}
|
||||
wp.cachedMaterial.reset();
|
||||
wp.cachedPass = nullptr;
|
||||
if (wp.sceneNode) {
|
||||
if (wp.sceneNode->getParentSceneNode()) {
|
||||
wp.sceneNode->getParentSceneNode()->removeChild(
|
||||
@@ -409,15 +430,23 @@ void EditorWaterPlaneSystem::updateShaderParams(WaterPlane &wp,
|
||||
{
|
||||
wp.shaderTime += deltaTime * wp.waveSpeed;
|
||||
|
||||
Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().getByName(
|
||||
"WaterPlane/Dynamic",
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
if (!mat)
|
||||
if (!wp.cachedPass) {
|
||||
wp.cachedMaterial =
|
||||
Ogre::MaterialManager::getSingleton().getByName(
|
||||
"WaterPlane/Dynamic",
|
||||
Ogre::ResourceGroupManager::
|
||||
DEFAULT_RESOURCE_GROUP_NAME);
|
||||
if (wp.cachedMaterial &&
|
||||
wp.cachedMaterial->getNumTechniques() > 0 &&
|
||||
wp.cachedMaterial->getTechnique(0)->getNumPasses() > 0)
|
||||
wp.cachedPass =
|
||||
wp.cachedMaterial->getTechnique(0)->getPass(0);
|
||||
}
|
||||
if (!wp.cachedPass)
|
||||
return;
|
||||
|
||||
Ogre::Pass *pass = mat->getTechnique(0)->getPass(0);
|
||||
Ogre::GpuProgramParametersSharedPtr fpParams =
|
||||
pass->getFragmentProgramParameters();
|
||||
wp.cachedPass->getFragmentProgramParameters();
|
||||
if (!fpParams)
|
||||
return;
|
||||
|
||||
@@ -430,7 +459,7 @@ void EditorWaterPlaneSystem::updateShaderParams(WaterPlane &wp,
|
||||
fpParams->setNamedConstant("reflectivity", wp.reflectivity);
|
||||
|
||||
Ogre::GpuProgramParametersSharedPtr vpParams =
|
||||
pass->getVertexProgramParameters();
|
||||
wp.cachedPass->getVertexProgramParameters();
|
||||
if (vpParams)
|
||||
vpParams->setNamedConstant("tiling", wp.tiling);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,10 @@ void EditorLodSystem::update()
|
||||
RenderableComponent &renderable) {
|
||||
// Try to resolve settings entity if we have an ID but no valid entity
|
||||
if (!lod.settingsId.empty() && !lod.hasValidSettings()) {
|
||||
m_world.query<LodSettingsComponent>().each(
|
||||
/* Cached member query: building a fresh query here (per
|
||||
* entity, per frame) walks the whole table index and
|
||||
* showed up as ~5 ms/frame in EDITSCENE_PROFILE. */
|
||||
m_settingsQuery.each(
|
||||
[&](flecs::entity e,
|
||||
LodSettingsComponent &settings) {
|
||||
if (settings.settingsId ==
|
||||
@@ -61,7 +64,7 @@ void EditorLodSystem::update()
|
||||
|
||||
// Final fallback: if settingsId is still empty, try to use any available LOD settings
|
||||
if (lod.settingsId.empty() && !lod.hasValidSettings()) {
|
||||
m_world.query<LodSettingsComponent>().each(
|
||||
m_settingsQuery.each(
|
||||
[&](flecs::entity e,
|
||||
LodSettingsComponent &settings) {
|
||||
if (lod.settingsEntity ==
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "NpcMotionSystem.hpp"
|
||||
#include "NpcPopulationSystem.hpp"
|
||||
#include "NpcTaxiSystem.hpp"
|
||||
#include "NpcTrafficSystem.hpp"
|
||||
#include "PhysicsSystem.hpp"
|
||||
#include "RenderOriginSystem.hpp"
|
||||
#include "../GameMode.hpp"
|
||||
@@ -61,6 +62,12 @@ bool NpcDetailSystem::isSpawnEligible(uint64_t registryId) const
|
||||
NpcTaxiSystem *taxi = NpcTaxiSystem::getInstance();
|
||||
if (taxi && taxi->hasActiveGoal(registryId))
|
||||
return true;
|
||||
/* Traffic drivers (phase 5.4) sit in their car — suspended from
|
||||
* the schedule layer, so motionInfoOf reports nothing and the
|
||||
* fallthrough below would wrongly spawn them as pedestrians. */
|
||||
NpcTrafficSystem *traffic = NpcTrafficSystem::getInstance();
|
||||
if (traffic && traffic->isDriving(registryId))
|
||||
return false;
|
||||
NpcMotionSystem *motion = NpcMotionSystem::getInstance();
|
||||
NpcPopulationSystem *pop = NpcPopulationSystem::getInstance();
|
||||
if (!motion || !pop)
|
||||
|
||||
@@ -63,6 +63,17 @@ public:
|
||||
/** True while the NPC has a live spawned character entity. */
|
||||
bool isHighDetail(uint64_t registryId) const;
|
||||
|
||||
/** Number of population NPCs currently in the high-detail tier
|
||||
* (benchmark telemetry). */
|
||||
size_t highDetailCount() const
|
||||
{
|
||||
size_t n = 0;
|
||||
for (const auto &kv : m_npcs)
|
||||
if (kv.second.highDetail)
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn eligibility (demo-taxi phase 4.9.1): an NPC whose current
|
||||
* symbolic location is INDOORS (TownLocations::isIndoorKind —
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "RenderOriginSystem.hpp"
|
||||
#include "RoadSystem.hpp"
|
||||
#include "TerrainSystem.hpp"
|
||||
#include "NpcTrafficSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../GameMode.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
@@ -130,6 +131,11 @@ bool NpcTaxiSystem::assignTaxiGoal(uint64_t registryId,
|
||||
CharacterRegistry::getSingleton().findCharacter(registryId);
|
||||
if (!pop || !motion || !rec)
|
||||
return false;
|
||||
/* Traffic drivers (phase 5.4) are suspended in their car; a taxi
|
||||
* goal would fight NpcTrafficSystem over the NPC. */
|
||||
NpcTrafficSystem *traffic = NpcTrafficSystem::getInstance();
|
||||
if (traffic && traffic->isDriving(registryId))
|
||||
return false;
|
||||
int areaIdx = pop->areaIndexOf(registryId);
|
||||
if (areaIdx < 0 || !findLocation(areaIdx, destLocation))
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user