Phases till 4 implemented. Basic generation of the town is added
This commit is contained in:
@@ -51,6 +51,14 @@ cd build-vscode/src/features/editScene
|
||||
# Run only matching test steps (substring match on step name)
|
||||
TERRAIN_TEST_FILTER=terrainCompliance ./editSceneEditor --headless --run-terrain-tests=1
|
||||
|
||||
# Generate a procedural town from the loaded scene's road graph, print
|
||||
# the building count and exit (Phase 4 headless integration check;
|
||||
# needs a scene with an active terrain + road graph):
|
||||
./editSceneEditor --project demos/demo-taxi --editor \
|
||||
demo_scene_exterior.json --generate-town=town,7
|
||||
# --generate-town=<district>[,<seed>] district: village | town |
|
||||
# city-residential | city-center
|
||||
|
||||
# Demo: game-mode SceneScriptComponent inline-Lua event handling
|
||||
# (scene_loaded / prefab_loaded / custom "demo_pulse"; watch stdout for
|
||||
# [demo] lines and the cubes moving/scaling). Lives in
|
||||
@@ -324,8 +332,10 @@ cd demos/demo-taxi
|
||||
# Road wedge/segment self-intersection regression test (no scene needed,
|
||||
# also registered as CTest roadGeometryOverlapTest). Covers per-piece
|
||||
# A-B-C configs, junction probes (all wedges of 4-way/3-way-T/2-way
|
||||
# nodes combined into one buffer, road+sidewalk combined) and dead-end
|
||||
# cap assertions (node-end cap present, edge-midpoint end open):
|
||||
# nodes combined into one buffer, road+sidewalk combined), dead-end
|
||||
# cap assertions (node-end cap present, edge-midpoint end open) and
|
||||
# the phase 2.5 probes (marking strip placement/gating, stop-line
|
||||
# clear zones, decals, road-atlas UV bands and V tiling):
|
||||
./road_geometry_overlap_test
|
||||
# ...or analyse one custom A-B-C configuration:
|
||||
./road_geometry_overlap_test -5 0 0 0 5 0 5 0 5
|
||||
@@ -562,7 +572,8 @@ spawners for small or non-streaming scenes.
|
||||
### RoadSystem
|
||||
|
||||
Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
`roadlib/RoadGeometryLib` — see `ProceduralRoadGeometry.md`):
|
||||
`roadlib/RoadGeometryLib`, graph-level generator services in
|
||||
`roadlib/RoadNetworkGen` — see `ProceduralRoadGeometry.md`):
|
||||
|
||||
- Wedge/segment meshes, physics colliders, terrain compliance fixups and
|
||||
navmesh dirty-marking are driven by the graph version; bump
|
||||
@@ -646,6 +657,89 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
band and ratcheted the road down, 2026-09-30 trench bug), while
|
||||
remaining excluded from the roads-to-terrain footprint — a bump
|
||||
under an apron lowers, never lifts.
|
||||
- **Road markings, atlas UVs & decals** (phase 2.5,
|
||||
`ProceduralRoadGeometry.md` §18): `RoadConfig::markingsEnabled` +
|
||||
per-edge tri-state `RoadEdge::markingsMode` drive painted overlay
|
||||
strips (double-solid/dashed centerline, dashed lane lines, solid
|
||||
edge lines at the curbs, stop bars on the incoming half of junction
|
||||
arms) built by `buildWedgeMarkings`/`buildSegmentMarkings` 2 cm
|
||||
above the road top; junction interiors stay unpainted, 2-way corners
|
||||
paint through. `RoadConfig::textureTileLength` > 0 switches all
|
||||
road/sidewalk/apron strips to atlas UVs (U bands 0-0.5/0.5-0.75/
|
||||
0.75-1.0, V tiling per tile length); 0 keeps legacy meter UVs. The
|
||||
generated `RoadAtlasTexture` placeholder uses anisotropic asphalt
|
||||
noise (streaks along the lane) with default mipmaps.
|
||||
`RoadEdge::decals` (`RoadEdgeDecal`: edgeT + lateral + size + atlas
|
||||
rect) paints flat quads via `buildEdgeDecals`, per-edge so they
|
||||
never land on junctions. Overlays build into separate `_markings`/
|
||||
`_decals` chunk meshes with unlit CULL_NONE materials
|
||||
(`markingMaterialName`/`decalMaterialName`, auto-created when
|
||||
missing) and no colliders; all of it is hashed into the page
|
||||
content signature.
|
||||
- **Road network generator, crossings, lots & streets** (taxi demo
|
||||
phase 3, `roadlib/RoadNetworkGen.hpp/.cpp`,
|
||||
`ProceduralRoadGeometry.md` §19): graph-level services, all pure
|
||||
`RoadGraph` functions exercisable headlessly. `findCrossings()`/
|
||||
`resolveAllCrossings()` turn edge crossings into proper junctions
|
||||
(end-node reuse within `RoadConfig::crossingSnapDistance`, sliver
|
||||
collapse via `RoadGraph::mergeNodes`, edge splits; pairs separated
|
||||
by more than `crossingHeightThreshold` in Y are overpasses and left
|
||||
alone). `findLots()` enumerates enclosed faces (planar face walk,
|
||||
sharpest right turn from the incoming heading with the U-turn edge
|
||||
excluded) for building lots, filtered by `RoadLotFilter` (default:
|
||||
45 m min size on bbox + area, max 8 boundary edges) and extended
|
||||
with square fallback lots grown off edge sides that border no
|
||||
accepted face (`RoadLot::edgeFallback`); road edit mode draws all
|
||||
potential lots as dark grey outline loops
|
||||
(`RoadSystem::buildLotOutlines`); `RoadGraph::streets` +
|
||||
`nameStreet()`/`assignLotAddresses()` give streets and street+number
|
||||
lot addresses ("Lot N" fallback). `generateGrid()` emits a seeded
|
||||
deterministic settlement grid (jittered interior nodes, water/slope
|
||||
rejection via a height sampler) stamped with `generatedGroupId` —
|
||||
the editor refuses Remove/Split on grouped nodes/edges, a group is
|
||||
removed wholesale (`RoadSystem::clearGenerated`) and hooks into a
|
||||
manual road via `connectGroupToNode()`. Editor UI: terrain panel
|
||||
"Road Network Generator" section (`RoadSystem::generateRoadNetwork`
|
||||
— terrain heights + `WaterPhysics` water level — and
|
||||
`resolveCrossingsNow`), with a "Pick Origin on Terrain" button that
|
||||
arms a one-shot terrain click for the origin
|
||||
(`RoadSystem::armOriginPick`/`takePickedOrigin`, consumed by
|
||||
`EditorUISystem::handleRoadEditClick`) and a "Preview Grid" checkbox
|
||||
that draws the would-be grid as orange lines without touching the
|
||||
graph (`RoadSystem::updateGenPreview`/`clearGenPreview` — throwaway
|
||||
graph, refreshed on parameter change); the edge inspector assigns
|
||||
streets. Streets
|
||||
persist inline in the scene JSON when not streaming, in
|
||||
`roads/streets.json` when streaming; `generatedGroupId` rides both
|
||||
the inline node JSON and the region files.
|
||||
- **Procedural town generation** (taxi demo phase 4,
|
||||
`roadlib/TownGen.hpp/.cpp` + `systems/TownGenSystem.hpp/.cpp`,
|
||||
`ProceduralRoadGeometry.md` §20): `TownGen::generateTown()` (pure,
|
||||
headless) turns `findLots()` lots into buildings — district presets
|
||||
(village/town/city-residential/city-center: footprint, floors,
|
||||
density, setback), seeded deterministic draws, frontage ranking
|
||||
(street edge > fallback source edge > longest edge; fallback lots
|
||||
match their source edge geometrically via `RoadLot::sourceEdgeA`/
|
||||
`sourceEdgeB` — their polygon winding may be reversed, never assume
|
||||
`polygon[0]-polygon[1]` is the source edge), foundation block seated
|
||||
at the highest terrain corner, entrance stairs when the door ground
|
||||
drops > 0.35 m, sequential street addresses, curbside pickup POIs,
|
||||
water-level rejection. Lots are shrunk off the road body before
|
||||
placement (`shrinkLotOffRoads()`: the centerline-bounded lot polygon
|
||||
is inset per bounding edge by lanes + that side's sidewalk + apron +
|
||||
0.3 m; lots whose inset collapses are skipped). `TownGenSystem`
|
||||
(owned by `TerrainSystem`)
|
||||
materializes the result: a `"TownGen <district> seed <n>"` container
|
||||
(found by name prefix) with a `TownComponent`, one shared
|
||||
ProceduralTexture/Material pair, and per building an exterior-only
|
||||
CellGrid shell (front door with `doorsEnabled = false` + placeholder
|
||||
`town_interior.json` scene-switch path for Phase 8) with a flat
|
||||
full-footprint `RoofComponent` child (`posY = floors` —
|
||||
`CellGridSystem::buildRoofs()` only processes `RoofComponent`
|
||||
children of the grid entity) and serialized
|
||||
Primitive-box foundation/stairs children (Primitive children survive
|
||||
save/load; raw TriangleBufferComponent buffers don't). Editor UI:
|
||||
terrain panel "Town Generator" section.
|
||||
- **Chunked page meshes**: page assembly splits the page geometry into
|
||||
chunk meshes capped at `ROAD_PAGE_CHUNK_MAX_VERTICES` (60000) vertices,
|
||||
split on piece boundaries — each chunk gets its own mesh, static Jolt
|
||||
|
||||
@@ -31,6 +31,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/SpawnerRegionStore.cpp
|
||||
systems/RoadRegionStore.cpp
|
||||
systems/RoadSystem.cpp
|
||||
systems/TownGenSystem.cpp
|
||||
systems/LightSystem.cpp
|
||||
systems/CameraSystem.cpp
|
||||
systems/LodSystem.cpp
|
||||
@@ -828,6 +829,10 @@ add_test(NAME globalStateTest
|
||||
add_library(RoadGeometryLib STATIC
|
||||
roadlib/RoadGeometryLib.cpp
|
||||
roadlib/RoadGeometryLib.hpp
|
||||
roadlib/RoadNetworkGen.cpp
|
||||
roadlib/RoadNetworkGen.hpp
|
||||
roadlib/TownGen.cpp
|
||||
roadlib/TownGen.hpp
|
||||
)
|
||||
|
||||
target_include_directories(RoadGeometryLib PUBLIC
|
||||
@@ -837,6 +842,7 @@ target_include_directories(RoadGeometryLib PUBLIC
|
||||
target_link_libraries(RoadGeometryLib PUBLIC
|
||||
OgreMain
|
||||
OgreProcedural::OgreProcedural
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -526,6 +526,16 @@ boundary chord; `solvePinKernel` iterates Kaczmarz projections from
|
||||
the road miter corner seed, retrying once with the zone extended to
|
||||
the whole interior when the tight zone's kernel is empty.
|
||||
|
||||
**Memoization.** The scan is O(rows^2) per calling chain row, so
|
||||
`filletMiterPinZoneRaw` memoizes it per (wedge endpoints, node
|
||||
position, fillet shape, chain config) in a function-static cache (128
|
||||
entries, cleared on overflow; road building is main-thread
|
||||
synchronous). The entry round-trips the FULL pin point — an earlier
|
||||
version cached only X/Z, and on cache hits the pinning curb offset
|
||||
`K - center` inherited an uninitialized Y, which sent marking rows on
|
||||
pin-zone boundaries to y ~ -1.5e6 at runtime (2026-10-02 spike
|
||||
regression; covered by the suite's pin-zone cache round-trip probe).
|
||||
|
||||
**Impossible-corner detection.** A converged kernel only proves the
|
||||
fan triangles are consistently oriented around the pin point P — when
|
||||
the boundary cross-sections interpenetrate, the chain sweeps more than
|
||||
@@ -1278,3 +1288,354 @@ never fills up to meet the apron. Residual cosmetic limitation: on
|
||||
ground sitting just below road top the flat bench leaves the terrain
|
||||
flush with the road top across the whole band, so the terrain can poke
|
||||
through the apron mid-slope — reads as a natural curb cut.
|
||||
|
||||
## 18. Road Markings, Atlas UVs and Decals (M5.19, taxi demo phase 2.5)
|
||||
|
||||
Painted markings, road-atlas UVs and flat decal quads, all generated by
|
||||
the same builders as the road geometry so hand editing and procedural
|
||||
generators get them for free.
|
||||
|
||||
### 18.1 Marking kinds and placement rules
|
||||
|
||||
`RoadConfig::markingsEnabled` (default on) gates everything; each edge
|
||||
carries a tri-state `RoadEdge::markingsMode` (Inherit/On/Off, same
|
||||
pattern as the sidewalk modes) folded by
|
||||
`RoadGraph::edgeMarkingsEnabled()`. Four overlay buffers are built
|
||||
(`RoadMarkingBuffers{centerline, laneLines, edgeLines, stopLines}`) by
|
||||
`buildWedgeMarkings()` per wedge and `buildSegmentMarkings()` per
|
||||
dead-end straight segment:
|
||||
|
||||
- **Centerline**: separates the travel directions. 2+ lanes per
|
||||
direction -> double solid (two `markingLineWidth` strips with a
|
||||
one-line gap between them, at lat [-g-w, -g] and [g, g+w],
|
||||
g = w/2); 1+1 -> a single dashed line at lat 0. Resolved from the
|
||||
resolved lane counts, not configurable.
|
||||
- **Lane lines**: dashed lines between same-direction lanes (N lanes
|
||||
per side -> N-1 lines at lat k * laneWidth on that side).
|
||||
- **Edge lines**: solid strips at both curbs, inset
|
||||
`markingEdgeInset` from the curb (curb-relative, so they follow the
|
||||
widened curb chain through fillets and tapers).
|
||||
- **Stop lines**: junctions (degree > 2) only, one perpendicular bar
|
||||
across the incoming half of each arm, `markingStopSetback` past the
|
||||
fillet/clear boundary and `markingStopWidth` deep. The bar starts
|
||||
flush at the centerline strip's OUTER edge (lat 1.5*w double solid,
|
||||
0.5*w dashed) and the arm's longitudinal lines resume past the bar's
|
||||
far edge — sharing only boundary lines, so nothing coplanar-overlaps
|
||||
(the combined-overlay overlap check enforces this).
|
||||
|
||||
**Junction suppression**: at degree > 2 nodes all longitudinal lines
|
||||
stop at the clear boundary (fillet reach or junction half-width, plus
|
||||
the stop setback on the incoming side) and the wedge interior stays
|
||||
unpainted. At 2-neighbor nodes there is no clear zone and the
|
||||
markings follow the corner through. Dashes phase from the edge
|
||||
midpoint (wedge path d = 0), so up to one dash gap next to any node
|
||||
may legitimately be unpainted.
|
||||
|
||||
**Pin zones**: corner pin zones (§5.10) are subtracted from every
|
||||
marking range — the pinched cross-section there cannot host paint.
|
||||
The rows landing exactly on a zone boundary pin at the shared kernel
|
||||
pin point just like the road surface's own boundary row, so the
|
||||
paint edge meets the fan seam exactly.
|
||||
|
||||
Geometry: strips are emitted as quad rows (`ROAD_MARKING_STEP` 0.5 m)
|
||||
between `sampleWedgeMarking()` samples (center on the wedge centerline
|
||||
at paint height = road top + `ROAD_MARKING_LIFT` 0.02, lateral
|
||||
direction and curb distance from `computeCurbOffset`), so they follow
|
||||
fillets, lane tapers and height profiles exactly. Rows whose strip
|
||||
would hang past the curb are skipped. The paper-thin quads wind
|
||||
downward; the overlay material is CULL_NONE, so this is intentional.
|
||||
|
||||
### 18.2 Road atlas UVs
|
||||
|
||||
`RoadConfig::textureTileLength` > 0 switches every road/sidewalk/apron
|
||||
strip to atlas UVs (0 keeps the legacy meter UVs: U = along-road
|
||||
meters, V = lateral meters). U runs ACROSS the surface and selects
|
||||
the band of the shared square road atlas: 0.0-0.5 road-top asphalt,
|
||||
0.5-0.75 sidewalk, 0.75-1.0 apron; each strip maps its full width into
|
||||
its band (the road band is additionally split so the lanesIn-side curb
|
||||
starts at U = 0.25 within the band — symmetric widening convention).
|
||||
V runs along the travel direction and tiles once per
|
||||
`textureTileLength` world meters, phase-continuous across the edge
|
||||
midpoint (`atlasV()`), so long edges stop smearing the texture.
|
||||
`RoadSystem::syncRoadAtlasTextureUnit()` swaps the road material's
|
||||
texture to the generated `RoadAtlasTexture` placeholder while tiling
|
||||
is on. The placeholder's asphalt band uses anisotropic noise (16-px
|
||||
runs along image Y, the along-road V axis) so the grain reads as
|
||||
streaks along the lane, and keeps default mipmaps so distant
|
||||
minification blurs instead of throwing cross-road moire straps.
|
||||
|
||||
### 18.3 Decals
|
||||
|
||||
`RoadEdge::decals` is a serialized per-edge list of `RoadEdgeDecal`
|
||||
quads: centre at `edgeT` along the edge plus `lateralOffset` meters
|
||||
sideways (positive = right of A->B travel), `size` meters on both
|
||||
axes, sampling the `(uvX, uvY, uvW, uvH)` rect of the shared
|
||||
`RoadDecalAtlas` texture (generated placeholder: manhole, arrows,
|
||||
damage patches). `buildEdgeDecals()` places each quad on the
|
||||
piecewise road surface (road top + `ROAD_DECAL_LIFT` 0.025, so the
|
||||
mid-edge kink is followed exactly). Decals are per-edge by
|
||||
construction, so they never land on junction interiors; a cross-page
|
||||
edge paints each half's decals on the owning page only (same ownership
|
||||
split as the geometry). Pure visuals: no collider, no navmesh, no
|
||||
compliance footprint.
|
||||
|
||||
### 18.4 Integration
|
||||
|
||||
Markings and decals are built into separate overlay chunk buffers
|
||||
(`appendOverlay`/`flushOverlay`, same 60000-vertex chunk cap as the
|
||||
page geometry, `_markings`/`_decals` mesh-name suffixes) with their
|
||||
own materials and NO physics colliders — vehicles never feel the
|
||||
paint. `RoadSystem::ensureOverlayMaterials()` creates the defaults
|
||||
when missing: the marking material is unlit white (RTSS renders a
|
||||
lighting-disabled pass white, which is exactly road paint), CULL_NONE,
|
||||
depth write on; the decal material adds SBT_TRANSPARENT_ALPHA, depth
|
||||
write off and the clamped `RoadDecalAtlas` texture unit. Both are
|
||||
looked up by name (`RoadConfig::markingMaterialName` /
|
||||
`decalMaterialName`), so a project can pre-author replacements. The
|
||||
page content signature hashes all marking config fields, the per-edge
|
||||
`markingsMode` and the decal lists, so any paint edit rebuilds exactly
|
||||
the affected pages.
|
||||
|
||||
Headless coverage: `road_geometry_overlap_test` probes
|
||||
(`runMarkingSegmentProbe` dead-end content + per-edge gating,
|
||||
`runJunctionMarkingCase` 4-way 1+1 / 4-way 2+2 / 3-way T clear zones,
|
||||
stop bars, combined road+overlay overlap, `runMarkingTwoWayProbe`
|
||||
corner continuation, `runDecalProbe` placement/UVs/ownership,
|
||||
`runUVAtlasProbe` bands + V tiling + legacy mode).
|
||||
|
||||
## 19. Road Network Generator, Crossings, Lots and Streets (taxi demo phase 3)
|
||||
|
||||
Graph-level services for procedural settlement creation, living in
|
||||
`roadlib/RoadNetworkGen.hpp/.cpp` (namespace `RoadNetworkGen`, part of
|
||||
the `RoadGeometryLib` target). Everything is a pure function of
|
||||
`RoadGraph` data plus an optional terrain height sampler, so the whole
|
||||
namespace is exercisable headlessly.
|
||||
|
||||
### 19.1 Crossing detection and resolution
|
||||
|
||||
`findCrossings()` reports every edge pair whose closest points are
|
||||
nearer (in XZ) than the sum of the edge half-widths (lanes + enabled
|
||||
sidewalks) AND no further apart in Y than
|
||||
`RoadConfig::crossingHeightThreshold` (default 10 m) — a bigger Y gap
|
||||
is an overpass/underpass and is deliberately NOT a crossing. Edge
|
||||
pairs sharing a node are skipped.
|
||||
|
||||
`resolveCrossing()` turns one crossing into a proper junction. For
|
||||
each of the two edges: an end node within
|
||||
`RoadConfig::crossingSnapDistance` (default 5 m) of the crossing point
|
||||
is reused, an edge too short to split (at most
|
||||
`2 * ROAD_MIN_EDGE_LENGTH`) is collapsed via `RoadGraph::mergeNodes()`
|
||||
into its end nearer to the crossing, anything else is split at the
|
||||
crossing parameter. The two resulting junction nodes are then merged
|
||||
into one, placed at the crossing midpoint.
|
||||
`resolveAllCrossings()` re-detects after every fix (one fix per pass)
|
||||
until the graph is clean. `checkSane()` verifies the structural
|
||||
invariants afterwards: no duplicate edge pairs, no sliver edges below
|
||||
`ROAD_MIN_EDGE_LENGTH`, no isolated nodes (it intentionally does NOT
|
||||
run `RoadGraph::validate()`, whose wedge-angle rules reject
|
||||
hand-authored legacy scenes).
|
||||
|
||||
### 19.2 Lot detection
|
||||
|
||||
`findLots()` enumerates the enclosed faces of the graph (potential
|
||||
building lots) with a planar face walk: every directed edge is walked
|
||||
taking the sharpest right turn at each node (largest positive angle
|
||||
from the incoming heading in the atan2(z, x) sense), which yields
|
||||
interior faces as positive-area XZ cycles. Two implementation rules
|
||||
matter:
|
||||
|
||||
- the turn is measured from the heading, not from the reverse
|
||||
direction — a near-straight continuation is ~pi from the reverse
|
||||
direction and wraps to ~-pi, which would otherwise win the extremum
|
||||
and send the walk straight through 4-way junctions;
|
||||
- the U-turn edge back to the previous node is excluded from the
|
||||
search (its angle from the heading is exactly +-pi, i.e. it sits on
|
||||
the wrap boundary and would be picked at random depending on float
|
||||
rounding); it is only taken at dead ends, where it makes the walk
|
||||
retrace the edge and get the face rejected.
|
||||
|
||||
A face is rejected when its boundary walk traverses an edge twice or
|
||||
revisits a node (a spur/cul-de-sac pokes into it), or when another
|
||||
graph node lies strictly inside it — such areas are intersected by
|
||||
road and are not clean lots. Survivors get a polygon, area and
|
||||
area-weighted centroid.
|
||||
|
||||
Acceptance filtering (`RoadLotFilter`, defaults aimed at building
|
||||
lots): a face is discarded when its boundary has more than
|
||||
`maxBoundaryEdges` (8) edges, when its area is below
|
||||
`minLotSize`^2 (45 m -> 2025 m^2), or when its XZ bounding box is
|
||||
narrower than `minLotSize` in either axis — a lot must fit a
|
||||
45x45 m square.
|
||||
|
||||
Fallback lots (`RoadLotFilter::edgeFallback`, on by default): every
|
||||
directed edge side that does not already border an accepted face
|
||||
(`coveredLeft` tracks the left side of each directed edge, the
|
||||
CCW-face-interior side) grows a square lot with side = edge length
|
||||
when the edge is at least `minLotSize` long. A candidate square is
|
||||
rejected when any graph node lies strictly inside it (excluding the
|
||||
source edge's endpoints), when any edge endpoint lies inside it or
|
||||
properly crosses its boundary (touching/collinear does not count), or
|
||||
when it overlaps an already accepted lot. The overlap test
|
||||
(`polysOverlapXZ`) checks edge crossings plus CENTROID containment —
|
||||
not vertex containment: a fallback square sharing a boundary vertex
|
||||
with an accepted face ray-casts that vertex as "inside"
|
||||
(`pointInPolyXZ` boundary rule) and would be falsely rejected.
|
||||
Accepted squares get `edgeFallback = true`, empty `nodeIds` and a
|
||||
vertex-mean centroid. Editor: potential lots are drawn as dark grey
|
||||
outline loops in road edit mode (`RoadSystem::buildLotOutlines`, part
|
||||
of the visual-aids overlay).
|
||||
|
||||
### 19.3 Streets and addresses
|
||||
|
||||
`RoadGraph::streets` holds named edge sequences (`RoadStreet{name,
|
||||
edges}`; edges are normalized endpoint-id pairs, min first).
|
||||
`nameStreet()` (re)defines a street, dropping its edges from any other
|
||||
street; `findStreetForEdge()` looks up the street containing an edge.
|
||||
`assignLotAddresses()` assigns each lot to its nearest street and
|
||||
numbers lots sequentially along the street's direction; lots on no
|
||||
street fall back to "Lot N".
|
||||
|
||||
Streets persist inline in the scene JSON (`streets` array) for
|
||||
non-streaming terrains and in `heightmaps/<terrainId>/roads/streets.json`
|
||||
next to the region files when streaming (loaded once per terrain in
|
||||
`RoadSystem::syncRegions()` with `dropMissing = false` — pages are
|
||||
never all in the graph at once, so entries whose edges are currently
|
||||
unloaded are kept; written by `RoadSystem::flushRegionStore()`).
|
||||
|
||||
### 19.4 Seeded grid generator
|
||||
|
||||
`generateGrid()` emits `(blocksX+1) x (blocksZ+1)` intersections with
|
||||
seeded jitter (mt19937) on interior nodes only (boundary stays put, so
|
||||
settlements tile predictably), and edges along the grid lines. With a
|
||||
height sampler: nodes below `waterLevel` are rejected (their edges go
|
||||
with them) and edges steeper than `maxSlope` rise/run are skipped.
|
||||
Every created node is stamped with a `generatedGroupId` (0 = manual);
|
||||
the editor refuses Remove/Split on grouped nodes/edges — a group is a
|
||||
single unit, removed wholesale via `removeGroup()`.
|
||||
`connectGroupToNode()` connects a manual road node to the closest
|
||||
group node ("as if the group were a road node") and resolves
|
||||
crossings, which is how a settlement hooks into a hand-authored road.
|
||||
|
||||
Editor integration: the terrain panel's "Road Network Generator"
|
||||
section (origin, block counts/sizes, jitter, seed, avoid-water, max
|
||||
slope, crossing resolution; Generate / Resolve Crossings Now /
|
||||
per-group Remove buttons) wraps `RoadSystem::generateRoadNetwork()`
|
||||
(heights from `TerrainSystem::getHeightAt`, water level from the
|
||||
scene's `WaterPhysics` entity), `RoadSystem::resolveCrossingsNow()`
|
||||
and `RoadSystem::clearGenerated()`. "Pick Origin on Terrain" arms a
|
||||
one-shot terrain click (`RoadSystem::armOriginPick` /
|
||||
`takePickedOrigin`; `EditorUISystem::handleRoadEditClick` consumes the
|
||||
click before any road tool) that fills the origin fields. The
|
||||
"Preview Grid" checkbox draws the would-be grid as orange lines
|
||||
without touching the graph (`RoadSystem::updateGenPreview` runs
|
||||
`generateGrid()` into a throwaway graph with the same terrain/water
|
||||
inputs; refreshed on every parameter change, cleared by
|
||||
`clearGenPreview()` on uncheck, generation, scene clear or terrain
|
||||
switch). The edge inspector assigns the selected edge to a named
|
||||
street.
|
||||
|
||||
Headless coverage: `road_geometry_overlap_test` probes
|
||||
(`runCrossingResolutionProbe` X junction/end-snap T/collapse/overpass
|
||||
untouched, `runLotProbe` 2-block grid + spur rejection,
|
||||
`runStreetProbe` naming/reassignment/JSON round trip/unique addresses +
|
||||
unnamed fallback, `runGeneratorProbe` determinism, counts, water and
|
||||
slope rejection, two settlements in one graph, group connect/remove).
|
||||
|
||||
## 20. Procedural Town Generation (taxi demo phase 4)
|
||||
|
||||
`TownGen` (`roadlib/TownGen.hpp/.cpp`, part of `RoadGeometryLib`, pure
|
||||
— no Ogre scene dependencies beyond math types) turns the lots from
|
||||
`RoadNetworkGen::findLots()` into a building list:
|
||||
`generateTown(graph, TownParams, HeightSampler) -> TownResult`.
|
||||
|
||||
### 20.1 Districts
|
||||
|
||||
`District` presets (`districtParams()`): Village (1 floor, 6-10 m
|
||||
footprints, density 0.6), Town (1-3 floors, 8-14 m, 0.75),
|
||||
CityResidential (2-5 floors, 10-18 m, 0.85), CityCenter (4-8 floors,
|
||||
12-22 m, 1.0). Each preset carries footprint range, floor range,
|
||||
density, setback and a wall tint. `TownParams` = district + seed +
|
||||
density scale + water level.
|
||||
|
||||
### 20.2 Placement
|
||||
|
||||
Per lot, in fixed seed-draw order (density, width, depth, floors — so
|
||||
results are deterministic per seed): pick the **frontage** edge
|
||||
(ranking: boundary edge on a named street > the fallback lot's source
|
||||
edge > the longest boundary edge), build the lot frame (u along the
|
||||
frontage, v from the frontage midpoint towards the centroid), inset a
|
||||
setback rectangle, and place a width x depth footprint clamped to the
|
||||
available space. Lots too small after setback are skipped
|
||||
(`skippedTooSmall`); buildings whose floor would sit below
|
||||
`waterLevel` are skipped (`skippedWater`).
|
||||
|
||||
Because `findLots()` lot polygons run **centerline-to-centerline**,
|
||||
the lot is first shrunk off the roads (`shrinkLotOffRoads()`): every
|
||||
boundary edge matching a graph edge (geometric endpoint match, either
|
||||
direction — covers enclosed faces and fallback source edges alike) is
|
||||
offset inward by the road half-extent on the lot's side (all lanes x
|
||||
`laneWidth` / 2 + the lot side's sidewalk + apron + a 0.3 m margin);
|
||||
the inset vertices are the intersections of the consecutive offset
|
||||
lines (near-parallel edge pairs average the two offset positions of
|
||||
the shared vertex). Bounds and placement are measured from the inset
|
||||
polygon in the lot frame — the buildable front line `vMin` is the
|
||||
frontage pushed off the road, so the door, stairs and pickup POI stay
|
||||
inside the lot. A lot whose inset collapses (degenerate edge or
|
||||
reversed/vanished polygon) is skipped as too small.
|
||||
|
||||
Edge-fallback lots (the squares `findLots()` grows off lone road
|
||||
edges) have no `nodeIds` and their polygon winding may be reversed,
|
||||
so the source edge is matched **geometrically** via the new
|
||||
`RoadLot::sourceEdgeA`/`sourceEdgeB` node ids (endpoint positions,
|
||||
0.01 eps, either direction) — never assume `polygon[0]-polygon[1]`
|
||||
is the source edge.
|
||||
|
||||
### 20.3 Foundation, stairs, addresses, POIs
|
||||
|
||||
Uneven terrain is handled without terrain edits: the floor sits at
|
||||
the **highest** terrain corner of the footprint and a foundation
|
||||
block reaches below the lowest corner; when the door-side ground
|
||||
(one meter in front of the front face) drops more than 0.35 m below
|
||||
the floor, the building gets entrance stairs (`stairsDrop`).
|
||||
Addresses come from the frontage street, numbered sequentially per
|
||||
street in lot order ("Lot N" when the lot touches no street — the
|
||||
fallback-lot source edge also resolves through
|
||||
`findStreetForEdge()`). Every building gets a curbside pickup
|
||||
`TownPoi` (later phases use these as taxi pickup/dropoff and
|
||||
pedestrian origin/destination points).
|
||||
|
||||
### 20.4 Scene integration
|
||||
|
||||
`TownGenSystem` (`systems/TownGenSystem.hpp/.cpp`, owned by
|
||||
`TerrainSystem` next to `RoadSystem`) materializes a `TownResult`:
|
||||
a container entity named `"TownGen <district> seed <n>"` carrying a
|
||||
`TownComponent` (shared procedural material reference), one shared
|
||||
`ProceduralTexture` (named rects: wall/roof/foundation/door/winframe/
|
||||
floor/ceiling) + `ProceduralMaterial` pair, and per building an
|
||||
EditorMarker entity with an exterior-only `CellGridComponent` shell
|
||||
(perimeter windows, front-center door with `doorsEnabled = false`
|
||||
and the placeholder `doorSceneSwitchPath = "town_interior.json"` —
|
||||
Phase 8 enables the F1 scene switch) plus a `RoofComponent` child
|
||||
(flat roof over the whole footprint, `posY = floors`, slab + trim on
|
||||
top of the walls — `CellGridSystem::buildRoofs()` only processes
|
||||
`RoofComponent` children of the grid entity), plus serialized
|
||||
`Primitive`
|
||||
box children for the foundation and stairs (Primitive children
|
||||
survive save/load; a raw `TriangleBufferComponent` buffer does not)
|
||||
and the pickup POI child. Placement convention:
|
||||
`CellGridComponent::cellToWorld()` puts cell (x, y, z) at
|
||||
`x*cellSize`, so a shell spans `[0, width*cellSize]` from its node —
|
||||
`createBuilding()` shifts the building node so the shell lands
|
||||
CENTERED on `TownBuilding::center` (front face at local +Z), and the
|
||||
foundation/stairs/POI children position themselves in the same shell
|
||||
coordinates. `clear()` / `countTowns()` find towns by
|
||||
the `"TownGen "` name prefix. The terrain panel's "Town Generator"
|
||||
section (district, seed, density, Generate/Clear) wraps it.
|
||||
|
||||
Headless coverage: `runTownGenProbe` in
|
||||
`road_geometry_overlap_test` — building/POI counts on a 12-lot grid,
|
||||
floor-range per district, determinism, water rejection, slope
|
||||
stairs (some but not all on a tilted grid), sequential Main Street
|
||||
addresses including the fallback square, village single-floor, and
|
||||
the road-clearance check (every building footprint corner stays at
|
||||
least the road half-width off every edge centerline segment).
|
||||
|
||||
@@ -147,6 +147,68 @@ struct RoadConfig {
|
||||
*/
|
||||
float prefabSpawnDistance = 150.0f;
|
||||
float prefabDespawnDistance = 250.0f;
|
||||
|
||||
/**
|
||||
* Painted road markings (taxi demo phase 2.5). When enabled, the
|
||||
* geometry builders emit marking strips — centerline (double solid
|
||||
* for 2+ lanes per direction, single dashed for 1+1), dashed lines
|
||||
* between same-direction lanes, solid edge lines at the curbs and
|
||||
* stop lines on the incoming half of each arm before an
|
||||
* intersection (nodes with more than two neighbors) — as separate
|
||||
* overlay geometry slightly above the road top. Junction wedge
|
||||
* surfaces stay unmarked; at 2-neighbor nodes the markings follow
|
||||
* the corner through. Per-edge tri-state overrides:
|
||||
* RoadEdge::markingsMode.
|
||||
*/
|
||||
bool markingsEnabled = true;
|
||||
|
||||
/** Width of one painted line (centerline/lane/edge lines). */
|
||||
float markingLineWidth = 0.15f;
|
||||
|
||||
/** Dash length and gap of dashed lines (centerline 1+1, lanes). */
|
||||
float markingDashLength = 3.0f;
|
||||
float markingDashGap = 3.0f;
|
||||
|
||||
/** Stop line width along travel and setback from the junction. */
|
||||
float markingStopWidth = 0.4f;
|
||||
float markingStopSetback = 1.0f;
|
||||
|
||||
/** Edge line inset from the curb toward the road center. */
|
||||
float markingEdgeInset = 0.25f;
|
||||
|
||||
/**
|
||||
* Materials for the marking and decal overlay chunks. Created
|
||||
* procedurally by RoadSystem when missing (unlit white paint;
|
||||
* alpha-blended decal atlas).
|
||||
*/
|
||||
std::string markingMaterialName = "RoadMarkingMaterial";
|
||||
std::string decalMaterialName = "RoadDecalMaterial";
|
||||
|
||||
/**
|
||||
* Road texture V tiling (phase 2.5). 0 keeps the legacy UVs (U =
|
||||
* along-road meters, V = lateral meters). When > 0 the road mesh
|
||||
* UVs are regenerated as an atlas mapping: U runs ACROSS the
|
||||
* surface (no tiling) and selects the band of the shared road
|
||||
* atlas texture — 0.0-0.5 road-top asphalt, 0.5-0.75 sidewalk,
|
||||
* 0.75-1.0 apron — while V runs along the travel direction and
|
||||
* tiles once per this many world meters, so long edges stop
|
||||
* smearing the texture.
|
||||
*/
|
||||
float textureTileLength = 0.0f;
|
||||
|
||||
/**
|
||||
* Crossing resolution (taxi demo phase 3,
|
||||
* RoadNetworkGen::findCrossings/resolveCrossing). Two edges count as
|
||||
* intersecting when the XZ distance between their closest points is
|
||||
* at most the sum of their half-widths AND the closest points are at
|
||||
* most crossingHeightThreshold apart in Y (a larger Y gap is an
|
||||
* overpass/underpass and creates no junction). When the crossing
|
||||
* point lies within crossingSnapDistance meters of an edge end the
|
||||
* existing end node is reused for the junction instead of inserting
|
||||
* a split node.
|
||||
*/
|
||||
float crossingSnapDistance = 5.0f;
|
||||
float crossingHeightThreshold = 10.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -187,6 +249,17 @@ struct RoadNode {
|
||||
* existing edges.
|
||||
*/
|
||||
int id = 0;
|
||||
|
||||
/**
|
||||
* Procedural-generation group membership (taxi demo phase 3). 0 is a
|
||||
* manually authored node; a nonzero value marks the node as part of a
|
||||
* generated road grid (RoadNetworkGen::generateGrid stamps every node
|
||||
* it creates). Generated groups are treated as single unsplittable
|
||||
* units: the editor refuses to move/remove/split grouped nodes and
|
||||
* their interior edges, and the whole group can be removed or
|
||||
* connected to manual road at once.
|
||||
*/
|
||||
uint32_t generatedGroupId = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -257,6 +330,47 @@ enum class RoadSidewalkMode : int {
|
||||
Off = 2 /**< No sidewalk on this side even if globally enabled. */
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-edge road-marking tri-state (phase 2.5).
|
||||
*
|
||||
* Same folding rule as RoadSidewalkMode: Inherit follows the global
|
||||
* RoadConfig::markingsEnabled, On/Off force markings for this edge
|
||||
* regardless of it. See RoadGraph::edgeMarkingsEnabled().
|
||||
*/
|
||||
enum class RoadMarkingMode : int {
|
||||
Inherit = 0, /**< Follow RoadConfig::markingsEnabled. */
|
||||
On = 1, /**< Markings on this edge even if globally disabled. */
|
||||
Off = 2 /**< No markings on this edge even if globally enabled. */
|
||||
};
|
||||
|
||||
/**
|
||||
* A flat decal quad painted onto the road surface (phase 2.5): manhole
|
||||
* covers, arrows, damage patches, parking symbols.
|
||||
*
|
||||
* The quad is centred at normalized position @c edgeT along the edge,
|
||||
* shifted @c lateralOffset meters sideways (positive = right side of
|
||||
* A->B travel, same convention as RoadEdgePrefabSlot), spans
|
||||
* @c size meters on both axes and samples the @c uv* rect of the shared
|
||||
* decal atlas texture. Decals are pure visuals: no physics collider,
|
||||
* no navmesh contribution, no terrain-compliance footprint.
|
||||
*/
|
||||
struct RoadEdgeDecal {
|
||||
/** Normalized position along the edge from nodeA to nodeB. */
|
||||
float edgeT = 0.5f;
|
||||
|
||||
/** Lateral offset from the centerline (positive = right side). */
|
||||
float lateralOffset = 0.0f;
|
||||
|
||||
/** Quad size in meters (both axes). */
|
||||
float size = 1.0f;
|
||||
|
||||
/** Atlas rect: origin (uvX, uvY) and extent (uvW, uvH), 0..1. */
|
||||
float uvX = 0.0f;
|
||||
float uvY = 0.0f;
|
||||
float uvW = 0.5f;
|
||||
float uvH = 0.5f;
|
||||
};
|
||||
|
||||
/**
|
||||
* A connection between two RoadNode objects.
|
||||
*
|
||||
@@ -353,6 +467,19 @@ struct RoadEdge {
|
||||
|
||||
/** Prefab anchored at the centerline, at edgeT (edge midpoint). */
|
||||
RoadEdgePrefabSlot prefabMid;
|
||||
|
||||
/**
|
||||
* Per-edge marking tri-state (phase 2.5, see RoadMarkingMode).
|
||||
* Inherit (the default) follows the global
|
||||
* RoadConfig::markingsEnabled switch.
|
||||
*/
|
||||
RoadMarkingMode markingsMode = RoadMarkingMode::Inherit;
|
||||
|
||||
/**
|
||||
* Decal quads painted on this edge's road surface (phase 2.5).
|
||||
* Purely visual; see RoadEdgeDecal.
|
||||
*/
|
||||
std::vector<RoadEdgeDecal> decals;
|
||||
};
|
||||
|
||||
/** Minimum wedge swept angle; sharper wedges are rejected (M5.5). */
|
||||
@@ -361,6 +488,19 @@ static const float ROAD_WEDGE_MIN_ANGLE_DEG = 30.0f;
|
||||
/** Maximum wedge swept angle; only near-360 deg wedges are degenerate. */
|
||||
static const float ROAD_WEDGE_MAX_ANGLE_DEG = 359.9f;
|
||||
|
||||
/**
|
||||
* A named sequence of road edges (taxi demo phase 3). Streets give a
|
||||
* printable, code-accessible name to a run of edges so lots can be
|
||||
* referenced as "street name + number" addresses (see
|
||||
* RoadNetworkGen::assignLotAddresses). Edges are stored as normalized
|
||||
* endpoint id pairs (first < second) because node ids are stable while
|
||||
* edge vector indices are not.
|
||||
*/
|
||||
struct RoadStreet {
|
||||
std::string name;
|
||||
std::vector<std::pair<int, int> > edges;
|
||||
};
|
||||
|
||||
struct RoadGraph;
|
||||
struct RoadWedge;
|
||||
struct RoadStraightSegment;
|
||||
@@ -394,6 +534,13 @@ struct RoadGraph {
|
||||
/** All road edges in the graph. Ordering is not significant. */
|
||||
std::vector<RoadEdge> edges;
|
||||
|
||||
/**
|
||||
* Named streets (phase 3). Purely labelling data — geometry never
|
||||
* reads it. Mutated through RoadNetworkGen::nameStreet; kept here so
|
||||
* it travels with the graph through region-store flushes.
|
||||
*/
|
||||
std::vector<RoadStreet> streets;
|
||||
|
||||
/**
|
||||
* Monotonic version counter incremented by every mutating operation.
|
||||
*
|
||||
@@ -424,6 +571,18 @@ struct RoadGraph {
|
||||
config.sidewalkEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective marking enable of one edge (phase 2.5): the tri-state
|
||||
* mode folded with the global config.markingsEnabled. Geometry
|
||||
* builders call this directly instead of caching.
|
||||
*/
|
||||
bool edgeMarkingsEnabled(const RoadEdge &e) const
|
||||
{
|
||||
return e.markingsMode == RoadMarkingMode::On ||
|
||||
(e.markingsMode == RoadMarkingMode::Inherit &&
|
||||
config.markingsEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompute the resolved sidewalkLeft/sidewalkRight bools of every
|
||||
* edge from the per-side modes and the global
|
||||
@@ -702,6 +861,61 @@ struct RoadGraph {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge @p dropId into @p keepId (taxi demo phase 3): every edge
|
||||
* incident to @p dropId is re-pointed at @p keepId (edges that would
|
||||
* become duplicates or self-edges are removed instead), street
|
||||
* references are re-pointed too, and @p dropId is removed. Used by
|
||||
* crossing resolution to collapse an end node or a too-short edge
|
||||
* into the junction node.
|
||||
*
|
||||
* @return true on success (both nodes existed and differ).
|
||||
*/
|
||||
bool mergeNodes(int keepId, int dropId)
|
||||
{
|
||||
if (keepId == dropId)
|
||||
return false;
|
||||
if (!findNodeById(keepId) || !findNodeById(dropId))
|
||||
return false;
|
||||
|
||||
for (auto it = edges.begin(); it != edges.end();) {
|
||||
RoadEdge &e = *it;
|
||||
if (e.nodeA != dropId && e.nodeB != dropId) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (e.nodeA == dropId)
|
||||
e.nodeA = keepId;
|
||||
if (e.nodeB == dropId)
|
||||
e.nodeB = keepId;
|
||||
if (e.nodeA == e.nodeB ||
|
||||
findEdgeIndex(e.nodeA, e.nodeB) !=
|
||||
(int)(it - edges.begin())) {
|
||||
/* Self-edge after the merge, or another edge
|
||||
* already connects the same pair (findEdgeIndex
|
||||
* returns the FIRST match, so a mismatch means
|
||||
* this one is the duplicate). */
|
||||
it = edges.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (RoadStreet &s : streets)
|
||||
for (std::pair<int, int> &se : s.edges) {
|
||||
if (se.first == dropId)
|
||||
se.first = keepId;
|
||||
if (se.second == dropId)
|
||||
se.second = keepId;
|
||||
if (se.first > se.second)
|
||||
std::swap(se.first, se.second);
|
||||
}
|
||||
|
||||
removeNode(dropId);
|
||||
bumpVersion();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split an edge at a normalized position and insert a new node.
|
||||
*
|
||||
@@ -782,9 +996,11 @@ struct RoadGraph {
|
||||
/* Sidewalk passes transfer like the prefab slots below (both
|
||||
* halves keep the A->B orientation, so the side flags carry
|
||||
* over); the copies on both halves above are replaced by
|
||||
* edgeT-remapped placement. */
|
||||
* edgeT-remapped placement. Decals transfer the same way. */
|
||||
edgeA.sidewalkPasses.clear();
|
||||
edgeB.sidewalkPasses.clear();
|
||||
edgeA.decals.clear();
|
||||
edgeB.decals.clear();
|
||||
if (actualT > 1e-6f && actualT < 1.0f - 1e-6f) {
|
||||
auto remapSlot = [&](const RoadEdgePrefabSlot &src,
|
||||
RoadEdgePrefabSlot &dstA,
|
||||
@@ -830,6 +1046,20 @@ struct RoadGraph {
|
||||
edgeB.sidewalkPasses.push_back(q);
|
||||
}
|
||||
}
|
||||
|
||||
/* Decals transfer with the same edgeT remap. */
|
||||
for (const RoadEdgeDecal &d : oldEdge.decals) {
|
||||
if (d.edgeT <= actualT) {
|
||||
RoadEdgeDecal q = d;
|
||||
q.edgeT = d.edgeT / actualT;
|
||||
edgeA.decals.push_back(q);
|
||||
} else {
|
||||
RoadEdgeDecal q = d;
|
||||
q.edgeT = (d.edgeT - actualT) /
|
||||
(1.0f - actualT);
|
||||
edgeB.decals.push_back(q);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
edges.erase(edges.begin() + edgeIndex);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# demo-taxi — Implementation Plan
|
||||
|
||||
**Status: Phase 2 done (2026-09-17).** Phases 0-2 done (skeleton,
|
||||
sedan vehicles, road geometry verification + automatic compliance);
|
||||
Phase 3 (road-network generator) is next. This plan is a working
|
||||
draft — phases get refined, split or reordered as we work through
|
||||
them.
|
||||
**Status: Phase 4 done (2026-10-05).** Phases 0-4 done (skeleton,
|
||||
sedan vehicles, road geometry verification + automatic compliance,
|
||||
road markings/UVs/decals, road-network generator with crossings/lots/
|
||||
streets, procedural town generation with districts/foundations/stairs);
|
||||
Phase 4.5 (town parameters on road edge sides, building-layout DSL,
|
||||
multi-town areas — research + foundations) is next.
|
||||
This plan is a working draft — phases get refined, split or reordered
|
||||
as we work through them.
|
||||
|
||||
Taxi driver demo in the open world of `demo_scene_exterior.json`: a
|
||||
procedurally generated (but tightly controlled) settlement on the
|
||||
@@ -152,7 +155,10 @@ Non-goals (for now):
|
||||
the edge and often overlap because too little space is reserved for
|
||||
lanes and sidewalks; the procedural lane template emits front/back
|
||||
geometry even where the intersection should be open; terrain
|
||||
compliance is manual and tedious.
|
||||
compliance is manual and tedious. No road markings at all (center
|
||||
line, same-direction dashed lane lines, curb edge lines, stop lines
|
||||
before intersections), no road decals, and no road-texture V
|
||||
tiling.
|
||||
3. Road-network generation: roads are authored node-by-node with the
|
||||
Connect tool today; no layout generator.
|
||||
4. Town generation: District/Lot exist but arrange lots in a circle
|
||||
@@ -176,6 +182,12 @@ Non-goals (for now):
|
||||
10. Fare chain: no fare/timer/money rules (Lua module + HUD).
|
||||
11. Assets: sedans (several types x several colors, one taxi-marked),
|
||||
possibly street props (lamps, signs).
|
||||
12. Geometry/render hardening: inner-corner wedge midpoints need
|
||||
outward relaxation (with the center gap refilled via the mid-edge
|
||||
vertex shift) to kill residual overlaps on tight turns; marking /
|
||||
decal overlays Z-fight the road top at grazing angles and distance
|
||||
(needs a depth/shader-level fix, not more lift); no render
|
||||
batching yet — the bare exterior scene already draws ~85 batches.
|
||||
|
||||
## Phases
|
||||
|
||||
@@ -1005,24 +1017,275 @@ Follow-ups (2026-09-30, apron terrain interface):
|
||||
TPS camera) plus env-gated debug dumps (`ROAD_DUMP_DIR`,
|
||||
`ROAD_DUMP_PIECES`, `ROAD_DEBUG_PIN`).
|
||||
|
||||
### Phase 3 — Road-network generator
|
||||
### Phase 2.5 — Road markings, road texture & decals — DONE
|
||||
|
||||
Done 2026-10-02. Everything below is implemented; the headless probes
|
||||
live in `road_geometry_overlap_test` (`runMarkingSegmentProbe`,
|
||||
`runJunctionMarkingCase` 4-way 1+1 / 4-way 2+2 / 3-way T,
|
||||
`runMarkingTwoWayProbe`, `runDecalProbe`, `runUVAtlasProbe`) and the
|
||||
CTest gate `roadGeometryOverlapTest` is green. Implementation notes
|
||||
beyond the original text:
|
||||
|
||||
- The stop bar starts flush at the centerline strip's outer edge and
|
||||
the arm's longitudinal lines resume past the bar, so the bar never
|
||||
z-fights the lines it crosses (found by the combined-overlay probe).
|
||||
- Dashes phase from the edge midpoint, so up to one dash gap next to
|
||||
a node can be unpainted — the 2-way probe bounds that gap instead
|
||||
of demanding paint at a fixed offset.
|
||||
- Decals never land on junction interiors by construction: they are
|
||||
placed per edge (edgeT along the half-edge), and a cross-page edge
|
||||
paints each half's decals on the owning page only.
|
||||
- `RoadGeometryLib` doc: `ProceduralRoadGeometry.md` §18.
|
||||
|
||||
Original phase text follows.
|
||||
|
||||
Painted markings, proper road UVs and simple decals, all generated with
|
||||
the road geometry so both hand editing and the Phase 3 generator get
|
||||
them for free.
|
||||
|
||||
**Markings**
|
||||
|
||||
- **Per-edge control**: tri-state `RoadEdge::markingsLeftMode/RightMode`-style
|
||||
override (Inherit/On/Off, same pattern as the sidewalk modes)
|
||||
resolving against the global `RoadConfig::markingsEnabled`, so
|
||||
markings can be disabled (or forced on) per edge.
|
||||
- **Centerline**: separates the two travel directions. Wide roads
|
||||
(2+ lanes per direction) get a double solid line; narrow roads
|
||||
(1 lane per direction) get a single dashed centerline instead.
|
||||
Resolved automatically from the lane counts.
|
||||
- **Lane lines**: dashed lines between same-direction lanes
|
||||
(N lanes per side -> N-1 dashed lines on that side).
|
||||
- **Edge lines**: solid lines along both road edges at the curbs,
|
||||
running the full length of every edge.
|
||||
- **Stop lines**: a perpendicular line across the incoming
|
||||
(right-hand) half of each arm before an intersection (nodes with
|
||||
more than two neighbors), placed at the wedge boundary with a small
|
||||
setback so vehicles stop clear of the junction.
|
||||
- **Intersection suppression**: no markings inside the intersection
|
||||
area itself — at nodes with >2 neighbors the wedge surface stays
|
||||
unmarked and all longitudinal lines end at the wedge boundary. At
|
||||
2-neighbor nodes (turns) all markings continue through the corner.
|
||||
|
||||
**Road texture UVs**
|
||||
|
||||
- Proper UV coordinate generation in the road mesh: U runs across the
|
||||
road and does NOT tile (stretched across the surface width), V runs
|
||||
along the travel direction and tiles at a fixed world length per
|
||||
tile (`RoadConfig::textureTileLength`), so long edges stop smearing.
|
||||
- The road texture is a square atlas shared by all road surfaces;
|
||||
U bands assign each surface its slice: 0.0-0.5 road-top asphalt,
|
||||
0.5-0.75 sidewalk, 0.75-1.0 apron. Each strip maps its width into
|
||||
its U band; V tiles along the length for all of them. The asphalt
|
||||
band stretched across the lane width and tiled along the lane gives
|
||||
a believable enough asphalt look.
|
||||
|
||||
**Decals**
|
||||
|
||||
- Per-edge decal list (serialized with the edge, editable in the road
|
||||
panel): each decal is an offset along the edge, a lateral offset
|
||||
from the centerline, a size, and a rectangle in a decal atlas
|
||||
texture — manhole covers, painted text/arrows, road damage patches.
|
||||
- The atlas for this phase is a generated placeholder (a few
|
||||
procedural patches: manhole, arrows, damage spots, placeholder
|
||||
text), replaced by a real authored texture later — the per-edge
|
||||
decal data only references atlas rectangles, so swapping the texture
|
||||
is free.
|
||||
- Rendered as alpha-blended quads a hair above the road top. Note:
|
||||
Ogre mainline has no decal system (only test-media images by that
|
||||
name), so "Ogre decals" here means plain decal geometry built the
|
||||
same way as the marking strips, sharing the decal atlas material.
|
||||
Researched alternatives (2026-10-01): the only projection-decal
|
||||
implementation is the old Ogre 1.7 wiki tutorial (Intermediate
|
||||
Tutorial 6); current samples contain nothing newer — the
|
||||
`decalmap` texture_unit names in `Examples-Advanced.material` are
|
||||
just the legacy fixed-function term for the base diffuse map, and
|
||||
`setProjectiveTexturing` only appears in the Fresnel sample for
|
||||
reflections. A projective approach would also need receiver
|
||||
filtering (it would paint terrain/vehicles/characters too) and one
|
||||
projector pass per decal, so geometry decals stay the simpler and
|
||||
batchable choice for a per-edge decal list.
|
||||
- Decals are suppressed on junction wedges like the markings.
|
||||
|
||||
**Implementation**
|
||||
|
||||
- Markings and decals are thin geometry strips/quads built by
|
||||
`RoadGeometryLib` a small offset above the road top, using the same
|
||||
widened curb-offset chain machinery as sidewalks/aprons, so they
|
||||
follow fillets, lane tapers and pin zones exactly. One
|
||||
unlit/faintly-lit marking material (paint color in `RoadConfig`),
|
||||
one decal material with the atlas.
|
||||
- `RoadConfig` fields: `markingsEnabled`, line/dash widths, dash
|
||||
length/gap, stop-line width and setback, `textureTileLength`,
|
||||
`decalAtlasTexture`.
|
||||
|
||||
**Headless**
|
||||
|
||||
- Overlap-suite probes: marking strips present with the right counts
|
||||
for 1/2/4-lane edges, dashed count = lanes-1 per side, dashed
|
||||
centerline at 1+1 vs double solid when wider, solid edge lines at
|
||||
both curbs, stop lines on the incoming half at 3-/4-way nodes and
|
||||
none at 2-way nodes, markings continuing through 2-way turns,
|
||||
nothing on the junction wedges, no marking/road-top z-overlap;
|
||||
per-edge disable honored.
|
||||
- UV probe: V tiles at the configured world length, U bands map
|
||||
road/sidewalk/apron into 0.0-0.5 / 0.5-0.75 / 0.75-1.0.
|
||||
- Decal probe: position/orientation derived from edge + lateral
|
||||
offsets, rectangle mapped from the atlas, suppressed on junction
|
||||
wedges.
|
||||
|
||||
**Regression fixes (2026-10)**
|
||||
|
||||
- Marking strips no longer spike vertically on pin-zone boundary rows:
|
||||
the memoized `filletMiterPinZoneRaw` cache restored only the pin
|
||||
point's X/Z, leaving Y uninitialized on cache hits, so the pinned
|
||||
curb offset could point straight down with a garbage length (marking
|
||||
vertices at y ~ -1.5e6 at runtime). The cache now round-trips the
|
||||
full pin point.
|
||||
- The generated road-atlas asphalt band uses anisotropic noise
|
||||
(streaks along the lane) and default mipmaps; the isotropic
|
||||
no-mipmap version read as cross-road moire straps at distance.
|
||||
|
||||
### Phase 3 — Road-network generator — DONE
|
||||
|
||||
The tedious part today: every node/edge is hand-placed. Build a
|
||||
generator that emits a `RoadGraph` (region files when streaming):
|
||||
generator that emits a `RoadGraph` (region files when streaming), plus
|
||||
the graph-level services that both manual and procedural networks
|
||||
need.
|
||||
|
||||
Implemented in `roadlib/RoadNetworkGen.hpp/.cpp` (namespace
|
||||
`RoadNetworkGen`, pure `RoadGraph` functions, part of the
|
||||
`RoadGeometryLib` target; see `ProceduralRoadGeometry.md` §19) with
|
||||
`RoadSystem` wrappers (`generateRoadNetwork` /
|
||||
`resolveCrossingsNow` / `clearGenerated`), serialization (region
|
||||
files carry `generatedGroupId`; streets inline in the scene JSON or in
|
||||
`roads/streets.json` when streaming; `crossingSnapDistance` /
|
||||
`crossingHeightThreshold` in `roadConfig`), a terrain-panel "Road
|
||||
Network Generator" section (Generate / Resolve Crossings Now /
|
||||
per-group Remove; guards refuse Remove/Split on grouped nodes/edges;
|
||||
the edge inspector assigns streets) and headless probes
|
||||
(`runCrossingResolutionProbe`, `runLotProbe`, `runStreetProbe`,
|
||||
`runGeneratorProbe` in `road_geometry_overlap_test` — suite PASSED).
|
||||
|
||||
**Crossing resolution (graph level)** — DONE
|
||||
|
||||
- Edges must never cross without a junction. Two edges count as
|
||||
intersecting when the distance between their closest points is less
|
||||
than or equal to the road width (resolved lane counts x laneWidth,
|
||||
plus sidewalks where enabled) — unless the closest points are more
|
||||
than 10 m apart in Y, in which case the edges are NOT intersecting
|
||||
(overpass/underpass: one road legitimately passes over the other
|
||||
and no junction is created). → `findCrossings()` with
|
||||
`RoadConfig::crossingHeightThreshold` (default 10 m).
|
||||
- On detection it must be possible to create a proper junction: split
|
||||
both edges at the closest-point parameters and insert a new shared
|
||||
node. → `resolveCrossing()` + `RoadGraph::splitEdge`.
|
||||
- When a closest point is near an edge end (within a configurable snap
|
||||
distance), reuse that existing node for the junction instead of
|
||||
creating a new one; the other edge still splits.
|
||||
→ `RoadConfig::crossingSnapDistance` (default 5 m).
|
||||
- Edges too short to be split (the split would leave slivers below the
|
||||
minimum edge length) are collapsed into the junction node.
|
||||
→ `RoadGraph::mergeNodes()`.
|
||||
- The graph must stay sane at all times: no duplicate edges, no
|
||||
degenerate sliver edges, no dangling nodes, valences and region
|
||||
files consistent after every operation. → `checkSane()`
|
||||
(deliberately not `validate()` — its wedge-angle rules reject
|
||||
hand-authored legacy scenes).
|
||||
- Editor: a "resolve crossings" action (check, list, apply per
|
||||
crossing or for all). The generator runs it automatically on its
|
||||
own output and when connecting a procedural part to manual road.
|
||||
→ "Resolve Crossings Now" button; `runCrossingResolution` on
|
||||
`generateGrid()` and inside `connectGroupToNode()`. (A per-crossing
|
||||
list/apply UI is deferred — the resolve-all action covers the
|
||||
generator workflows.)
|
||||
|
||||
**Lots (enclosed areas)** — DONE
|
||||
|
||||
- Areas enclosed by road edges and not intersected by another edge
|
||||
(faces of the planar road graph, after crossing resolution) are
|
||||
tracked as potential lots — for manual and procedural networks
|
||||
alike. Phase 4 consumes them for building placement.
|
||||
→ `findLots()` (planar face walk, sharpest right turn from the
|
||||
incoming heading with the U-turn edge excluded; spur/inner-node
|
||||
rejection; area + centroid).
|
||||
- Lot filtering: a face must fit a 45x45 m square (XZ bbox + area,
|
||||
`RoadLotFilter::minLotSize`) and be bounded by at most 8 edges
|
||||
(`maxBoundaryEdges`); every edge side bordering no accepted face
|
||||
grows a square fallback lot (side = edge length) when the edge is
|
||||
at least 45 m long (`edgeFallback`, overlap/cleanness checked).
|
||||
→ `RoadLotFilter` defaults; `runLotProbe` covers the size/edge-count
|
||||
cutoffs, the ring and lone-edge fallback cases.
|
||||
- All potential lots are drawn as dark grey outline loops in road
|
||||
edit mode, like the other helper geometry.
|
||||
→ `RoadSystem::buildLotOutlines` (visual-aids overlay).
|
||||
|
||||
**Streets and addresses** — DONE
|
||||
|
||||
- A sequence of road edges can be marked as a street and given a name
|
||||
(printable and code-accessible). Lots are referenced by street name
|
||||
+ sequential number to form a unique address; lots on unnamed
|
||||
streets are addressed by sequential number alone. These are mostly
|
||||
display/reference labels (HUD fare text, debug panels).
|
||||
→ `RoadGraph::streets`, `nameStreet()`, `findStreetForEdge()`,
|
||||
`assignLotAddresses()` ("Lot N" fallback).
|
||||
|
||||
**Generator** — DONE
|
||||
|
||||
- Grid layout first (block size, road width, jitter), explicit seed,
|
||||
fully deterministic. (Organic/radial layouts are deferred to
|
||||
Phase 11.) A network is bounded to the settlement square
|
||||
(max 1000 x 1000 m); generating another settlement is just another
|
||||
generator invocation at a different origin.
|
||||
→ `GridParams` + `generateGrid()` (mt19937 jitter on interior nodes
|
||||
only; group id auto-assigned per invocation).
|
||||
- A generated road grid is a single unsplittable unit in the road
|
||||
graph — its interior nodes/edges are not individually editable. It
|
||||
connects to manual road as if it were a road node; the connection to
|
||||
the closest node of the procedural part is handled automatically
|
||||
(and crossing resolution runs on the connecting edge).
|
||||
→ `RoadNode::generatedGroupId` (editor guards), `removeGroup()`,
|
||||
`connectGroupToNode()`.
|
||||
- Terrain-aware placement: sample `getHeightAt`, reject or relocate
|
||||
segments over water or too-steep slopes, then run the Phase 2
|
||||
automatic compliance pass.
|
||||
→ `HeightSampler` (wired to `TerrainSystem::getHeightAt` in
|
||||
`RoadSystem::generateRoadNetwork`, water level from the scene's
|
||||
`WaterPhysics` entity), `avoidWater` node rejection + `maxSlope`
|
||||
edge skipping. (Relocating instead of rejecting and the automatic
|
||||
post-generation compliance pass are left to Phase 4 workflows —
|
||||
the user runs the existing compliance buttons.)
|
||||
- Editor entry point: a "Generate Road Network" panel on the terrain
|
||||
(parameters, seed, generate/regenerate, clear).
|
||||
- Headless: generate a network, assert node/edge counts, intersection
|
||||
valences, no self-intersections, region files written and reload
|
||||
identically; two settlements in one world do not interfere.
|
||||
→ "Road Network Generator" tree node in the terrain road section,
|
||||
with a "Pick Origin on Terrain" button that arms a one-shot terrain
|
||||
click to fill the origin XZ (`RoadSystem::armOriginPick` /
|
||||
`takePickedOrigin`, consumed by
|
||||
`EditorUISystem::handleRoadEditClick`) and a "Preview Grid" checkbox
|
||||
that draws the would-be grid as orange lines without touching the
|
||||
graph (`RoadSystem::updateGenPreview`/`clearGenPreview`, refreshed
|
||||
on every parameter change).
|
||||
|
||||
**Headless** — DONE
|
||||
|
||||
- Generate a network; assert node/edge counts, intersection valences,
|
||||
no self-intersections, region files written and reload identically;
|
||||
two settlements in one world do not interfere.
|
||||
→ `runGeneratorProbe` (determinism, counts, sanity, two settlements,
|
||||
group connect/remove). (Region-file reload is covered indirectly:
|
||||
group ids/streets ride the existing region JSON paths, and the
|
||||
streets JSON round trip is asserted in `runStreetProbe`.)
|
||||
- Crossing resolution: crossing edges become a 4-way junction, end
|
||||
snap reuses the existing node, too-short edges collapse, and the
|
||||
graph stays sane (no slivers/duplicates/dangling nodes) after each
|
||||
case; an XZ crossing with more than 10 m of Y separation at the
|
||||
closest points is left untouched (overpass).
|
||||
→ `runCrossingResolutionProbe`.
|
||||
- Lot tracking: enclosed blocks are found, areas intersected by an
|
||||
edge are not, lot counts match the grid parameters.
|
||||
→ `runLotProbe` + the 9-lot count in `runGeneratorProbe`.
|
||||
- Streets: naming an edge sequence yields unique, stable addresses
|
||||
across save/load; the unnamed-street fallback numbering works.
|
||||
→ `runStreetProbe`.
|
||||
|
||||
### Phase 4 — Procedural town generation
|
||||
|
||||
@@ -1052,6 +1315,88 @@ On top of the generated road network:
|
||||
all buildings on dry land above water level, every building carries
|
||||
its (disabled) scene-switch door config, navmesh rebuilds.
|
||||
|
||||
**Phase 4 status (2026-10-04): implemented.** `TownGen` pure lib
|
||||
(`roadlib/TownGen.hpp/.cpp`) + `TownGenSystem` + a "Town Generator"
|
||||
section in the terrain panel; `findLots()` fallback squares gained
|
||||
`sourceEdgeA`/`sourceEdgeB` so their frontage orients to the road and
|
||||
addresses resolve through the street table. Done: district presets
|
||||
(village/town/city-residential/city-center: footprint range, floors,
|
||||
density, setback), seeded deterministic placement on `findLots()` lots,
|
||||
frontage = street edge > fallback source edge > longest edge, floor on
|
||||
a foundation block seated at the highest terrain corner, entrance
|
||||
stairs (solid steps) when the door ground drops > 0.35 m, per-building
|
||||
pickup POI children, sequential street addresses ("Lot N" fallback),
|
||||
water-level rejection, disabled scene-switch exterior doors
|
||||
(`doorsEnabled = false`, placeholder `town_interior.json` wired in
|
||||
Phase 8), headless probes (counts/floors/determinism/water/stairs/
|
||||
addresses — the navmesh-rebuild assert is covered by code review:
|
||||
CellGrid dirty marks dirty the navmesh). Fix pass (2026-10-05):
|
||||
lots are shrunk off the road body before placement
|
||||
(`shrinkLotOffRoads()` — `findLots()` polygons run
|
||||
centerline-to-centerline, so each bounding edge is inset by lanes +
|
||||
sidewalk + apron + 0.3 m, collapses skip the lot; probe now asserts
|
||||
every building corner keeps the road half-width off every edge
|
||||
segment), buildings get their flat roof (a full-footprint
|
||||
`RoofComponent` child at `posY = floors` — `buildRoofs()` only reads
|
||||
RoofComponent children), and the building node is shifted so the
|
||||
CellGrid shell (cells at `x*cellSize` from the node) lands centered
|
||||
on the computed footprint with foundation/stairs/POI in shell
|
||||
coordinates. Deferred: per-page building
|
||||
streaming store (town buildings are always-loaded scene entities for
|
||||
now — revisit when a settlement grows past a few hundred buildings),
|
||||
street props (no lamp/sign/tree prefab assets exist yet — add assets,
|
||||
then a roadside slot pass), terrain compliance per lot (superseded by
|
||||
foundation blocks + stairs, which tolerate slopes directly).
|
||||
|
||||
### Phase 4.5 — Town parameters, building-layout DSL, multi-town areas (research + foundations)
|
||||
|
||||
Phase 4 proved the pipeline but exposed three structural gaps: town
|
||||
regeneration discards all manual tuning (parameters live only in the
|
||||
generator dialog, not on the map), buildings are fixed shells with no
|
||||
layout control, and nothing scopes generation to one settlement when
|
||||
the world holds several. This phase researches and lays the
|
||||
foundations for all three. Research output lands in
|
||||
`ProceduralRoadGeometry.md` (new §21) before implementation.
|
||||
|
||||
- **Per-lot parameters on road edge sides.** The persistent home for
|
||||
per-building/per-lot settings is the **side of a road edge** (each
|
||||
`RoadEdge` gets a left/right `RoadLotParams` block: district
|
||||
override, density, setback, footprint/floor ranges, roof type and
|
||||
params, no-build flag, layout reference). Lots are already derived
|
||||
from edge frontages, so edge-side parameters survive regeneration —
|
||||
re-running the town generator re-reads them instead of resetting
|
||||
them. Persisted with the graph (inline scene JSON, region files
|
||||
when streaming), edited in the edge inspector. Research: the exact
|
||||
field set, how edge-side blocks map to lots (enclosed faces touch
|
||||
several edges — precedence rules), how overrides merge with district
|
||||
presets, migration of existing scenes (absent = inherit).
|
||||
- **Building layout DSL.** A small declarative language describing a
|
||||
building layout — floors, rooms, walls, windows, doors, roofs, all
|
||||
with parameters — evaluated into CellGrid cells + `RoofComponent`
|
||||
sets. One layout definition must drive BOTH the exterior shell
|
||||
(Phase 4 path) and the interior scene (Phase 8), so the DSL is the
|
||||
single source of truth for a building's shape. Research: grammar
|
||||
(Lua table DSL vs custom text format vs JSON), parameterization
|
||||
(variables, per-floor overrides, random draws under the town seed),
|
||||
a template library (named layouts referenced from edge-side lot
|
||||
params), and the evaluation boundary between `TownGen` (placement)
|
||||
and `TownGenSystem` (materialization).
|
||||
- **Multiple towns: areas in the road graph.** `RoadGraph` gains a
|
||||
`townAreas` list — square or circular areas (center, half-size or
|
||||
radius, district + params, seed) scoping generation: `findLots()`
|
||||
results are filtered per area, each area generates into its own
|
||||
`"TownGen <name>"` container, and a world can hold several
|
||||
independent settlements from the start. Research: area/lot overlap
|
||||
rules (lots straddling the boundary), area editing UX (viewport
|
||||
outline, panel), streaming interaction (areas vs region stores).
|
||||
- **Headless coverage:** edge-side params round-trip (serialize /
|
||||
region store / regeneration keeps overrides), layout DSL evaluation
|
||||
(a sample layout produces the expected cell flags and roof set),
|
||||
two overlapping-scope areas generate independent deterministic
|
||||
towns.
|
||||
- **Docs:** `ProceduralRoadGeometry.md` §21 (params model, DSL
|
||||
grammar, areas), AGENTS.md, this plan's status blocks.
|
||||
|
||||
### Phase 5 — NPC vehicle traffic
|
||||
|
||||
- AI driver system (new, engine-level): a vehicle follows the road
|
||||
@@ -1180,6 +1525,39 @@ demo needs them:
|
||||
- More vehicle types beyond sedans (the Phase 1 pipeline is already
|
||||
generic): vans, trucks, buses; service vehicles.
|
||||
|
||||
### Phase 11.5 — Geometry polish, marking depth & render batching
|
||||
|
||||
Deferred hardening items (noted 2026-10-03 — fix, don't forget). Pull
|
||||
the phase earlier if any item starts blocking daily work; all three
|
||||
must land before the Phase 12 performance/polish pass.
|
||||
|
||||
- **Wedge midpoint relaxation (inner corners).** Inner (converging)
|
||||
corner wedges can still overlap neighbouring geometry on tight
|
||||
turns. Relax the midpoints of wedges with an inner corner outward
|
||||
(per corner, in the spirit of `relaxImpossibleCorners()` but acting
|
||||
on the wedge midpoint/seam side instead of the node) so the overlap
|
||||
clears and the corner looks nicer. The center gap this opens is
|
||||
filled by shifting the edge vertices as usual (the existing mid-edge
|
||||
seam machinery), so the mid-edge seams stay gapless. Headless:
|
||||
tight-corner overlap-suite cases keep zero crossing/coplanar pairs
|
||||
with relaxation on, and straight/near-straight corners are
|
||||
unaffected (signature-gated like the other rebuild triggers).
|
||||
- **Marking Z-fighting.** The marking/decal overlays (2 cm lift) still
|
||||
Z-fight the road top at grazing angles and at distance. Fix at the
|
||||
material/shader level instead of adding more lift: bias the overlay
|
||||
depth toward the camera (Ogre depth-bias material settings, or a
|
||||
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.
|
||||
|
||||
### Phase 12 — Demo assembly, polish, docs
|
||||
|
||||
- Assemble the full demo scene: at least one full settlement on the
|
||||
@@ -1189,7 +1567,8 @@ demo needs them:
|
||||
"home"/menu-adjacent space.
|
||||
- Balance pass: speeds, timers, fare slack, traffic density.
|
||||
- Performance pass: streaming radii, navmesh tile budget, active NPC
|
||||
caps — measure FPS with both settlements placed.
|
||||
caps — measure FPS with both settlements placed; verify the Phase
|
||||
11.5 batch budget holds in the assembled scene.
|
||||
- Docs: update `AGENTS.md` (both levels), `GameFeatures202609.md`
|
||||
(F13+ entries), demo README section; full headless test battery
|
||||
wired into the demo like demo-sokoban's.
|
||||
|
||||
@@ -849,15 +849,15 @@
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4004.76416015625,
|
||||
"y": 10.909640312194824,
|
||||
"z": 3964.12255859375
|
||||
"x": 4002.519287109375,
|
||||
"y": 11.094489097595215,
|
||||
"z": 3962.923095703125
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.991229236125946,
|
||||
"x": -0.053019944578409195,
|
||||
"y": 0.12085098028182983,
|
||||
"z": 0.0069722202606499195
|
||||
"w": 0.9888900518417358,
|
||||
"x": -0.0026904146652668715,
|
||||
"y": 0.14861378073692322,
|
||||
"z": 0.0017891188617795706
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
@@ -1254,15 +1254,15 @@
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4017.775390625,
|
||||
"y": 10.960867881774902,
|
||||
"z": 3963.472900390625
|
||||
"x": 4016.510498046875,
|
||||
"y": 11.157183647155762,
|
||||
"z": 3964.1669921875
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.9645168781280518,
|
||||
"x": -0.05992506444454193,
|
||||
"y": 0.25656017661094666,
|
||||
"z": 0.017121003940701485
|
||||
"w": 0.9334654211997986,
|
||||
"x": -0.0019687723834067583,
|
||||
"y": 0.35863012075424194,
|
||||
"z": 0.004789790604263544
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
@@ -1470,10 +1470,22 @@
|
||||
"roadConfig": {
|
||||
"apronDrop": 1.0,
|
||||
"apronEnabled": true,
|
||||
"apronSkirtDepth": 2.0,
|
||||
"apronWidth": 2.0,
|
||||
"cornerFilletLength": 12.0,
|
||||
"crossingHeightThreshold": 10.0,
|
||||
"crossingSnapDistance": 5.0,
|
||||
"decalMaterialName": "RoadDecalMaterial",
|
||||
"laneWidth": 5.0,
|
||||
"lanesPerDirection": 1,
|
||||
"markingDashGap": 3.0,
|
||||
"markingDashLength": 3.0,
|
||||
"markingEdgeInset": 0.25,
|
||||
"markingLineWidth": 0.15000000596046448,
|
||||
"markingMaterialName": "RoadMarkingMaterial",
|
||||
"markingStopSetback": 1.0,
|
||||
"markingStopWidth": 0.4000000059604645,
|
||||
"markingsEnabled": true,
|
||||
"prefabDespawnDistance": 250.0,
|
||||
"prefabSpawnDistance": 150.0,
|
||||
"roadLodDistance": 200.0,
|
||||
@@ -1485,7 +1497,8 @@
|
||||
"sidewalkHeight": 0.15000000596046448,
|
||||
"sidewalkMeshTemplate": "",
|
||||
"sidewalkThickness": 0.30000001192092896,
|
||||
"sidewalkWidth": 1.5
|
||||
"sidewalkWidth": 1.5,
|
||||
"textureTileLength": 10.0
|
||||
},
|
||||
"streamingEnabled": true,
|
||||
"terrainId": 4242424300000001,
|
||||
@@ -1595,7 +1608,7 @@
|
||||
0.800000011920929
|
||||
],
|
||||
"sunSphereSize": 5.0,
|
||||
"timeOfDay": 15.492432594299316,
|
||||
"timeOfDay": 0.6397704482078552,
|
||||
"timeSpeed": 0.12999999523162842
|
||||
},
|
||||
"transform": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
@@ -5,6 +5,9 @@
|
||||
#include "systems/SceneSerializer.hpp"
|
||||
#include "systems/CharacterRegistry.hpp"
|
||||
#include "systems/TerrainTests.hpp"
|
||||
#include "systems/TerrainSystem.hpp"
|
||||
#include "systems/TownGenSystem.hpp"
|
||||
#include "roadlib/TownGen.hpp"
|
||||
#include "OgreRoot.h"
|
||||
|
||||
struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
@@ -21,6 +24,47 @@ struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
}
|
||||
};
|
||||
|
||||
/** Frame-driven town generation (editor, headless testing).
|
||||
* Waits a few frames for the terrain/road systems to activate,
|
||||
* generates a town from the loaded scene's road graph, then exits.
|
||||
* Exceptions propagate to main()'s handler (exit code 1). */
|
||||
struct GenerateTownFrameListener : public Ogre::FrameListener {
|
||||
Ogre::Root *root;
|
||||
TownGen::TownParams params;
|
||||
int frames = 0;
|
||||
int result = -1;
|
||||
GenerateTownFrameListener(Ogre::Root *r,
|
||||
const TownGen::TownParams &p)
|
||||
: root(r), params(p) {}
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
if (++frames < 5)
|
||||
return true;
|
||||
if (result >= 0 || frames > 3600) {
|
||||
if (result < 0) {
|
||||
std::cerr << "generate-town: timed out waiting "
|
||||
<< "for the terrain" << std::endl;
|
||||
result = 0;
|
||||
}
|
||||
root->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
TownGenSystem *townGen =
|
||||
ts ? ts->getTownGenSystem() : nullptr;
|
||||
if (!townGen)
|
||||
return true; /* terrain not active yet */
|
||||
std::string error;
|
||||
int count = townGen->generate(params, &error);
|
||||
std::cout << "generate-town: " << count << " buildings"
|
||||
<< (error.empty() ? "" : " (" + error + ")")
|
||||
<< std::endl;
|
||||
result = 0;
|
||||
root->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/** Frame-driven terrain test runner.
|
||||
* Terrain init needs the render loop active (GPU context).
|
||||
* Runs the suite on the first frame, then exits. */
|
||||
@@ -65,6 +109,8 @@ int main(int argc, char *argv[])
|
||||
bool exitAfterFirstFrame = false;
|
||||
bool headless = false;
|
||||
int terrainTestIterations = 0;
|
||||
bool generateTown = false;
|
||||
TownGen::TownParams genTownParams;
|
||||
Ogre::String sceneFile;
|
||||
Ogre::String projectDir;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
@@ -82,6 +128,29 @@ int main(int argc, char *argv[])
|
||||
debugBuoyancy = true;
|
||||
} else if (arg == "--exit-after-first-frame") {
|
||||
exitAfterFirstFrame = true;
|
||||
} else if (arg.find("--generate-town=") == 0) {
|
||||
/* --generate-town=<district>[,<seed>] with district
|
||||
* village|town|city-residential|city-center */
|
||||
generateTown = true;
|
||||
Ogre::String val = arg.substr(16);
|
||||
size_t comma = val.find(',');
|
||||
Ogre::String district = val.substr(0, comma);
|
||||
if (comma != Ogre::String::npos)
|
||||
genTownParams.seed =
|
||||
Ogre::StringConverter::parseInt(
|
||||
val.substr(comma + 1), 1);
|
||||
if (district == "town")
|
||||
genTownParams.district =
|
||||
TownGen::District::Town;
|
||||
else if (district == "city-residential")
|
||||
genTownParams.district =
|
||||
TownGen::District::CityResidential;
|
||||
else if (district == "city-center")
|
||||
genTownParams.district =
|
||||
TownGen::District::CityCenter;
|
||||
else
|
||||
genTownParams.district =
|
||||
TownGen::District::Village;
|
||||
} else if (arg.find("--run-terrain-tests=") == 0) {
|
||||
Ogre::String val = arg.substr(20);
|
||||
terrainTestIterations = Ogre::StringConverter::parseInt(
|
||||
@@ -190,13 +259,17 @@ int main(int argc, char *argv[])
|
||||
ExitAfterFirstFrameListener exitListener(app.getRoot());
|
||||
if (exitAfterFirstFrame)
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
GenerateTownFrameListener genTownListener(app.getRoot(),
|
||||
genTownParams);
|
||||
if (generateTown)
|
||||
app.getRoot()->addFrameListener(&genTownListener);
|
||||
app.getRoot()->startRendering();
|
||||
|
||||
/* Destroy scene entities while the systems are still alive
|
||||
* (same teardown-ordering reason as the demo mains: spawner
|
||||
* OnRemove observers dereference their systems). */
|
||||
if (gameSceneStarted)
|
||||
app.clearScene();
|
||||
/* Destroy scene entities while the systems (and OGRE) are
|
||||
* still alive: components holding OGRE resources (procedural
|
||||
* materials/textures, spawner OnRemove observers) must not
|
||||
* be torn down at ecs_fini after Root is gone. */
|
||||
app.clearScene();
|
||||
app.closeApp();
|
||||
|
||||
/* Propagate terrain test result as process exit code so CI can
|
||||
|
||||
@@ -63,6 +63,29 @@ float halfEdgeU(const RoadHalfEdge &he, const RoadGraph &graph, float t)
|
||||
return t;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Road atlas UV mapping (phase 2.5)
|
||||
*
|
||||
* When RoadConfig::textureTileLength > 0 the road mesh UVs are
|
||||
* regenerated as an atlas mapping instead of meter coordinates: U runs
|
||||
* ACROSS the surface (no tiling) and selects the band of the shared
|
||||
* road atlas texture (0.0-0.5 asphalt road top, 0.5-0.75 sidewalk,
|
||||
* 0.75-1.0 apron), while V runs along the travel direction and tiles
|
||||
* once per textureTileLength meters. textureTileLength == 0 keeps the
|
||||
* legacy meter UVs (U along the road, V lateral meters).
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
static bool atlasUVsEnabled(const RoadGraph &graph)
|
||||
{
|
||||
return graph.config.textureTileLength > 1e-6f;
|
||||
}
|
||||
|
||||
/** Along-road V coordinate: meters tiled by textureTileLength. */
|
||||
static float atlasV(const RoadGraph &graph, float meters)
|
||||
{
|
||||
return meters / graph.config.textureTileLength;
|
||||
}
|
||||
|
||||
float nodeRoadLevel(const RoadGraph &graph, int nodeId)
|
||||
{
|
||||
float sum = 0.0f;
|
||||
@@ -1685,7 +1708,7 @@ static bool filletMiterPinZoneRaw(const RoadWedge &wedge,
|
||||
bool sidewalkEnabled, apronEnabled;
|
||||
bool has;
|
||||
float pinStart, pinEnd;
|
||||
float pinX, pinZ;
|
||||
float pinX, pinY, pinZ;
|
||||
bool impossible;
|
||||
};
|
||||
static std::vector<PinZoneCacheEntry> s_cache;
|
||||
@@ -1735,6 +1758,7 @@ static bool filletMiterPinZoneRaw(const RoadWedge &wedge,
|
||||
pinStart = e.pinStart;
|
||||
pinEnd = e.pinEnd;
|
||||
pinPoint.x = e.pinX;
|
||||
pinPoint.y = e.pinY;
|
||||
pinPoint.z = e.pinZ;
|
||||
}
|
||||
if (impossible)
|
||||
@@ -1762,6 +1786,7 @@ static bool filletMiterPinZoneRaw(const RoadWedge &wedge,
|
||||
key.pinStart = ps;
|
||||
key.pinEnd = pe;
|
||||
key.pinX = pp.x;
|
||||
key.pinY = pp.y;
|
||||
key.pinZ = pp.z;
|
||||
key.impossible = imp;
|
||||
if (s_cache.size() >= 128)
|
||||
@@ -2187,9 +2212,24 @@ void transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
|
||||
/* UVs. */
|
||||
float widthD = off.length();
|
||||
v.mUV.x = (d <= L1) ? halfEdgeU(h1, graph, L1 - d)
|
||||
: halfEdgeU(h2, graph, d - L1);
|
||||
v.mUV.y = v.mUV.y * widthD + in1;
|
||||
float uMeters = (d <= L1) ? halfEdgeU(h1, graph, L1 - d)
|
||||
: halfEdgeU(h2, graph, d - L1);
|
||||
if (atlasUVsEnabled(graph)) {
|
||||
/* U across the asphalt band (0..0.5): the lateral
|
||||
* meter position from the lanesIn-side curb over the
|
||||
* arm's full road width; V tiles along the road. */
|
||||
const RoadHalfEdge &hArm = (d <= L1) ? h1 : h2;
|
||||
float lw = graph.config.laneWidth;
|
||||
float fullW = (hArm.lanesIn + hArm.lanesOut) * lw;
|
||||
if (fullW < 1e-4f)
|
||||
fullW = 1e-4f;
|
||||
float latM = hArm.lanesIn * lw + v.mUV.y * widthD;
|
||||
v.mUV.x = 0.5f * latM / fullW;
|
||||
v.mUV.y = atlasV(graph, uMeters);
|
||||
} else {
|
||||
v.mUV.x = uMeters;
|
||||
v.mUV.y = v.mUV.y * widthD + in1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Normal. The wedge bend is a reflection of the template:
|
||||
@@ -2512,10 +2552,21 @@ bool computeSegmentBand(const RoadStraightSegment &segment,
|
||||
c[0].y = c[3].y = y0;
|
||||
c[1].y = c[2].y = yL;
|
||||
|
||||
uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f);
|
||||
uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f);
|
||||
uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), inW + outW);
|
||||
uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), inW + outW);
|
||||
if (atlasUVsEnabled(graph)) {
|
||||
/* U across the asphalt band (0 at the -r curb, 0.5 at +r),
|
||||
* V tiles along the road. */
|
||||
float v0 = atlasV(graph, halfEdgeU(he, graph, t0));
|
||||
float v1 = atlasV(graph, halfEdgeU(he, graph, L));
|
||||
uvc[0] = Ogre::Vector2(0.0f, v0);
|
||||
uvc[1] = Ogre::Vector2(0.0f, v1);
|
||||
uvc[2] = Ogre::Vector2(0.5f, v1);
|
||||
uvc[3] = Ogre::Vector2(0.5f, v0);
|
||||
} else {
|
||||
uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f);
|
||||
uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f);
|
||||
uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), inW + outW);
|
||||
uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), inW + outW);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2650,10 +2701,18 @@ static void transformSidewalkVertices(Procedural::TriangleBuffer &strip,
|
||||
}
|
||||
float worldY = surfY + lift + v.mPosition.y;
|
||||
|
||||
/* UVs: phase-continuous u, v across [0, sidewalkWidth]. */
|
||||
v.mUV.x = (d <= L1) ? halfEdgeU(h1, graph, L1 - d)
|
||||
: halfEdgeU(h2, graph, d - L1);
|
||||
v.mUV.y = v.mUV.y * sw;
|
||||
/* UVs: phase-continuous u, v across [0, sidewalkWidth]; with
|
||||
* road-atlas UVs (textureTileLength > 0) U selects the
|
||||
* sidewalk band (0.5-0.75) and V tiles along the road. */
|
||||
float uMeters = (d <= L1) ? halfEdgeU(h1, graph, L1 - d)
|
||||
: halfEdgeU(h2, graph, d - L1);
|
||||
if (atlasUVsEnabled(graph)) {
|
||||
v.mUV.x = 0.5f + 0.25f * v.mUV.y;
|
||||
v.mUV.y = atlasV(graph, uMeters);
|
||||
} else {
|
||||
v.mUV.x = uMeters;
|
||||
v.mUV.y = v.mUV.y * sw;
|
||||
}
|
||||
|
||||
/* Normals: same explicit axis mapping as the road (the
|
||||
* bend is a reflection of the template). */
|
||||
@@ -3009,10 +3068,20 @@ bool computeSegmentSidewalkBand(const RoadStraightSegment &segment,
|
||||
c[0].y = c[3].y = halfEdgeHeightAt(he, graph, t0) + sh;
|
||||
c[1].y = c[2].y = halfEdgeHeightAt(he, graph, L) + sh;
|
||||
|
||||
uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f);
|
||||
uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f);
|
||||
uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), sw);
|
||||
uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), sw);
|
||||
if (atlasUVsEnabled(graph)) {
|
||||
/* Sidewalk band (U 0.5-0.75), V tiles along the road. */
|
||||
float v0 = atlasV(graph, halfEdgeU(he, graph, t0));
|
||||
float v1 = atlasV(graph, halfEdgeU(he, graph, L));
|
||||
uvc[0] = Ogre::Vector2(0.5f, v0);
|
||||
uvc[1] = Ogre::Vector2(0.5f, v1);
|
||||
uvc[2] = Ogre::Vector2(0.75f, v1);
|
||||
uvc[3] = Ogre::Vector2(0.75f, v0);
|
||||
} else {
|
||||
uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f);
|
||||
uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f);
|
||||
uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), sw);
|
||||
uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), sw);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3205,10 +3274,18 @@ static void transformApronVertices(Procedural::TriangleBuffer &strip,
|
||||
v.mPosition.x * drop * xSlope +
|
||||
v.mPosition.y;
|
||||
|
||||
/* UVs: phase-continuous u, v across [0, apronWidth]. */
|
||||
v.mUV.x = (d <= L1) ? halfEdgeU(h1, graph, L1 - d)
|
||||
: halfEdgeU(h2, graph, d - L1);
|
||||
v.mUV.y = v.mUV.y * aw;
|
||||
/* UVs: phase-continuous u, v across [0, apronWidth]; with
|
||||
* road-atlas UVs (textureTileLength > 0) U selects the
|
||||
* apron band (0.75-1.0) and V tiles along the road. */
|
||||
float uMeters = (d <= L1) ? halfEdgeU(h1, graph, L1 - d)
|
||||
: halfEdgeU(h2, graph, d - L1);
|
||||
if (atlasUVsEnabled(graph)) {
|
||||
v.mUV.x = 0.75f + 0.25f * v.mUV.y;
|
||||
v.mUV.y = atlasV(graph, uMeters);
|
||||
} else {
|
||||
v.mUV.x = uMeters;
|
||||
v.mUV.y = v.mUV.y * aw;
|
||||
}
|
||||
|
||||
/* Normals: same explicit axis mapping as the road (the
|
||||
* bend is a reflection of the template). */
|
||||
@@ -3375,10 +3452,20 @@ bool buildSegmentApronGeometry(const RoadStraightSegment &segment,
|
||||
c[3].y = c[0].y - drop;
|
||||
|
||||
Ogre::Vector2 uvc[4];
|
||||
uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f);
|
||||
uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f);
|
||||
uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), aw);
|
||||
uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), aw);
|
||||
if (atlasUVsEnabled(graph)) {
|
||||
/* Apron band (U 0.75-1.0), V tiles along the road. */
|
||||
float v0 = atlasV(graph, halfEdgeU(he, graph, t0));
|
||||
float v1 = atlasV(graph, halfEdgeU(he, graph, L));
|
||||
uvc[0] = Ogre::Vector2(0.75f, v0);
|
||||
uvc[1] = Ogre::Vector2(0.75f, v1);
|
||||
uvc[2] = Ogre::Vector2(1.0f, v1);
|
||||
uvc[3] = Ogre::Vector2(1.0f, v0);
|
||||
} else {
|
||||
uvc[0] = Ogre::Vector2(halfEdgeU(he, graph, t0), 0.0f);
|
||||
uvc[1] = Ogre::Vector2(halfEdgeU(he, graph, L), 0.0f);
|
||||
uvc[2] = Ogre::Vector2(halfEdgeU(he, graph, L), aw);
|
||||
uvc[3] = Ogre::Vector2(halfEdgeU(he, graph, t0), aw);
|
||||
}
|
||||
|
||||
/* The slab carries the apron skirt: bottom face and outer
|
||||
* wall extend apronSkirtDepth below the sloped top so the
|
||||
@@ -3430,6 +3517,448 @@ bool buildSegmentApronGeometry(const RoadStraightSegment &segment,
|
||||
return built;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Road markings (phase 2.5)
|
||||
*
|
||||
* Painted overlay strips sampled from the same wedge centerline/curb
|
||||
* chains as the road surface, emitted slightly above the road top so
|
||||
* they read as paint. Purely visual: these buffers never feed
|
||||
* physics, navmesh or terrain compliance.
|
||||
*
|
||||
* Per-wedge coverage rule (each piece painted exactly once): a wedge's
|
||||
* first arm (d in [0, L1]) paints the centerline plus the OUTGOING
|
||||
* side of its edge (dashed lane lines between same-direction lanes and
|
||||
* the solid curb edge line); the second arm paints only the INCOMING
|
||||
* side of its edge. Every half-edge is the first arm of exactly one
|
||||
* wedge and the second arm of exactly one wedge, so the centerline and
|
||||
* both road sides of every edge are painted exactly once. At junction
|
||||
* nodes (degree > 2) every arm stops short of the corner by a clear
|
||||
* zone (the fillet extent, or the widest incident half-width when no
|
||||
* fillet is active, plus markingStopSetback) and the incoming arm of
|
||||
* each wedge carries a stop line at the clear boundary; at 2-neighbor
|
||||
* nodes the markings follow the corner through (no clear zone, no stop
|
||||
* line). The corner pin zone is cut from all ranges.
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
static const float ROAD_MARKING_LIFT = 0.02f;
|
||||
static const float ROAD_DECAL_LIFT = 0.025f;
|
||||
static const float ROAD_MARKING_STEP = 0.5f;
|
||||
|
||||
/** One paint-height sample of a marking strip row. */
|
||||
struct MarkingSample {
|
||||
/** XZ at the road centerline, Y at paint height. */
|
||||
Ogre::Vector3 center;
|
||||
/** Normalized horizontal direction toward the covered side. */
|
||||
Ogre::Vector3 latDir;
|
||||
/** Curb distance from the centerline along latDir. */
|
||||
float widthD = 0.0f;
|
||||
};
|
||||
|
||||
static bool sampleWedgeMarking(const RoadWedge &wedge,
|
||||
const RoadGraph &graph, float d, float yO,
|
||||
float halfThick, MarkingSample &s)
|
||||
{
|
||||
const RoadHalfEdge &h1 = wedge.first;
|
||||
const RoadHalfEdge &h2 = wedge.second;
|
||||
float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f;
|
||||
|
||||
s.center = wedgeCenterAt(wedge, graph, d);
|
||||
Ogre::Vector3 off = computeCurbOffset(wedge, graph, d);
|
||||
s.widthD = off.length();
|
||||
s.latDir = off;
|
||||
if (s.latDir.length() < 1e-4f)
|
||||
s.latDir = (d <= L1) ? roadRightVec(h1.direction) :
|
||||
-roadRightVec(h2.direction);
|
||||
else
|
||||
s.latDir.normalise();
|
||||
|
||||
float surfY;
|
||||
if (d < L1 - 1e-4f)
|
||||
surfY = halfEdgeHeightAt(h1, graph, L1 - d);
|
||||
else if (d > L1 + 1e-4f)
|
||||
surfY = halfEdgeHeightAt(h2, graph, d - L1);
|
||||
else
|
||||
surfY = yO;
|
||||
s.center.y = surfY + halfThick + ROAD_MARKING_LIFT;
|
||||
return s.widthD > 1e-3f;
|
||||
}
|
||||
|
||||
static void emitMarkingQuad(Procedural::TriangleBuffer &out,
|
||||
const MarkingSample &a, const MarkingSample &b,
|
||||
float latA0, float latA1, float latB0, float latB1)
|
||||
{
|
||||
Ogre::Vector3 p00 = a.center + a.latDir * latA0;
|
||||
Ogre::Vector3 p01 = a.center + a.latDir * latA1;
|
||||
Ogre::Vector3 p10 = b.center + b.latDir * latB0;
|
||||
Ogre::Vector3 p11 = b.center + b.latDir * latB1;
|
||||
Ogre::Vector2 uv(0.0f, 0.0f);
|
||||
emitTri(out, p00, p10, p11, uv, uv, uv);
|
||||
emitTri(out, p00, p11, p01, uv, uv, uv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit one solid marking strip between path distances d0 and d1.
|
||||
* When @p curbRelative is false, [lat0, lat1] are fixed lateral offsets
|
||||
* from the centerline; when true, lat0 is the curb inset and lat1 the
|
||||
* line width (the strip follows the curb at widthD - inset).
|
||||
*/
|
||||
static void emitMarkingLine(Procedural::TriangleBuffer &out,
|
||||
const RoadWedge &wedge, const RoadGraph &graph,
|
||||
float d0, float d1, float yO, float halfThick,
|
||||
float lat0, float lat1, bool curbRelative)
|
||||
{
|
||||
if (d1 - d0 < 1e-3f)
|
||||
return;
|
||||
int rows = std::max(1, (int)std::ceil((d1 - d0) / ROAD_MARKING_STEP));
|
||||
MarkingSample prev, cur;
|
||||
if (!sampleWedgeMarking(wedge, graph, d0, yO, halfThick, prev))
|
||||
return;
|
||||
for (int i = 1; i <= rows; ++i) {
|
||||
float d = d0 + (d1 - d0) * (float)i / (float)rows;
|
||||
if (!sampleWedgeMarking(wedge, graph, d, yO, halfThick, cur))
|
||||
return;
|
||||
float a0, a1, b0, b1;
|
||||
if (curbRelative) {
|
||||
a1 = prev.widthD - lat0;
|
||||
a0 = a1 - lat1;
|
||||
b1 = cur.widthD - lat0;
|
||||
b0 = b1 - lat1;
|
||||
/* Pinched corner rows narrower than inset+width:
|
||||
* clamp to a sliver next to the centerline. */
|
||||
a0 = std::max(a0, 0.02f);
|
||||
b0 = std::max(b0, 0.02f);
|
||||
if (a0 >= a1 - 1e-4f || b0 >= b1 - 1e-4f) {
|
||||
prev = cur;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
a0 = b0 = lat0;
|
||||
a1 = b1 = lat1;
|
||||
/* Skip quads that would hang past the curb. */
|
||||
if (lat1 > prev.widthD - 0.02f ||
|
||||
lat1 > cur.widthD - 0.02f) {
|
||||
prev = cur;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
emitMarkingQuad(out, prev, cur, a0, a1, b0, b1);
|
||||
prev = cur;
|
||||
}
|
||||
}
|
||||
|
||||
static void emitMarkingDashed(Procedural::TriangleBuffer &out,
|
||||
const RoadWedge &wedge, const RoadGraph &graph,
|
||||
float d0, float d1, float yO, float halfThick,
|
||||
float latCenter, float lineWidth)
|
||||
{
|
||||
float dl = graph.config.markingDashLength;
|
||||
float dg = graph.config.markingDashGap;
|
||||
float s = d0;
|
||||
while (s < d1 - 1e-3f) {
|
||||
float e = std::min(s + dl, d1);
|
||||
emitMarkingLine(out, wedge, graph, s, e, yO, halfThick,
|
||||
latCenter - lineWidth * 0.5f,
|
||||
latCenter + lineWidth * 0.5f, false);
|
||||
s = e + dg;
|
||||
}
|
||||
}
|
||||
|
||||
/** Subtract the corner pin zone [ps, pe] from [d0, d1]. */
|
||||
static void cutPinZone(float d0, float d1, float ps, float pe, bool pin,
|
||||
std::vector<std::pair<float, float>> &out)
|
||||
{
|
||||
if (!pin) {
|
||||
out.push_back(std::make_pair(d0, d1));
|
||||
return;
|
||||
}
|
||||
if (d0 < ps - 1e-4f)
|
||||
out.push_back(std::make_pair(d0, std::min(d1, ps)));
|
||||
if (d1 > pe + 1e-4f)
|
||||
out.push_back(std::make_pair(std::max(d0, pe), d1));
|
||||
}
|
||||
|
||||
/** Widest incident half-width at a node (junction clear base). */
|
||||
static float junctionHalfWidth(const RoadGraph &graph, int nodeId)
|
||||
{
|
||||
float w = 0.0f;
|
||||
for (int nid : graph.getNeighborIds(nodeId)) {
|
||||
int ei = graph.findEdgeIndex(nodeId, nid);
|
||||
if (ei < 0)
|
||||
continue;
|
||||
int lo = 0, li = 0;
|
||||
graph.resolveLaneCounts(graph.edges[(size_t)ei], nodeId, lo,
|
||||
li);
|
||||
w = std::max(w, (lo + li) * graph.config.laneWidth * 0.5f);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
bool buildWedgeMarkings(const RoadWedge &wedge, const RoadGraph &graph,
|
||||
RoadMarkingBuffers &out)
|
||||
{
|
||||
if (wedge.degenerate)
|
||||
return false;
|
||||
const RoadNode *node = graph.findNodeById(wedge.nodeId);
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
const RoadHalfEdge &h1 = wedge.first;
|
||||
const RoadHalfEdge &h2 = wedge.second;
|
||||
const RoadConfig &cfg = graph.config;
|
||||
float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f;
|
||||
float L2 = h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f;
|
||||
float lw = cfg.laneWidth;
|
||||
float lineW = cfg.markingLineWidth;
|
||||
float yO = node->position.y + nodeRoadLevel(graph, wedge.nodeId);
|
||||
float halfThick = std::max(0.01f, cfg.roadThickness) * 0.5f;
|
||||
|
||||
bool junction = graph.getNeighborIds(wedge.nodeId).size() > 2;
|
||||
WedgeFillet f = wedgeFillet(wedge, graph);
|
||||
float clearBase = junctionHalfWidth(graph, wedge.nodeId);
|
||||
|
||||
float ps = 0.0f, pe = 0.0f;
|
||||
bool pin = filletPinZone(wedge, graph, ps, pe, nullptr);
|
||||
|
||||
bool built = false;
|
||||
for (int arm = 0; arm < 2; ++arm) {
|
||||
const RoadHalfEdge &h = arm == 0 ? h1 : h2;
|
||||
if (h.edgeIndex < 0 || h.edgeIndex >= (int)graph.edges.size())
|
||||
continue;
|
||||
if (!graph.edgeMarkingsEnabled(
|
||||
graph.edges[(size_t)h.edgeIndex]))
|
||||
continue;
|
||||
|
||||
/* Covered side lane count: a wedge paints its first arm's
|
||||
* OUTGOING side and its second arm's INCOMING side. */
|
||||
int sideLanes = arm == 0 ? h.lanesOut : h.lanesIn;
|
||||
float clear = junction ? (f.active ? (arm == 0 ? f.f1 : f.f2)
|
||||
: clearBase) +
|
||||
cfg.markingStopSetback
|
||||
: 0.0f;
|
||||
float d0 = arm == 0 ? 0.0f : L1 + clear;
|
||||
float d1 = arm == 0 ? L1 - clear : L1 + L2;
|
||||
if (d1 - d0 < 1e-3f)
|
||||
continue;
|
||||
|
||||
/* Stop line: junctions only, on the incoming half — the
|
||||
* second arm of the wedge covers the incoming side. */
|
||||
float sw = cfg.markingStopWidth;
|
||||
bool stopLine = junction && arm == 1 && h.lanesIn > 0 &&
|
||||
L1 + clear + sw < L1 + L2 - 1e-3f;
|
||||
|
||||
/* When a stop line is emitted the longitudinal lines of this
|
||||
* arm resume past it, so the perpendicular bar never
|
||||
* z-fights the lane/edge strips crossing its span. */
|
||||
std::vector<std::pair<float, float>> ranges;
|
||||
cutPinZone(stopLine ? L1 + clear + sw : d0, d1, ps, pe, pin,
|
||||
ranges);
|
||||
|
||||
for (const auto &rg : ranges) {
|
||||
/* Centerline: only the first arm (each half-edge
|
||||
* is the first arm of exactly one wedge). */
|
||||
if (arm == 0) {
|
||||
if (h.lanesOut >= 2 || h.lanesIn >= 2) {
|
||||
float g = lineW * 0.5f;
|
||||
emitMarkingLine(out.centerline, wedge,
|
||||
graph, rg.first,
|
||||
rg.second, yO,
|
||||
halfThick, -g - lineW,
|
||||
-g, false);
|
||||
emitMarkingLine(out.centerline, wedge,
|
||||
graph, rg.first,
|
||||
rg.second, yO,
|
||||
halfThick, g,
|
||||
g + lineW, false);
|
||||
} else {
|
||||
emitMarkingDashed(out.centerline, wedge,
|
||||
graph, rg.first,
|
||||
rg.second, yO,
|
||||
halfThick, 0.0f,
|
||||
lineW);
|
||||
}
|
||||
}
|
||||
/* Dashed lines between same-direction lanes. */
|
||||
for (int k = 1; k < sideLanes; ++k)
|
||||
emitMarkingDashed(out.laneLines, wedge, graph,
|
||||
rg.first, rg.second, yO,
|
||||
halfThick, k * lw, lineW);
|
||||
/* Solid edge line at the curb. */
|
||||
emitMarkingLine(out.edgeLines, wedge, graph, rg.first,
|
||||
rg.second, yO, halfThick,
|
||||
cfg.markingEdgeInset, lineW, true);
|
||||
built = true;
|
||||
}
|
||||
|
||||
if (stopLine) {
|
||||
MarkingSample sa, sb;
|
||||
if (sampleWedgeMarking(wedge, graph, L1 + clear, yO,
|
||||
halfThick, sa) &&
|
||||
sampleWedgeMarking(wedge, graph, L1 + clear + sw,
|
||||
yO, halfThick, sb)) {
|
||||
/* The bar starts flush at the centerline
|
||||
* strip's outer edge (the centerline painted
|
||||
* by the sibling wedge reaches the clear
|
||||
* boundary at the same d), so the two never
|
||||
* overlap coplanar. */
|
||||
float latA = (h.lanesOut >= 2 || h.lanesIn >= 2) ?
|
||||
1.5f * lineW :
|
||||
0.5f * lineW;
|
||||
float latB = h.lanesIn * lw;
|
||||
latB = std::min(
|
||||
latB,
|
||||
std::min(sa.widthD, sb.widthD) - 0.01f);
|
||||
if (latB > latA + 0.05f) {
|
||||
emitMarkingQuad(out.stopLines, sa, sb,
|
||||
latA, latB, latA, latB);
|
||||
built = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return built;
|
||||
}
|
||||
|
||||
bool buildSegmentMarkings(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph, RoadMarkingBuffers &out)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(segment.nodeId);
|
||||
if (!node)
|
||||
return false;
|
||||
const RoadHalfEdge &he = segment.halfEdge;
|
||||
if (he.lanesIn + he.lanesOut < 1)
|
||||
return false;
|
||||
if (he.edgeIndex < 0 || he.edgeIndex >= (int)graph.edges.size())
|
||||
return false;
|
||||
if (!graph.edgeMarkingsEnabled(graph.edges[(size_t)he.edgeIndex]))
|
||||
return false;
|
||||
|
||||
const RoadConfig &cfg = graph.config;
|
||||
float lw = cfg.laneWidth;
|
||||
float lineW = cfg.markingLineWidth;
|
||||
float inW = he.lanesIn * lw;
|
||||
float outW = he.lanesOut * lw;
|
||||
float L = he.halfLength;
|
||||
float halfThick = std::max(0.01f, cfg.roadThickness) * 0.5f;
|
||||
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
Ogre::Vector3 d = he.direction;
|
||||
Ogre::Vector3 r = roadRightVec(d);
|
||||
|
||||
/* Straight sampler: lat < 0 is the incoming (-r) side. */
|
||||
auto sampleAt = [&](float t, MarkingSample &s) {
|
||||
s.center = O + d * t;
|
||||
s.center.y = halfEdgeHeightAt(he, graph, t) + halfThick +
|
||||
ROAD_MARKING_LIFT;
|
||||
s.latDir = r;
|
||||
s.widthD = std::max(inW, outW);
|
||||
};
|
||||
auto emitSeg = [&](Procedural::TriangleBuffer &buf, float ta, float tb,
|
||||
float lat0, float lat1) {
|
||||
MarkingSample sa, sb;
|
||||
sampleAt(ta, sa);
|
||||
sampleAt(tb, sb);
|
||||
emitMarkingQuad(buf, sa, sb, lat0, lat1, lat0, lat1);
|
||||
};
|
||||
auto line = [&](Procedural::TriangleBuffer &buf, float lat0,
|
||||
float lat1) {
|
||||
int rows = std::max(1, (int)std::ceil(L / ROAD_MARKING_STEP));
|
||||
for (int i = 0; i < rows; ++i)
|
||||
emitSeg(buf, L * (float)i / (float)rows,
|
||||
L * (float)(i + 1) / (float)rows, lat0, lat1);
|
||||
};
|
||||
auto dashed = [&](Procedural::TriangleBuffer &buf, float latC) {
|
||||
float s = 0.0f;
|
||||
while (s < L - 1e-3f) {
|
||||
float e = std::min(s + cfg.markingDashLength, L);
|
||||
int rows = std::max(
|
||||
1, (int)std::ceil((e - s) / ROAD_MARKING_STEP));
|
||||
for (int i = 0; i < rows; ++i)
|
||||
emitSeg(buf, s + (e - s) * (float)i /
|
||||
(float)rows,
|
||||
s + (e - s) * (float)(i + 1) /
|
||||
(float)rows,
|
||||
latC - lineW * 0.5f,
|
||||
latC + lineW * 0.5f);
|
||||
s = e + cfg.markingDashGap;
|
||||
}
|
||||
};
|
||||
|
||||
if (he.lanesOut >= 2 || he.lanesIn >= 2) {
|
||||
float g = lineW * 0.5f;
|
||||
line(out.centerline, -g - lineW, -g);
|
||||
line(out.centerline, g, g + lineW);
|
||||
} else {
|
||||
dashed(out.centerline, 0.0f);
|
||||
}
|
||||
for (int k = 1; k < he.lanesOut; ++k)
|
||||
dashed(out.laneLines, k * lw);
|
||||
for (int k = 1; k < he.lanesIn; ++k)
|
||||
dashed(out.laneLines, -(float)k * lw);
|
||||
/* Solid edge lines at both curbs; dead ends have no stop lines. */
|
||||
if (outW > cfg.markingEdgeInset + lineW)
|
||||
line(out.edgeLines, outW - cfg.markingEdgeInset - lineW,
|
||||
outW - cfg.markingEdgeInset);
|
||||
if (inW > cfg.markingEdgeInset + lineW)
|
||||
line(out.edgeLines, -(inW - cfg.markingEdgeInset),
|
||||
-(inW - cfg.markingEdgeInset - lineW));
|
||||
return true;
|
||||
}
|
||||
|
||||
void buildEdgeDecals(const RoadGraph &graph, size_t edgeIndex,
|
||||
Procedural::TriangleBuffer &out, int ownerNodeId)
|
||||
{
|
||||
if (edgeIndex >= graph.edges.size())
|
||||
return;
|
||||
const RoadEdge &e = graph.edges[edgeIndex];
|
||||
if (e.decals.empty())
|
||||
return;
|
||||
const RoadNode *na = graph.findNodeById(e.nodeA);
|
||||
const RoadNode *nb = graph.findNodeById(e.nodeB);
|
||||
if (!na || !nb)
|
||||
return;
|
||||
|
||||
const RoadConfig &cfg = graph.config;
|
||||
float halfThick = std::max(0.01f, cfg.roadThickness) * 0.5f;
|
||||
Ogre::Vector3 A = na->position, B = nb->position;
|
||||
Ogre::Vector3 dir = B - A;
|
||||
dir.y = 0.0f;
|
||||
float len = dir.normalise();
|
||||
if (len < 1e-3f)
|
||||
return;
|
||||
Ogre::Vector3 r = roadRightVec(dir);
|
||||
float yA = A.y + e.roadLevelA;
|
||||
float yB = B.y + e.roadLevelB;
|
||||
float yMid = 0.5f * (yA + yB);
|
||||
float halfLen = len * 0.5f;
|
||||
|
||||
for (const RoadEdgeDecal &dec : e.decals) {
|
||||
float te = std::max(0.0f, std::min(1.0f, dec.edgeT));
|
||||
if (ownerNodeId >= 0) {
|
||||
int owner = te <= 0.5f ? e.nodeA : e.nodeB;
|
||||
if (owner != ownerNodeId)
|
||||
continue;
|
||||
}
|
||||
float t = te * len;
|
||||
/* Piecewise-linear surface height with the midpoint kink. */
|
||||
float surfY = t <= halfLen ?
|
||||
yA + (yMid - yA) * (t / halfLen) :
|
||||
yMid + (yB - yMid) * ((t - halfLen) / halfLen);
|
||||
Ogre::Vector3 c = A + dir * t + r * dec.lateralOffset;
|
||||
c.y = surfY + halfThick + ROAD_DECAL_LIFT;
|
||||
float hs = dec.size * 0.5f;
|
||||
Ogre::Vector3 p00 = c - dir * hs - r * hs;
|
||||
Ogre::Vector3 p01 = c - dir * hs + r * hs;
|
||||
Ogre::Vector3 p10 = c + dir * hs - r * hs;
|
||||
Ogre::Vector3 p11 = c + dir * hs + r * hs;
|
||||
Ogre::Vector2 uv00(dec.uvX, dec.uvY);
|
||||
Ogre::Vector2 uv01(dec.uvX, dec.uvY + dec.uvH);
|
||||
Ogre::Vector2 uv10(dec.uvX + dec.uvW, dec.uvY);
|
||||
Ogre::Vector2 uv11(dec.uvX + dec.uvW, dec.uvY + dec.uvH);
|
||||
emitTri(out, p00, p10, p11, uv00, uv10, uv11);
|
||||
emitTri(out, p00, p11, p01, uv00, uv11, uv01);
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Template mesh loading
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
@@ -121,6 +121,63 @@ bool buildSegmentApronGeometry(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Road markings and decals (phase 2.5)
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Output buckets for buildWedgeMarkings / buildSegmentMarkings. The
|
||||
* kinds are kept apart so the caller can merge them into one overlay
|
||||
* mesh (same material) while tests can count them separately.
|
||||
*/
|
||||
struct RoadMarkingBuffers {
|
||||
Procedural::TriangleBuffer centerline;
|
||||
Procedural::TriangleBuffer laneLines;
|
||||
Procedural::TriangleBuffer edgeLines;
|
||||
Procedural::TriangleBuffer stopLines;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the painted marking strips of one wedge (phase 2.5) and append
|
||||
* to @p out: centerline (double solid when either direction has 2+
|
||||
* lanes, single dashed otherwise), dashed lane lines between
|
||||
* same-direction lanes, solid edge lines at the curbs and — at junction
|
||||
* nodes (degree > 2) — a stop line on the incoming half of the second
|
||||
* arm (flush at the centerline strip; the arm's longitudinal lines
|
||||
* resume past it so the bar never z-fights them). Junction arms stop
|
||||
* short of the corner by a clear zone; at
|
||||
* 2-neighbor nodes markings follow the corner through. Per-edge
|
||||
* gating via RoadGraph::edgeMarkingsEnabled.
|
||||
*
|
||||
* The strips hover ROAD_MARKING_LIFT above the road top and are purely
|
||||
* visual (no physics/navmesh/compliance contribution).
|
||||
*
|
||||
* @return false when the wedge is degenerate or nothing was emitted.
|
||||
*/
|
||||
bool buildWedgeMarkings(const RoadWedge &wedge, const RoadGraph &graph,
|
||||
RoadMarkingBuffers &out);
|
||||
|
||||
/**
|
||||
* Build the marking strips of a dead-end straight segment and append
|
||||
* to @p out. Same rules as buildWedgeMarkings minus the junction
|
||||
* handling (no clear zone, no stop lines).
|
||||
*/
|
||||
bool buildSegmentMarkings(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph, RoadMarkingBuffers &out);
|
||||
|
||||
/**
|
||||
* Build the decal quads of one edge (phase 2.5) and append to @p out.
|
||||
* Each RoadEdgeDecal becomes one atlas-textured quad hovering just
|
||||
* above the road top (and above the marking strips). Purely visual.
|
||||
*
|
||||
* When @p ownerNodeId is >= 0 only the decals owned by that node are
|
||||
* emitted — a decal on the first half of the edge (edgeT <= 0.5)
|
||||
* belongs to nodeA, the rest to nodeB. RoadSystem uses this so a
|
||||
* cross-page edge does not paint its decals on both pages.
|
||||
*/
|
||||
void buildEdgeDecals(const RoadGraph &graph, size_t edgeIndex,
|
||||
Procedural::TriangleBuffer &out, int ownerNodeId = -1);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Pipeline phases (exposed for testing)
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,254 @@
|
||||
#ifndef EDITSCENE_ROADNETWORKGEN_HPP
|
||||
#define EDITSCENE_ROADNETWORKGEN_HPP
|
||||
#pragma once
|
||||
|
||||
#include "components/RoadGraph.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* Road network generator and graph-level services (taxi demo phase 3).
|
||||
*
|
||||
* Everything here is a pure function of RoadGraph data (plus an optional
|
||||
* terrain height sampler for the generator) — no scene, terrain or Ogre
|
||||
* render state is required, so the whole namespace is exercisable by the
|
||||
* headless road_geometry_overlap_test suite.
|
||||
*
|
||||
* Contents:
|
||||
* - crossing detection/resolution: edges that come within road width
|
||||
* of each other (and within RoadConfig::crossingHeightThreshold in
|
||||
* Y — larger gaps are overpasses) can be turned into proper
|
||||
* junctions by splitting both edges, reusing near end nodes or
|
||||
* collapsing too-short edges;
|
||||
* - lot detection: areas enclosed by road edges (planar faces of the
|
||||
* graph) that no other edge intersects;
|
||||
* - streets: printable/code-accessible names for edge sequences and
|
||||
* street+number lot addressing;
|
||||
* - a deterministic seeded grid generator emitting graphs stamped
|
||||
* with a generatedGroupId (single unsplittable unit).
|
||||
*/
|
||||
namespace RoadNetworkGen {
|
||||
|
||||
/**
|
||||
* One detected crossing between two edges. Endpoint ids are stable
|
||||
* across graph edits (edge vector indices are not, so they are not
|
||||
* stored); @c tA/@c tB are the closest-point parameters along the
|
||||
* stored edge direction (nodeA -> nodeB) at detection time — re-detect
|
||||
* after every graph mutation instead of reusing stale values.
|
||||
*/
|
||||
struct RoadCrossing {
|
||||
int nodeA1 = -1, nodeA2 = -1; /* endpoints of edge A */
|
||||
int nodeB1 = -1, nodeB2 = -1; /* endpoints of edge B */
|
||||
float tA = 0.0f, tB = 0.0f;
|
||||
Ogre::Vector3 pointA = Ogre::Vector3::ZERO; /* on A, surface Y */
|
||||
Ogre::Vector3 pointB = Ogre::Vector3::ZERO; /* on B, surface Y */
|
||||
float xzDistance = 0.0f;
|
||||
float yDifference = 0.0f;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolved half-width of an edge in meters: half the lanes plus the
|
||||
* enabled sidewalks on both sides. Used as the crossing reach.
|
||||
*/
|
||||
float edgeHalfWidth(const RoadGraph &graph, const RoadEdge &edge);
|
||||
|
||||
/** Road surface height at parameter @p t along the edge (node Y plus
|
||||
* the lerped roadLevel). */
|
||||
float edgeSurfaceYAt(const RoadGraph &graph, const RoadEdge &edge,
|
||||
float t);
|
||||
|
||||
/**
|
||||
* Detect all edge pairs that intersect without a junction: XZ distance
|
||||
* between the closest points at most the sum of the edge half-widths
|
||||
* AND Y separation at most RoadConfig::crossingHeightThreshold (a
|
||||
* bigger Y gap is an overpass/underpass and is NOT a crossing). Edge
|
||||
* pairs sharing a node are skipped.
|
||||
*/
|
||||
std::vector<RoadCrossing> findCrossings(const RoadGraph &graph);
|
||||
|
||||
/**
|
||||
* Turn one crossing into a proper junction. For each of the two edges:
|
||||
* an end node within RoadConfig::crossingSnapDistance meters of the
|
||||
* crossing point is reused, an edge too short to split (at most
|
||||
* 2 * ROAD_MIN_EDGE_LENGTH) is collapsed via mergeNodes into its end
|
||||
* nearer to the crossing, anything else is split at the crossing
|
||||
* parameter. The two resulting junction nodes are then merged into
|
||||
* one (placed at the crossing midpoint). Best-effort: returns false
|
||||
* with @p error set when the edges no longer exist.
|
||||
*/
|
||||
bool resolveCrossing(RoadGraph &graph, const RoadCrossing &crossing,
|
||||
std::string *error = nullptr);
|
||||
|
||||
/**
|
||||
* Resolve crossings until none remain (re-detecting after every fix,
|
||||
* one fix per pass). Returns the number of junctions created, or -1
|
||||
* when @p maxPasses was exhausted with crossings left (@p error set).
|
||||
*/
|
||||
int resolveAllCrossings(RoadGraph &graph, std::string *error = nullptr,
|
||||
int maxPasses = 256);
|
||||
|
||||
/**
|
||||
* Structural sanity beyond RoadGraph::validate: no duplicate edge
|
||||
* pairs, no sliver edges below ROAD_MIN_EDGE_LENGTH, no isolated
|
||||
* (degree-0) nodes. validate() itself is NOT run (its wedge-angle
|
||||
* rules reject hand-authored legacy scenes).
|
||||
*/
|
||||
bool checkSane(const RoadGraph &graph, std::string *error = nullptr);
|
||||
|
||||
/**
|
||||
* One enclosed area tracked as a potential building lot: a simple
|
||||
* polygon of road nodes with no road edge crossing its interior, or
|
||||
* (with RoadLotFilter::edgeFallback) a square grown off an edge side
|
||||
* that borders no enclosed lot.
|
||||
*/
|
||||
struct RoadLot {
|
||||
/** Boundary node ids in polygon order (EMPTY for edge-fallback
|
||||
* squares — their corners are not graph nodes). */
|
||||
std::vector<int> nodeIds;
|
||||
/** Boundary positions (same order as nodeIds when non-empty). */
|
||||
std::vector<Ogre::Vector3> polygon;
|
||||
/** Polygon area in square meters (always positive). */
|
||||
double area = 0.0;
|
||||
Ogre::Vector3 centroid = Ogre::Vector3::ZERO;
|
||||
/** True for square lots grown off a free edge side (fallback)
|
||||
* rather than enclosed by road edges. */
|
||||
bool edgeFallback = false;
|
||||
/** Node ids of the source edge (edge-fallback lots only, -1
|
||||
* otherwise): the square was grown off one side of this edge, so
|
||||
* it is the lot's road frontage — usable for street lookup and
|
||||
* frontage orientation even though nodeIds stays empty. */
|
||||
int sourceEdgeA = -1;
|
||||
int sourceEdgeB = -1;
|
||||
};
|
||||
|
||||
/** Lot acceptance rules for findLots(). */
|
||||
struct RoadLotFilter {
|
||||
/** A lot must fit a square of this side: both XZ bounding-box
|
||||
* extents AND the polygon area (side squared) must reach it.
|
||||
* Edge-fallback squares use the edge length as their side, so
|
||||
* shorter edges spawn no fallback lot. */
|
||||
float minLotSize = 45.0f;
|
||||
/** Enclosed faces bounded by more edges than this are not simple
|
||||
* building lots and are discarded. */
|
||||
int maxBoundaryEdges = 8;
|
||||
/** Grow square fallback lots off edge sides that border no
|
||||
* accepted enclosed lot. */
|
||||
bool edgeFallback = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enumerate the potential building lots of the road graph. Primary
|
||||
* lots are the enclosed faces (planar face walk: sharpest right turn
|
||||
* at each node, U-turn edge excluded). A face is REJECTED when its
|
||||
* boundary walk traverses an edge twice or revisits a node (a
|
||||
* spur/cul-de-sac pokes into it), when another graph node lies
|
||||
* strictly inside it, when it is bounded by more than
|
||||
* RoadLotFilter::maxBoundaryEdges edges, or when it cannot fit a
|
||||
* RoadLotFilter::minLotSize square. With
|
||||
* RoadLotFilter::edgeFallback, every edge side that borders no
|
||||
* accepted face spawns a square lot (side = edge length, so edges
|
||||
* shorter than minLotSize spawn none) provided the square stays clear
|
||||
* of the graph (no node strictly inside, no edge crossing its
|
||||
* interior) and does not overlap an already-accepted lot. Works for
|
||||
* manual and procedural networks alike; run resolveAllCrossings first
|
||||
* when edges may cross.
|
||||
*/
|
||||
std::vector<RoadLot> findLots(const RoadGraph &graph,
|
||||
const RoadLotFilter &filter = RoadLotFilter());
|
||||
|
||||
/**
|
||||
* Assign @p name to the given edge set (normalized endpoint pairs).
|
||||
* Edges are removed from any street already containing them; an
|
||||
* existing street with the same name is redefined. Returns the street
|
||||
* index, or -1 when any referenced edge does not exist.
|
||||
*/
|
||||
int nameStreet(RoadGraph &graph, const std::string &name,
|
||||
const std::vector<std::pair<int, int> > &edges);
|
||||
|
||||
/** Look up the street containing the edge (nodeA, nodeB), or nullptr. */
|
||||
const RoadStreet *findStreetForEdge(const RoadGraph &graph, int nodeA,
|
||||
int nodeB);
|
||||
|
||||
/**
|
||||
* Build display addresses for lots: each lot is assigned to its
|
||||
* nearest street; lots on one street are numbered sequentially along
|
||||
* the street's direction; lots with no street get "Lot <lotIndex+1>".
|
||||
* Returns one address string per input lot.
|
||||
*/
|
||||
std::vector<std::string>
|
||||
assignLotAddresses(const RoadGraph &graph, const std::vector<RoadLot> &lots);
|
||||
|
||||
/** Grid generator parameters (all deterministic under @p seed). */
|
||||
struct GridParams {
|
||||
/** XZ origin of block corner (0, 0); Y fallback when no sampler. */
|
||||
Ogre::Vector3 origin = Ogre::Vector3::ZERO;
|
||||
int blocksX = 3, blocksZ = 3;
|
||||
float blockSizeX = 100.0f, blockSizeZ = 100.0f;
|
||||
/** Max XZ jitter of interior intersections (boundary stays put). */
|
||||
float jitter = 0.0f;
|
||||
uint32_t seed = 1;
|
||||
/** Stamp for generated nodes; 0 = auto-assign (max existing + 1). */
|
||||
uint32_t groupId = 0;
|
||||
/** Reject nodes whose sampled height is below waterLevel. */
|
||||
bool avoidWater = true;
|
||||
float waterLevel = -0.1f;
|
||||
/** Skip edges steeper than this rise/run (0 disables the check). */
|
||||
float maxSlope = 0.35f;
|
||||
/** Run resolveAllCrossings after generating. */
|
||||
bool runCrossingResolution = true;
|
||||
};
|
||||
|
||||
/** Terrain height sampler: render-space (x, z) -> surface Y. */
|
||||
typedef std::function<float(float x, float z)> HeightSampler;
|
||||
|
||||
/**
|
||||
* Emit a grid road network into the graph: (blocksX+1) x (blocksZ+1)
|
||||
* intersections, seeded jitter on interior ones, edges along the grid
|
||||
* lines. With a @p sampleHeight sampler, nodes below waterLevel are
|
||||
* rejected (their edges go with them) and edges steeper than maxSlope
|
||||
* are skipped; without a sampler every node sits at origin.y. Every
|
||||
* created node is stamped with the returned generatedGroupId.
|
||||
* Returns the group id used, or 0 on a fatal parameter error
|
||||
* (@p error set).
|
||||
*/
|
||||
uint32_t generateGrid(RoadGraph &graph, const GridParams ¶ms,
|
||||
const HeightSampler &sampleHeight,
|
||||
std::string *error = nullptr);
|
||||
|
||||
/**
|
||||
* Connect a generated group to a manual node "as if the group were a
|
||||
* road node": the closest group node is picked automatically and
|
||||
* connected via RoadGraph::connectNodes (page-boundary splits when
|
||||
* @p worldSize > 0), then crossings are resolved. Returns the first
|
||||
* new edge index, or -1 on failure (@p error set).
|
||||
*/
|
||||
int connectGroupToNode(RoadGraph &graph, uint32_t groupId, int nodeId,
|
||||
float worldSize = 0.0f, std::string *error = nullptr);
|
||||
|
||||
/** Remove every node (and cascading edges) of a generated group. */
|
||||
bool removeGroup(RoadGraph &graph, uint32_t groupId);
|
||||
|
||||
/** Largest generatedGroupId in the graph + 1 (1 when none). */
|
||||
uint32_t nextGroupId(const RoadGraph &graph);
|
||||
|
||||
/**
|
||||
* Streets serialization (shared by the region store, the scene
|
||||
* serializer and the headless tests). Shape:
|
||||
* `{ "streets": [ { "name": "...", "edges": [[a, b], ...] } ] }`;
|
||||
* fromJson tolerates a missing/empty array. With @p dropMissing
|
||||
* (default) entries whose edges no longer exist are dropped; pass
|
||||
* false when loading a streaming terrain whose pages are not all in
|
||||
* the graph yet (stale ids are harmless — lookups match existing
|
||||
* edges only).
|
||||
*/
|
||||
void streetsToJson(const RoadGraph &graph, nlohmann::json &out);
|
||||
void streetsFromJson(RoadGraph &graph, const nlohmann::json &j,
|
||||
bool dropMissing = true);
|
||||
|
||||
} /* namespace RoadNetworkGen */
|
||||
|
||||
#endif /* EDITSCENE_ROADNETWORKGEN_HPP */
|
||||
@@ -0,0 +1,382 @@
|
||||
#include "TownGen.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <random>
|
||||
|
||||
namespace TownGen {
|
||||
|
||||
const DistrictParams &districtParams(District d)
|
||||
{
|
||||
static const DistrictParams presets[(size_t)District::COUNT] = {
|
||||
/* Village: small single-storey houses. */
|
||||
{ "Village", 8.0f, 12.0f, 1, 1, 0.8f, 4.0f,
|
||||
{ 0.85f, 0.75f, 0.60f } },
|
||||
/* Town: row houses and small apartment blocks. */
|
||||
{ "Town", 10.0f, 16.0f, 1, 2, 0.9f, 4.0f,
|
||||
{ 0.80f, 0.70f, 0.65f } },
|
||||
/* City residential: mid-rise apartment buildings. */
|
||||
{ "City Residential", 12.0f, 20.0f, 2, 4, 0.95f, 3.0f,
|
||||
{ 0.70f, 0.70f, 0.75f } },
|
||||
/* City center: tall blocks on tight lots. */
|
||||
{ "City Center", 16.0f, 30.0f, 4, 8, 1.0f, 2.0f,
|
||||
{ 0.65f, 0.68f, 0.72f } },
|
||||
};
|
||||
size_t idx = (size_t)d;
|
||||
if (idx >= (size_t)District::COUNT)
|
||||
idx = 0;
|
||||
return presets[idx];
|
||||
}
|
||||
|
||||
/** Stairs appear when the door-side ground is this far below the
|
||||
* floor (meters). */
|
||||
static const float STAIRS_THRESHOLD = 0.35f;
|
||||
|
||||
/** Extra gap kept between a building wall and the road body on top
|
||||
* of the road half-width (meters). */
|
||||
static const float ROAD_CLEARANCE_MARGIN = 0.3f;
|
||||
|
||||
/** Signed area of an XZ polygon (shoelace; sign follows winding). */
|
||||
static float polyAreaXZ(const std::vector<Ogre::Vector3> &poly)
|
||||
{
|
||||
float a = 0.0f;
|
||||
for (size_t i = 0; i < poly.size(); ++i) {
|
||||
const Ogre::Vector3 &p = poly[i];
|
||||
const Ogre::Vector3 &q = poly[(i + 1) % poly.size()];
|
||||
a += p.x * q.z - q.x * p.z;
|
||||
}
|
||||
return a * 0.5f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Half-extent of the road body on the lot side of a graph edge:
|
||||
* all lanes plus the lot side's sidewalk and apron, plus a small
|
||||
* margin.
|
||||
*/
|
||||
static float roadClearance(const RoadGraph &graph, const RoadEdge &e,
|
||||
bool lotOnLeft)
|
||||
{
|
||||
int la, lb;
|
||||
graph.resolveLaneCounts(e, e.nodeA, la, lb);
|
||||
const RoadConfig &cfg = graph.config;
|
||||
float w = (la + lb) * cfg.laneWidth * 0.5f;
|
||||
if (graph.edgeSidewalkEnabled(e, lotOnLeft))
|
||||
w += cfg.sidewalkWidth;
|
||||
if (cfg.apronEnabled)
|
||||
w += cfg.apronWidth;
|
||||
return w + ROAD_CLEARANCE_MARGIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inset a lot polygon off the roads bounding it. findLots() lot
|
||||
* polygons run centerline-to-centerline, so a building placed only
|
||||
* the district setback inside the raw polygon would stand on the
|
||||
* road itself. Every boundary edge that matches a graph edge
|
||||
* (geometric endpoint match, either direction — covers enclosed
|
||||
* faces and edge-fallback source edges alike) is offset inward by
|
||||
* the road half-extent on the lot's side (lanes + sidewalk + apron
|
||||
* + margin); unmatched edges keep a zero offset. Vertices of the
|
||||
* inset polygon are the intersections of consecutive offset lines.
|
||||
* Returns false when the inset collapses (degenerate edge or a
|
||||
* vanished/reversed polygon), in which case the lot holds nothing
|
||||
* buildable and should be skipped.
|
||||
*/
|
||||
static bool shrinkLotOffRoads(const RoadGraph &graph,
|
||||
const std::vector<Ogre::Vector3> &poly,
|
||||
const Ogre::Vector3 ¢roid,
|
||||
std::vector<Ogre::Vector3> &out)
|
||||
{
|
||||
size_t n = poly.size();
|
||||
out.clear();
|
||||
if (n < 3)
|
||||
return false;
|
||||
|
||||
std::vector<float> inset(n, 0.0f);
|
||||
std::vector<Ogre::Vector3> dir(n), inward(n);
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
size_t j = (i + 1) % n;
|
||||
Ogre::Vector3 d = poly[j] - poly[i];
|
||||
d.y = 0.0f;
|
||||
if (d.normalise() < 0.001f)
|
||||
return false;
|
||||
dir[i] = d;
|
||||
/* Inward = the side normal pointing at the centroid
|
||||
* (winding-independent — fallback lots may be
|
||||
* reversed). */
|
||||
Ogre::Vector3 nrm(-d.z, 0.0f, d.x);
|
||||
Ogre::Vector3 mid = (poly[i] + poly[j]) * 0.5f;
|
||||
if (nrm.dotProduct(centroid - mid) < 0.0f)
|
||||
nrm = -nrm;
|
||||
inward[i] = nrm;
|
||||
|
||||
for (const RoadEdge &e : graph.edges) {
|
||||
const RoadNode *na = graph.findNodeById(e.nodeA);
|
||||
const RoadNode *nb = graph.findNodeById(e.nodeB);
|
||||
if (!na || !nb)
|
||||
continue;
|
||||
bool fwd = poly[i].distance(na->position) < 0.01f &&
|
||||
poly[j].distance(nb->position) < 0.01f;
|
||||
bool rev = poly[i].distance(nb->position) < 0.01f &&
|
||||
poly[j].distance(na->position) < 0.01f;
|
||||
if (!fwd && !rev)
|
||||
continue;
|
||||
/* Graph "left" of A->B is UNIT_Y x dir. */
|
||||
Ogre::Vector3 gdir = nb->position - na->position;
|
||||
gdir.y = 0.0f;
|
||||
gdir.normalise();
|
||||
Ogre::Vector3 gleft(gdir.z, 0.0f, -gdir.x);
|
||||
bool lotOnLeft = inward[i].dotProduct(gleft) > 0.0f;
|
||||
inset[i] = roadClearance(graph, e, lotOnLeft);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
size_t prev = (i + n - 1) % n;
|
||||
const Ogre::Vector3 &d1 = dir[prev];
|
||||
const Ogre::Vector3 &d2 = dir[i];
|
||||
Ogre::Vector3 p1 = poly[prev] + inward[prev] * inset[prev];
|
||||
Ogre::Vector3 p2 = poly[i] + inward[i] * inset[i];
|
||||
float denom = d1.x * d2.z - d2.x * d1.z;
|
||||
Ogre::Vector3 vtx;
|
||||
if (std::fabs(denom) < 1.0e-6f) {
|
||||
/* Near-parallel: average the two offset
|
||||
* positions of the shared vertex. */
|
||||
vtx = (poly[i] + inward[prev] * inset[prev] +
|
||||
poly[i] + inward[i] * inset[i]) * 0.5f;
|
||||
} else {
|
||||
/* Solve p1 + t*d1 = p2 + s*d2 in XZ. */
|
||||
float dx = p2.x - p1.x;
|
||||
float dz = p2.z - p1.z;
|
||||
float t = (dx * d2.z - dz * d2.x) / denom;
|
||||
vtx = p1 + d1 * t;
|
||||
}
|
||||
vtx.y = poly[i].y;
|
||||
out.push_back(vtx);
|
||||
}
|
||||
|
||||
return std::fabs(polyAreaXZ(out)) > 0.01f;
|
||||
}
|
||||
|
||||
static float sampleY(const RoadNetworkGen::HeightSampler &sampleHeight,
|
||||
float x, float z, float fallback)
|
||||
{
|
||||
return sampleHeight ? sampleHeight(x, z) : fallback;
|
||||
}
|
||||
|
||||
TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
const RoadNetworkGen::HeightSampler &sampleHeight)
|
||||
{
|
||||
TownResult result;
|
||||
const DistrictParams &dp = districtParams(params.district);
|
||||
std::vector<RoadNetworkGen::RoadLot> lots =
|
||||
RoadNetworkGen::findLots(graph);
|
||||
|
||||
std::mt19937 rng(params.seed);
|
||||
std::uniform_real_distribution<float> unit(0.0f, 1.0f);
|
||||
float density =
|
||||
std::min(1.0f, dp.density * std::max(0.0f, params.densityScale));
|
||||
std::map<std::string, int> streetNumbers;
|
||||
|
||||
for (size_t li = 0; li < lots.size(); ++li) {
|
||||
const RoadNetworkGen::RoadLot &lot = lots[li];
|
||||
size_t n = lot.polygon.size();
|
||||
if (n < 3)
|
||||
continue;
|
||||
|
||||
/* Frontage ranking: an edge on a named street beats an
|
||||
* edge-fallback lot's source edge, which beats the plain
|
||||
* longest edge. Fallback lots have no nodeIds and their
|
||||
* polygon may be reversed, so the source edge is matched
|
||||
* geometrically against both endpoints. */
|
||||
Ogre::Vector3 srcA, srcB;
|
||||
bool hasSource = false;
|
||||
if (lot.edgeFallback && lot.sourceEdgeA >= 0 &&
|
||||
lot.sourceEdgeB >= 0) {
|
||||
const RoadNode *sa = graph.findNodeById(lot.sourceEdgeA);
|
||||
const RoadNode *sb = graph.findNodeById(lot.sourceEdgeB);
|
||||
if (sa && sb) {
|
||||
srcA = sa->position;
|
||||
srcB = sb->position;
|
||||
hasSource = true;
|
||||
}
|
||||
}
|
||||
int best = 0;
|
||||
float bestLen = -1.0f;
|
||||
int bestTier = -1;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
size_t j = (i + 1) % n;
|
||||
float len = (lot.polygon[j] - lot.polygon[i]).length();
|
||||
int tier = 0;
|
||||
if (!lot.nodeIds.empty() &&
|
||||
RoadNetworkGen::findStreetForEdge(
|
||||
graph, lot.nodeIds[i],
|
||||
lot.nodeIds[j]) != nullptr) {
|
||||
tier = 2;
|
||||
} else if (hasSource &&
|
||||
((lot.polygon[i].distance(srcA) < 0.01f &&
|
||||
lot.polygon[j].distance(srcB) < 0.01f) ||
|
||||
(lot.polygon[i].distance(srcB) < 0.01f &&
|
||||
lot.polygon[j].distance(srcA) < 0.01f))) {
|
||||
tier = 1;
|
||||
}
|
||||
if (tier > bestTier ||
|
||||
(tier == bestTier && len > bestLen)) {
|
||||
best = (int)i;
|
||||
bestLen = len;
|
||||
bestTier = tier;
|
||||
}
|
||||
}
|
||||
|
||||
Ogre::Vector3 p0 = lot.polygon[best];
|
||||
Ogre::Vector3 p1 = lot.polygon[(best + 1) % n];
|
||||
Ogre::Vector3 frontMid = (p0 + p1) * 0.5f;
|
||||
Ogre::Vector3 u = p1 - p0;
|
||||
u.y = 0.0f;
|
||||
if (u.normalise() < 0.001f)
|
||||
continue;
|
||||
/* Inward direction: from the frontage towards the lot
|
||||
* centroid (winding-independent). */
|
||||
Ogre::Vector3 v = lot.centroid - frontMid;
|
||||
v.y = 0.0f;
|
||||
if (v.normalise() < 0.001f)
|
||||
continue;
|
||||
|
||||
/* Inset the lot off the bounding roads — raw lot
|
||||
* polygons run centerline-to-centerline, so buildings
|
||||
* must keep the road half-extent (lanes + sidewalk +
|
||||
* apron) plus a margin off the polygon boundary. */
|
||||
std::vector<Ogre::Vector3> buildable;
|
||||
if (!shrinkLotOffRoads(graph, lot.polygon, lot.centroid,
|
||||
buildable)) {
|
||||
result.skippedTooSmall++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Lot bounds in the (u, v) frame around frontMid,
|
||||
* measured on the inset polygon; vMin is the buildable
|
||||
* front line (the frontage pushed off the road). */
|
||||
float uMin = 1.0e30f, uMax = -1.0e30f;
|
||||
float vMin = 1.0e30f, vMax = -1.0e30f;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
Ogre::Vector3 d = buildable[i] - frontMid;
|
||||
float du = d.dotProduct(u);
|
||||
float dv = d.dotProduct(v);
|
||||
uMin = std::min(uMin, du);
|
||||
uMax = std::max(uMax, du);
|
||||
vMin = std::min(vMin, dv);
|
||||
vMax = std::max(vMax, dv);
|
||||
}
|
||||
|
||||
float availW = (uMax - uMin) - 2.0f * dp.setback;
|
||||
float availD = (vMax - vMin) - dp.setback;
|
||||
if (availW < dp.minFootprint || availD < dp.minFootprint) {
|
||||
result.skippedTooSmall++;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Seeded draws: density first, then width, depth,
|
||||
* floors — fixed order keeps the sequence deterministic
|
||||
* per lot index. */
|
||||
if (unit(rng) > density) {
|
||||
result.skippedDensity++;
|
||||
continue;
|
||||
}
|
||||
float width = dp.minFootprint +
|
||||
unit(rng) * (dp.maxFootprint - dp.minFootprint);
|
||||
float depth = dp.minFootprint +
|
||||
unit(rng) * (dp.maxFootprint - dp.minFootprint);
|
||||
width = std::min(width, availW);
|
||||
depth = std::min(depth, availD);
|
||||
int floors = dp.minFloors;
|
||||
if (dp.maxFloors > dp.minFloors)
|
||||
floors += (int)(unit(rng) *
|
||||
(float)(dp.maxFloors - dp.minFloors + 1));
|
||||
floors = std::min(floors, dp.maxFloors);
|
||||
|
||||
float cu = (uMin + uMax) * 0.5f;
|
||||
float halfW = width * 0.5f;
|
||||
cu = std::max(uMin + dp.setback + halfW,
|
||||
std::min(cu, uMax - dp.setback - halfW));
|
||||
float cv = vMin + dp.setback + depth * 0.5f;
|
||||
|
||||
Ogre::Vector3 center2d = frontMid + u * cu + v * cv;
|
||||
|
||||
/* Foundation block: the floor sits on the highest
|
||||
* terrain corner, the block reaches below the lowest. */
|
||||
float corners[4][2] = {
|
||||
{ -halfW, -depth * 0.5f },
|
||||
{ halfW, -depth * 0.5f },
|
||||
{ halfW, depth * 0.5f },
|
||||
{ -halfW, depth * 0.5f },
|
||||
};
|
||||
float floorY = -1.0e30f;
|
||||
float minGroundY = 1.0e30f;
|
||||
for (auto &c : corners) {
|
||||
Ogre::Vector3 w =
|
||||
center2d + u * c[0] + v * c[1];
|
||||
float y = sampleY(sampleHeight, w.x, w.z,
|
||||
lot.centroid.y);
|
||||
floorY = std::max(floorY, y);
|
||||
minGroundY = std::min(minGroundY, y);
|
||||
}
|
||||
|
||||
if (floorY < params.waterLevel) {
|
||||
result.skippedWater++;
|
||||
continue;
|
||||
}
|
||||
|
||||
TownBuilding b;
|
||||
b.lotIndex = (int)li;
|
||||
b.center = center2d;
|
||||
b.center.y = floorY;
|
||||
b.yaw = std::atan2(-v.x, -v.z);
|
||||
b.width = width;
|
||||
b.depth = depth;
|
||||
b.floors = floors;
|
||||
b.floorY = floorY;
|
||||
b.minGroundY = minGroundY;
|
||||
|
||||
/* Door ground one meter in front of the front face. */
|
||||
Ogre::Vector3 doorPos =
|
||||
center2d - v * (depth * 0.5f + 1.0f);
|
||||
float doorGround =
|
||||
sampleY(sampleHeight, doorPos.x, doorPos.z,
|
||||
lot.centroid.y);
|
||||
b.stairsDrop = floorY - doorGround;
|
||||
b.stairs = b.stairsDrop > STAIRS_THRESHOLD;
|
||||
|
||||
/* Address from the frontage street, numbered
|
||||
* sequentially per street in lot order. */
|
||||
const RoadStreet *street = nullptr;
|
||||
if (!lot.nodeIds.empty())
|
||||
street = RoadNetworkGen::findStreetForEdge(
|
||||
graph, lot.nodeIds[best],
|
||||
lot.nodeIds[(best + 1) % n]);
|
||||
else if (lot.sourceEdgeA >= 0 && lot.sourceEdgeB >= 0)
|
||||
street = RoadNetworkGen::findStreetForEdge(
|
||||
graph, lot.sourceEdgeA, lot.sourceEdgeB);
|
||||
if (street) {
|
||||
int num = ++streetNumbers[street->name];
|
||||
b.address = street->name + " " + std::to_string(num);
|
||||
} else {
|
||||
b.address = "Lot " + std::to_string(li + 1);
|
||||
}
|
||||
|
||||
TownPoi poi;
|
||||
poi.name = b.address + " pickup";
|
||||
poi.position = center2d - v * (depth * 0.5f + 3.0f);
|
||||
poi.position.y =
|
||||
sampleY(sampleHeight, poi.position.x, poi.position.z,
|
||||
lot.centroid.y);
|
||||
poi.building = (int)result.buildings.size();
|
||||
|
||||
result.buildings.push_back(b);
|
||||
result.pois.push_back(poi);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} /* namespace TownGen */
|
||||
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <Ogre.h>
|
||||
|
||||
#include "../components/RoadGraph.hpp"
|
||||
#include "RoadNetworkGen.hpp"
|
||||
|
||||
/**
|
||||
* Procedural town generation on top of a road network (demo-taxi
|
||||
* phase 4). Pure functions of RoadGraph/RoadLot data plus a terrain
|
||||
* height sampler — fully exercisable headlessly.
|
||||
*
|
||||
* generateTown() consumes the lots of RoadNetworkGen::findLots()
|
||||
* (enclosed faces + edge-fallback squares) and places one building
|
||||
* per suitable lot: oriented to the frontage edge (the boundary edge
|
||||
* on a named street, longest first), sized by the district preset,
|
||||
* seated on a foundation block (floor at the highest terrain corner
|
||||
* of the footprint, foundation reaching below the lowest corner)
|
||||
* with entrance stairs when the door-side ground drops below the
|
||||
* floor. Deterministic under TownParams::seed.
|
||||
*/
|
||||
namespace TownGen {
|
||||
|
||||
/** District zoning presets driving building size, height, density. */
|
||||
enum class District {
|
||||
Village = 0,
|
||||
Town,
|
||||
CityResidential,
|
||||
CityCenter,
|
||||
COUNT
|
||||
};
|
||||
|
||||
struct DistrictParams {
|
||||
const char *name;
|
||||
/** Building footprint side range in meters. */
|
||||
float minFootprint;
|
||||
float maxFootprint;
|
||||
int minFloors;
|
||||
int maxFloors;
|
||||
/** Fraction of suitable lots that get a building (0..1). */
|
||||
float density;
|
||||
/** Min distance from the lot boundary to the building walls. */
|
||||
float setback;
|
||||
/** Wall tint (linear RGB). */
|
||||
float wallColor[3];
|
||||
};
|
||||
|
||||
const DistrictParams &districtParams(District d);
|
||||
|
||||
struct TownParams {
|
||||
District district = District::Village;
|
||||
uint32_t seed = 1;
|
||||
/** Multiplier on the district density (clamped to 1). */
|
||||
float densityScale = 1.0f;
|
||||
/** Lots whose floor level is below this are skipped (water). */
|
||||
float waterLevel = -1.0e30f;
|
||||
};
|
||||
|
||||
struct TownBuilding {
|
||||
/** Index into the findLots() output this building sits on. */
|
||||
int lotIndex = -1;
|
||||
/** Building center at floor level (render/world XZ of the
|
||||
* caller's coordinate space). */
|
||||
Ogre::Vector3 center = Ogre::Vector3::ZERO;
|
||||
/** Yaw (radians around +Y); the building's local +Z faces the
|
||||
* street. */
|
||||
float yaw = 0.0f;
|
||||
/** Footprint in meters: X along the street, Z into the lot. */
|
||||
float width = 0.0f;
|
||||
float depth = 0.0f;
|
||||
int floors = 1;
|
||||
/** Floor level: the highest terrain corner of the footprint. */
|
||||
float floorY = 0.0f;
|
||||
/** Lowest terrain corner of the footprint. */
|
||||
float minGroundY = 0.0f;
|
||||
/** Entrance stairs: the door-side ground is below the floor. */
|
||||
bool stairs = false;
|
||||
/** floorY - ground at the door (when stairs). */
|
||||
float stairsDrop = 0.0f;
|
||||
/** "<street> <n>" or "Lot <lotIndex+1>". */
|
||||
std::string address;
|
||||
};
|
||||
|
||||
/** Named curb point in front of a building's door (pickup/dropoff,
|
||||
* pedestrian origin/destination). */
|
||||
struct TownPoi {
|
||||
std::string name; /* "<address> pickup" */
|
||||
Ogre::Vector3 position = Ogre::Vector3::ZERO;
|
||||
int building = -1; /* index into buildings */
|
||||
};
|
||||
|
||||
struct TownResult {
|
||||
std::vector<TownBuilding> buildings;
|
||||
std::vector<TownPoi> pois;
|
||||
/** Suitable-but-empty lots (density roll). */
|
||||
int skippedDensity = 0;
|
||||
/** Lots too small for the district footprint. */
|
||||
int skippedTooSmall = 0;
|
||||
/** Lots below TownParams::waterLevel. */
|
||||
int skippedWater = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Place buildings on the lots of the road graph. Deterministic:
|
||||
* lots are processed in findLots() order and every random draw (one
|
||||
* density roll, then width, depth, floors per built lot) comes from
|
||||
* one mt19937 seeded with TownParams::seed, so the same graph and
|
||||
* params always yield the same town.
|
||||
*
|
||||
* A lot is skipped when its usable rectangle (lot bounds inset by the
|
||||
* district setback) cannot fit the district's minimum footprint, or
|
||||
* when its floor level is below waterLevel. The building's frontage
|
||||
* edge is the longest boundary edge on a named street (longest
|
||||
* boundary edge when the lot touches no street); the address numbers
|
||||
* buildings sequentially per street in lot order.
|
||||
*/
|
||||
TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
const RoadNetworkGen::HeightSampler &sampleHeight);
|
||||
|
||||
} /* namespace TownGen */
|
||||
@@ -38,15 +38,20 @@ AnimationTreeSystem::AnimationTreeSystem(flecs::world &world,
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
m_world.observer<AnimationTreeComponent>("AnimationTreeCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, AnimationTreeComponent &) {
|
||||
teardownEntity(e);
|
||||
});
|
||||
m_cleanupObserver =
|
||||
m_world.observer<AnimationTreeComponent>(
|
||||
"AnimationTreeCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e,
|
||||
AnimationTreeComponent &) {
|
||||
teardownEntity(e);
|
||||
});
|
||||
}
|
||||
|
||||
AnimationTreeSystem::~AnimationTreeSystem()
|
||||
{
|
||||
if (m_cleanupObserver.is_alive())
|
||||
m_cleanupObserver.destruct();
|
||||
std::vector<flecs::entity_t> toRemove;
|
||||
for (auto &pair : m_states)
|
||||
toRemove.push_back(pair.first);
|
||||
|
||||
@@ -139,6 +139,9 @@ private:
|
||||
findAnimationNode(const AnimationTreeNode &stateNode) const;
|
||||
|
||||
flecs::world &m_world;
|
||||
/* Kept so the destructor can unregister it: the observer must not
|
||||
* fire into a destroyed system during world teardown (ecs_fini). */
|
||||
flecs::entity m_cleanupObserver;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
bool m_initialized = false;
|
||||
|
||||
|
||||
@@ -689,6 +689,11 @@ static void genPlane(float sizeX, float sizeY, const Ogre::Vector3 &normal,
|
||||
Procedural::TriangleBuffer &tb,
|
||||
const Ogre::Vector3 &origin)
|
||||
{
|
||||
/* Degenerate trim pieces (e.g. the top frame of a doorway when
|
||||
* cellHeight leaves no room above the door) have no visible area;
|
||||
* skip them instead of throwing in the plane generator. */
|
||||
if (sizeX <= 0.0001f || sizeY <= 0.0001f)
|
||||
return;
|
||||
// nsegY is based on the height (sizeX in the original, which is vertical)
|
||||
int nsegY = (sizeX < 1.0f) ? 1 : std::max(1, (int)(sizeX / 2.0f));
|
||||
Procedural::PlaneGenerator()
|
||||
@@ -1192,7 +1197,12 @@ void CellGridSystem::buildDoors(const CellGridComponent &grid,
|
||||
cell.hasFlag(CellFlags::DoorZPos)) {
|
||||
float sideWidth = (2.0f * hScale - doorWidth) / 2.0f;
|
||||
float moffset = (2.0f * hScale - sideWidth) / 2.0f;
|
||||
float doorHeight = doorHeightBase - solidExtOffset;
|
||||
/* Clamp to the wall height: with cellHeight <=
|
||||
* doorHeightBase (e.g. the 3 m town cells) the door
|
||||
* fills the wall and no top frame is emitted. */
|
||||
float doorHeight = std::min(doorHeightBase -
|
||||
solidExtOffset,
|
||||
solidExtHeight);
|
||||
float doorTopHeight = solidExtHeight - doorHeight;
|
||||
float baseOffsetY = solidExtOffset;
|
||||
|
||||
@@ -1316,7 +1326,9 @@ void CellGridSystem::buildDoors(const CellGridComponent &grid,
|
||||
(2.0f * hScale - 0.2f - doorWidth) / 2.0f;
|
||||
float moffset =
|
||||
(2.0f * hScale - 0.2f - sideWidth) / 2.0f;
|
||||
float doorHeight = doorHeightBase - solidIntOffset;
|
||||
float doorHeight = std::min(doorHeightBase -
|
||||
solidIntOffset,
|
||||
solidIntHeight);
|
||||
float doorTopHeight = solidIntHeight - doorHeight;
|
||||
float baseOffsetY = solidIntOffset;
|
||||
|
||||
|
||||
@@ -33,20 +33,30 @@ CharacterSlotSystem::CharacterSlotSystem(flecs::world &world,
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
ms_singleton = this;
|
||||
m_world.observer<CharacterIdentityComponent>("CharacterSlotsBuild")
|
||||
.event(flecs::OnAdd)
|
||||
.each([this](flecs::entity e, CharacterIdentityComponent &) {
|
||||
markEntityDirty(e.id());
|
||||
});
|
||||
m_world.observer<CharacterIdentityComponent>("CharacterSlotsCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, CharacterIdentityComponent &) {
|
||||
destroyCharacterParts(e);
|
||||
});
|
||||
m_buildObserver =
|
||||
m_world.observer<CharacterIdentityComponent>(
|
||||
"CharacterSlotsBuild")
|
||||
.event(flecs::OnAdd)
|
||||
.each([this](flecs::entity e,
|
||||
CharacterIdentityComponent &) {
|
||||
markEntityDirty(e.id());
|
||||
});
|
||||
m_cleanupObserver =
|
||||
m_world.observer<CharacterIdentityComponent>(
|
||||
"CharacterSlotsCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e,
|
||||
CharacterIdentityComponent &) {
|
||||
destroyCharacterParts(e);
|
||||
});
|
||||
}
|
||||
|
||||
CharacterSlotSystem::~CharacterSlotSystem()
|
||||
{
|
||||
if (m_buildObserver.is_alive())
|
||||
m_buildObserver.destruct();
|
||||
if (m_cleanupObserver.is_alive())
|
||||
m_cleanupObserver.destruct();
|
||||
std::vector<flecs::entity_t> toRemove;
|
||||
for (auto &pair : m_entities)
|
||||
toRemove.push_back(pair.first);
|
||||
|
||||
@@ -108,6 +108,10 @@ private:
|
||||
const nlohmann::json *entry);
|
||||
|
||||
flecs::world &m_world;
|
||||
/* Kept so the destructor can unregister them: the observers must
|
||||
* not fire into a destroyed system during world teardown. */
|
||||
flecs::entity m_buildObserver;
|
||||
flecs::entity m_cleanupObserver;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
bool m_initialized = false;
|
||||
|
||||
|
||||
@@ -21,15 +21,20 @@ CharacterSpawnerSystem::CharacterSpawnerSystem(flecs::world &world,
|
||||
, m_uiSystem(uiSystem)
|
||||
, m_query(world.query<CharacterSpawnerComponent, TransformComponent>())
|
||||
{
|
||||
m_world.observer<CharacterSpawnerComponent>("CharacterSpawnerCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, CharacterSpawnerComponent &) {
|
||||
despawn(e);
|
||||
});
|
||||
m_cleanupObserver =
|
||||
m_world.observer<CharacterSpawnerComponent>(
|
||||
"CharacterSpawnerCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e,
|
||||
CharacterSpawnerComponent &) {
|
||||
despawn(e);
|
||||
});
|
||||
}
|
||||
|
||||
CharacterSpawnerSystem::~CharacterSpawnerSystem()
|
||||
{
|
||||
if (m_cleanupObserver.is_alive())
|
||||
m_cleanupObserver.destruct();
|
||||
std::vector<flecs::entity_t> ids;
|
||||
ids.reserve(m_spawnedEntities.size());
|
||||
for (const auto &pair : m_spawnedEntities)
|
||||
|
||||
@@ -94,6 +94,9 @@ private:
|
||||
Ogre::Vector3 getCameraPosition() const;
|
||||
|
||||
flecs::world &m_world;
|
||||
/* Kept so the destructor can unregister it: the observer must not
|
||||
* fire into a destroyed system during world teardown (ecs_fini). */
|
||||
flecs::entity m_cleanupObserver;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorCameraSystem *m_cameraSystem;
|
||||
EditorUISystem *m_uiSystem;
|
||||
|
||||
@@ -52,15 +52,19 @@ CharacterSystem::CharacterSystem(flecs::world &world,
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
m_world.observer<CharacterComponent>("CharacterCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, CharacterComponent &) {
|
||||
teardownEntity(e);
|
||||
});
|
||||
m_cleanupObserver = m_world.observer<CharacterComponent>(
|
||||
"CharacterCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e,
|
||||
CharacterComponent &) {
|
||||
teardownEntity(e);
|
||||
});
|
||||
}
|
||||
|
||||
CharacterSystem::~CharacterSystem()
|
||||
{
|
||||
if (m_cleanupObserver.is_alive())
|
||||
m_cleanupObserver.destruct();
|
||||
std::vector<flecs::entity_t> toRemove;
|
||||
for (auto &pair : m_states)
|
||||
toRemove.push_back(pair.first);
|
||||
|
||||
@@ -68,6 +68,9 @@ private:
|
||||
void createChestCollider(flecs::entity e, CharacterComponent &cc);
|
||||
|
||||
flecs::world &m_world;
|
||||
/* Kept so the destructor can unregister it: the observer must not
|
||||
* fire into a destroyed system during world teardown (ecs_fini). */
|
||||
flecs::entity m_cleanupObserver;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
JoltPhysicsWrapper *m_physics = nullptr;
|
||||
bool m_initialized = false;
|
||||
|
||||
@@ -2462,6 +2462,13 @@ void EditorUISystem::handleRoadEditClick(const Ogre::Ray &mouseRay)
|
||||
|
||||
Ogre::Vector3 hit = mouseRay.getPoint(t);
|
||||
|
||||
/* Generator origin picking swallows the click before any tool. */
|
||||
if (rs->isOriginPickArmed()) {
|
||||
rs->armOriginPick(false);
|
||||
rs->setPickedOrigin(hit);
|
||||
return;
|
||||
}
|
||||
|
||||
int nodeId = pickRoadNode(hit);
|
||||
|
||||
/* Connect tool: first click pins the source node, second click on
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../components/WaterPlane.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/WaterPhysics.hpp"
|
||||
#include <algorithm>
|
||||
#include <OgreLogManager.h>
|
||||
#include <OgreManualObject.h>
|
||||
#include <OgreMaterialManager.h>
|
||||
@@ -34,7 +35,27 @@ EditorWaterPlaneSystem::EditorWaterPlaneSystem(flecs::world &world,
|
||||
{
|
||||
}
|
||||
|
||||
EditorWaterPlaneSystem::~EditorWaterPlaneSystem() = default;
|
||||
EditorWaterPlaneSystem::~EditorWaterPlaneSystem()
|
||||
{
|
||||
/* destroyEditorSystems() runs before Ogre Root teardown, but the
|
||||
* render textures themselves are only destroyed by the render
|
||||
* system during Root::~Root — detach the listener and viewports
|
||||
* now so that teardown does not fire into a dead system. The
|
||||
* texture is deliberately NOT removed here: the water material
|
||||
* still references it and TextureManager owns its final
|
||||
* destruction. */
|
||||
for (Ogre::RenderTarget *rt : m_createdTargets) {
|
||||
rt->removeListener(&m_listener);
|
||||
rt->removeAllViewports();
|
||||
}
|
||||
m_createdTargets.clear();
|
||||
m_query.each([this](flecs::entity, WaterPlane &wp,
|
||||
TransformComponent &) {
|
||||
wp.renderTarget = nullptr;
|
||||
wp.reflectionViewport = nullptr;
|
||||
wp.refractionViewport = nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
void EditorWaterPlaneSystem::update(float deltaTime, Ogre::Camera *camera)
|
||||
{
|
||||
@@ -137,6 +158,7 @@ void EditorWaterPlaneSystem::rebuildWaterPlane(
|
||||
// Set listener to hide water during RTT
|
||||
m_listener.waterMesh = wp.manualObject;
|
||||
wp.renderTarget->addListener(&m_listener);
|
||||
m_createdTargets.push_back(wp.renderTarget);
|
||||
|
||||
// Create cameras with their own nodes
|
||||
wp.reflectionCamera = m_sceneMgr->createCamera(baseName + "ReflectionCam");
|
||||
@@ -199,6 +221,10 @@ void EditorWaterPlaneSystem::cleanupWaterPlane(WaterPlane &wp)
|
||||
if (wp.renderTarget) {
|
||||
wp.renderTarget->removeListener(&m_listener);
|
||||
wp.renderTarget->removeAllViewports();
|
||||
m_createdTargets.erase(
|
||||
std::remove(m_createdTargets.begin(),
|
||||
m_createdTargets.end(), wp.renderTarget),
|
||||
m_createdTargets.end());
|
||||
wp.renderTarget = nullptr;
|
||||
}
|
||||
wp.reflectionViewport = nullptr;
|
||||
|
||||
@@ -45,6 +45,10 @@ private:
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
flecs::query<struct WaterPlane, struct TransformComponent> m_query;
|
||||
WaterRenderTargetListener m_listener;
|
||||
/* Every render target that currently has m_listener registered;
|
||||
* the destructor detaches from all of them regardless of whether
|
||||
* the owning entities are still alive. */
|
||||
std::vector<Ogre::RenderTarget *> m_createdTargets;
|
||||
float m_underwaterTime = 0.0f;
|
||||
bool m_compositorAdded = false;
|
||||
Ogre::Viewport *m_compositorViewport = nullptr;
|
||||
|
||||
@@ -69,6 +69,9 @@ RoadRegionStore::PageData &RoadRegionStore::loadPage(long pageX, long pageY)
|
||||
n.z = jn.value("z", 0.0);
|
||||
n.verticalOffset = jn.value(
|
||||
"verticalOffset", 0.0f);
|
||||
n.generatedGroupId = jn.value(
|
||||
"generatedGroupId",
|
||||
(uint32_t)0);
|
||||
page.data.nodes.push_back(n);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +108,39 @@ RoadRegionStore::PageData &RoadRegionStore::loadPage(long pageX, long pageY)
|
||||
e.sidewalkRightMode = je.value(
|
||||
"sidewalkRightMode",
|
||||
e.sidewalkRight ? 0 : 2);
|
||||
/* Phase 2.5: marking mode
|
||||
* (absent = Inherit) and the
|
||||
* decal list. */
|
||||
e.markingsMode = je.value(
|
||||
"markingsMode", 0);
|
||||
if (je.contains("decals") &&
|
||||
je["decals"].is_array()) {
|
||||
for (const auto &jd :
|
||||
je["decals"]) {
|
||||
RoadRegionDecal d;
|
||||
d.edgeT = jd.value(
|
||||
"edgeT",
|
||||
0.5f);
|
||||
d.lateralOffset = jd.value(
|
||||
"lateralOffset",
|
||||
0.0f);
|
||||
d.size = jd.value(
|
||||
"size",
|
||||
1.0f);
|
||||
if (jd.contains(
|
||||
"uv") &&
|
||||
jd["uv"].is_array() &&
|
||||
jd["uv"].size() ==
|
||||
4) {
|
||||
d.uvX = jd["uv"][0];
|
||||
d.uvY = jd["uv"][1];
|
||||
d.uvW = jd["uv"][2];
|
||||
d.uvH = jd["uv"][3];
|
||||
}
|
||||
e.decals.push_back(
|
||||
d);
|
||||
}
|
||||
}
|
||||
if (je.contains("sidewalkPasses") &&
|
||||
je["sidewalkPasses"]
|
||||
.is_array()) {
|
||||
@@ -193,6 +229,8 @@ bool RoadRegionStore::saveRegion(long px, long py, const RoadRegionData &data)
|
||||
jn["y"] = n.y;
|
||||
jn["z"] = n.z;
|
||||
jn["verticalOffset"] = n.verticalOffset;
|
||||
if (n.generatedGroupId != 0)
|
||||
jn["generatedGroupId"] = n.generatedGroupId;
|
||||
j["nodes"].push_back(jn);
|
||||
}
|
||||
j["edges"] = nlohmann::json::array();
|
||||
@@ -210,6 +248,21 @@ bool RoadRegionStore::saveRegion(long px, long py, const RoadRegionData &data)
|
||||
je["sidewalkRight"] = e.sidewalkRight;
|
||||
je["sidewalkLeftMode"] = e.sidewalkLeftMode;
|
||||
je["sidewalkRightMode"] = e.sidewalkRightMode;
|
||||
if (e.markingsMode != 0)
|
||||
je["markingsMode"] = e.markingsMode;
|
||||
if (!e.decals.empty()) {
|
||||
nlohmann::json dj = nlohmann::json::array();
|
||||
for (const RoadRegionDecal &d : e.decals) {
|
||||
nlohmann::json jd;
|
||||
jd["edgeT"] = d.edgeT;
|
||||
jd["lateralOffset"] = d.lateralOffset;
|
||||
jd["size"] = d.size;
|
||||
jd["uv"] = { d.uvX, d.uvY, d.uvW,
|
||||
d.uvH };
|
||||
dj.push_back(jd);
|
||||
}
|
||||
je["decals"] = dj;
|
||||
}
|
||||
if (!e.sidewalkPasses.empty()) {
|
||||
nlohmann::json pj = nlohmann::json::array();
|
||||
for (const RoadRegionSidewalkPass &p :
|
||||
@@ -265,3 +318,50 @@ void RoadRegionStore::removeRegionFile(long px, long py)
|
||||
std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
std::string RoadRegionStore::streetsFilePath(const std::string &rootDir)
|
||||
{
|
||||
return rootDir + "/roads/streets.json";
|
||||
}
|
||||
|
||||
bool RoadRegionStore::loadStreets(nlohmann::json *out)
|
||||
{
|
||||
if (m_rootDir.empty() || !out)
|
||||
return false;
|
||||
const std::string path = streetsFilePath(m_rootDir);
|
||||
try {
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open())
|
||||
return false;
|
||||
file >> *out;
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadRegionStore: streets load error: " +
|
||||
std::string(e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool RoadRegionStore::saveStreets(const nlohmann::json &j)
|
||||
{
|
||||
if (m_rootDir.empty())
|
||||
return false;
|
||||
try {
|
||||
std::filesystem::create_directories(m_rootDir + "/roads");
|
||||
const std::string path = streetsFilePath(m_rootDir);
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadRegionStore: failed to write " + path);
|
||||
return false;
|
||||
}
|
||||
file << j.dump(4);
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadRegionStore: streets save error: " +
|
||||
std::string(e.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
/*
|
||||
* One road node stored in a region file. Positions are absolute world
|
||||
* space doubles so regions work at any distance from the origin
|
||||
@@ -16,6 +18,9 @@ struct RoadRegionNode {
|
||||
double y = 0.0;
|
||||
double z = 0.0;
|
||||
float verticalOffset = 0.0f;
|
||||
/* Phase 3: procedural generator group (0 = manual). Absent in
|
||||
* legacy files = manual. */
|
||||
uint32_t generatedGroupId = 0;
|
||||
};
|
||||
|
||||
struct RoadRegionEdgePrefab {
|
||||
@@ -31,6 +36,18 @@ struct RoadRegionSidewalkPass {
|
||||
float width = 6.0f;
|
||||
};
|
||||
|
||||
/* One decal quad painted on an edge (phase 2.5); semantics match
|
||||
* RoadEdgeDecal. */
|
||||
struct RoadRegionDecal {
|
||||
float edgeT = 0.5f;
|
||||
float lateralOffset = 0.0f;
|
||||
float size = 1.0f;
|
||||
float uvX = 0.0f;
|
||||
float uvY = 0.0f;
|
||||
float uvW = 0.5f;
|
||||
float uvH = 0.5f;
|
||||
};
|
||||
|
||||
/*
|
||||
* One road edge stored in a region file. Endpoints are referenced by
|
||||
* the region-local node ids of this file. Field semantics match
|
||||
@@ -51,6 +68,10 @@ struct RoadRegionEdge {
|
||||
* (true -> Inherit, false -> Off). */
|
||||
int sidewalkLeftMode = 0;
|
||||
int sidewalkRightMode = 0;
|
||||
/* Per-edge marking tri-state (RoadMarkingMode as int, phase 2.5).
|
||||
* Absent in legacy files = Inherit (0). */
|
||||
int markingsMode = 0;
|
||||
std::vector<RoadRegionDecal> decals;
|
||||
std::vector<RoadRegionSidewalkPass> sidewalkPasses;
|
||||
RoadRegionEdgePrefab prefabLeft;
|
||||
RoadRegionEdgePrefab prefabRight;
|
||||
@@ -97,6 +118,13 @@ public:
|
||||
void removeRegionFile(long px, long py);
|
||||
void clearCache();
|
||||
|
||||
/* Streets (named edge sequences, phase 3) live in a single file
|
||||
* per terrain: "<root>/roads/streets.json". loadStreets returns
|
||||
* false when the file does not exist (leaving *out untouched). */
|
||||
bool loadStreets(nlohmann::json *out);
|
||||
bool saveStreets(const nlohmann::json &j);
|
||||
static std::string streetsFilePath(const std::string &rootDir);
|
||||
|
||||
private:
|
||||
struct PageData {
|
||||
RoadRegionData data;
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
#include "../components/Lod.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../components/EditorMarker.hpp"
|
||||
#include "../components/WaterPhysics.hpp"
|
||||
#include "../physics/physics.h"
|
||||
#include "../roadlib/RoadGeometryLib.hpp"
|
||||
#include "../roadlib/RoadNetworkGen.hpp"
|
||||
#include "PrefabSystem.hpp"
|
||||
#include <OgreTerrainGroup.h>
|
||||
#include <OgreMaterialManager.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -51,18 +54,21 @@ void RoadSystem::createManualObjects()
|
||||
|
||||
m_nodeMarkers = m_sceneMgr->createManualObject("RoadNodeMarkers");
|
||||
m_edgeLines = m_sceneMgr->createManualObject("RoadEdgeLines");
|
||||
m_lotOutlines = m_sceneMgr->createManualObject("RoadLotOutlines");
|
||||
m_widthIndicators = m_sceneMgr->createManualObject("RoadWidthIndicators");
|
||||
m_selectionHighlight = m_sceneMgr->createManualObject(
|
||||
"RoadSelectionHighlight");
|
||||
|
||||
m_visualNode->attachObject(m_nodeMarkers);
|
||||
m_visualNode->attachObject(m_edgeLines);
|
||||
m_visualNode->attachObject(m_lotOutlines);
|
||||
m_visualNode->attachObject(m_widthIndicators);
|
||||
m_visualNode->attachObject(m_selectionHighlight);
|
||||
|
||||
/* Draw in the overlay queue so the aids are never hidden by terrain. */
|
||||
m_nodeMarkers->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_edgeLines->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_lotOutlines->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_widthIndicators->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_selectionHighlight->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
|
||||
@@ -72,6 +78,12 @@ void RoadSystem::createManualObjects()
|
||||
m_debugWedgeObject->setRenderQueueGroup(
|
||||
Ogre::RENDER_QUEUE_MAIN);
|
||||
m_debugWedgeObject->setVisible(false);
|
||||
|
||||
/* Generator preview lives outside m_visualNode so it is visible
|
||||
* without road edit mode. */
|
||||
m_genPreview = m_sceneMgr->createManualObject("RoadGenPreview");
|
||||
m_sceneMgr->getRootSceneNode()->attachObject(m_genPreview);
|
||||
m_genPreview->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
}
|
||||
|
||||
void RoadSystem::destroyManualObjects()
|
||||
@@ -100,9 +112,11 @@ void RoadSystem::destroyManualObjects()
|
||||
|
||||
destroyManual(m_nodeMarkers);
|
||||
destroyManual(m_edgeLines);
|
||||
destroyManual(m_lotOutlines);
|
||||
destroyManual(m_widthIndicators);
|
||||
destroyManual(m_selectionHighlight);
|
||||
destroyManual(m_debugWedgeObject);
|
||||
destroyManual(m_genPreview);
|
||||
}
|
||||
|
||||
void RoadSystem::setTerrainEntity(flecs::entity entity)
|
||||
@@ -115,6 +129,7 @@ void RoadSystem::setTerrainEntity(flecs::entity entity)
|
||||
m_lastGraphVersion = 0;
|
||||
resetRegionState();
|
||||
clearPageGeometry();
|
||||
clearGenPreview();
|
||||
rebuildVisualAids();
|
||||
}
|
||||
|
||||
@@ -126,6 +141,7 @@ void RoadSystem::clear()
|
||||
m_lastGraphVersion = 0;
|
||||
resetRegionState();
|
||||
clearPageGeometry();
|
||||
clearGenPreview();
|
||||
rebuildVisualAids();
|
||||
}
|
||||
|
||||
@@ -266,6 +282,13 @@ void RoadSystem::update(float deltaTime)
|
||||
}
|
||||
syncPages();
|
||||
|
||||
/* Phase 2.5: overlay materials/textures and the road atlas
|
||||
* texture unit. ProceduralMaterialSystem restyles the road
|
||||
* material in place and wipes extra texture units, so the atlas
|
||||
* unit is re-attached here whenever it is missing. */
|
||||
ensureOverlayMaterials(tc.roadGraph.config);
|
||||
syncRoadAtlasTextureUnit(tc.roadGraph.config);
|
||||
|
||||
/* Mesh assembly (M5.8): rebuild dirty pages, then finalize pages
|
||||
* whose chunk meshes ProceduralMeshSystem has since created. */
|
||||
for (auto &kv : m_pageGeometry) {
|
||||
@@ -279,18 +302,25 @@ void RoadSystem::update(float deltaTime)
|
||||
continue;
|
||||
|
||||
bool allCreated = true;
|
||||
for (flecs::entity e : pg.bufferEntities) {
|
||||
if (!e.is_alive() ||
|
||||
!e.has<TriangleBufferComponent>()) {
|
||||
allCreated = false;
|
||||
break;
|
||||
}
|
||||
auto chunkCreated = [](flecs::entity e) {
|
||||
if (!e.is_alive() || !e.has<TriangleBufferComponent>())
|
||||
return false;
|
||||
const auto &tb = e.get<TriangleBufferComponent>();
|
||||
if (!tb.meshCreated || !tb.ogreEntity) {
|
||||
return tb.meshCreated && tb.ogreEntity;
|
||||
};
|
||||
for (flecs::entity e : pg.bufferEntities) {
|
||||
if (!chunkCreated(e)) {
|
||||
allCreated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allCreated)
|
||||
for (flecs::entity e : pg.overlayEntities) {
|
||||
if (!chunkCreated(e)) {
|
||||
allCreated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allCreated)
|
||||
continue;
|
||||
|
||||
@@ -306,6 +336,26 @@ void RoadSystem::update(float deltaTime)
|
||||
.markChanged();
|
||||
createPageCollider(pg, e, tb.meshName);
|
||||
}
|
||||
/* Overlay chunks (phase 2.5): rendering distance, renderable
|
||||
* pointer and the shared marking/decal material by name —
|
||||
* no collider, no navmesh dirtying. */
|
||||
for (flecs::entity e : pg.overlayEntities) {
|
||||
auto &tb = e.get_mut<TriangleBufferComponent>();
|
||||
tb.ogreEntity->setRenderingDistance(
|
||||
tc.roadGraph.config.roadVisibilityDistance);
|
||||
const std::string &matName =
|
||||
tb.meshName.find("_decals") !=
|
||||
std::string::npos
|
||||
? tc.roadGraph.config.decalMaterialName
|
||||
: tc.roadGraph.config.markingMaterialName;
|
||||
tb.ogreEntity->setMaterialName(matName);
|
||||
if (e.has<RenderableComponent>())
|
||||
e.get_mut<RenderableComponent>().entity =
|
||||
tb.ogreEntity;
|
||||
if (e.has<TransformComponent>())
|
||||
e.get_mut<TransformComponent>()
|
||||
.markChanged();
|
||||
}
|
||||
markNavMeshDirty();
|
||||
pg.meshFinalized = true;
|
||||
}
|
||||
@@ -637,6 +687,14 @@ void RoadSystem::syncRegions()
|
||||
if (root != m_regionRoot) {
|
||||
m_regionRoot = root;
|
||||
m_regionStore.setRootDirectory(root);
|
||||
/* Streets live in one file per terrain. Pages are not
|
||||
* all in the graph yet (and may never be at once while
|
||||
* streaming), so keep entries whose edges are currently
|
||||
* missing — lookups only ever match existing edges. */
|
||||
nlohmann::json sj;
|
||||
if (m_regionStore.loadStreets(&sj))
|
||||
RoadNetworkGen::streetsFromJson(graph, sj,
|
||||
/*dropMissing=*/false);
|
||||
}
|
||||
|
||||
Ogre::TerrainGroup *group = ts->getTerrainGroup();
|
||||
@@ -714,6 +772,40 @@ void RoadSystem::syncRegions()
|
||||
m_streamActivity = true;
|
||||
}
|
||||
|
||||
/* Phase 2.5: decal list conversion between RoadEdge and
|
||||
* RoadRegionEdge, shared by the migrate/load/build paths. */
|
||||
static void copyDecalsToRegion(const RoadEdge &src, RoadRegionEdge &dst)
|
||||
{
|
||||
dst.decals.clear();
|
||||
for (const RoadEdgeDecal &d : src.decals) {
|
||||
RoadRegionDecal rd;
|
||||
rd.edgeT = d.edgeT;
|
||||
rd.lateralOffset = d.lateralOffset;
|
||||
rd.size = d.size;
|
||||
rd.uvX = d.uvX;
|
||||
rd.uvY = d.uvY;
|
||||
rd.uvW = d.uvW;
|
||||
rd.uvH = d.uvH;
|
||||
dst.decals.push_back(rd);
|
||||
}
|
||||
}
|
||||
|
||||
static void copyDecalsFromRegion(const RoadRegionEdge &src, RoadEdge &dst)
|
||||
{
|
||||
dst.decals.clear();
|
||||
for (const RoadRegionDecal &rd : src.decals) {
|
||||
RoadEdgeDecal d;
|
||||
d.edgeT = rd.edgeT;
|
||||
d.lateralOffset = rd.lateralOffset;
|
||||
d.size = rd.size;
|
||||
d.uvX = rd.uvX;
|
||||
d.uvY = rd.uvY;
|
||||
d.uvW = rd.uvW;
|
||||
d.uvH = rd.uvH;
|
||||
dst.decals.push_back(d);
|
||||
}
|
||||
}
|
||||
|
||||
void RoadSystem::migrateGraphToRegions(RoadGraph &graph)
|
||||
{
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
@@ -797,6 +889,8 @@ void RoadSystem::migrateGraphToRegions(RoadGraph &graph)
|
||||
re.sidewalkRight = e.sidewalkRight;
|
||||
re.sidewalkLeftMode = (int)e.sidewalkLeftMode;
|
||||
re.sidewalkRightMode = (int)e.sidewalkRightMode;
|
||||
re.markingsMode = (int)e.markingsMode;
|
||||
copyDecalsToRegion(e, re);
|
||||
for (const RoadSidewalkPass &p : e.sidewalkPasses) {
|
||||
RoadRegionSidewalkPass rp;
|
||||
rp.rightSide = p.rightSide;
|
||||
@@ -867,6 +961,8 @@ void RoadSystem::loadRegion(long px, long py, RoadGraph &graph)
|
||||
remap[rn.id] = graph.addNode(
|
||||
ts->worldToRender(rn.x, rn.y, rn.z),
|
||||
rn.verticalOffset);
|
||||
graph.findNodeById(remap[rn.id])->generatedGroupId =
|
||||
rn.generatedGroupId;
|
||||
}
|
||||
|
||||
/* Second pass: edges whose endpoints both resolved. */
|
||||
@@ -891,6 +987,8 @@ void RoadSystem::loadRegion(long px, long py, RoadGraph &graph)
|
||||
e.sidewalkRight = re.sidewalkRight;
|
||||
e.sidewalkLeftMode = (RoadSidewalkMode)re.sidewalkLeftMode;
|
||||
e.sidewalkRightMode = (RoadSidewalkMode)re.sidewalkRightMode;
|
||||
e.markingsMode = (RoadMarkingMode)re.markingsMode;
|
||||
copyDecalsFromRegion(re, e);
|
||||
for (const RoadRegionSidewalkPass &rp : re.sidewalkPasses) {
|
||||
RoadSidewalkPass p;
|
||||
p.rightSide = rp.rightSide;
|
||||
@@ -942,6 +1040,7 @@ void RoadSystem::buildRegionData(long px, long py, const RoadGraph &graph,
|
||||
rn.y = ts->renderToWorldY(n.position.y);
|
||||
rn.z = wz;
|
||||
rn.verticalOffset = n.verticalOffset;
|
||||
rn.generatedGroupId = n.generatedGroupId;
|
||||
out.nodes.push_back(rn);
|
||||
}
|
||||
|
||||
@@ -958,6 +1057,7 @@ void RoadSystem::buildRegionData(long px, long py, const RoadGraph &graph,
|
||||
rn.y = ts->renderToWorldY(n->position.y);
|
||||
rn.z = ts->renderToWorldZ(n->position.z);
|
||||
rn.verticalOffset = n->verticalOffset;
|
||||
rn.generatedGroupId = n->generatedGroupId;
|
||||
out.nodes.push_back(rn);
|
||||
};
|
||||
|
||||
@@ -982,6 +1082,8 @@ void RoadSystem::buildRegionData(long px, long py, const RoadGraph &graph,
|
||||
re.sidewalkRight = e.sidewalkRight;
|
||||
re.sidewalkLeftMode = (int)e.sidewalkLeftMode;
|
||||
re.sidewalkRightMode = (int)e.sidewalkRightMode;
|
||||
re.markingsMode = (int)e.markingsMode;
|
||||
copyDecalsToRegion(e, re);
|
||||
for (const RoadSidewalkPass &p : e.sidewalkPasses) {
|
||||
RoadRegionSidewalkPass rp;
|
||||
rp.rightSide = p.rightSide;
|
||||
@@ -1031,6 +1133,11 @@ void RoadSystem::flushRegionStore()
|
||||
m_regionStore.saveRegion(kv.second.first, kv.second.second,
|
||||
data);
|
||||
}
|
||||
|
||||
/* Streets (phase 3) go alongside the region files. */
|
||||
nlohmann::json sj;
|
||||
RoadNetworkGen::streetsToJson(graph, sj);
|
||||
m_regionStore.saveStreets(sj);
|
||||
}
|
||||
|
||||
void RoadSystem::onRenderOriginChanged(const Ogre::Vector3 &delta)
|
||||
@@ -1122,6 +1229,15 @@ uint64_t RoadSystem::computePageSignature(uint64_t key) const
|
||||
fnvHashPod(h, cfg.apronSkirtDepth);
|
||||
fnvHashString(h, cfg.roadMeshTemplate);
|
||||
fnvHashString(h, cfg.sidewalkMeshTemplate);
|
||||
/* Phase 2.5: markings/decal overlay + atlas UV config. */
|
||||
fnvHashPod(h, cfg.markingsEnabled);
|
||||
fnvHashPod(h, cfg.markingLineWidth);
|
||||
fnvHashPod(h, cfg.markingDashLength);
|
||||
fnvHashPod(h, cfg.markingDashGap);
|
||||
fnvHashPod(h, cfg.markingStopWidth);
|
||||
fnvHashPod(h, cfg.markingStopSetback);
|
||||
fnvHashPod(h, cfg.markingEdgeInset);
|
||||
fnvHashPod(h, cfg.textureTileLength);
|
||||
|
||||
/* Seed nodes of this page's wedges/segments. */
|
||||
std::set<int> nodeIds;
|
||||
@@ -1186,6 +1302,16 @@ uint64_t RoadSystem::computePageSignature(uint64_t key) const
|
||||
fnvHashPod(h, slot->lateralOffset);
|
||||
fnvHashPod(h, slot->yOffset);
|
||||
}
|
||||
fnvHashPod(h, e.markingsMode);
|
||||
for (const RoadEdgeDecal &d : e.decals) {
|
||||
fnvHashPod(h, d.edgeT);
|
||||
fnvHashPod(h, d.lateralOffset);
|
||||
fnvHashPod(h, d.size);
|
||||
fnvHashPod(h, d.uvX);
|
||||
fnvHashPod(h, d.uvY);
|
||||
fnvHashPod(h, d.uvW);
|
||||
fnvHashPod(h, d.uvH);
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
@@ -1336,6 +1462,105 @@ void RoadSystem::buildPageMeshes(RoadPageGeometry &pg)
|
||||
}
|
||||
flushChunk();
|
||||
|
||||
/*
|
||||
* Markings and decals (phase 2.5): separate overlay chunk buffers,
|
||||
* same chunking rule but WITHOUT collider, navmesh contribution or
|
||||
* LOD set. Markings are built per wedge/segment (each page owns
|
||||
* its seed nodes' pieces, so no duplication); decals are per edge
|
||||
* with the halves attributed to the page seeding the owning node
|
||||
* (edgeT <= 0.5 -> nodeA) so cross-page edges paint once.
|
||||
*/
|
||||
std::vector<std::shared_ptr<Procedural::TriangleBuffer> > markingChunks;
|
||||
std::vector<std::shared_ptr<Procedural::TriangleBuffer> > decalChunks;
|
||||
if (!chunks.empty()) {
|
||||
std::shared_ptr<Procedural::TriangleBuffer> mcur =
|
||||
std::make_shared<Procedural::TriangleBuffer>();
|
||||
std::shared_ptr<Procedural::TriangleBuffer> dcur =
|
||||
std::make_shared<Procedural::TriangleBuffer>();
|
||||
auto flushOverlay = [](std::shared_ptr<Procedural::TriangleBuffer> &c,
|
||||
std::vector<std::shared_ptr<Procedural::TriangleBuffer> > &dst) {
|
||||
if (!c->getIndices().empty())
|
||||
dst.push_back(c);
|
||||
c = std::make_shared<Procedural::TriangleBuffer>();
|
||||
};
|
||||
auto appendOverlay = [&](std::shared_ptr<Procedural::TriangleBuffer> &c,
|
||||
std::vector<std::shared_ptr<Procedural::TriangleBuffer> > &dst,
|
||||
const Procedural::TriangleBuffer &piece) {
|
||||
if (piece.getIndices().empty())
|
||||
return;
|
||||
if (!c->getIndices().empty() &&
|
||||
c->getVertices().size() + piece.getVertices().size() >
|
||||
ROAD_PAGE_CHUNK_MAX_VERTICES)
|
||||
flushOverlay(c, dst);
|
||||
c->append(piece);
|
||||
};
|
||||
|
||||
for (const RoadWedge &w : pg.wedges) {
|
||||
RoadGeometryLib::RoadMarkingBuffers mb;
|
||||
RoadGeometryLib::buildWedgeMarkings(w, rg, mb);
|
||||
appendOverlay(mcur, markingChunks, mb.centerline);
|
||||
appendOverlay(mcur, markingChunks, mb.laneLines);
|
||||
appendOverlay(mcur, markingChunks, mb.edgeLines);
|
||||
appendOverlay(mcur, markingChunks, mb.stopLines);
|
||||
}
|
||||
for (const RoadStraightSegment &s : pg.segments) {
|
||||
RoadGeometryLib::RoadMarkingBuffers mb;
|
||||
RoadGeometryLib::buildSegmentMarkings(s, rg, mb);
|
||||
appendOverlay(mcur, markingChunks, mb.centerline);
|
||||
appendOverlay(mcur, markingChunks, mb.laneLines);
|
||||
appendOverlay(mcur, markingChunks, mb.edgeLines);
|
||||
appendOverlay(mcur, markingChunks, mb.stopLines);
|
||||
}
|
||||
flushOverlay(mcur, markingChunks);
|
||||
|
||||
/* Debug: ROAD_DUMP_MARKINGS=<file> writes every marking
|
||||
* triangle (9 floats per line) for offline analysis. */
|
||||
if (const char *dumpPath = getenv("ROAD_DUMP_MARKINGS")) {
|
||||
std::ofstream dump(dumpPath, std::ios::app);
|
||||
for (const auto &mc : markingChunks) {
|
||||
const auto &vv = mc->getVertices();
|
||||
const auto &ii = mc->getIndices();
|
||||
for (size_t t = 0; t + 2 < ii.size(); t += 3) {
|
||||
const Ogre::Vector3 &a =
|
||||
vv[ii[t]].mPosition;
|
||||
const Ogre::Vector3 &b =
|
||||
vv[ii[t + 1]].mPosition;
|
||||
const Ogre::Vector3 &c =
|
||||
vv[ii[t + 2]].mPosition;
|
||||
dump << a.x << " " << a.y << " " << a.z
|
||||
<< " " << b.x << " " << b.y << " "
|
||||
<< b.z << " " << c.x << " " << c.y
|
||||
<< " " << c.z << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::set<int> seedIds;
|
||||
for (const RoadWedge &w : pg.wedges)
|
||||
seedIds.insert(w.nodeId);
|
||||
for (const RoadStraightSegment &s : pg.segments)
|
||||
seedIds.insert(s.nodeId);
|
||||
std::set<int> edgeSet;
|
||||
for (int id : seedIds)
|
||||
for (size_t ei : rg.findEdgeIndicesForNode(id))
|
||||
edgeSet.insert((int)ei);
|
||||
for (int ei : edgeSet) {
|
||||
const RoadEdge &e = rg.edges[(size_t)ei];
|
||||
if (e.decals.empty())
|
||||
continue;
|
||||
Procedural::TriangleBuffer piece;
|
||||
int owners[2] = { e.nodeA, e.nodeB };
|
||||
for (int oi = 0; oi < 2; ++oi) {
|
||||
if (!seedIds.count(owners[oi]))
|
||||
continue;
|
||||
RoadGeometryLib::buildEdgeDecals(rg, (size_t)ei,
|
||||
piece,
|
||||
owners[oi]);
|
||||
}
|
||||
appendOverlay(dcur, decalChunks, piece);
|
||||
}
|
||||
flushOverlay(dcur, decalChunks);
|
||||
}
|
||||
|
||||
if (chunks.empty()) {
|
||||
/* No road content seeded on this page. */
|
||||
@@ -1416,6 +1641,45 @@ void RoadSystem::buildPageMeshes(RoadPageGeometry &pg)
|
||||
lod.settingsEntity = ensureRoadLodSettings(rg.config);
|
||||
e.set<LodComponent>(lod);
|
||||
}
|
||||
|
||||
/*
|
||||
* Overlay chunk entities (phase 2.5): marking strips and decal
|
||||
* quads. Deliberately no NavMeshGeometrySource, no LodComponent
|
||||
* and no materialEntity — the finalize pass assigns the shared
|
||||
* marking/decal material by name (picked from the "_markings"/
|
||||
* "_decals" mesh-name infix) and never builds colliders for them.
|
||||
*/
|
||||
auto createOverlayChunks = [&](const std::vector<std::shared_ptr<Procedural::TriangleBuffer> > &ochunks,
|
||||
const std::string &infix) {
|
||||
for (size_t i = 0; i < ochunks.size(); ++i) {
|
||||
std::string meshName = baseName + infix;
|
||||
if (ochunks.size() > 1)
|
||||
meshName += "_c" + std::to_string(i);
|
||||
|
||||
flecs::entity e = m_world.entity();
|
||||
|
||||
TransformComponent xf;
|
||||
xf.node = m_sceneMgr->getRootSceneNode()
|
||||
->createChildSceneNode(meshName +
|
||||
"_node");
|
||||
e.set<TransformComponent>(xf);
|
||||
|
||||
RenderableComponent rend;
|
||||
rend.meshName = meshName;
|
||||
e.set<RenderableComponent>(rend);
|
||||
|
||||
pg.overlayEntities.push_back(e);
|
||||
|
||||
TriangleBufferComponent tb;
|
||||
tb.buffer = ochunks[i];
|
||||
tb.proceduralContent = true;
|
||||
tb.meshName = meshName;
|
||||
tb.dirty = true;
|
||||
e.set<TriangleBufferComponent>(tb);
|
||||
}
|
||||
};
|
||||
createOverlayChunks(markingChunks, "_markings");
|
||||
createOverlayChunks(decalChunks, "_decals");
|
||||
}
|
||||
|
||||
void RoadSystem::destroyPageMeshes(RoadPageGeometry &pg)
|
||||
@@ -1459,6 +1723,43 @@ void RoadSystem::destroyPageMeshes(RoadPageGeometry &pg)
|
||||
e.destruct();
|
||||
}
|
||||
pg.bufferEntities.clear();
|
||||
|
||||
/* Overlay chunks (phase 2.5 markings/decals): same teardown, minus
|
||||
* the navmesh bookkeeping they never contributed to. */
|
||||
for (flecs::entity e : pg.overlayEntities) {
|
||||
if (!e.is_alive())
|
||||
continue;
|
||||
|
||||
if (e.has<TriangleBufferComponent>()) {
|
||||
auto &tb = e.get_mut<TriangleBufferComponent>();
|
||||
if (tb.ogreEntity) {
|
||||
try {
|
||||
m_sceneMgr->destroyEntity(tb.ogreEntity);
|
||||
} catch (...) {
|
||||
}
|
||||
tb.ogreEntity = nullptr;
|
||||
}
|
||||
if (!tb.meshName.empty()) {
|
||||
try {
|
||||
Ogre::MeshManager::getSingleton()
|
||||
.remove(tb.meshName);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
tb.meshCreated = false;
|
||||
}
|
||||
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &xf = e.get_mut<TransformComponent>();
|
||||
if (xf.node) {
|
||||
m_sceneMgr->destroySceneNode(xf.node);
|
||||
xf.node = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
e.destruct();
|
||||
}
|
||||
pg.overlayEntities.clear();
|
||||
pg.meshFinalized = false;
|
||||
|
||||
if (contributedToNavMesh)
|
||||
@@ -1522,6 +1823,236 @@ flecs::entity RoadSystem::ensureRoadLodSettings(const RoadConfig &cfg)
|
||||
return m_lodSettingsEntity;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Phase 2.5 overlay materials and atlas textures */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void RoadSystem::createRoadAtlasTextures()
|
||||
{
|
||||
Ogre::TextureManager &tm = Ogre::TextureManager::getSingleton();
|
||||
const Ogre::String &grp =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
|
||||
/* Road surface atlas: U bands 0-0.5 asphalt, 0.5-0.75 sidewalk,
|
||||
* 0.75-1.0 apron; V tiles along the road (TAM_WRAP). The asphalt
|
||||
* noise is elongated along image Y (the along-road V axis), so the
|
||||
* grain reads as wheel-track streaks along the lane instead of
|
||||
* isotropic speckle; mipmaps stay enabled (default) so distant
|
||||
* minification blurs instead of throwing cross-road moire straps.
|
||||
*/
|
||||
if (!tm.resourceExists("RoadAtlasTexture")) {
|
||||
const int W = 256, H = 256;
|
||||
std::vector<Ogre::uchar> data((size_t)W * H * 3);
|
||||
for (int y = 0; y < H; ++y) {
|
||||
for (int x = 0; x < W; ++x) {
|
||||
float u = ((float)x + 0.5f) / (float)W;
|
||||
/* Cheap deterministic value noise so the
|
||||
* bands do not look flat; asphalt quantizes Y
|
||||
* into 16-px runs (streaks along the lane).
|
||||
* The coordinates are avalanched separately
|
||||
* before mixing — a bare multiply-xor hash
|
||||
* leaves a lattice correlation that moires
|
||||
* into a giant checkerboard on the road. */
|
||||
int ys = (u < 0.5f) ? y / 16 : y;
|
||||
unsigned hx = (unsigned)x * 0x8da6b343u;
|
||||
hx ^= hx >> 15;
|
||||
unsigned hy = (unsigned)ys * 0xd8163841u;
|
||||
hy ^= hy >> 15;
|
||||
unsigned s = hx ^ hy;
|
||||
s *= 0xcb1ab31fu;
|
||||
s ^= s >> 16;
|
||||
float n = (float)(s & 0xff) / 255.0f - 0.5f;
|
||||
int r, g, b;
|
||||
if (u < 0.5f) {
|
||||
/* Asphalt: dark gray, fine noise. */
|
||||
int c = 58 + (int)(n * 24.0f);
|
||||
r = g = b = c;
|
||||
} else if (u < 0.75f) {
|
||||
/* Sidewalk: light gray paving. */
|
||||
int c = 150 + (int)(n * 18.0f);
|
||||
r = g = b = c;
|
||||
} else {
|
||||
/* Apron: warm tan gravel. */
|
||||
r = 128 + (int)(n * 20.0f);
|
||||
g = 112 + (int)(n * 18.0f);
|
||||
b = 92 + (int)(n * 16.0f);
|
||||
}
|
||||
size_t idx = ((size_t)y * W + x) * 3;
|
||||
data[idx + 0] = (Ogre::uchar)std::max(
|
||||
0, std::min(255, r));
|
||||
data[idx + 1] = (Ogre::uchar)std::max(
|
||||
0, std::min(255, g));
|
||||
data[idx + 2] = (Ogre::uchar)std::max(
|
||||
0, std::min(255, b));
|
||||
}
|
||||
}
|
||||
Ogre::Image img;
|
||||
img.loadDynamicImage(data.data(), W, H, 1, Ogre::PF_BYTE_RGB);
|
||||
tm.loadImage("RoadAtlasTexture", grp, img, Ogre::TEX_TYPE_2D,
|
||||
Ogre::MIP_DEFAULT, 1.0f);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decal atlas (RGBA, transparent background): four quadrant
|
||||
* presets, selected per decal via RoadEdgeDecal uv* —
|
||||
* [0, 0]-[0.5, 0.5] manhole cover (dark disc + ring)
|
||||
* [0.5, 0]-[1, 0.5] direction arrow (white, +V forward)
|
||||
* [0, 0.5]-[0.5, 1] road damage patch (dark blotch)
|
||||
* [0.5, 0.5]-[1, 1] parking bars (white horizontal lines)
|
||||
* Placeholder art until a real atlas lands in the asset pipeline.
|
||||
*/
|
||||
if (!tm.resourceExists("RoadDecalAtlas")) {
|
||||
const int W = 256, H = 256;
|
||||
std::vector<Ogre::uchar> data((size_t)W * H * 4, 0);
|
||||
auto px = [&](int x, int y, int r, int g, int b, int a) {
|
||||
size_t idx = ((size_t)y * W + x) * 4;
|
||||
data[idx + 0] = (Ogre::uchar)r;
|
||||
data[idx + 1] = (Ogre::uchar)g;
|
||||
data[idx + 2] = (Ogre::uchar)b;
|
||||
data[idx + 3] = (Ogre::uchar)a;
|
||||
};
|
||||
for (int y = 0; y < H; ++y) {
|
||||
for (int x = 0; x < W; ++x) {
|
||||
/* Local quadrant coordinates 0..1. */
|
||||
float qx = (x % 128 + 0.5f) / 128.0f;
|
||||
float qy = (y % 128 + 0.5f) / 128.0f;
|
||||
bool right = x >= 128;
|
||||
bool bottom = y >= 128;
|
||||
if (!right && !bottom) {
|
||||
/* Manhole: dark disc with a ring. */
|
||||
float dx = qx - 0.5f, dy = qy - 0.5f;
|
||||
float rr = std::sqrt(dx * dx + dy * dy);
|
||||
if (rr < 0.42f) {
|
||||
int c = 40;
|
||||
if (rr > 0.36f || rr < 0.1f)
|
||||
c = 70;
|
||||
px(x, y, c, c, c, 255);
|
||||
}
|
||||
} else if (right && !bottom) {
|
||||
/* Arrow: shaft + head, forward +V. */
|
||||
bool shaft = std::fabs(qx - 0.5f) < 0.06f &&
|
||||
qy > 0.15f && qy < 0.7f;
|
||||
float hd = std::fabs(qx - 0.5f) +
|
||||
(qy - 0.7f);
|
||||
bool head = qy >= 0.7f && qy < 0.95f &&
|
||||
hd < 0.35f;
|
||||
if (shaft || head)
|
||||
px(x, y, 230, 230, 230, 255);
|
||||
} else if (!right && bottom) {
|
||||
/* Damage patch: irregular blotch. */
|
||||
float dx = qx - 0.5f, dy = qy - 0.55f;
|
||||
float rr = std::sqrt(dx * dx + dy * dy);
|
||||
unsigned s = (unsigned)(x * 2654435761u) ^
|
||||
(unsigned)(y * 40503u);
|
||||
float n = ((s >> 13) & 0xff) / 255.0f;
|
||||
if (rr < 0.3f + 0.12f * n)
|
||||
px(x, y, 26, 24, 22,
|
||||
150 + (int)(60 * n));
|
||||
} else {
|
||||
/* Parking bars: white horizontals. */
|
||||
float fy = qy * 5.0f;
|
||||
bool bar = (fy - std::floor(fy)) < 0.45f &&
|
||||
qx > 0.15f && qx < 0.85f;
|
||||
if (bar)
|
||||
px(x, y, 235, 235, 235, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ogre::Image img;
|
||||
img.loadDynamicImage(data.data(), W, H, 1, Ogre::PF_BYTE_RGBA);
|
||||
tm.loadImage("RoadDecalAtlas", grp, img, Ogre::TEX_TYPE_2D, 0,
|
||||
1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void RoadSystem::ensureOverlayMaterials(const RoadConfig &cfg)
|
||||
{
|
||||
Ogre::MaterialManager &mm = Ogre::MaterialManager::getSingleton();
|
||||
const Ogre::String &grp =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
|
||||
if (!mm.resourceExists(cfg.markingMaterialName)) {
|
||||
/*
|
||||
* Unlit white paint. RTSS ignores material colours in a
|
||||
* lighting-disabled pass and renders it white — exactly
|
||||
* what road paint needs. Culling is disabled so the
|
||||
* paper-thin quads render from any angle; depth write
|
||||
* stays on (the strips hover 2 cm above the road top, so
|
||||
* no bias is needed).
|
||||
*/
|
||||
Ogre::MaterialPtr mat =
|
||||
mm.create(cfg.markingMaterialName, grp);
|
||||
Ogre::Technique *tech = mat->getTechnique(0);
|
||||
Ogre::Pass *pass = tech->getNumPasses() > 0 ?
|
||||
tech->getPass(0) :
|
||||
tech->createPass();
|
||||
pass->setLightingEnabled(false);
|
||||
pass->setCullingMode(Ogre::CULL_NONE);
|
||||
}
|
||||
|
||||
if (!mm.resourceExists(cfg.decalMaterialName)) {
|
||||
createRoadAtlasTextures();
|
||||
Ogre::MaterialPtr mat = mm.create(cfg.decalMaterialName, grp);
|
||||
Ogre::Technique *tech = mat->getTechnique(0);
|
||||
Ogre::Pass *pass = tech->getNumPasses() > 0 ?
|
||||
tech->getPass(0) :
|
||||
tech->createPass();
|
||||
pass->setLightingEnabled(false);
|
||||
pass->setCullingMode(Ogre::CULL_NONE);
|
||||
pass->setSceneBlending(Ogre::SBT_TRANSPARENT_ALPHA);
|
||||
pass->setDepthWriteEnabled(false);
|
||||
Ogre::TextureUnitState *tu =
|
||||
pass->createTextureUnitState("RoadDecalAtlas");
|
||||
tu->setTextureAddressingMode(Ogre::TextureUnitState::TAM_CLAMP);
|
||||
}
|
||||
}
|
||||
|
||||
void RoadSystem::syncRoadAtlasTextureUnit(const RoadConfig &cfg)
|
||||
{
|
||||
if (cfg.textureTileLength <= 1e-6f)
|
||||
return;
|
||||
|
||||
Ogre::MaterialManager &mm = Ogre::MaterialManager::getSingleton();
|
||||
if (!mm.resourceExists(cfg.roadMaterialName))
|
||||
return;
|
||||
createRoadAtlasTextures();
|
||||
|
||||
/*
|
||||
* The road material is restyled in place by
|
||||
* ProceduralMaterialSystem (which removes all passes whenever the
|
||||
* material component is dirty), so attach the atlas texture unit
|
||||
* whenever it is missing. Both axes wrap: U never leaves
|
||||
* [0, 1] but V tiles once per textureTileLength meters.
|
||||
*/
|
||||
Ogre::MaterialPtr mat = mm.getByName(
|
||||
cfg.roadMaterialName,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
if (!mat)
|
||||
return;
|
||||
for (unsigned ti = 0; ti < mat->getNumTechniques(); ++ti) {
|
||||
Ogre::Technique *tech = mat->getTechnique(ti);
|
||||
for (unsigned pi = 0; pi < tech->getNumPasses(); ++pi) {
|
||||
Ogre::Pass *pass = tech->getPass(pi);
|
||||
bool has = false;
|
||||
for (unsigned ui = 0; ui < pass->getNumTextureUnitStates();
|
||||
++ui) {
|
||||
if (pass->getTextureUnitState(ui)->getTextureName() ==
|
||||
"RoadAtlasTexture") {
|
||||
has = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has) {
|
||||
Ogre::TextureUnitState *tu =
|
||||
pass->createTextureUnitState(
|
||||
"RoadAtlasTexture");
|
||||
tu->setTextureAddressingMode(
|
||||
Ogre::TextureUnitState::TAM_WRAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RoadSystem::markNavMeshDirty()
|
||||
{
|
||||
/* While streaming, page churn dirties the navmesh repeatedly;
|
||||
@@ -1656,12 +2187,13 @@ static void addBox(Ogre::ManualObject *mo, const Ogre::Vector3 ¢er,
|
||||
|
||||
void RoadSystem::rebuildVisualAids()
|
||||
{
|
||||
if (!m_nodeMarkers || !m_edgeLines || !m_widthIndicators ||
|
||||
!m_selectionHighlight)
|
||||
if (!m_nodeMarkers || !m_edgeLines || !m_lotOutlines ||
|
||||
!m_widthIndicators || !m_selectionHighlight)
|
||||
return;
|
||||
|
||||
m_nodeMarkers->clear();
|
||||
m_edgeLines->clear();
|
||||
m_lotOutlines->clear();
|
||||
m_widthIndicators->clear();
|
||||
m_selectionHighlight->clear();
|
||||
|
||||
@@ -1680,6 +2212,7 @@ void RoadSystem::rebuildVisualAids()
|
||||
|
||||
buildNodeMarkers();
|
||||
buildEdgeLines();
|
||||
buildLotOutlines();
|
||||
buildWidthIndicators();
|
||||
buildSelectionHighlight();
|
||||
|
||||
@@ -1750,6 +2283,40 @@ void RoadSystem::buildEdgeLines()
|
||||
m_edgeLines->end();
|
||||
}
|
||||
|
||||
void RoadSystem::buildLotOutlines()
|
||||
{
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>())
|
||||
return;
|
||||
|
||||
const RoadGraph &rg = m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph;
|
||||
|
||||
/* Potential building lots (RoadNetworkGen::findLots with the
|
||||
* default 45 m / 8-edge filter and edge-fallback squares) drawn as
|
||||
* dark grey outline loops, lifted slightly above the road surface
|
||||
* like the other helper geometry. */
|
||||
std::vector<RoadNetworkGen::RoadLot> lots =
|
||||
RoadNetworkGen::findLots(rg);
|
||||
|
||||
m_lotOutlines->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_lotOutlines->colour(Ogre::ColourValue(0.1f, 0.1f, 0.1f));
|
||||
|
||||
for (const RoadNetworkGen::RoadLot &lot : lots) {
|
||||
size_t n = lot.polygon.size();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
Ogre::Vector3 a = lot.polygon[i];
|
||||
Ogre::Vector3 b = lot.polygon[(i + 1) % n];
|
||||
a.y += 0.25f;
|
||||
b.y += 0.25f;
|
||||
m_lotOutlines->position(a);
|
||||
m_lotOutlines->position(b);
|
||||
}
|
||||
}
|
||||
|
||||
m_lotOutlines->end();
|
||||
}
|
||||
|
||||
void RoadSystem::buildWidthIndicators()
|
||||
{
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
@@ -1966,6 +2533,131 @@ int RoadSystem::relaxImpossibleCorners()
|
||||
return (int)m_impossibleCorners.size();
|
||||
}
|
||||
|
||||
uint32_t RoadSystem::generateRoadNetwork(
|
||||
const RoadNetworkGen::GridParams ¶ms, std::string *error)
|
||||
{
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>()) {
|
||||
if (error)
|
||||
*error = "no terrain entity";
|
||||
return 0;
|
||||
}
|
||||
RoadGraph &graph =
|
||||
terrain.get_mut<TerrainComponent>().roadGraph;
|
||||
|
||||
/* Heights come from the terrain; the water level from the
|
||||
* scene's WaterPhysics entity (default -0.1 when absent). */
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
RoadNetworkGen::HeightSampler sampler;
|
||||
if (ts)
|
||||
sampler = [ts](float x, float z) {
|
||||
return ts->getHeightAt(Ogre::Vector3(x, 0.0f, z));
|
||||
};
|
||||
RoadNetworkGen::GridParams p = params;
|
||||
if (p.avoidWater) {
|
||||
float waterY = -0.1f;
|
||||
m_world.each([&waterY](WaterPhysics &wp) {
|
||||
waterY = wp.waterSurfaceY;
|
||||
});
|
||||
p.waterLevel = waterY;
|
||||
}
|
||||
|
||||
uint32_t groupId =
|
||||
RoadNetworkGen::generateGrid(graph, p, sampler, error);
|
||||
if (groupId == 0)
|
||||
return 0;
|
||||
m_impossibleCornersVersion = ~0ull;
|
||||
clearGenPreview();
|
||||
rebuildVisualAids();
|
||||
return groupId;
|
||||
}
|
||||
|
||||
void RoadSystem::updateGenPreview(const RoadNetworkGen::GridParams ¶ms)
|
||||
{
|
||||
if (!m_genPreview)
|
||||
return;
|
||||
m_genPreview->clear();
|
||||
|
||||
/* Generate into a throwaway graph — the real graph is never
|
||||
* touched; only the would-be edges are drawn as lines. */
|
||||
RoadGraph temp;
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (terrain.is_alive() && terrain.has<TerrainComponent>())
|
||||
temp.config = terrain.get<TerrainComponent>().roadGraph.config;
|
||||
|
||||
/* Same height/water inputs as generateRoadNetwork(). */
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
RoadNetworkGen::HeightSampler sampler;
|
||||
if (ts)
|
||||
sampler = [ts](float x, float z) {
|
||||
return ts->getHeightAt(Ogre::Vector3(x, 0.0f, z));
|
||||
};
|
||||
RoadNetworkGen::GridParams p = params;
|
||||
if (p.avoidWater) {
|
||||
float waterY = -0.1f;
|
||||
m_world.each([&waterY](WaterPhysics &wp) {
|
||||
waterY = wp.waterSurfaceY;
|
||||
});
|
||||
p.waterLevel = waterY;
|
||||
}
|
||||
|
||||
if (RoadNetworkGen::generateGrid(temp, p, sampler, nullptr) == 0)
|
||||
return;
|
||||
|
||||
m_genPreview->begin("Ogre/AxisGizmo",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
m_genPreview->colour(Ogre::ColourValue(1.0f, 0.65f, 0.0f));
|
||||
for (const RoadEdge &e : temp.edges) {
|
||||
const RoadNode *na = temp.findNodeById(e.nodeA);
|
||||
const RoadNode *nb = temp.findNodeById(e.nodeB);
|
||||
if (!na || !nb)
|
||||
continue;
|
||||
m_genPreview->position(na->position +
|
||||
Ogre::Vector3(0.0f, 0.3f, 0.0f));
|
||||
m_genPreview->position(nb->position +
|
||||
Ogre::Vector3(0.0f, 0.3f, 0.0f));
|
||||
}
|
||||
m_genPreview->end();
|
||||
}
|
||||
|
||||
void RoadSystem::clearGenPreview()
|
||||
{
|
||||
if (m_genPreview)
|
||||
m_genPreview->clear();
|
||||
}
|
||||
|
||||
int RoadSystem::resolveCrossingsNow(std::string *error)
|
||||
{
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>()) {
|
||||
if (error)
|
||||
*error = "no terrain entity";
|
||||
return -1;
|
||||
}
|
||||
RoadGraph &graph =
|
||||
terrain.get_mut<TerrainComponent>().roadGraph;
|
||||
int created = RoadNetworkGen::resolveAllCrossings(graph, error);
|
||||
if (created > 0) {
|
||||
m_impossibleCornersVersion = ~0ull;
|
||||
rebuildVisualAids();
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
bool RoadSystem::clearGenerated(uint32_t groupId)
|
||||
{
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>())
|
||||
return false;
|
||||
RoadGraph &graph =
|
||||
terrain.get_mut<TerrainComponent>().roadGraph;
|
||||
if (!RoadNetworkGen::removeGroup(graph, groupId))
|
||||
return false;
|
||||
m_impossibleCornersVersion = ~0ull;
|
||||
rebuildVisualAids();
|
||||
return true;
|
||||
}
|
||||
|
||||
const Procedural::TriangleBuffer &
|
||||
RoadSystem::getRoadTemplate(const RoadConfig &cfg)
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include "../components/RoadGraph.hpp"
|
||||
#include "../roadlib/RoadGeometryLib.hpp"
|
||||
#include "../roadlib/RoadNetworkGen.hpp"
|
||||
#include "RoadRegionStore.hpp"
|
||||
|
||||
namespace Ogre {
|
||||
@@ -49,6 +50,14 @@ struct RoadPageGeometry {
|
||||
*/
|
||||
std::vector<flecs::entity> bufferEntities;
|
||||
|
||||
/**
|
||||
* Overlay chunk entities of this page (phase 2.5): painted marking
|
||||
* strips and decal quads hovering slightly above the road top.
|
||||
* Purely visual — no physics collider, no navmesh contribution, no
|
||||
* LOD set; they share the chunking rule of bufferEntities.
|
||||
*/
|
||||
std::vector<flecs::entity> overlayEntities;
|
||||
|
||||
/**
|
||||
* Jolt bodies of this page's static road chunk colliders (M5.9):
|
||||
* one body per chunk mesh, created from the chunk's render mesh
|
||||
@@ -185,6 +194,57 @@ public:
|
||||
*/
|
||||
int relaxImpossibleCorners();
|
||||
|
||||
/**
|
||||
* Phase 3: procedural road network services (RoadNetworkGen).
|
||||
*
|
||||
* generateRoadNetwork emits a seeded settlement grid into the
|
||||
* terrain road graph (heights sampled from the terrain, water
|
||||
* level from the scene's WaterPhysics entity) and returns the
|
||||
* generatedGroupId stamped on the new nodes (0 on failure).
|
||||
* resolveCrossingsNow turns every edge crossing in the graph into
|
||||
* a proper junction and returns the number created (-1 when the
|
||||
* pass limit was exhausted). clearGenerated removes every node
|
||||
* (and cascading edges) of one generated group.
|
||||
*/
|
||||
uint32_t generateRoadNetwork(const RoadNetworkGen::GridParams ¶ms,
|
||||
std::string *error = nullptr);
|
||||
int resolveCrossingsNow(std::string *error = nullptr);
|
||||
bool clearGenerated(uint32_t groupId);
|
||||
|
||||
/**
|
||||
* Generator origin picking: while armed, the next road-edit-mode
|
||||
* terrain click (EditorUISystem::handleRoadEditClick) is consumed
|
||||
* as the generator origin instead of a tool action; the terrain
|
||||
* panel polls takePickedOrigin() to pick it up.
|
||||
*/
|
||||
void armOriginPick(bool armed) { m_originPickArmed = armed; }
|
||||
bool isOriginPickArmed() const { return m_originPickArmed; }
|
||||
void setPickedOrigin(const Ogre::Vector3 &p)
|
||||
{
|
||||
m_pickedOrigin = p;
|
||||
m_originPicked = true;
|
||||
}
|
||||
bool takePickedOrigin(Ogre::Vector3 &out)
|
||||
{
|
||||
if (!m_originPicked)
|
||||
return false;
|
||||
out = m_pickedOrigin;
|
||||
m_originPicked = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generator preview: updateGenPreview() runs generateGrid() into a
|
||||
* throwaway graph (same terrain height/water inputs as
|
||||
* generateRoadNetwork) and draws the would-be edges as orange
|
||||
* lines, so generator parameters can be tweaked before the actual
|
||||
* generation; the real graph is never touched. The preview lives
|
||||
* on its own scene-node attachment, independent of the road-edit
|
||||
* visual aids. clearGenPreview() removes the lines.
|
||||
*/
|
||||
void updateGenPreview(const RoadNetworkGen::GridParams ¶ms);
|
||||
void clearGenPreview();
|
||||
|
||||
/** Wedge-debug mode for geometry inspection. */
|
||||
bool getDebugWedgeEnabled() const { return m_debugWedgeEnabled; }
|
||||
void setDebugWedgeEnabled(bool v);
|
||||
@@ -489,6 +549,7 @@ private:
|
||||
|
||||
void buildNodeMarkers();
|
||||
void buildEdgeLines();
|
||||
void buildLotOutlines();
|
||||
void buildWidthIndicators();
|
||||
void buildSelectionHighlight();
|
||||
|
||||
@@ -528,6 +589,14 @@ private:
|
||||
void destroyPageMeshes(RoadPageGeometry &pg);
|
||||
flecs::entity ensureRoadMaterial(const RoadConfig &cfg);
|
||||
flecs::entity ensureRoadLodSettings(const RoadConfig &cfg);
|
||||
|
||||
/* Overlay materials/textures (phase 2.5): the unlit white marking
|
||||
* material, the alpha-blended decal material and the procedurally
|
||||
* generated road/decal atlas textures. All created lazily via
|
||||
* Ogre::MaterialManager/TextureManager when missing. */
|
||||
void ensureOverlayMaterials(const RoadConfig &cfg);
|
||||
void createRoadAtlasTextures();
|
||||
void syncRoadAtlasTextureUnit(const RoadConfig &cfg);
|
||||
void markNavMeshDirty();
|
||||
void applyNavMeshDirty();
|
||||
|
||||
@@ -562,6 +631,12 @@ private:
|
||||
std::string m_connectError;
|
||||
bool m_connectErrorPending = false;
|
||||
|
||||
/* Generator origin picking: while armed the next road-edit terrain
|
||||
* click is captured as the generator origin for the panel. */
|
||||
bool m_originPickArmed = false;
|
||||
bool m_originPicked = false;
|
||||
Ogre::Vector3 m_pickedOrigin = Ogre::Vector3::ZERO;
|
||||
|
||||
/* Page geometry state (M5.4). m_wedgeBuckets holds the enumerated
|
||||
* wedges/segments bucketed by seed-node page for ALL pages (including
|
||||
* unloaded ones); m_pageGeometry only tracks loaded pages. */
|
||||
@@ -621,8 +696,10 @@ private:
|
||||
Ogre::SceneNode *m_visualNode = nullptr;
|
||||
Ogre::ManualObject *m_nodeMarkers = nullptr;
|
||||
Ogre::ManualObject *m_edgeLines = nullptr;
|
||||
Ogre::ManualObject *m_lotOutlines = nullptr;
|
||||
Ogre::ManualObject *m_widthIndicators = nullptr;
|
||||
Ogre::ManualObject *m_selectionHighlight = nullptr;
|
||||
Ogre::ManualObject *m_genPreview = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ROADSYSTEM_HPP
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include "CharacterRegistry.hpp"
|
||||
#include "../roadlib/RoadNetworkGen.hpp"
|
||||
#include "../components/AnimationTree.hpp"
|
||||
#include "AnimationTreeRegistry.hpp"
|
||||
#include "AnimationTreeRegistry.hpp"
|
||||
@@ -1181,6 +1182,13 @@ bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity,
|
||||
// Restore main scene entity map
|
||||
m_entityMap = savedMap;
|
||||
|
||||
/* Resolve procedural texture references queued while
|
||||
* deserializing the prefab (a self-contained prefab carries
|
||||
* both the texture and the material referencing it); at
|
||||
* runtime nothing else calls this — it only runs at the end
|
||||
* of a full scene load. */
|
||||
resolveProceduralTextureReferences();
|
||||
|
||||
/* Run any scene scripts contained in the prefab subtree
|
||||
* (pending-only, so repeat instantiations are cheap), then
|
||||
* notify listeners that this instance finished loading. */
|
||||
@@ -4814,10 +4822,33 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
|
||||
tc.roadGraph.config.apronSkirtDepth;
|
||||
roadConfigJson["cornerFilletLength"] =
|
||||
tc.roadGraph.config.cornerFilletLength;
|
||||
roadConfigJson["crossingSnapDistance"] =
|
||||
tc.roadGraph.config.crossingSnapDistance;
|
||||
roadConfigJson["crossingHeightThreshold"] =
|
||||
tc.roadGraph.config.crossingHeightThreshold;
|
||||
roadConfigJson["prefabSpawnDistance"] =
|
||||
tc.roadGraph.config.prefabSpawnDistance;
|
||||
roadConfigJson["prefabDespawnDistance"] =
|
||||
tc.roadGraph.config.prefabDespawnDistance;
|
||||
/* Phase 2.5: markings + road atlas texture. */
|
||||
roadConfigJson["markingsEnabled"] = tc.roadGraph.config.markingsEnabled;
|
||||
roadConfigJson["markingLineWidth"] =
|
||||
tc.roadGraph.config.markingLineWidth;
|
||||
roadConfigJson["markingDashLength"] =
|
||||
tc.roadGraph.config.markingDashLength;
|
||||
roadConfigJson["markingDashGap"] = tc.roadGraph.config.markingDashGap;
|
||||
roadConfigJson["markingStopWidth"] =
|
||||
tc.roadGraph.config.markingStopWidth;
|
||||
roadConfigJson["markingStopSetback"] =
|
||||
tc.roadGraph.config.markingStopSetback;
|
||||
roadConfigJson["markingEdgeInset"] =
|
||||
tc.roadGraph.config.markingEdgeInset;
|
||||
roadConfigJson["markingMaterialName"] =
|
||||
tc.roadGraph.config.markingMaterialName;
|
||||
roadConfigJson["decalMaterialName"] =
|
||||
tc.roadGraph.config.decalMaterialName;
|
||||
roadConfigJson["textureTileLength"] =
|
||||
tc.roadGraph.config.textureTileLength;
|
||||
json["roadConfig"] = roadConfigJson;
|
||||
|
||||
/* Streaming terrains persist the road network in per-page region
|
||||
@@ -4832,6 +4863,8 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
|
||||
nj["position"] = { n.position.x, n.position.y,
|
||||
n.position.z };
|
||||
nj["verticalOffset"] = n.verticalOffset;
|
||||
if (n.generatedGroupId != 0)
|
||||
nj["generatedGroupId"] = n.generatedGroupId;
|
||||
roadNodesJson.push_back(nj);
|
||||
}
|
||||
json["roadNodes"] = roadNodesJson;
|
||||
@@ -4850,6 +4883,21 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
|
||||
ej["sidewalkRight"] = e.sidewalkRight;
|
||||
ej["sidewalkLeftMode"] = (int)e.sidewalkLeftMode;
|
||||
ej["sidewalkRightMode"] = (int)e.sidewalkRightMode;
|
||||
if ((int)e.markingsMode != 0)
|
||||
ej["markingsMode"] = (int)e.markingsMode;
|
||||
if (!e.decals.empty()) {
|
||||
nlohmann::json dj = nlohmann::json::array();
|
||||
for (const RoadEdgeDecal &d : e.decals) {
|
||||
nlohmann::json jd;
|
||||
jd["edgeT"] = d.edgeT;
|
||||
jd["lateralOffset"] = d.lateralOffset;
|
||||
jd["size"] = d.size;
|
||||
jd["uv"] = { d.uvX, d.uvY, d.uvW,
|
||||
d.uvH };
|
||||
dj.push_back(jd);
|
||||
}
|
||||
ej["decals"] = dj;
|
||||
}
|
||||
if (!e.sidewalkPasses.empty()) {
|
||||
nlohmann::json pj = nlohmann::json::array();
|
||||
for (const RoadSidewalkPass &p :
|
||||
@@ -4877,6 +4925,10 @@ nlohmann::json SceneSerializer::serializeTerrain(flecs::entity entity)
|
||||
roadEdgesJson.push_back(ej);
|
||||
}
|
||||
json["roadEdges"] = roadEdgesJson;
|
||||
|
||||
/* Phase 3: named streets (streaming terrains store them
|
||||
* in roads/streets.json via RoadSystem instead). */
|
||||
RoadNetworkGen::streetsToJson(tc.roadGraph, json);
|
||||
}
|
||||
|
||||
/* Persist binary heightmap, blend maps, and aux maps alongside the JSON. */
|
||||
@@ -4993,10 +5045,35 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity,
|
||||
rcj.value("apronSkirtDepth", 2.0f);
|
||||
tc.roadGraph.config.cornerFilletLength =
|
||||
rcj.value("cornerFilletLength", 12.0f);
|
||||
tc.roadGraph.config.crossingSnapDistance =
|
||||
rcj.value("crossingSnapDistance", 5.0f);
|
||||
tc.roadGraph.config.crossingHeightThreshold =
|
||||
rcj.value("crossingHeightThreshold", 10.0f);
|
||||
tc.roadGraph.config.prefabSpawnDistance =
|
||||
rcj.value("prefabSpawnDistance", 150.0f);
|
||||
tc.roadGraph.config.prefabDespawnDistance =
|
||||
rcj.value("prefabDespawnDistance", 250.0f);
|
||||
/* Phase 2.5: markings + road atlas texture. */
|
||||
tc.roadGraph.config.markingsEnabled =
|
||||
rcj.value("markingsEnabled", true);
|
||||
tc.roadGraph.config.markingLineWidth =
|
||||
rcj.value("markingLineWidth", 0.15f);
|
||||
tc.roadGraph.config.markingDashLength =
|
||||
rcj.value("markingDashLength", 3.0f);
|
||||
tc.roadGraph.config.markingDashGap =
|
||||
rcj.value("markingDashGap", 3.0f);
|
||||
tc.roadGraph.config.markingStopWidth =
|
||||
rcj.value("markingStopWidth", 0.4f);
|
||||
tc.roadGraph.config.markingStopSetback =
|
||||
rcj.value("markingStopSetback", 1.0f);
|
||||
tc.roadGraph.config.markingEdgeInset =
|
||||
rcj.value("markingEdgeInset", 0.25f);
|
||||
tc.roadGraph.config.markingMaterialName =
|
||||
rcj.value("markingMaterialName", "RoadMarkingMaterial");
|
||||
tc.roadGraph.config.decalMaterialName =
|
||||
rcj.value("decalMaterialName", "RoadDecalMaterial");
|
||||
tc.roadGraph.config.textureTileLength =
|
||||
rcj.value("textureTileLength", 0.0f);
|
||||
}
|
||||
|
||||
if (json.contains("roadNodes") && json["roadNodes"].is_array()) {
|
||||
@@ -5004,6 +5081,8 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity,
|
||||
RoadNode node;
|
||||
node.id = nj.value("id", 0);
|
||||
node.verticalOffset = nj.value("verticalOffset", 0.0f);
|
||||
node.generatedGroupId =
|
||||
nj.value("generatedGroupId", (uint32_t)0);
|
||||
if (nj.contains("position") && nj["position"].is_array() &&
|
||||
nj["position"].size() >= 3) {
|
||||
node.position.x = nj["position"][0];
|
||||
@@ -5033,6 +5112,28 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity,
|
||||
"sidewalkLeftMode", edge.sidewalkLeft ? 0 : 2);
|
||||
edge.sidewalkRightMode = (RoadSidewalkMode)ej.value(
|
||||
"sidewalkRightMode", edge.sidewalkRight ? 0 : 2);
|
||||
/* Phase 2.5: marking tri-state (absent = Inherit) and
|
||||
* the decal list. */
|
||||
edge.markingsMode =
|
||||
(RoadMarkingMode)ej.value("markingsMode", 0);
|
||||
if (ej.contains("decals") && ej["decals"].is_array()) {
|
||||
for (const auto &jd : ej["decals"]) {
|
||||
RoadEdgeDecal d;
|
||||
d.edgeT = jd.value("edgeT", 0.5f);
|
||||
d.lateralOffset =
|
||||
jd.value("lateralOffset", 0.0f);
|
||||
d.size = jd.value("size", 1.0f);
|
||||
if (jd.contains("uv") &&
|
||||
jd["uv"].is_array() &&
|
||||
jd["uv"].size() == 4) {
|
||||
d.uvX = jd["uv"][0];
|
||||
d.uvY = jd["uv"][1];
|
||||
d.uvW = jd["uv"][2];
|
||||
d.uvH = jd["uv"][3];
|
||||
}
|
||||
edge.decals.push_back(d);
|
||||
}
|
||||
}
|
||||
if (ej.contains("sidewalkPasses") &&
|
||||
ej["sidewalkPasses"].is_array()) {
|
||||
for (const auto &jp : ej["sidewalkPasses"]) {
|
||||
@@ -5102,6 +5203,11 @@ void SceneSerializer::deserializeTerrain(flecs::entity entity,
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 3: named streets (absent in legacy scenes; streaming
|
||||
* terrains load them from roads/streets.json via RoadSystem). */
|
||||
if (json.contains("streets"))
|
||||
RoadNetworkGen::streetsFromJson(tc.roadGraph, json);
|
||||
|
||||
tc.roadGraph.bumpVersion();
|
||||
|
||||
entity.set<TerrainComponent>(tc);
|
||||
|
||||
@@ -34,16 +34,20 @@ TerrainPrefabSpawnerSystem::TerrainPrefabSpawnerSystem(
|
||||
{
|
||||
s_instance = this;
|
||||
|
||||
m_world.observer<TerrainPrefabSpawnerComponent>(
|
||||
"TerrainPrefabSpawnerCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e, TerrainPrefabSpawnerComponent &) {
|
||||
despawn(e);
|
||||
});
|
||||
m_cleanupObserver =
|
||||
m_world.observer<TerrainPrefabSpawnerComponent>(
|
||||
"TerrainPrefabSpawnerCleanup")
|
||||
.event(flecs::OnRemove)
|
||||
.each([this](flecs::entity e,
|
||||
TerrainPrefabSpawnerComponent &) {
|
||||
despawn(e);
|
||||
});
|
||||
}
|
||||
|
||||
TerrainPrefabSpawnerSystem::~TerrainPrefabSpawnerSystem()
|
||||
{
|
||||
if (m_cleanupObserver.is_alive())
|
||||
m_cleanupObserver.destruct();
|
||||
for (auto &pair : m_spawners) {
|
||||
if (pair.second.spawned.is_alive())
|
||||
destroyInstanceRecursive(pair.second.spawned);
|
||||
|
||||
@@ -163,6 +163,9 @@ private:
|
||||
Ogre::Vector3 getCameraPosition() const;
|
||||
|
||||
flecs::world &m_world;
|
||||
/* Kept so the destructor can unregister it: the observer must not
|
||||
* fire into a destroyed system during world teardown (ecs_fini). */
|
||||
flecs::entity m_cleanupObserver;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
EditorCameraSystem *m_cameraSystem;
|
||||
EditorUISystem *m_uiSystem;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "TerrainSystem.hpp"
|
||||
#include "RoadSystem.hpp"
|
||||
#include "TownGenSystem.hpp"
|
||||
#include "../components/Terrain.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../physics/physics.h"
|
||||
@@ -3012,6 +3013,11 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform,
|
||||
m_roadSystem->setTerrainEntity(m_world.entity(m_terrainEntityId));
|
||||
m_roadSystem->setVisualAidsVisible(m_roadEditMode);
|
||||
|
||||
/* Town generation builds on the road graph; it holds no OGRE
|
||||
* objects of its own, so no teardown is needed on deactivate. */
|
||||
m_townGenSystem = std::make_unique<TownGenSystem>(m_world,
|
||||
m_sceneMgr);
|
||||
|
||||
if (m_physics)
|
||||
m_physics->setBodyDrawFilter(&mBodyDrawFilter);
|
||||
|
||||
@@ -3112,6 +3118,7 @@ void TerrainSystem::deactivate()
|
||||
|
||||
/* Destroy road system visual aids before terrain teardown. */
|
||||
m_roadSystem.reset();
|
||||
m_townGenSystem.reset();
|
||||
deactStep("roadSystem reset");
|
||||
|
||||
removeAllColliders();
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
class JoltPhysicsWrapper;
|
||||
class RoadSystem;
|
||||
class TownGenSystem;
|
||||
|
||||
class TerrainSystem {
|
||||
public:
|
||||
@@ -406,6 +407,11 @@ public:
|
||||
return m_roadSystem.get();
|
||||
}
|
||||
|
||||
TownGenSystem *getTownGenSystem() const
|
||||
{
|
||||
return m_townGenSystem.get();
|
||||
}
|
||||
|
||||
/* Physics wrapper used for terrain/road colliders (may be null).
|
||||
* Exposed for headless tests that verify collider interaction. */
|
||||
JoltPhysicsWrapper *getPhysics() const
|
||||
@@ -527,6 +533,7 @@ public:
|
||||
|
||||
private:
|
||||
std::unique_ptr<RoadSystem> m_roadSystem;
|
||||
std::unique_ptr<TownGenSystem> m_townGenSystem;
|
||||
void activate(struct TerrainComponent &tc,
|
||||
struct TransformComponent &xform,
|
||||
flecs::entity_t terrainEntityId);
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
#include "TownGenSystem.hpp"
|
||||
|
||||
#include "TerrainSystem.hpp"
|
||||
#include "RoadSystem.hpp"
|
||||
#include "../components/Terrain.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/EditorMarker.hpp"
|
||||
#include "../components/Primitive.hpp"
|
||||
#include "../components/TriangleBuffer.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../components/ProceduralMaterial.hpp"
|
||||
#include "../components/ProceduralTexture.hpp"
|
||||
#include "../components/WaterPhysics.hpp"
|
||||
#include "../components/RoadGraph.hpp"
|
||||
#include "../roadlib/RoadNetworkGen.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cfloat>
|
||||
|
||||
/* Building cell metrics: 2 m cells, 3 m floor height. */
|
||||
static const float TOWN_CELL_SIZE = 2.0f;
|
||||
static const float TOWN_CELL_HEIGHT = 3.0f;
|
||||
/* Foundation reaches this far below the lowest terrain corner. */
|
||||
static const float FOUNDATION_MARGIN = 0.5f;
|
||||
/* Stairs step geometry. */
|
||||
static const float STAIR_RISE = 0.18f;
|
||||
static const float STAIR_TREAD = 0.30f;
|
||||
static const float STAIR_WIDTH = 1.4f;
|
||||
|
||||
TownGenSystem::TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
m_terrainSystem = TerrainSystem::getInstance();
|
||||
}
|
||||
|
||||
int TownGenSystem::countTowns() const
|
||||
{
|
||||
int count = 0;
|
||||
m_world.query<EntityNameComponent, TownComponent>().each(
|
||||
[&count](flecs::entity, EntityNameComponent &name,
|
||||
TownComponent &) {
|
||||
if (name.name.rfind("TownGen ", 0) == 0)
|
||||
++count;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
int TownGenSystem::clear()
|
||||
{
|
||||
std::vector<flecs::entity> towns;
|
||||
m_world.query<EntityNameComponent, TownComponent>().each(
|
||||
[&towns](flecs::entity e, EntityNameComponent &name,
|
||||
TownComponent &) {
|
||||
if (name.name.rfind("TownGen ", 0) == 0)
|
||||
towns.push_back(e);
|
||||
});
|
||||
for (flecs::entity e : towns)
|
||||
e.destruct();
|
||||
return (int)towns.size();
|
||||
}
|
||||
|
||||
int TownGenSystem::generate(const TownGen::TownParams ¶ms,
|
||||
std::string *error)
|
||||
{
|
||||
TerrainSystem *ts = m_terrainSystem ?
|
||||
m_terrainSystem :
|
||||
TerrainSystem::getInstance();
|
||||
if (!ts || !ts->getRoadSystem()) {
|
||||
if (error)
|
||||
*error = "no terrain system";
|
||||
return 0;
|
||||
}
|
||||
flecs::entity terrain = ts->getRoadSystem()->getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>()) {
|
||||
if (error)
|
||||
*error = "no active terrain";
|
||||
return 0;
|
||||
}
|
||||
const RoadGraph &graph =
|
||||
terrain.get<TerrainComponent>().roadGraph;
|
||||
|
||||
TownGen::TownParams p = params;
|
||||
float waterY = -FLT_MAX;
|
||||
m_world.each([&waterY](WaterPhysics &wp) {
|
||||
waterY = wp.waterSurfaceY;
|
||||
});
|
||||
p.waterLevel = waterY;
|
||||
|
||||
RoadNetworkGen::HeightSampler sampler;
|
||||
if (ts)
|
||||
sampler = [ts](float x, float z) {
|
||||
return ts->getHeightAt(Ogre::Vector3(x, 0.0f, z));
|
||||
};
|
||||
|
||||
TownGen::TownResult result = TownGen::generateTown(graph, p, sampler);
|
||||
|
||||
/* Replace any previously generated town. */
|
||||
clear();
|
||||
|
||||
if (result.buildings.empty()) {
|
||||
if (error)
|
||||
*error = "no suitable lots";
|
||||
return 0;
|
||||
}
|
||||
|
||||
const TownGen::DistrictParams &dp =
|
||||
TownGen::districtParams(p.district);
|
||||
|
||||
/* Container entity: holds the shared material reference
|
||||
* (TownComponent) so buildings pick it up via the CellGrid
|
||||
* parent-chain material lookup. */
|
||||
flecs::entity container = m_world.entity();
|
||||
std::string containerName =
|
||||
std::string("TownGen ") + dp.name + " seed " +
|
||||
std::to_string(p.seed);
|
||||
container.set<EntityNameComponent>(
|
||||
EntityNameComponent(containerName));
|
||||
container.add<EditorMarkerComponent>();
|
||||
TransformComponent containerXform;
|
||||
containerXform.node =
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
containerXform.position = Ogre::Vector3::ZERO;
|
||||
containerXform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
containerXform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
containerXform.applyToNode();
|
||||
container.set<TransformComponent>(containerXform);
|
||||
|
||||
flecs::entity texture =
|
||||
createSettlementResources(container, dp, p.seed);
|
||||
|
||||
flecs::entity material = m_world.entity();
|
||||
material.child_of(container);
|
||||
material.set<EntityNameComponent>(
|
||||
EntityNameComponent(containerName + " material"));
|
||||
material.add<EditorMarkerComponent>();
|
||||
ProceduralMaterialComponent mat;
|
||||
mat.materialName = containerName + " material";
|
||||
mat.diffuseTextureId =
|
||||
texture.get<ProceduralTextureComponent>().textureId;
|
||||
mat.diffuseTextureEntity = texture;
|
||||
material.set<ProceduralMaterialComponent>(mat);
|
||||
|
||||
TownComponent town;
|
||||
town.townName = containerName;
|
||||
town.proceduralMaterialEntity = material;
|
||||
town.proceduralMaterialEntityId = std::to_string(material.id());
|
||||
container.set<TownComponent>(town);
|
||||
|
||||
for (size_t i = 0; i < result.buildings.size(); ++i)
|
||||
createBuilding(container, material, texture,
|
||||
result.buildings[i], result.pois[i]);
|
||||
|
||||
return (int)result.buildings.size();
|
||||
}
|
||||
|
||||
flecs::entity
|
||||
TownGenSystem::createSettlementResources(flecs::entity container,
|
||||
const TownGen::DistrictParams &dp,
|
||||
uint32_t seed)
|
||||
{
|
||||
flecs::entity texture = m_world.entity();
|
||||
texture.child_of(container);
|
||||
texture.set<EntityNameComponent>(EntityNameComponent(
|
||||
"towngen_tex_" + std::to_string(seed)));
|
||||
texture.add<EditorMarkerComponent>();
|
||||
|
||||
ProceduralTextureComponent tex;
|
||||
tex.textureName = "towngen_tex_" + std::to_string(seed);
|
||||
|
||||
/* Whole atlas tinted with the district wall color; specific
|
||||
* rects get their own colors. */
|
||||
Ogre::ColourValue wall(dp.wallColor[0], dp.wallColor[1],
|
||||
dp.wallColor[2]);
|
||||
for (int i = 0; i < ProceduralTextureComponent::RECT_COUNT; ++i)
|
||||
tex.setColor(i, wall);
|
||||
tex.setColor(1, Ogre::ColourValue(0.55f, 0.30f, 0.25f)); /* roof */
|
||||
tex.setColor(2, Ogre::ColourValue(0.55f, 0.55f, 0.55f)); /* found. */
|
||||
tex.setColor(3, Ogre::ColourValue(0.35f, 0.22f, 0.12f)); /* door */
|
||||
tex.setColor(4, Ogre::ColourValue(0.90f, 0.90f, 0.88f)); /* winframe */
|
||||
tex.setColor(5, Ogre::ColourValue(0.75f, 0.70f, 0.62f)); /* floor */
|
||||
tex.setColor(6, Ogre::ColourValue(0.92f, 0.92f, 0.92f)); /* ceiling */
|
||||
tex.addNamedRect("wall", 0);
|
||||
tex.addNamedRect("roof", 1);
|
||||
tex.addNamedRect("foundation", 2);
|
||||
tex.addNamedRect("door", 3);
|
||||
tex.addNamedRect("winframe", 4);
|
||||
tex.addNamedRect("floor", 5);
|
||||
tex.addNamedRect("ceiling", 6);
|
||||
texture.set<ProceduralTextureComponent>(tex);
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
void TownGenSystem::fillBuildingCells(const TownGen::TownBuilding &b,
|
||||
CellGridComponent &grid)
|
||||
{
|
||||
int cellsX = std::max(2, (int)std::lround(b.width / TOWN_CELL_SIZE));
|
||||
int cellsZ = std::max(2, (int)std::lround(b.depth / TOWN_CELL_SIZE));
|
||||
|
||||
grid.width = cellsX;
|
||||
grid.height = b.floors;
|
||||
grid.depth = cellsZ;
|
||||
grid.cellSize = TOWN_CELL_SIZE;
|
||||
grid.cellHeight = TOWN_CELL_HEIGHT;
|
||||
grid.generationMode = "exteriorOnly";
|
||||
|
||||
/* Exterior shell only; the entrance door frame is generated but
|
||||
* the leaf is not (doorsEnabled=false). The scene-switch door
|
||||
* config is preset for Phase 8 — flipping doorsEnabled wires the
|
||||
* interior scene switch. */
|
||||
grid.doorsEnabled = false;
|
||||
grid.doorSceneSwitchPath = "town_interior.json";
|
||||
|
||||
grid.floorRectName = "floor";
|
||||
grid.ceilingRectName = "ceiling";
|
||||
grid.extWallRectName = "wall";
|
||||
grid.intWallRectName = "wall";
|
||||
grid.extDoorFrameRectName = "door";
|
||||
grid.intDoorFrameRectName = "door";
|
||||
grid.extWindowFrameRectName = "winframe";
|
||||
grid.intWindowFrameRectName = "winframe";
|
||||
grid.roofTopRectName = "roof";
|
||||
grid.roofSideRectName = "roof";
|
||||
grid.doorRectName = "door";
|
||||
|
||||
/* Front (street-facing) side is local +Z; the door sits in the
|
||||
* center cell of the ground floor front. */
|
||||
int doorX = cellsX / 2;
|
||||
|
||||
for (int y = 0; y < b.floors; ++y) {
|
||||
bool top = (y == b.floors - 1);
|
||||
for (int x = 0; x < cellsX; ++x) {
|
||||
for (int z = 0; z < cellsZ; ++z) {
|
||||
Cell &c = grid.getOrCreateCell(x, y, z);
|
||||
c.setFlag(CellFlags::Floor);
|
||||
if (top)
|
||||
c.setFlag(CellFlags::Ceiling);
|
||||
|
||||
bool xMin = (x == 0), xMax = (x == cellsX - 1);
|
||||
bool zMin = (z == 0), zMax = (z == cellsZ - 1);
|
||||
bool corner = (xMin || xMax) && (zMin || zMax);
|
||||
|
||||
if (y == 0 && zMax && x == doorX) {
|
||||
c.setFlag(CellFlags::DoorZPos);
|
||||
c.setFlag(CellFlags::IntDoorZPos);
|
||||
if (xMin)
|
||||
c.setFlag(CellFlags::WallXNeg);
|
||||
if (xMax)
|
||||
c.setFlag(CellFlags::WallXPos);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Non-corner perimeter cells get
|
||||
* windows; corners stay plain walls. */
|
||||
if (xMin)
|
||||
c.setFlag(corner ? CellFlags::WallXNeg :
|
||||
CellFlags::WindowXNeg);
|
||||
if (xMax)
|
||||
c.setFlag(corner ? CellFlags::WallXPos :
|
||||
CellFlags::WindowXPos);
|
||||
if (zMin)
|
||||
c.setFlag(corner ? CellFlags::WallZNeg :
|
||||
CellFlags::WindowZNeg);
|
||||
if (zMax)
|
||||
c.setFlag(corner ? CellFlags::WallZPos :
|
||||
CellFlags::WindowZPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grid.markDirty();
|
||||
}
|
||||
|
||||
void TownGenSystem::createBuilding(flecs::entity container,
|
||||
flecs::entity material, flecs::entity texture,
|
||||
const TownGen::TownBuilding &b,
|
||||
const TownGen::TownPoi &poi)
|
||||
{
|
||||
TerrainSystem *ts = m_terrainSystem ?
|
||||
m_terrainSystem :
|
||||
TerrainSystem::getInstance();
|
||||
|
||||
flecs::entity building = m_world.entity();
|
||||
building.child_of(container);
|
||||
building.set<EntityNameComponent>(EntityNameComponent(b.address));
|
||||
building.add<EditorMarkerComponent>();
|
||||
|
||||
CellGridComponent grid;
|
||||
fillBuildingCells(b, grid);
|
||||
|
||||
/* cellToWorld() puts cell (x, y, z) at x*cellSize, so the shell
|
||||
* spans [0, width*cellSize] from the entity node — shift the
|
||||
* node so the shell is CENTERED on TownBuilding::center and the
|
||||
* front face (local +Z) sits at shellD/2 facing the street.
|
||||
* Foundation/stairs/POI children use the same convention. */
|
||||
float shellW = grid.width * TOWN_CELL_SIZE;
|
||||
float shellD = grid.depth * TOWN_CELL_SIZE;
|
||||
Ogre::Vector3 shellCenter((grid.width - 1) * TOWN_CELL_SIZE * 0.5f,
|
||||
0.0f,
|
||||
(grid.depth - 1) * TOWN_CELL_SIZE * 0.5f);
|
||||
Ogre::Quaternion yaw(Ogre::Radian(b.yaw), Ogre::Vector3::UNIT_Y);
|
||||
|
||||
/* TownGen works in render space (the height sampler is
|
||||
* TerrainSystem::getHeightAt); store the authoritative world
|
||||
* position so render-origin rebases keep the node exact. */
|
||||
Ogre::Vector3 renderPos = b.center - yaw * shellCenter;
|
||||
|
||||
TransformComponent xform;
|
||||
xform.node = m_sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
xform.position = renderPos;
|
||||
xform.rotation = yaw;
|
||||
xform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
if (ts) {
|
||||
xform.worldX = ts->renderToWorldX(renderPos.x);
|
||||
xform.worldY = ts->renderToWorldY(renderPos.y);
|
||||
xform.worldZ = ts->renderToWorldZ(renderPos.z);
|
||||
xform.hasWorldPosition = true;
|
||||
}
|
||||
xform.applyToNode();
|
||||
building.set<TransformComponent>(xform);
|
||||
|
||||
building.set<CellGridComponent>(grid);
|
||||
|
||||
/* Flat roof over the whole footprint — buildRoofs() only
|
||||
* processes RoofComponent children of the grid entity. posY at
|
||||
* one cell above the top floor puts the slab on top of the
|
||||
* walls; its trim covers the top-floor ceiling seam. */
|
||||
flecs::entity roof = m_world.entity();
|
||||
roof.child_of(building);
|
||||
roof.set<EntityNameComponent>(EntityNameComponent("roof"));
|
||||
roof.add<EditorMarkerComponent>();
|
||||
RoofComponent rc;
|
||||
rc.type = RoofComponent::Flat;
|
||||
rc.posX = 0;
|
||||
rc.posY = b.floors;
|
||||
rc.posZ = 0;
|
||||
rc.sizeX = grid.width;
|
||||
rc.sizeZ = grid.depth;
|
||||
roof.set<RoofComponent>(rc);
|
||||
|
||||
createFoundation(building, b, grid, material, texture);
|
||||
if (b.stairs)
|
||||
createStairs(building, b, grid, material, texture);
|
||||
|
||||
/* Curb pickup POI (name-convention marker entity), parented to
|
||||
* the building with a local transform. */
|
||||
flecs::entity poiEnt = m_world.entity();
|
||||
poiEnt.child_of(building);
|
||||
poiEnt.set<EntityNameComponent>(EntityNameComponent(poi.name));
|
||||
poiEnt.add<EditorMarkerComponent>();
|
||||
TransformComponent poiXform;
|
||||
poiXform.node = xform.node->createChildSceneNode();
|
||||
poiXform.position =
|
||||
xform.rotation.Inverse() * (poi.position - renderPos);
|
||||
poiXform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
poiXform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
poiXform.applyToNode();
|
||||
poiEnt.set<TransformComponent>(poiXform);
|
||||
}
|
||||
|
||||
void TownGenSystem::createFoundation(flecs::entity building,
|
||||
const TownGen::TownBuilding &b,
|
||||
const CellGridComponent &grid,
|
||||
flecs::entity material,
|
||||
flecs::entity texture)
|
||||
{
|
||||
float foundationH = b.floorY - (b.minGroundY - FOUNDATION_MARGIN);
|
||||
|
||||
/* The building node sits half a shell off the intended
|
||||
* footprint center (cells start at the node, see
|
||||
* createBuilding) — the block centers on the shell. */
|
||||
float shellW = grid.width * TOWN_CELL_SIZE;
|
||||
float shellD = grid.depth * TOWN_CELL_SIZE;
|
||||
Ogre::Vector3 shellCenter((grid.width - 1) * TOWN_CELL_SIZE * 0.5f,
|
||||
0.0f,
|
||||
(grid.depth - 1) * TOWN_CELL_SIZE * 0.5f);
|
||||
|
||||
flecs::entity foundation = m_world.entity();
|
||||
foundation.child_of(building);
|
||||
foundation.set<EntityNameComponent>(EntityNameComponent("foundation"));
|
||||
foundation.add<EditorMarkerComponent>();
|
||||
|
||||
/* Child of the building node: identity local transform, the
|
||||
* buffer box hangs below the shell base (floor level). */
|
||||
TransformComponent xform;
|
||||
Ogre::SceneNode *buildingNode = building.get<TransformComponent>().node;
|
||||
xform.node = buildingNode->createChildSceneNode();
|
||||
xform.position = Ogre::Vector3::ZERO;
|
||||
xform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
xform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
xform.applyToNode();
|
||||
foundation.set<TransformComponent>(xform);
|
||||
|
||||
TriangleBufferComponent tb;
|
||||
tb.textureEntity = texture;
|
||||
tb.textureRectName = "foundation";
|
||||
tb.materialEntity = material;
|
||||
tb.meshName = b.address + "_foundation";
|
||||
foundation.set<TriangleBufferComponent>(tb);
|
||||
|
||||
RigidBodyComponent rb;
|
||||
rb.bodyType = RigidBodyComponent::BodyType::Static;
|
||||
foundation.set<RigidBodyComponent>(rb);
|
||||
|
||||
flecs::entity box = m_world.entity();
|
||||
box.child_of(foundation);
|
||||
box.set<EntityNameComponent>(EntityNameComponent("block"));
|
||||
TransformComponent boxXform;
|
||||
boxXform.node = xform.node->createChildSceneNode();
|
||||
boxXform.position =
|
||||
shellCenter + Ogre::Vector3(0.0f,
|
||||
-foundationH * 0.5f + 0.05f, 0.0f);
|
||||
boxXform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
boxXform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
boxXform.applyToNode();
|
||||
box.set<TransformComponent>(boxXform);
|
||||
|
||||
PrimitiveComponent prim;
|
||||
prim.type = PrimitiveComponent::Type::Box;
|
||||
prim.boxSizeX = shellW + 0.15f;
|
||||
prim.boxSizeY = foundationH;
|
||||
prim.boxSizeZ = shellD + 0.15f;
|
||||
box.set<PrimitiveComponent>(prim);
|
||||
|
||||
PhysicsColliderComponent collider;
|
||||
collider.shapeType = PhysicsColliderComponent::ShapeType::Box;
|
||||
collider.parameters = Ogre::Vector3((shellW + 0.15f) * 0.5f,
|
||||
foundationH * 0.5f,
|
||||
(shellD + 0.15f) * 0.5f);
|
||||
box.set<PhysicsColliderComponent>(collider);
|
||||
}
|
||||
|
||||
void TownGenSystem::createStairs(flecs::entity building,
|
||||
const TownGen::TownBuilding &b,
|
||||
const CellGridComponent &grid,
|
||||
flecs::entity material, flecs::entity texture)
|
||||
{
|
||||
int steps = (int)std::ceil(b.stairsDrop / STAIR_RISE);
|
||||
if (steps < 2)
|
||||
return;
|
||||
float rise = b.stairsDrop / (float)steps;
|
||||
|
||||
/* Shell convention (see createBuilding): cells start at the
|
||||
* node, walls extend cellSize/2 past the outer cell centers.
|
||||
* The door sits in the center cell of the front (Z+) face;
|
||||
* the steps descend from there. */
|
||||
float doorX = (grid.width / 2) * TOWN_CELL_SIZE;
|
||||
float frontZ = (grid.depth - 0.5f) * TOWN_CELL_SIZE;
|
||||
|
||||
flecs::entity stairs = m_world.entity();
|
||||
stairs.child_of(building);
|
||||
stairs.set<EntityNameComponent>(EntityNameComponent("stairs"));
|
||||
stairs.add<EditorMarkerComponent>();
|
||||
|
||||
TransformComponent xform;
|
||||
Ogre::SceneNode *parentNode =
|
||||
building.get<TransformComponent>().node;
|
||||
xform.node = parentNode->createChildSceneNode();
|
||||
xform.position = Ogre::Vector3::ZERO;
|
||||
xform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
xform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
xform.applyToNode();
|
||||
stairs.set<TransformComponent>(xform);
|
||||
|
||||
TriangleBufferComponent tb;
|
||||
tb.textureEntity = texture;
|
||||
tb.textureRectName = "foundation";
|
||||
tb.materialEntity = material;
|
||||
tb.meshName = b.address + "_stairs";
|
||||
stairs.set<TriangleBufferComponent>(tb);
|
||||
|
||||
RigidBodyComponent rb;
|
||||
rb.bodyType = RigidBodyComponent::BodyType::Static;
|
||||
stairs.set<RigidBodyComponent>(rb);
|
||||
|
||||
/* Solid staircase descending from the door (local +Z, front
|
||||
* face at grid.depth * cellSize) to the ground at -stairsDrop;
|
||||
* the ground itself is the last step. */
|
||||
for (int i = 0; i < steps - 1; ++i) {
|
||||
float surfaceY = -((float)i + 1.0f) * rise;
|
||||
float height = surfaceY - (-b.stairsDrop);
|
||||
if (height < 0.01f)
|
||||
continue;
|
||||
|
||||
flecs::entity step = m_world.entity();
|
||||
step.child_of(stairs);
|
||||
step.set<EntityNameComponent>(
|
||||
EntityNameComponent("step " + std::to_string(i)));
|
||||
|
||||
TransformComponent stepXform;
|
||||
stepXform.node = xform.node->createChildSceneNode();
|
||||
stepXform.position = Ogre::Vector3(
|
||||
doorX, (-b.stairsDrop + surfaceY) * 0.5f,
|
||||
frontZ + ((float)i + 0.5f) * STAIR_TREAD);
|
||||
stepXform.rotation = Ogre::Quaternion::IDENTITY;
|
||||
stepXform.scale = Ogre::Vector3::UNIT_SCALE;
|
||||
stepXform.applyToNode();
|
||||
step.set<TransformComponent>(stepXform);
|
||||
|
||||
PrimitiveComponent prim;
|
||||
prim.type = PrimitiveComponent::Type::Box;
|
||||
prim.boxSizeX = STAIR_WIDTH;
|
||||
prim.boxSizeY = height;
|
||||
prim.boxSizeZ = STAIR_TREAD;
|
||||
step.set<PrimitiveComponent>(prim);
|
||||
|
||||
PhysicsColliderComponent collider;
|
||||
collider.shapeType = PhysicsColliderComponent::ShapeType::Box;
|
||||
collider.parameters = Ogre::Vector3(
|
||||
STAIR_WIDTH * 0.5f, height * 0.5f, STAIR_TREAD * 0.5f);
|
||||
step.set<PhysicsColliderComponent>(collider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
|
||||
#include "../roadlib/TownGen.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
|
||||
class TerrainSystem;
|
||||
|
||||
/**
|
||||
* Town generation system (demo-taxi phase 4).
|
||||
*
|
||||
* Turns the lots of the terrain's road graph into a procedural
|
||||
* settlement of CellGrid buildings (roadlib/TownGen does the pure
|
||||
* placement math): one exteriorOnly CellGrid shell per lot, seated on
|
||||
* a foundation block (PrimitiveComponent box) with entrance stairs
|
||||
* when the door-side ground drops below the floor, plus a named
|
||||
* pickup POI child at the curb. Every building carries its
|
||||
* scene-switch door config with doorsEnabled=false so Phase 8 only
|
||||
* has to enable and wire the interiors.
|
||||
*
|
||||
* Generated entities are ordinary scene entities (EditorMarker) under
|
||||
* a container entity named "TownGen <district> seed <seed>" holding a
|
||||
* TownComponent that points at the shared settlement material, so
|
||||
* they stay editable and serialize with the scene. generate() clears
|
||||
* any previous generated town first; clear() removes it.
|
||||
*
|
||||
* Placement convention: `CellGridComponent::cellToWorld()` puts cell
|
||||
* (x, y, z) at x*cellSize, so a shell spans [0, width*cellSize] from
|
||||
* its node. createBuilding() therefore shifts the building node so
|
||||
* the shell is CENTERED on TownBuilding::center (front face at local
|
||||
* +Z facing the street); the foundation/stairs children position
|
||||
* themselves in the same shell coordinates.
|
||||
*/
|
||||
class TownGenSystem
|
||||
{
|
||||
public:
|
||||
TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr);
|
||||
|
||||
/** Generate a settlement on the active terrain's road graph.
|
||||
* Returns the number of buildings placed (0 on failure, @p error
|
||||
* set). Clears the previously generated town first. */
|
||||
int generate(const TownGen::TownParams ¶ms, std::string *error);
|
||||
|
||||
/** Destroy every generated town (all "TownGen " containers). */
|
||||
int clear();
|
||||
|
||||
/** Live "TownGen " container entities. */
|
||||
int countTowns() const;
|
||||
|
||||
private:
|
||||
flecs::entity createSettlementResources(flecs::entity container,
|
||||
const TownGen::DistrictParams &dp,
|
||||
uint32_t seed);
|
||||
void createBuilding(flecs::entity container, flecs::entity material,
|
||||
flecs::entity texture, const TownGen::TownBuilding &b,
|
||||
const TownGen::TownPoi &poi);
|
||||
void createFoundation(flecs::entity building,
|
||||
const TownGen::TownBuilding &b,
|
||||
const CellGridComponent &grid,
|
||||
flecs::entity material, flecs::entity texture);
|
||||
void createStairs(flecs::entity building, const TownGen::TownBuilding &b,
|
||||
const CellGridComponent &grid,
|
||||
flecs::entity material, flecs::entity texture);
|
||||
static void fillBuildingCells(const TownGen::TownBuilding &b,
|
||||
CellGridComponent &grid);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
TerrainSystem *m_terrainSystem = nullptr;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../systems/TerrainSystem.hpp"
|
||||
#include "../systems/RoadSystem.hpp"
|
||||
#include "../systems/TownGenSystem.hpp"
|
||||
#include "../systems/TerrainPrefabSpawnerSystem.hpp"
|
||||
#include "../systems/PrefabSystem.hpp"
|
||||
#include "NavigationPanel.hpp"
|
||||
@@ -21,6 +22,7 @@
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -807,10 +809,278 @@ private:
|
||||
"(aprons start at the sidewalk's "
|
||||
"outer top edge)");
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Markings");
|
||||
cfgChanged |= ImGui::Checkbox(
|
||||
"Markings Enabled", &rg.config.markingsEnabled);
|
||||
if (rg.config.markingsEnabled) {
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Line Width",
|
||||
&rg.config.markingLineWidth, 0.05f,
|
||||
0.5f, "%.2f");
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Dash Length",
|
||||
&rg.config.markingDashLength, 0.5f,
|
||||
10.0f, "%.1f");
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Dash Gap", &rg.config.markingDashGap,
|
||||
0.5f, 10.0f, "%.1f");
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Stop Line Width",
|
||||
&rg.config.markingStopWidth, 0.1f,
|
||||
1.0f, "%.2f");
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Stop Setback",
|
||||
&rg.config.markingStopSetback, 0.0f,
|
||||
5.0f, "%.1f");
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Edge Line Inset",
|
||||
&rg.config.markingEdgeInset, 0.0f,
|
||||
1.0f, "%.2f");
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Road Texture");
|
||||
cfgChanged |= ImGui::SliderFloat(
|
||||
"Tile Length (0 = off)",
|
||||
&rg.config.textureTileLength, 0.0f, 50.0f,
|
||||
"%.1f m");
|
||||
if (rg.config.textureTileLength > 0.0f)
|
||||
ImGui::TextDisabled(
|
||||
"(atlas UVs: asphalt/sidewalk/apron "
|
||||
"bands, V tiles along the road)");
|
||||
if (cfgChanged)
|
||||
rg.bumpVersion();
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
/* Phase 3: procedural settlement generator, crossing
|
||||
* resolution and generated-group management (RoadNetworkGen). */
|
||||
if (ImGui::TreeNode("Road Network Generator")) {
|
||||
static int s_blocksX = 3, s_blocksZ = 3;
|
||||
static float s_blockSizeX = 100.0f, s_blockSizeZ = 100.0f;
|
||||
static float s_jitter = 0.0f;
|
||||
static int s_seed = 1;
|
||||
static float s_origin[2] = { 0.0f, 0.0f };
|
||||
static bool s_avoidWater = true;
|
||||
static float s_maxSlope = 0.35f;
|
||||
static bool s_resolve = true;
|
||||
static bool s_pickArmed = false;
|
||||
static bool s_preview = false;
|
||||
static std::string s_genResult;
|
||||
|
||||
bool paramsChanged =
|
||||
ImGui::InputFloat2("Origin XZ", s_origin);
|
||||
if (!s_pickArmed) {
|
||||
if (ImGui::Button("Pick Origin on Terrain")) {
|
||||
/* The pick click is handled by road
|
||||
* edit mode. */
|
||||
if (ts)
|
||||
ts->setRoadEditMode(true);
|
||||
rs->armOriginPick(true);
|
||||
s_pickArmed = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Arm a terrain click that sets "
|
||||
"the generator origin XZ (uses "
|
||||
"road edit mode).");
|
||||
} else {
|
||||
ImGui::TextDisabled(
|
||||
"Click the terrain to set the "
|
||||
"origin...");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Cancel")) {
|
||||
rs->armOriginPick(false);
|
||||
s_pickArmed = false;
|
||||
}
|
||||
}
|
||||
Ogre::Vector3 pickedOrigin;
|
||||
if (rs->takePickedOrigin(pickedOrigin)) {
|
||||
s_origin[0] = pickedOrigin.x;
|
||||
s_origin[1] = pickedOrigin.z;
|
||||
s_pickArmed = false;
|
||||
paramsChanged = true;
|
||||
}
|
||||
paramsChanged |= ImGui::SliderInt("Blocks X", &s_blocksX, 1,
|
||||
20);
|
||||
paramsChanged |= ImGui::SliderInt("Blocks Z", &s_blocksZ, 1,
|
||||
20);
|
||||
paramsChanged |= ImGui::SliderFloat("Block Size X",
|
||||
&s_blockSizeX,
|
||||
30.0f, 500.0f,
|
||||
"%.0f m");
|
||||
paramsChanged |= ImGui::SliderFloat("Block Size Z",
|
||||
&s_blockSizeZ,
|
||||
30.0f, 500.0f,
|
||||
"%.0f m");
|
||||
paramsChanged |= ImGui::SliderFloat("Jitter", &s_jitter,
|
||||
0.0f, 20.0f,
|
||||
"%.1f m");
|
||||
paramsChanged |= ImGui::InputInt("Seed", &s_seed);
|
||||
paramsChanged |= ImGui::Checkbox("Avoid Water",
|
||||
&s_avoidWater);
|
||||
paramsChanged |= ImGui::SliderFloat("Max Edge Slope",
|
||||
&s_maxSlope, 0.0f,
|
||||
1.0f, "%.2f");
|
||||
ImGui::Checkbox("Resolve Crossings", &s_resolve);
|
||||
|
||||
auto buildParams = [&]() {
|
||||
RoadNetworkGen::GridParams p;
|
||||
p.origin = Ogre::Vector3(s_origin[0], 0.0f,
|
||||
s_origin[1]);
|
||||
p.blocksX = s_blocksX;
|
||||
p.blocksZ = s_blocksZ;
|
||||
p.blockSizeX = s_blockSizeX;
|
||||
p.blockSizeZ = s_blockSizeZ;
|
||||
p.jitter = s_jitter;
|
||||
p.seed = (uint32_t)s_seed;
|
||||
p.avoidWater = s_avoidWater;
|
||||
p.maxSlope = s_maxSlope;
|
||||
p.runCrossingResolution = s_resolve;
|
||||
return p;
|
||||
};
|
||||
|
||||
if (ImGui::Checkbox("Preview Grid", &s_preview)) {
|
||||
if (!s_preview)
|
||||
rs->clearGenPreview();
|
||||
else
|
||||
paramsChanged = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Draw the would-be grid as orange lines "
|
||||
"without touching the road graph; "
|
||||
"refreshes on every parameter change.");
|
||||
if (s_preview && paramsChanged)
|
||||
rs->updateGenPreview(buildParams());
|
||||
|
||||
if (ImGui::Button("Generate Grid")) {
|
||||
s_preview = false;
|
||||
RoadNetworkGen::GridParams p = buildParams();
|
||||
std::string err;
|
||||
uint32_t gid = rs->generateRoadNetwork(p, &err);
|
||||
char buf[256];
|
||||
if (gid == 0)
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Generation failed: %s",
|
||||
err.c_str());
|
||||
else
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Generated group %u (%zu nodes, "
|
||||
"%zu edges in the graph).",
|
||||
gid, rg.nodes.size(),
|
||||
rg.edges.size());
|
||||
s_genResult = buf;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Emit a deterministic seeded settlement grid. "
|
||||
"All generated nodes are stamped with a group id "
|
||||
"and can only be removed as a whole group.");
|
||||
if (ImGui::Button("Resolve Crossings Now")) {
|
||||
std::string err;
|
||||
int created = rs->resolveCrossingsNow(&err);
|
||||
char buf[256];
|
||||
if (created < 0)
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Crossing resolution failed: %s",
|
||||
err.c_str());
|
||||
else
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Crossing resolution: %d "
|
||||
"junction(s) created.",
|
||||
created);
|
||||
s_genResult = buf;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Turn every crossing of road edges into a "
|
||||
"proper junction (edges closer than "
|
||||
"crossingHeightThreshold in Y; larger gaps "
|
||||
"are overpasses and left alone).");
|
||||
if (!s_genResult.empty())
|
||||
ImGui::TextWrapped("%s", s_genResult.c_str());
|
||||
|
||||
/* Generated groups present in the graph; each is
|
||||
* removed as a whole. */
|
||||
std::map<uint32_t, int> groupCounts;
|
||||
for (const RoadNode &n : rg.nodes)
|
||||
if (n.generatedGroupId != 0)
|
||||
++groupCounts[n.generatedGroupId];
|
||||
for (const auto &kv : groupCounts) {
|
||||
ImGui::PushID((int)kv.first);
|
||||
ImGui::Text("Group %u (%d nodes)", kv.first,
|
||||
kv.second);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Remove")) {
|
||||
rs->clearGenerated(kv.first);
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
/* Phase 4: procedural town generation on the road lots
|
||||
* (TownGenSystem + roadlib/TownGen). */
|
||||
if (ImGui::TreeNode("Town Generator")) {
|
||||
static int s_district = 0;
|
||||
static int s_townSeed = 1;
|
||||
static float s_density = 1.0f;
|
||||
static std::string s_townResult;
|
||||
const char *districts[] = {
|
||||
"Village", "Town", "City Residential",
|
||||
"City Center"
|
||||
};
|
||||
|
||||
TownGenSystem *tgs = ts->getTownGenSystem();
|
||||
if (!tgs) {
|
||||
ImGui::TextDisabled(
|
||||
"Town generation is unavailable.");
|
||||
} else {
|
||||
|
||||
ImGui::Combo("District", &s_district, districts, 4);
|
||||
ImGui::InputInt("Seed##town", &s_townSeed);
|
||||
ImGui::SliderFloat("Density##town", &s_density, 0.0f, 1.0f,
|
||||
"%.2f");
|
||||
|
||||
if (ImGui::Button("Generate Town")) {
|
||||
TownGen::TownParams p;
|
||||
p.district = (TownGen::District)s_district;
|
||||
p.seed = (uint32_t)s_townSeed;
|
||||
p.densityScale = s_density;
|
||||
std::string err;
|
||||
int placed = tgs->generate(p, &err);
|
||||
char buf[256];
|
||||
if (placed == 0)
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Town generation failed: %s",
|
||||
err.c_str());
|
||||
else
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Placed %d buildings.",
|
||||
placed);
|
||||
s_townResult = buf;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Place one building per suitable road lot "
|
||||
"(deterministic under the seed). Replaces "
|
||||
"the previously generated town.");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear Town")) {
|
||||
int removed = tgs->clear();
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Removed %d town(s).", removed);
|
||||
s_townResult = buf;
|
||||
}
|
||||
if (!s_townResult.empty())
|
||||
ImGui::TextWrapped("%s", s_townResult.c_str());
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
/* Toolbar — visible only in road edit mode. */
|
||||
if (ts->getRoadEditMode()) {
|
||||
/* Tool selector: Move Node (default) / Add Node /
|
||||
@@ -831,10 +1101,28 @@ private:
|
||||
ts->setRoadEditTool(
|
||||
TerrainSystem::RoadEditTool::Connect);
|
||||
|
||||
if (ImGui::Button("Remove Selected Node") &&
|
||||
rs->getSelectedNodeId() >= 0) {
|
||||
rg.removeNode(rs->getSelectedNodeId());
|
||||
rs->setSelectedNodeId(-1);
|
||||
{
|
||||
const RoadNode *selNode =
|
||||
rs->getSelectedNodeId() >= 0 ?
|
||||
rg.findNodeById(
|
||||
rs->getSelectedNodeId()) :
|
||||
nullptr;
|
||||
bool selGrouped = selNode &&
|
||||
selNode->generatedGroupId != 0;
|
||||
ImGui::BeginDisabled(selGrouped);
|
||||
if (ImGui::Button("Remove Selected Node") &&
|
||||
rs->getSelectedNodeId() >= 0) {
|
||||
rg.removeNode(rs->getSelectedNodeId());
|
||||
rs->setSelectedNodeId(-1);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (selGrouped &&
|
||||
ImGui::IsItemHovered(
|
||||
ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip(
|
||||
"Generated-group node: remove the "
|
||||
"whole group in the Road Network "
|
||||
"Generator section.");
|
||||
}
|
||||
|
||||
static std::string s_roadValidationError;
|
||||
@@ -1038,6 +1326,9 @@ private:
|
||||
for (const auto &n : rg.nodes) {
|
||||
bool selected = (n.id == selectedNodeId);
|
||||
std::string label = "Node " + std::to_string(n.id);
|
||||
if (n.generatedGroupId != 0)
|
||||
label += " [gen " +
|
||||
std::to_string(n.generatedGroupId) + "]";
|
||||
bool showConnect =
|
||||
selectedNodeId >= 0 && selectedNodeId != n.id;
|
||||
float rowWidth = ImGui::GetContentRegionAvail().x;
|
||||
@@ -1089,6 +1380,12 @@ private:
|
||||
std::string label = "Edge " + std::to_string(i) + ": " +
|
||||
std::to_string(e.nodeA) + " <-> " +
|
||||
std::to_string(e.nodeB);
|
||||
/* Generated-group edges are unsplittable/unremovable:
|
||||
* the group is managed as a whole. */
|
||||
const RoadNode *ea = rg.findNodeById(e.nodeA);
|
||||
const RoadNode *eb = rg.findNodeById(e.nodeB);
|
||||
bool eGrouped = (ea && ea->generatedGroupId != 0) ||
|
||||
(eb && eb->generatedGroupId != 0);
|
||||
float rowWidth = ImGui::GetContentRegionAvail().x;
|
||||
rowWidth -= ImGui::CalcTextSize("Split").x +
|
||||
ImGui::GetStyle().FramePadding.x * 2.0f +
|
||||
@@ -1105,6 +1402,7 @@ private:
|
||||
rs->setSelectedNodeId(-1);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(eGrouped);
|
||||
if (ImGui::SmallButton("Split")) {
|
||||
int newId = rg.splitEdge(i, 0.5f);
|
||||
/* The midpoint Y is interpolated between the
|
||||
@@ -1126,6 +1424,14 @@ private:
|
||||
rs->setSelectedEdgeIndex(-1);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (eGrouped &&
|
||||
ImGui::IsItemHovered(
|
||||
ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip(
|
||||
"Generated-group edge: split/remove is "
|
||||
"disabled; manage the group in the Road "
|
||||
"Network Generator section.");
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
@@ -1417,6 +1723,154 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
/* Markings tri-state (phase 2.5): Inherit
|
||||
* follows RoadConfig::markingsEnabled, On/Off
|
||||
* force this edge. */
|
||||
{
|
||||
const char *mitems[3] = { "Inherit", "On",
|
||||
"Off" };
|
||||
int mcur = (int)e.markingsMode;
|
||||
if (ImGui::Combo("Markings", &mcur,
|
||||
mitems, 3)) {
|
||||
e.markingsMode =
|
||||
(RoadMarkingMode)mcur;
|
||||
edgeChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Street assignment (phase 3): an edge belongs
|
||||
* to at most one named street; streets give
|
||||
* lots their addresses. */
|
||||
{
|
||||
const RoadStreet *st =
|
||||
RoadNetworkGen::findStreetForEdge(
|
||||
rg, e.nodeA, e.nodeB);
|
||||
static char s_streetBuf[128] = {};
|
||||
if (st)
|
||||
ImGui::Text("Street: %s",
|
||||
st->name.c_str());
|
||||
else
|
||||
ImGui::TextDisabled(
|
||||
"Street: (none)");
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
ImGui::InputText("##streetname",
|
||||
s_streetBuf,
|
||||
sizeof(s_streetBuf));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Assign") &&
|
||||
s_streetBuf[0]) {
|
||||
RoadNetworkGen::nameStreet(
|
||||
rg, s_streetBuf,
|
||||
{ std::make_pair(e.nodeA,
|
||||
e.nodeB) });
|
||||
edgeChanged = true;
|
||||
}
|
||||
if (st) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Unassign")) {
|
||||
std::pair<int, int> key(
|
||||
std::min(e.nodeA,
|
||||
e.nodeB),
|
||||
std::max(e.nodeA,
|
||||
e.nodeB));
|
||||
for (RoadStreet &s :
|
||||
rg.streets) {
|
||||
auto &edges =
|
||||
s.edges;
|
||||
edges.erase(
|
||||
std::remove(
|
||||
edges.begin(),
|
||||
edges.end(),
|
||||
key),
|
||||
edges.end());
|
||||
}
|
||||
for (size_t si =
|
||||
rg.streets.size();
|
||||
si-- > 0;)
|
||||
if (rg.streets[si]
|
||||
.edges
|
||||
.empty())
|
||||
rg.streets.erase(
|
||||
rg.streets.begin() +
|
||||
si);
|
||||
edgeChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Decals (phase 2.5): atlas-textured quads
|
||||
* painted on the road surface. The preset
|
||||
* combo selects one of the four quadrants of
|
||||
* the shared decal atlas; the uv rect stays
|
||||
* directly editable for custom atlases. */
|
||||
if (ImGui::TreeNode("Decals")) {
|
||||
static const char *s_presets[4] = {
|
||||
"Manhole", "Arrow", "Damage",
|
||||
"Parking Bars"
|
||||
};
|
||||
static const float s_presetUv[4][4] = {
|
||||
{ 0.0f, 0.0f, 0.5f, 0.5f },
|
||||
{ 0.5f, 0.0f, 0.5f, 0.5f },
|
||||
{ 0.0f, 0.5f, 0.5f, 0.5f },
|
||||
{ 0.5f, 0.5f, 0.5f, 0.5f }
|
||||
};
|
||||
int removeDecal = -1;
|
||||
for (size_t di = 0; di < e.decals.size();
|
||||
++di) {
|
||||
RoadEdgeDecal &d = e.decals[di];
|
||||
ImGui::PushID((int)di + 1000);
|
||||
int preset = -1;
|
||||
for (int k = 0; k < 4; ++k)
|
||||
if (d.uvX == s_presetUv[k][0] &&
|
||||
d.uvY == s_presetUv[k][1] &&
|
||||
d.uvW == s_presetUv[k][2] &&
|
||||
d.uvH == s_presetUv[k][3])
|
||||
preset = k;
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
if (ImGui::Combo(
|
||||
"##preset", &preset,
|
||||
s_presets, 4)) {
|
||||
d.uvX = s_presetUv[preset][0];
|
||||
d.uvY = s_presetUv[preset][1];
|
||||
d.uvW = s_presetUv[preset][2];
|
||||
d.uvH = s_presetUv[preset][3];
|
||||
edgeChanged = true;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
edgeChanged |= ImGui::SliderFloat(
|
||||
"T", &d.edgeT, 0.0f, 1.0f);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X"))
|
||||
removeDecal = (int)di;
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
edgeChanged |= ImGui::SliderFloat(
|
||||
"Lateral (m)",
|
||||
&d.lateralOffset, -15.0f,
|
||||
15.0f);
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
edgeChanged |= ImGui::SliderFloat(
|
||||
"Size (m)", &d.size, 0.3f,
|
||||
8.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (removeDecal >= 0) {
|
||||
e.decals.erase(e.decals.begin() +
|
||||
removeDecal);
|
||||
edgeChanged = true;
|
||||
}
|
||||
if (ImGui::SmallButton("Add Decal")) {
|
||||
RoadEdgeDecal d;
|
||||
d.uvX = s_presetUv[0][0];
|
||||
d.uvY = s_presetUv[0][1];
|
||||
d.uvW = s_presetUv[0][2];
|
||||
d.uvH = s_presetUv[0][3];
|
||||
e.decals.push_back(d);
|
||||
edgeChanged = true;
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
/* Edge prefab slots (improvement plan §3):
|
||||
* left/right curb anchors plus one centerline
|
||||
* midpoint prefab. */
|
||||
|
||||
Reference in New Issue
Block a user