Procedural road geometry fixes
This commit is contained in:
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.13.0)
|
||||
project(world2)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
enable_testing()
|
||||
set(BLENDER "${CMAKE_SOURCE_DIR}/../../blender-bin/bin/blender" CACHE STRING "Blender path")
|
||||
set(CREATE_DIRECTORIES
|
||||
${CMAKE_BINARY_DIR}/assets/blender/shapes/male
|
||||
|
||||
@@ -402,6 +402,7 @@ target_link_libraries(editSceneEditor
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
PackageArchive
|
||||
RoadGeometryLib
|
||||
lua
|
||||
SDL2::SDL2
|
||||
)
|
||||
|
||||
@@ -32,9 +32,12 @@ Phase 1: buildConcatenatedStrip(template, N)
|
||||
→ Straight strip of N concatenated template copies along -Z.
|
||||
|
||||
Phase 2: transformWedgeVertices(strip, wedge, graph)
|
||||
→ Bend the strip into the wedge shape. The outer-curb offset is
|
||||
interpolated through a narrow blend zone at the node, so the
|
||||
cross-section direction varies continuously — no gaps.
|
||||
→ Bend the strip into the wedge shape. The outer-curb offset
|
||||
follows the mitered curb chain — pinned at the miter corner K
|
||||
for inner wedges (sweep < 180°, so cross-sections cannot fold
|
||||
over each other), blended through K over a narrow zone for
|
||||
outer wedges — so the cross-section direction varies
|
||||
continuously: no gaps, no folds.
|
||||
|
||||
Phase 3: shiftSeamVertices(strip, wedge, graph)
|
||||
→ Shift centerline-side vertices near the node slightly past O
|
||||
@@ -55,6 +58,15 @@ static bool buildSegmentGeometry(const RoadStraightSegment &segment,
|
||||
|
||||
Degenerate wedges (sweptAngleDeg > 270°, flagged by `enumerateWedges()`) return false and emit nothing.
|
||||
|
||||
The single implementation lives in `roadlib/RoadGeometryLib.cpp`
|
||||
(namespace `RoadGeometryLib`); the public `RoadSystem` statics forward
|
||||
to it. The transformed wedge strip is already a closed tube (the
|
||||
template supplies top, bottom and curb faces), so it is appended to
|
||||
the output verbatim — slab extrusion (§8) applies to straight
|
||||
segments only. `RoadGeometryLib` also provides
|
||||
`loadTemplateFromMesh()` (template loading per §2) and
|
||||
`makeFallbackTemplate()`.
|
||||
|
||||
## 4. Phase 1 — Concatenated Strip
|
||||
|
||||
**Function**: `static void buildConcatenatedStrip(Procedural::TriangleBuffer &out, const Procedural::TriangleBuffer &templ, int N)`
|
||||
@@ -104,18 +116,58 @@ in1 = H1.lanesIn * lw // UV lateral offset for continuity
|
||||
yO = O.y + nodeRoadLevel(graph, seedNode)
|
||||
```
|
||||
|
||||
### 5.2 Blend Zone
|
||||
### 5.2 Corner Regimes and Blend Zone
|
||||
|
||||
A narrow symmetric zone around the node where the outer-curb offset
|
||||
transitions continuously from `w1 * r1` to `-w2 * r2`:
|
||||
The two constant-width curb lines
|
||||
|
||||
```
|
||||
W = min(ROAD_SEAM_OVERLAP * 4, // ~0.2 units — tight, keeps corners sharp
|
||||
L1 * 0.5f, L2 * 0.5f) // clamped for very short edges
|
||||
A(s) = O + offA + dir1 * s s in [0, L1]
|
||||
B(s) = O + offB + dir2 * s s in [0, L2]
|
||||
|
||||
offA = w1 * r1 // H1-side curb end at the node
|
||||
offB = -w2 * r2 // H2-side curb end at the node
|
||||
```
|
||||
|
||||
If L1 < ROAD_SEAM_OVERLAP or L2 < ROAD_SEAM_OVERLAP, W is set to 0
|
||||
(no blending needed — both segment ends are at nearly the same point).
|
||||
meet at the miter corner K. Expressed as parameters along each
|
||||
direction from the node-side curb ends:
|
||||
|
||||
```
|
||||
det = dir1.z * dir2.x - dir1.x * dir2.z
|
||||
rhs = offB - offA
|
||||
t1 = (dir2.x * rhs.z - dir2.z * rhs.x) / det
|
||||
t2 = (dir1.x * rhs.z - dir1.z * rhs.x) / det
|
||||
|
||||
cornerOff = offA + dir1 * t1 == offB + dir2 * t2
|
||||
K = O + cornerOff
|
||||
```
|
||||
|
||||
`|det| < 0.05` means the curb lines are (nearly) collinear —
|
||||
near-straight wedges drop the corner entirely.
|
||||
|
||||
* **Inner corner (converging, sweep < 180°): `t1 > 0` and `t2 > 0`** —
|
||||
the curb lines meet ahead of the node, inside the wedge. When K
|
||||
lies on both curb segments (`t1 <= L1` and `t2 <= L2`) the curb is
|
||||
**pinned at K** over the whole corner zone `[L1 - t1, L1 + t2]` (see
|
||||
§5.4): the curb arc around an inner corner is shorter than the
|
||||
centerline arc, so blending parallel cross-sections through the zone
|
||||
would fold them over each other — overlapping geometry when flat,
|
||||
turning into grossly intersecting ramp sheets once the two ends
|
||||
differ in height.
|
||||
* **Outer corner (diverging, sweep > 180°): `t1 < 0`** — the curb
|
||||
lines meet behind the node; cross-sections fan out and cannot fold.
|
||||
The offset blends through K over a narrow symmetric zone around the
|
||||
node:
|
||||
|
||||
```
|
||||
W = min(ROAD_SEAM_OVERLAP * 4, // ~0.2 units — keeps corners sharp
|
||||
L1 * 0.5f, L2 * 0.5f) // clamped for very short edges
|
||||
```
|
||||
|
||||
If L1 < ROAD_SEAM_OVERLAP or L2 < ROAD_SEAM_OVERLAP, W is set to 0
|
||||
(no blending needed — both segment ends are at nearly the same point).
|
||||
* **Notch fallback** — an inner corner whose K falls outside either
|
||||
curb segment (`t1 > L1` or `t2 > L2`; a very wide road on very short
|
||||
edges) uses the same narrow blend as an outer corner.
|
||||
|
||||
### 5.3 Centerline Position
|
||||
|
||||
@@ -137,45 +189,78 @@ at O is shared between adjacent wedges.
|
||||
|
||||
### 5.4 Outer-Curb Offset (Continuous Across the Node)
|
||||
|
||||
The outer-curb offset `offset(d)` is the vector from `center(d)` to the
|
||||
outer curb at distance d. It transitions **continuously** from `w1 * r1`
|
||||
(H1 side) to `-w2 * r2` (H2 side) through the blend zone:
|
||||
The outer-curb offset `offset(d)` is the vector from `center(d)` to
|
||||
the outer curb at distance d. Its behaviour depends on the corner
|
||||
regime (§5.2).
|
||||
|
||||
**Inner corner — curb pinned at the miter corner K:**
|
||||
|
||||
```
|
||||
zoneStart = L1 - t1
|
||||
zoneEnd = L1 + t2
|
||||
|
||||
if d <= zoneStart: offset(d) = offA
|
||||
if d >= zoneEnd: offset(d) = offB
|
||||
else: offset(d) = K - center(d)
|
||||
```
|
||||
|
||||
Inside the zone every cross-section aims its outer-curb end exactly at
|
||||
K, so consecutive sections share the endpoint K and cannot cross each
|
||||
other. The rule is C0-continuous: at `zoneStart` it equals offA
|
||||
exactly (K lies on curb line A) and at `zoneEnd` it equals offB. The
|
||||
outer curb wall collapses to the vertical line at K inside the zone
|
||||
(zero-area quads) — the geometrically correct miter joint.
|
||||
|
||||
**Outer corner, notch fallback, or no corner — narrow blend:**
|
||||
|
||||
```
|
||||
if W == 0 or d <= L1 - W:
|
||||
offset(d) = w1 * r1
|
||||
offset(d) = offA
|
||||
elif d >= L1 + W:
|
||||
offset(d) = -w2 * r2
|
||||
else:
|
||||
offset(d) = offB
|
||||
elif no corner (|det| < 0.05):
|
||||
t = (d - (L1 - W)) / (2 * W) // 0 → 1 across blend zone
|
||||
offset(d) = lerp(w1 * r1, -w2 * r2, t)
|
||||
offset(d) = lerp(offA, offB, t)
|
||||
elif d <= L1:
|
||||
t = (d - (L1 - W)) / W
|
||||
offset(d) = lerp(offA, cornerOff, t)
|
||||
else:
|
||||
t = (d - L1) / W
|
||||
offset(d) = lerp(cornerOff, offB, t)
|
||||
```
|
||||
|
||||
Linear vector interpolation works because both `w1*r1` and `-w2*r2`
|
||||
point into the wedge interior (they are the directions to the outer
|
||||
curb on each side). The interpolated vector never passes through
|
||||
zero for non-degenerate wedges — it always points somewhere within
|
||||
the wedge.
|
||||
The blend passes exactly through the miter corner at the node, so no
|
||||
hole opens at the outer corner. Both `offA` and `offB` point into the
|
||||
wedge interior, so the interpolated vector never passes through zero
|
||||
for non-degenerate wedges.
|
||||
|
||||
### 5.5 Road Width Interpolation
|
||||
### 5.5 Effective Road Width
|
||||
|
||||
The scalar road half-width varies linearly across the wedge:
|
||||
The scalar road half-width at distance d is the offset magnitude:
|
||||
|
||||
```
|
||||
width(d) = w1 + (w2 - w1) * (d / L)
|
||||
width(d) = |offset(d)|
|
||||
```
|
||||
|
||||
It equals w1 on the first half-edge and w2 on the second half-edge,
|
||||
widens through outer miter corners, and shrinks toward the pinned
|
||||
corner K for inner wedges. It is used for the lateral UV scale only
|
||||
(§5.7).
|
||||
|
||||
### 5.6 Surface Height
|
||||
|
||||
```
|
||||
roadSurfaceY(d):
|
||||
if d <= L1: return halfEdgeHeightAt(H1, graph, d)
|
||||
if d <= L1: return halfEdgeHeightAt(H1, graph, L1 - d)
|
||||
else: return halfEdgeHeightAt(H2, graph, d - L1)
|
||||
```
|
||||
|
||||
`halfEdgeHeightAt(he, graph, d)` (existing helper, RoadSystem.cpp:1183)
|
||||
returns the absolute world Y of the road surface at distance d from
|
||||
the seed node, using linear interpolation of the edge's roadLevel values.
|
||||
`halfEdgeHeightAt(he, graph, t)` returns the absolute world Y of the
|
||||
road surface at distance t **from the seed node** O. d is the distance
|
||||
from the wedge start M_A, so the distance from O along H1 is L1 - d
|
||||
(back toward the midpoint), while the distance from O along H2 is
|
||||
d - L1 (forward toward M_B). Passing d directly to H1 inverts the
|
||||
height profile along the first half-edge.
|
||||
|
||||
### 5.7 Per-Vertex Transform
|
||||
|
||||
@@ -184,24 +269,25 @@ For each vertex `v` at template position (vx, vy, vz):
|
||||
```
|
||||
d = -vz // guaranteed to be in [0, L]
|
||||
|
||||
localWidth = width(d)
|
||||
lateral = vx * localWidth // template X∈[0,1] → world distance
|
||||
lateralDir = normalize(offset(d)) // unit vector toward outer curb
|
||||
|
||||
worldXZ = center(d) + lateral * lateralDir
|
||||
// Template X maps along the curb offset (direction AND magnitude —
|
||||
// the offset itself widens through outer miter corners and aims at
|
||||
// the pinned corner K for inner wedges):
|
||||
worldXZ = center(d) + offset(d) * vx
|
||||
worldY = roadSurfaceY(d) + vy
|
||||
|
||||
v.position = Vector3(worldXZ.x, worldY, worldXZ.z)
|
||||
|
||||
// UV — longitudinal U from halfEdgeU (phase-continuous), lateral V scaled
|
||||
v.uv.x = (d <= L1) ? halfEdgeU(H1, graph, d)
|
||||
// UV — longitudinal U from halfEdgeU (phase-continuous), lateral V
|
||||
// scaled by the effective width:
|
||||
v.uv.x = (d <= L1) ? halfEdgeU(H1, graph, L1 - d)
|
||||
: halfEdgeU(H2, graph, d - L1)
|
||||
v.uv.y = v.uv.y * localWidth + in1
|
||||
v.uv.y = v.uv.y * width(d) + in1
|
||||
|
||||
// Normal — rotate template-forward (-Z) to segment direction:
|
||||
// Normal — rotate template-forward (-Z) to segment direction by the
|
||||
// SIGNED angle around Y:
|
||||
segDir = (d <= L1) ? dir1 : dir2
|
||||
Ogre::Quaternion q(segDir.angleBetween(Ogre::Vector3::NEGATIVE_UNIT_Z),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
theta = atan2(-segDir.x, -segDir.z)
|
||||
Ogre::Quaternion q(Ogre::Radian(theta), Ogre::Vector3::UNIT_Y);
|
||||
v.normal = q * v.normal;
|
||||
```
|
||||
|
||||
@@ -346,25 +432,39 @@ Where `refPoint` is the centroid of `centerSurf`.
|
||||
|
||||
### 8.3 Application
|
||||
|
||||
- **Wedge**: After Phase 2+3, the strip contains the center-surface triangles
|
||||
(from the template index buffer, transformed). Pass to `extrudeToSlab`.
|
||||
- **Segment**: After building the center-surface band, pass to `extrudeToSlab`.
|
||||
- **Wedge**: no extrusion. After Phase 2+3 the strip is already a
|
||||
closed tube around the road body — the template supplies top, bottom
|
||||
and curb faces, and `appendTemplateCopy` drops only the template
|
||||
caps and the centerline wall (the open ends butt exactly against the
|
||||
neighbouring pieces at the edge midpoints, the open centerline side
|
||||
against the adjacent wedge). The transformed strip is appended to
|
||||
the output verbatim. (Re-extruding it additionally stacked coplanar
|
||||
sheets at the strip's center surface and doubled the slab
|
||||
thickness.)
|
||||
- **Segment**: the center-surface band (§7) is flat, so it is passed
|
||||
to `extrudeToSlab`, keeping the far-end edge open (it meets the
|
||||
neighbour node's piece exactly).
|
||||
|
||||
In both cases, the boundary edges are: the outer curb chain, the start cap,
|
||||
and the end cap. Centerline edges (O→M_A, O→M_B) are interior and get no
|
||||
skirts — they meet adjacent wedge pieces.
|
||||
In the segment case the boundary edges are: the outer curb chain, the
|
||||
start cap, and the end cap. Centerline edges (O→M_A, O→M_B) are
|
||||
interior and get no skirts — they meet adjacent road pieces.
|
||||
|
||||
## 9. Seam Suppression Summary
|
||||
|
||||
| Mechanism | What it fixes | Where |
|
||||
|-----------|--------------|-------|
|
||||
| Continuous curb offset (§5.4) | Outer-corner gap where H1 and H2 diverge | Phase 2 |
|
||||
| Curb pinned at miter corner K (§5.4) | Inner-corner cross-section fold (overlapping geometry) | Phase 2 |
|
||||
| ROAD_SEAM_OVERLAP on segments (§7) | Center gap for dead-end nodes | Segment band |
|
||||
| Center seam shifting (§6) | Center hole where >2 wedges meet | Phase 3 |
|
||||
| Slab extrusion (§8) | Road must be a closed solid | Post-Phase 3 |
|
||||
| Slab extrusion (§8) | Road must be a closed solid | Segments |
|
||||
|
||||
## 10. Internal Functions (Testable)
|
||||
|
||||
All of these live in namespace `RoadGeometryLib`
|
||||
(`roadlib/RoadGeometryLib.cpp`); the public `RoadSystem` statics for
|
||||
the entry points forward to them.
|
||||
|
||||
```cpp
|
||||
// Phase 1
|
||||
static void buildConcatenatedStrip(Procedural::TriangleBuffer &out,
|
||||
@@ -398,12 +498,16 @@ static Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
|
||||
| Test | Setup | d=0 expected | d=L1 (node) expected | d=L expected |
|
||||
|------|-------|-------------|---------------------|-------------|
|
||||
| 90° wedge, w1=w2=3 | dir1=+X, dir2=+Z | (0,0,3) | (1.5,0,1.5) | (3,0,0) |
|
||||
| 270° wedge, w1=w2=3 | dir1=+Z, dir2=+X | (-3,0,0) | (-1.5,0,-1.5) | (0,0,-3) |
|
||||
| 90° wedge, w1=w2=3 | dir1=+X, dir2=+Z | (0,0,3) | (3,0,3) — pinned at K | (3,0,0) |
|
||||
| 270° wedge, w1=w2=3 | dir1=+Z, dir2=+X | (-3,0,0) | (-3,0,-3) — blend through K | (0,0,-3) |
|
||||
| 180° straight, w1=w2=3 | dir1=+X, dir2=-X | (0,0,3) | (0,0,3) | (0,0,3) |
|
||||
| Asymmetric w1=6,w2=3 | 90° | (0,0,6) | (1.5,0,4.5) | (3,0,0) |
|
||||
| Asymmetric w1=6,w2=3 | 90° | (0,0,6) | (3,0,6) — pinned at K | (3,0,0) |
|
||||
| Blend zone continuity | Any | offset varies with d | no discontinuity at L1 | — |
|
||||
|
||||
For converging (inner) wedges the offset at the zone boundaries is
|
||||
exactly offA (at d = L1 - t1) and offB (at d = L1 + t2); inside the
|
||||
zone it is `K - center(d)`.
|
||||
|
||||
### 11.2 Integration Tests (matching existing `testRoadWedgeGeometry`)
|
||||
|
||||
| Test | Expected |
|
||||
@@ -411,7 +515,8 @@ static Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
| Segment slab (0,0,0)–(20,0,0), 1+1 lanes | X∈[-0.05,10], Z∈[-3,3], Y top≈+0.15, bot≈-0.15 |
|
||||
| Elevated nodes y=10 | Top Y≈10.15 (not 20.15 — regression test) |
|
||||
| Asymmetric (2 out, 1 in) | Z∈[-3,6] |
|
||||
| 90° L-corner | All XZ∈[0,10]², outer corner near (3,y,3), node vertex at (0,y,0) |
|
||||
| 90° L-corner | All XZ∈[0,10]², outer corner at (3,y,3), node vertex at (0,y,0), Y∈[-0.15,+0.15] (no second extrusion) |
|
||||
| 135° converging corner, flat and with corner node raised | No coplanar-overlapping or piercing triangle pairs in any wedge or segment (fold regression) |
|
||||
| 270° wrap | XZ∈[-3,10]², outer corner near (-3,y,-3) |
|
||||
| 180° straight-through | Two rectangular halves, Z∈[0,3] and [-3,0], no bowing |
|
||||
| Degenerate (>270°) | Returns false, empty output |
|
||||
@@ -487,3 +592,36 @@ The current implementation ignores the template from M5.3. This
|
||||
specification makes `getRoadTemplate()` meaningful: a custom `.mesh`
|
||||
with curb profiles or road crowning produces detailed geometry
|
||||
automatically through the concatenate-and-transform pipeline.
|
||||
|
||||
## 14. Standalone Demo (RoadGeometryDemo)
|
||||
|
||||
Target `RoadGeometryDemo` (`road_demo/main.cpp`) is a small OGRE +
|
||||
ImGui application for interactive inspection of the wedge pipeline.
|
||||
It links only `RoadGeometryLib` — no ECS, terrain or physics — so it
|
||||
builds and starts fast.
|
||||
|
||||
- Three world-space points A, B, C define two road edges A–B and B–C.
|
||||
Sliders adjust every point coordinate (including Y, for height
|
||||
differences at the corner node); the two wedges at node B (smaller
|
||||
and larger sweep) are rebuilt live via
|
||||
`RoadGeometryLib::buildWedgeGeometry`.
|
||||
- Road configuration sliders: lane width, lanes per direction, road
|
||||
thickness.
|
||||
- "Wedge to display" radio: Smaller / Larger / Both.
|
||||
- "Template Mesh" panel: type an OGRE `.mesh` resource name and press
|
||||
**Load** — the mesh is loaded through
|
||||
`RoadGeometryLib::loadTemplateFromMesh` and normalised into template
|
||||
space per §2 (convention violations are logged but the mesh is still
|
||||
used). Enable **Use custom template** to build the wedges with it
|
||||
instead of the generated fallback box (§2); a status line shows the
|
||||
current template state.
|
||||
- The generated slab is drawn solid plus a wireframe overlay, with
|
||||
visual aids for the nodes, edge midpoints and edge lines.
|
||||
- Camera: right-drag to orbit, mouse wheel to zoom, ESC to exit.
|
||||
|
||||
Build and run:
|
||||
|
||||
```bash
|
||||
cmake --build <build-dir> --target RoadGeometryDemo
|
||||
./RoadGeometryDemo
|
||||
```
|
||||
|
||||
@@ -2135,7 +2135,7 @@ verification plan, manual test procedures, and open questions.
|
||||
| M5.3 Road mesh template | ✅ complete | `RoadSystem::getRoadTemplate()`, fallback box, `roadTemplate` test |
|
||||
| M5.4 Road geometry generation | ✅ complete | page tracking + wedge bucketing in `RoadSystem`, `roadPageAssignment` test |
|
||||
| M5.5 Wedge enumeration | ✅ complete | `enumerateWedges()` in `RoadGraph.hpp`, `validate()` angle checks, `roadWedgeEnumeration` test |
|
||||
| M5.6 Wedge geometry | ✅ complete | mitered polyline sweep: `computeWedgeOutline`/`triangulateOutline` + `emitSlab` in `RoadSystem`, `roadWedgeGeometry` test |
|
||||
| M5.6 Wedge geometry | ✅ complete | template-strip polyline sweep in `RoadGeometryLib` (curb pinned at the miter corner for inner wedges), forwarded from `RoadSystem`, `roadWedgeGeometry` test |
|
||||
| M5.7 Edge length constraint | ✅ complete | `snapToIntegerLength()` + `ROAD_MIN_EDGE_LENGTH`; `splitEdge` snaps, `joinNodes` warns, `validate` rejects short edges; `roadEdgeLength` test green |
|
||||
| M5.8 Mesh assembly per page | ✅ complete | page entities with `TriangleBufferComponent(proceduralContent)` + `RenderableComponent` + `NavMeshGeometrySource` + `LodComponent`, `roadPageMeshes` test |
|
||||
| M5.9 Road physics colliders | ✅ complete | `createPageCollider`/`destroyPageCollider` in `RoadSystem`, asserted in `roadPageMeshes` |
|
||||
@@ -2656,6 +2656,33 @@ section uses the miter frame, so both segments' curb lines meet in one
|
||||
outer corner `X` and the road keeps its exact width through the turn —
|
||||
no holes at the node, no overlaps, no width distortion at corners.
|
||||
|
||||
**Status update (2026-08-09): inner-corner fold and double extrusion
|
||||
fixed; implementation consolidated in `RoadGeometryLib`.** Two defects
|
||||
were found with the standalone demo (`road_demo/main.cpp`, target
|
||||
`RoadGeometryDemo`; see ProceduralRoadGeometry.md §14):
|
||||
|
||||
1. Inner corner wedges (sweep < 180°) self-intersected: blending
|
||||
cross-sections through the miter corner folded consecutive sections
|
||||
over each other near the corner (coplanar z-fighting sheets when
|
||||
flat, grossly intersecting ramps once the two ends differed in
|
||||
height). The curb is now **pinned at the miter corner K** over the
|
||||
whole corner zone `[L1 - t1, L1 + t2]` (t1/t2 are the corner
|
||||
parameters along each curb line — ProceduralRoadGeometry.md
|
||||
§5.2/§5.4); outer wedges keep the narrow blend through K.
|
||||
2. Wedge strips were extruded a second time (`extrudeToSlab` on top of
|
||||
the already closed template tube), doubling the slab thickness and
|
||||
stacking coplanar sheets. The transformed strip is now appended
|
||||
verbatim; `extrudeToSlab` remains for the flat segment bands only.
|
||||
|
||||
The geometry pipeline now lives once in `roadlib/RoadGeometryLib.cpp`
|
||||
(namespace `RoadGeometryLib`) — including `loadTemplateFromMesh()` —
|
||||
and the `RoadSystem` statics forward to it, replacing the duplicated
|
||||
copy in `RoadSystem.cpp`. The `roadWedgeGeometry` headless test gained
|
||||
a self-intersection checker (coplanar-overlap + piercing triangle
|
||||
pairs) with a 135° converging-corner regression case (flat and with a
|
||||
raised corner node) plus wedge slab-thickness bounds; all 22 headless
|
||||
tests pass.
|
||||
|
||||
An earlier radial-sweep attempt (2026-07-31, reverted 2026-08-02) swept a
|
||||
constant-width band along the outer-curb polyline: it left holes at every
|
||||
node center, collapsed > 180° wedges to a diagonal band across the node,
|
||||
@@ -2705,7 +2732,9 @@ Headless coverage: `roadWedgeGeometry` test (segment extents incl. top and
|
||||
bottom surfaces, elevated-node height regression, asymmetric lanes, 90°
|
||||
mitered hexagon without overshoot + outer corner (3,3) + node vertex,
|
||||
270° wrap-around miter corner (-3,-3), 180° straight-through rectangles,
|
||||
degenerate wedge rejection).
|
||||
degenerate wedge rejection, wedge slab-thickness bounds, and a
|
||||
self-intersection scan — coplanar-overlap + piercing triangle pairs — on
|
||||
a 135° converging corner, flat and with a raised corner node).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* Three world-space points A, B, C define two road edges A-B and B-C.
|
||||
* The wedge at node B bounded by its incident half-edge midpoints is
|
||||
* generated via RoadGeometryLib and rendered as a ManualObject.
|
||||
* ImGui controls let you adjust points/config in real-time.
|
||||
* ImGui controls let you adjust points/config in real-time; a custom
|
||||
* template mesh (OGRE .mesh resource name) can be loaded to preview
|
||||
* user road cross-sections instead of the generated fallback box.
|
||||
*
|
||||
* Build: cmake --build <build> --target RoadGeometryDemo
|
||||
* Run: ./RoadGeometryDemo
|
||||
@@ -109,11 +111,17 @@ private:
|
||||
float m_roadThickness = 0.3f;
|
||||
|
||||
/* State. */
|
||||
uint64_t m_lastConfigHash = 0;
|
||||
bool m_dirty = true;
|
||||
|
||||
/* 0 = smaller-angle wedge, 1 = larger-angle wedge, 2 = both */
|
||||
int m_wedgeMode = 2;
|
||||
|
||||
/* Custom template mesh selection. */
|
||||
char m_templateName[256] = {};
|
||||
bool m_useCustomTemplate = false;
|
||||
bool m_customTemplateLoaded = false;
|
||||
std::string m_templateStatus = "no custom template loaded";
|
||||
Procedural::TriangleBuffer m_customTemplate;
|
||||
};
|
||||
|
||||
DemoApp::DemoApp()
|
||||
@@ -246,16 +254,8 @@ bool DemoApp::frameStarted(const Ogre::FrameEvent &evt)
|
||||
|
||||
m_cameraMan->frameRendered(evt);
|
||||
|
||||
/* Detect config changes and rebuild. */
|
||||
uint64_t hash = (uint64_t)(m_pointA.x * 1000.0f) +
|
||||
((uint64_t)(m_pointA.z * 1000.0f) << 12) +
|
||||
((uint64_t)(m_pointB.x * 1000.0f) << 24) +
|
||||
((uint64_t)(m_pointB.z * 1000.0f) << 36) +
|
||||
((uint64_t)(m_pointC.x * 1000.0f) << 48) +
|
||||
((uint64_t)(m_pointC.z * 1000.0f) << 56);
|
||||
|
||||
if (hash != m_lastConfigHash || m_dirty) {
|
||||
m_lastConfigHash = hash;
|
||||
/* Rebuild if any parameter changed. */
|
||||
if (m_dirty) {
|
||||
m_dirty = false;
|
||||
rebuildWedgeGeometry();
|
||||
}
|
||||
@@ -330,9 +330,6 @@ void DemoApp::rebuildWedgeGeometry()
|
||||
Ogre::Vector3 posA = m_pointA;
|
||||
Ogre::Vector3 posB = m_pointB;
|
||||
Ogre::Vector3 posC = m_pointC;
|
||||
posA.y = 0.0f;
|
||||
posB.y = 0.0f;
|
||||
posC.y = 0.0f;
|
||||
|
||||
int idA = graph.addNode(posA, 0.0f);
|
||||
int idB = graph.addNode(posB, 0.0f);
|
||||
@@ -366,7 +363,14 @@ void DemoApp::rebuildWedgeGeometry()
|
||||
auto buildWedge = [&](const RoadWedge &w,
|
||||
Procedural::TriangleBuffer &buf) -> bool {
|
||||
Procedural::TriangleBuffer tmp;
|
||||
if (!RoadGeometryLib::buildWedgeGeometry(w, graph, tmp))
|
||||
bool built;
|
||||
if (m_useCustomTemplate && m_customTemplateLoaded)
|
||||
built = RoadGeometryLib::buildWedgeGeometry(
|
||||
w, graph, m_customTemplate, tmp);
|
||||
else
|
||||
built = RoadGeometryLib::buildWedgeGeometry(w, graph,
|
||||
tmp);
|
||||
if (!built)
|
||||
return false;
|
||||
int base = (int)buf.getVertices().size();
|
||||
for (const auto &v : tmp.getVertices()) {
|
||||
@@ -457,7 +461,7 @@ void DemoApp::rebuildWedgeGeometry()
|
||||
m_wedgeWireframe->setVisible(true);
|
||||
|
||||
m_wedgeTriangles->begin(
|
||||
"BaseWhiteNoLighting",
|
||||
"BaseWhite",
|
||||
Ogre::RenderOperation::OT_TRIANGLE_LIST);
|
||||
for (const auto &v : tb.getVertices()) {
|
||||
m_wedgeTriangles->position(v.mPosition);
|
||||
@@ -521,18 +525,21 @@ void DemoApp::renderImGui()
|
||||
|
||||
ImGui::TextColored(ImVec4(0, 1, 0, 1), "Point A (green)");
|
||||
changed |= ImGui::SliderFloat("A.x##A", &m_pointA.x, -20.0f, 20.0f);
|
||||
changed |= ImGui::SliderFloat("A.y##A", &m_pointA.y, -10.0f, 10.0f);
|
||||
changed |= ImGui::SliderFloat("A.z##A", &m_pointA.z, -20.0f, 20.0f);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextColored(ImVec4(1, 0.5f, 0, 1), "Point B (orange, seed)");
|
||||
changed |= ImGui::SliderFloat("B.x##B", &m_pointB.x, -20.0f, 20.0f);
|
||||
changed |= ImGui::SliderFloat("B.y##B", &m_pointB.y, -10.0f, 10.0f);
|
||||
changed |= ImGui::SliderFloat("B.z##B", &m_pointB.z, -20.0f, 20.0f);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Point C (red)");
|
||||
changed |= ImGui::SliderFloat("C.x##C", &m_pointC.x, -20.0f, 20.0f);
|
||||
changed |= ImGui::SliderFloat("C.y##C", &m_pointC.y, -10.0f, 10.0f);
|
||||
changed |= ImGui::SliderFloat("C.z##C", &m_pointC.z, -20.0f, 20.0f);
|
||||
|
||||
ImGui::Separator();
|
||||
@@ -544,6 +551,30 @@ void DemoApp::renderImGui()
|
||||
changed |= ImGui::SliderFloat("Road Thickness", &m_roadThickness,
|
||||
0.05f, 2.0f);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Template Mesh (optional)");
|
||||
ImGui::InputText("Mesh name", m_templateName, sizeof(m_templateName));
|
||||
if (ImGui::Button("Load")) {
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (RoadGeometryLib::loadTemplateFromMesh(m_templateName,
|
||||
buf)) {
|
||||
m_customTemplate = buf;
|
||||
m_customTemplateLoaded = true;
|
||||
m_templateStatus =
|
||||
std::string("loaded: ") + m_templateName;
|
||||
} else {
|
||||
m_customTemplateLoaded = false;
|
||||
m_useCustomTemplate = false;
|
||||
m_templateStatus = std::string("load failed: ") +
|
||||
m_templateName;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
ImGui::TextDisabled("%s", m_templateStatus.c_str());
|
||||
if (m_customTemplateLoaded &&
|
||||
ImGui::Checkbox("Use custom template", &m_useCustomTemplate))
|
||||
changed = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Derived info. */
|
||||
|
||||
@@ -263,6 +263,28 @@ static void buildWedgeStrip(Procedural::TriangleBuffer &out,
|
||||
* Phase 2 — Vertex Transformation
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/** Centerline position (polyline MA -> O -> MB) at path distance d. */
|
||||
static Ogre::Vector3 wedgeCenterAt(const RoadWedge &wedge,
|
||||
const RoadGraph &graph, float d)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(wedge.nodeId);
|
||||
if (!node)
|
||||
return Ogre::Vector3::ZERO;
|
||||
|
||||
const RoadHalfEdge &h1 = wedge.first;
|
||||
const RoadHalfEdge &h2 = wedge.second;
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f;
|
||||
float L2 = h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f;
|
||||
|
||||
Ogre::Vector3 MA = O + h1.direction * L1;
|
||||
Ogre::Vector3 MB = O + h2.direction * L2;
|
||||
|
||||
if (d <= L1)
|
||||
return MA + (O - MA) * (d / L1);
|
||||
return O + (MB - O) * ((d - L1) / L2);
|
||||
}
|
||||
|
||||
Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
float d)
|
||||
@@ -284,23 +306,59 @@ Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
Ogre::Vector3 offA = r1 * (h1.lanesOut * lw);
|
||||
Ogre::Vector3 offB = r2 * (-h2.lanesIn * lw);
|
||||
|
||||
/*
|
||||
* Miter corner: intersection of the two constant-width curb
|
||||
* lines, expressed as parameters t1/t2 along each direction from
|
||||
* the node-side curb ends:
|
||||
*
|
||||
* offA + dir1 * t1 == offB + dir2 * t2 == cornerOff
|
||||
*
|
||||
* t1 > 0 (and t2 > 0) means the curb lines converge ahead of the
|
||||
* node: the wedge is an inner corner (sweep < 180 deg).
|
||||
*/
|
||||
bool hasCorner = false;
|
||||
bool converging = false;
|
||||
float t1 = 0.0f;
|
||||
float t2 = 0.0f;
|
||||
Ogre::Vector3 cornerOff;
|
||||
float det = dir1.z * dir2.x - dir1.x * dir2.z;
|
||||
if (std::fabs(det) >= 0.05f) {
|
||||
Ogre::Vector3 rhs = offB - offA;
|
||||
t1 = (dir2.x * rhs.z - dir2.z * rhs.x) / det;
|
||||
t2 = (dir1.x * rhs.z - dir1.z * rhs.x) / det;
|
||||
cornerOff = offA + dir1 * t1;
|
||||
hasCorner = true;
|
||||
converging = t1 > 0.0f && t2 > 0.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* Inner corner with the miter corner K lying on both curb
|
||||
* segments: pin the curb at K over the whole corner zone
|
||||
* [L1 - t1, L1 + t2]. Rows before the zone use offA, rows after
|
||||
* use offB, rows inside aim straight at K (offset K - center(d)).
|
||||
* A blend through K would fold the cross-sections over each
|
||||
* other, because the curb arc around the inner corner is shorter
|
||||
* than the centerline arc; pinned rows share the endpoint K and
|
||||
* cannot cross. When K falls outside either curb segment (wide
|
||||
* road on short edges) the old blend is kept instead.
|
||||
*/
|
||||
if (converging && t1 <= L1 && t2 <= L2) {
|
||||
float zoneStart = L1 - t1;
|
||||
float zoneEnd = L1 + t2;
|
||||
if (d <= zoneStart)
|
||||
return offA;
|
||||
if (d >= zoneEnd)
|
||||
return offB;
|
||||
Ogre::Vector3 K = node->position + cornerOff;
|
||||
return K - wedgeCenterAt(wedge, graph, d);
|
||||
}
|
||||
|
||||
/* Blend zone width. */
|
||||
float W = std::min(SEAM_OVERLAP * 4.0f,
|
||||
std::min(L1 * 0.5f, L2 * 0.5f));
|
||||
if (L1 < SEAM_OVERLAP || L2 < SEAM_OVERLAP)
|
||||
W = 0.0f;
|
||||
|
||||
/* Miter corner: intersection of the two constant-width curb lines. */
|
||||
bool hasCorner = false;
|
||||
Ogre::Vector3 cornerOff;
|
||||
float det = dir1.z * dir2.x - dir1.x * dir2.z;
|
||||
if (std::fabs(det) >= 0.05f) {
|
||||
Ogre::Vector3 rhs = offB - offA;
|
||||
float t1x = (-rhs.x * dir2.z + dir2.x * rhs.z) / det;
|
||||
cornerOff = offA + dir1 * t1x;
|
||||
hasCorner = true;
|
||||
}
|
||||
|
||||
if (W <= 0.0f) {
|
||||
if (d < L1)
|
||||
return offA;
|
||||
@@ -339,12 +397,9 @@ void transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
Ogre::Vector3 dir1 = h1.direction;
|
||||
Ogre::Vector3 dir2 = h2.direction;
|
||||
float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f;
|
||||
float L2 = h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f;
|
||||
float L = L1 + L2;
|
||||
float L = L1 + (h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f);
|
||||
float in1 = h1.lanesIn * graph.config.laneWidth;
|
||||
|
||||
Ogre::Vector3 MA = O + dir1 * L1;
|
||||
Ogre::Vector3 MB = O + dir2 * L2;
|
||||
float yO = O.y + nodeRoadLevel(graph, wedge.nodeId);
|
||||
|
||||
for (auto &v : strip.getVertices()) {
|
||||
@@ -355,11 +410,7 @@ void transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
d = L;
|
||||
|
||||
/* Centerline (polyline MA -> O -> MB). */
|
||||
Ogre::Vector3 center;
|
||||
if (d <= L1)
|
||||
center = MA + (O - MA) * (d / L1);
|
||||
else
|
||||
center = O + (MB - O) * ((d - L1) / L2);
|
||||
Ogre::Vector3 center = wedgeCenterAt(wedge, graph, d);
|
||||
|
||||
/* World position from curb offset. */
|
||||
Ogre::Vector3 off = computeCurbOffset(wedge, graph, d);
|
||||
@@ -562,8 +613,20 @@ bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
/* Phase 3. */
|
||||
shiftSeamVertices(strip, wedge, graph);
|
||||
|
||||
/* Slab extrusion. */
|
||||
extrudeToSlab(out, strip, graph.config.roadThickness);
|
||||
/*
|
||||
* The transformed strip is already a closed tube around the road
|
||||
* body (the template supplies top, bottom and curb faces; the
|
||||
* template caps and centerline wall are dropped by
|
||||
* appendTemplateCopy and butt exactly against the neighbouring
|
||||
* pieces), so it is appended verbatim. Re-extruding it into a
|
||||
* slab would double the road thickness and stack coplanar sheets
|
||||
* at the strip's center surface.
|
||||
*/
|
||||
int base = (int)out.getVertices().size();
|
||||
for (const auto &v : strip.getVertices())
|
||||
out.getVertices().push_back(v);
|
||||
for (int idx : strip.getIndices())
|
||||
out.getIndices().push_back(base + idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -640,4 +703,168 @@ bool buildSegmentGeometry(const RoadStraightSegment &segment,
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Template mesh loading
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
bool loadTemplateFromMesh(const std::string &meshName,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
if (meshName.empty())
|
||||
return false;
|
||||
|
||||
Ogre::MeshPtr mesh;
|
||||
try {
|
||||
mesh = Ogre::MeshManager::getSingleton().load(
|
||||
meshName,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadGeometryLib: road mesh template '" + meshName +
|
||||
"' unavailable (" + e.what() +
|
||||
"), using fallback box");
|
||||
return false;
|
||||
}
|
||||
if (!mesh || mesh->getNumSubMeshes() == 0)
|
||||
return false;
|
||||
|
||||
Procedural::TriangleBuffer tb;
|
||||
|
||||
for (unsigned si = 0; si < mesh->getNumSubMeshes(); ++si) {
|
||||
Ogre::SubMesh *sub = mesh->getSubMesh(si);
|
||||
Ogre::VertexData *vd = sub->useSharedVertices ?
|
||||
mesh->sharedVertexData :
|
||||
sub->vertexData;
|
||||
if (!vd || !sub->indexData || !sub->indexData->indexBuffer)
|
||||
continue;
|
||||
|
||||
const Ogre::VertexElement *posElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_POSITION);
|
||||
if (!posElem)
|
||||
continue;
|
||||
const Ogre::VertexElement *normElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_NORMAL);
|
||||
const Ogre::VertexElement *uvElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_TEXTURE_COORDINATES, 0);
|
||||
|
||||
int base = (int)tb.getVertices().size();
|
||||
tb.getVertices().reserve(base + vd->vertexCount);
|
||||
|
||||
/* Positions (required). */
|
||||
{
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
posElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
posElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
Procedural::TriangleBuffer::Vertex tv;
|
||||
tv.mPosition =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
tv.mNormal = Ogre::Vector3::UNIT_Y;
|
||||
/* Convention fallback: UVs span the X/Z
|
||||
* extents. */
|
||||
tv.mUV = Ogre::Vector2(p[0], p[2]);
|
||||
tb.getVertices().push_back(tv);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Normals (optional). */
|
||||
if (normElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
normElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
normElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mNormal =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* UVs (optional). */
|
||||
if (uvElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
uvElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
uvElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mUV =
|
||||
Ogre::Vector2(p[0], p[1]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Indices (16- or 32-bit). */
|
||||
Ogre::HardwareIndexBufferSharedPtr ibuf =
|
||||
sub->indexData->indexBuffer;
|
||||
size_t start = sub->indexData->indexStart;
|
||||
size_t count = sub->indexData->indexCount;
|
||||
tb.getIndices().reserve(tb.getIndices().size() + count);
|
||||
if (ibuf->getType() == Ogre::HardwareIndexBuffer::IT_16BIT) {
|
||||
const uint16_t *p = static_cast<const uint16_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
} else {
|
||||
const uint32_t *p = static_cast<const uint32_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (tb.getVertices().empty() || tb.getIndices().empty())
|
||||
return false;
|
||||
|
||||
/*
|
||||
* Normalize into template space (ProceduralRoadGeometry.md
|
||||
* section 2): X in [0, span] with X=0 at the centerline side,
|
||||
* Z in [-span, 0] with 0 at the wedge start. A mesh spanning
|
||||
* roughly 1 unit on both axes is conforming; violations are
|
||||
* warned about but the mesh is still used as-is.
|
||||
*/
|
||||
Ogre::Vector3 mn = tb.getVertices()[0].mPosition;
|
||||
Ogre::Vector3 mx = mn;
|
||||
for (const auto &v : tb.getVertices()) {
|
||||
mn.makeFloor(v.mPosition);
|
||||
mx.makeCeil(v.mPosition);
|
||||
}
|
||||
Ogre::Vector3 span = mx - mn;
|
||||
for (auto &v : tb.getVertices()) {
|
||||
v.mPosition.x -= mn.x;
|
||||
v.mPosition.z -= mx.z;
|
||||
}
|
||||
if (span.x < 0.5f || span.x > 2.0f || span.z < 0.5f ||
|
||||
span.z > 2.0f) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadGeometryLib: road mesh template '" + meshName +
|
||||
"' violates the template conventions "
|
||||
"(X [0,1] lateral, Z [-1,0] longitudinal, unit "
|
||||
"extents); spans are (" +
|
||||
Ogre::StringConverter::toString(span) +
|
||||
"), using it anyway");
|
||||
}
|
||||
|
||||
out = tb;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace RoadGeometryLib
|
||||
|
||||
@@ -28,9 +28,12 @@ namespace RoadGeometryLib {
|
||||
*
|
||||
* The wedge piece is built by the three-phase pipeline described in
|
||||
* ProceduralRoadGeometry.md: a strip of concatenated template copies
|
||||
* is bent along the wedge's 2-segment centerline polyline with
|
||||
* continuous curb offset through the miter corner, then extruded
|
||||
* into a solid slab.
|
||||
* is bent along the wedge's 2-segment centerline polyline with the
|
||||
* outer-curb offset following the mitered curb chain (pinned at the
|
||||
* miter corner for inner wedges so cross-sections cannot fold).
|
||||
* The transformed strip is already a closed tube around the road
|
||||
* body (the template supplies top/bottom/curb faces), so it is
|
||||
* appended verbatim — no second extrusion is applied.
|
||||
*
|
||||
* @return false when the wedge is degenerate and nothing was emitted.
|
||||
*/
|
||||
@@ -104,6 +107,19 @@ void extrudeToSlab(Procedural::TriangleBuffer &out,
|
||||
*/
|
||||
Procedural::TriangleBuffer makeFallbackTemplate(float roadThickness);
|
||||
|
||||
/**
|
||||
* Load a road cross-section template from an OGRE mesh: the mesh
|
||||
* triangles are read verbatim and normalised into template space
|
||||
* (X in [0, span] with X=0 at the centerline side, Z in [-span, 0]
|
||||
* with 0 at the wedge start). Meshes violating the unit-extent
|
||||
* template conventions are warned about but still used. Returns
|
||||
* false when the mesh could not be loaded or has no usable
|
||||
* geometry. Used by RoadSystem and by the road demo for
|
||||
* user-selected template meshes.
|
||||
*/
|
||||
bool loadTemplateFromMesh(const std::string &meshName,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Utility helpers
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "../components/Lod.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../physics/physics.h"
|
||||
#include "../roadlib/RoadGeometryLib.hpp"
|
||||
#include "PrefabSystem.hpp"
|
||||
#include <OgreTerrainGroup.h>
|
||||
#include <OgreMaterialManager.h>
|
||||
@@ -915,771 +916,35 @@ RoadSystem::getRoadTemplate(const RoadConfig &cfg)
|
||||
m_templateThickness = cfg.roadThickness;
|
||||
m_templateBuffer = Procedural::TriangleBuffer();
|
||||
|
||||
if (!loadTemplateFromMesh(cfg.roadMeshTemplate))
|
||||
buildFallbackTemplate(cfg.roadThickness);
|
||||
if (!RoadGeometryLib::loadTemplateFromMesh(cfg.roadMeshTemplate,
|
||||
m_templateBuffer))
|
||||
m_templateBuffer =
|
||||
RoadGeometryLib::makeFallbackTemplate(cfg.roadThickness);
|
||||
|
||||
return m_templateBuffer;
|
||||
}
|
||||
|
||||
bool RoadSystem::loadTemplateFromMesh(const std::string &meshName)
|
||||
{
|
||||
if (meshName.empty())
|
||||
return false;
|
||||
|
||||
Ogre::MeshPtr mesh;
|
||||
try {
|
||||
mesh = Ogre::MeshManager::getSingleton().load(
|
||||
meshName,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadSystem: road mesh template '" + meshName +
|
||||
"' unavailable (" + e.what() +
|
||||
"), using fallback box");
|
||||
return false;
|
||||
}
|
||||
if (!mesh || mesh->getNumSubMeshes() == 0)
|
||||
return false;
|
||||
|
||||
Procedural::TriangleBuffer tb;
|
||||
|
||||
for (unsigned si = 0; si < mesh->getNumSubMeshes(); ++si) {
|
||||
Ogre::SubMesh *sub = mesh->getSubMesh(si);
|
||||
Ogre::VertexData *vd = sub->useSharedVertices ?
|
||||
mesh->sharedVertexData :
|
||||
sub->vertexData;
|
||||
if (!vd || !sub->indexData || !sub->indexData->indexBuffer)
|
||||
continue;
|
||||
|
||||
const Ogre::VertexElement *posElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_POSITION);
|
||||
if (!posElem)
|
||||
continue;
|
||||
const Ogre::VertexElement *normElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_NORMAL);
|
||||
const Ogre::VertexElement *uvElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_TEXTURE_COORDINATES, 0);
|
||||
|
||||
int base = (int)tb.getVertices().size();
|
||||
tb.getVertices().reserve(base + vd->vertexCount);
|
||||
|
||||
/* Positions (required). */
|
||||
{
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
posElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
posElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
Procedural::TriangleBuffer::Vertex tv;
|
||||
tv.mPosition =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
tv.mNormal = Ogre::Vector3::UNIT_Y;
|
||||
/* Convention fallback: UVs span the X/Z
|
||||
* extents. */
|
||||
tv.mUV = Ogre::Vector2(p[0], p[2]);
|
||||
tb.getVertices().push_back(tv);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Normals (optional). */
|
||||
if (normElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
normElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
normElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mNormal =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* UVs (optional). */
|
||||
if (uvElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
uvElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
uvElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mUV =
|
||||
Ogre::Vector2(p[0], p[1]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Indices (16- or 32-bit). */
|
||||
Ogre::HardwareIndexBufferSharedPtr ibuf =
|
||||
sub->indexData->indexBuffer;
|
||||
size_t start = sub->indexData->indexStart;
|
||||
size_t count = sub->indexData->indexCount;
|
||||
tb.getIndices().reserve(tb.getIndices().size() + count);
|
||||
if (ibuf->getType() == Ogre::HardwareIndexBuffer::IT_16BIT) {
|
||||
const uint16_t *p = static_cast<const uint16_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
} else {
|
||||
const uint32_t *p = static_cast<const uint32_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (tb.getVertices().empty() || tb.getIndices().empty())
|
||||
return false;
|
||||
|
||||
/*
|
||||
* Normalize into template space (ProceduralRoadGeometry.md
|
||||
* section 2): X in [0, span] with X=0 at the centerline side,
|
||||
* Z in [-span, 0] with 0 at the wedge start. A mesh spanning
|
||||
* roughly 1 unit on both axes is conforming; violations are
|
||||
* warned about but the mesh is still used as-is.
|
||||
*/
|
||||
Ogre::Vector3 mn = tb.getVertices()[0].mPosition;
|
||||
Ogre::Vector3 mx = mn;
|
||||
for (const auto &v : tb.getVertices()) {
|
||||
mn.makeFloor(v.mPosition);
|
||||
mx.makeCeil(v.mPosition);
|
||||
}
|
||||
Ogre::Vector3 span = mx - mn;
|
||||
for (auto &v : tb.getVertices()) {
|
||||
v.mPosition.x -= mn.x;
|
||||
v.mPosition.z -= mx.z;
|
||||
}
|
||||
if (span.x < 0.5f || span.x > 2.0f || span.z < 0.5f ||
|
||||
span.z > 2.0f) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadSystem: road mesh template '" + meshName +
|
||||
"' violates the template conventions "
|
||||
"(X [0,1] lateral, Z [-1,0] longitudinal, unit "
|
||||
"extents); spans are (" +
|
||||
Ogre::StringConverter::toString(span) +
|
||||
"), using it anyway");
|
||||
}
|
||||
|
||||
m_templateBuffer = tb;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RoadSystem::buildFallbackTemplate(float roadThickness)
|
||||
{
|
||||
m_templateBuffer = makeFallbackTemplate(roadThickness);
|
||||
}
|
||||
|
||||
Procedural::TriangleBuffer
|
||||
RoadSystem::makeFallbackTemplate(float roadThickness)
|
||||
{
|
||||
float h = std::max(0.01f, roadThickness) * 0.5f;
|
||||
|
||||
Procedural::TriangleBuffer tb;
|
||||
auto &verts = tb.getVertices();
|
||||
auto &indices = tb.getIndices();
|
||||
verts.reserve(24);
|
||||
indices.reserve(36);
|
||||
|
||||
struct Corner {
|
||||
Ogre::Vector3 p;
|
||||
Ogre::Vector2 uv;
|
||||
};
|
||||
|
||||
/*
|
||||
* One quad face: 4 vertices, 2 triangles, counter-clockwise
|
||||
* seen from outside (Ogre front face). Template space
|
||||
* (ProceduralRoadGeometry.md section 2): X in [0,1] is lateral
|
||||
* (X=0 centerline, X=1 outer curb), Z in [-1,0] is longitudinal
|
||||
* (0 at the wedge start). UVs map u to the longitudinal extent
|
||||
* (-z) and v to the lateral extent (x) so every face spans
|
||||
* (0,0)-(1,1); Phase 2 rescales v by the local road width.
|
||||
*/
|
||||
auto addFace = [&](const Corner &a, const Corner &b, const Corner &c,
|
||||
const Corner &d, const Ogre::Vector3 &normal) {
|
||||
int base = (int)verts.size();
|
||||
for (const Corner *q : { &a, &b, &c, &d }) {
|
||||
Procedural::TriangleBuffer::Vertex v;
|
||||
v.mPosition = q->p;
|
||||
v.mNormal = normal;
|
||||
v.mUV = q->uv;
|
||||
verts.push_back(v);
|
||||
}
|
||||
indices.push_back(base + 0);
|
||||
indices.push_back(base + 1);
|
||||
indices.push_back(base + 2);
|
||||
indices.push_back(base + 0);
|
||||
indices.push_back(base + 2);
|
||||
indices.push_back(base + 3);
|
||||
};
|
||||
|
||||
/* Top (+Y): X in [0,1], Z in [-1,0]. */
|
||||
addFace({ { 0, h, 0 }, { 0, 0 } }, { { 1, h, 0 }, { 0, 1 } },
|
||||
{ { 1, h, -1 }, { 1, 1 } }, { { 0, h, -1 }, { 1, 0 } },
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
/* Bottom (-Y). */
|
||||
addFace({ { 0, -h, 0 }, { 0, 0 } }, { { 0, -h, -1 }, { 1, 0 } },
|
||||
{ { 1, -h, -1 }, { 1, 1 } }, { { 1, -h, 0 }, { 0, 1 } },
|
||||
Ogre::Vector3::NEGATIVE_UNIT_Y);
|
||||
/* Start cap (+Z, z = 0; dropped from wedge strips). */
|
||||
addFace({ { 0, -h, 0 }, { 0, 0 } }, { { 1, -h, 0 }, { 0, 1 } },
|
||||
{ { 1, h, 0 }, { 0, 1 } }, { { 0, h, 0 }, { 0, 0 } },
|
||||
Ogre::Vector3::UNIT_Z);
|
||||
/* End cap (-Z, z = -1; dropped from wedge strips). */
|
||||
addFace({ { 1, -h, -1 }, { 1, 1 } }, { { 0, -h, -1 }, { 1, 0 } },
|
||||
{ { 0, h, -1 }, { 1, 0 } }, { { 1, h, -1 }, { 1, 1 } },
|
||||
Ogre::Vector3::NEGATIVE_UNIT_Z);
|
||||
/* Outer curb wall (+X). */
|
||||
addFace({ { 1, -h, 0 }, { 0, 1 } }, { { 1, -h, -1 }, { 1, 1 } },
|
||||
{ { 1, h, -1 }, { 1, 1 } }, { { 1, h, 0 }, { 0, 1 } },
|
||||
Ogre::Vector3::UNIT_X);
|
||||
/* Centerline wall (-X; dropped from wedge strips). */
|
||||
addFace({ { 0, -h, -1 }, { 1, 0 } }, { { 0, -h, 0 }, { 0, 0 } },
|
||||
{ { 0, h, 0 }, { 0, 0 } }, { { 0, h, -1 }, { 1, 0 } },
|
||||
Ogre::Vector3::NEGATIVE_UNIT_X);
|
||||
|
||||
return tb;
|
||||
return RoadGeometryLib::makeFallbackTemplate(roadThickness);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Wedge / segment geometry generation (M5.6) */
|
||||
/* */
|
||||
/* All geometry generation lives in RoadGeometryLib */
|
||||
/* (roadlib/RoadGeometryLib.cpp); the RoadSystem methods below */
|
||||
/* forward to it so headless tests can keep calling the RoadSystem */
|
||||
/* statics. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Right-of-travel direction for a horizontal road direction @p d.
|
||||
*
|
||||
* Template convention: +X forward x +Y up = +Z right, so for any
|
||||
* normalized horizontal direction the right side is d x UNIT_Y. This is
|
||||
* also the direction of increasing atan2(z, x) angle, i.e. the side a
|
||||
* wedge sweeps toward from its first half-edge.
|
||||
*/
|
||||
static Ogre::Vector3 roadRightVec(const Ogre::Vector3 &d)
|
||||
{
|
||||
return d.crossProduct(Ogre::Vector3::UNIT_Y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute road surface heights at both ends of a half-edge.
|
||||
*
|
||||
* yNode is the surface height at the seed node; yMid is the surface
|
||||
* height at the edge midpoint, averaged between the linearly
|
||||
* interpolated heights of both edge ends.
|
||||
*/
|
||||
static void halfEdgeHeights(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float &yNode, float &yMid)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(he.nodeId);
|
||||
const RoadNode *neighbor = graph.findNodeById(he.neighborId);
|
||||
float nodeY = node ? node->position.y : 0.0f;
|
||||
float neighborY = neighbor ? neighbor->position.y : nodeY;
|
||||
|
||||
yNode = nodeY + he.roadLevelAtNode;
|
||||
yMid = 0.5f * (yNode + neighborY + he.roadLevelAtNeighbor);
|
||||
}
|
||||
|
||||
/** Road surface height at distance @p t along a half-edge. */
|
||||
static float halfEdgeHeightAt(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float t)
|
||||
{
|
||||
float yNode, yMid;
|
||||
halfEdgeHeights(he, graph, yNode, yMid);
|
||||
float l = he.halfLength > 1e-4f ? he.halfLength : 1e-4f;
|
||||
return yNode + (yMid - yNode) * (t / l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Along-road texture coordinate at distance @p t from the seed node.
|
||||
*
|
||||
* For the nodeA half of an edge this is simply t; for the nodeB half it
|
||||
* is 2*halfLength - t, so u stays phase-continuous across the edge
|
||||
* midpoint where the two halves meet.
|
||||
*/
|
||||
static float halfEdgeU(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float t)
|
||||
{
|
||||
if (he.edgeIndex >= 0 && he.edgeIndex < (int)graph.edges.size() &&
|
||||
graph.edges[he.edgeIndex].nodeB == he.nodeId)
|
||||
return 2.0f * he.halfLength - t;
|
||||
return t;
|
||||
}
|
||||
|
||||
/** Overlap distance to close gaps at wedge boundaries (spec 5.4 step 4). */
|
||||
static const float ROAD_SEAM_OVERLAP = 0.05f;
|
||||
|
||||
/** Append one triangle; degenerate (zero-area) triangles are skipped. */
|
||||
static void emitTri(Procedural::TriangleBuffer &out, const Ogre::Vector3 &p0,
|
||||
const Ogre::Vector3 &p1, const Ogre::Vector3 &p2,
|
||||
const Ogre::Vector2 &uv0, const Ogre::Vector2 &uv1,
|
||||
const Ogre::Vector2 &uv2)
|
||||
{
|
||||
Ogre::Vector3 n = (p1 - p0).crossProduct(p2 - p0);
|
||||
if (n.squaredLength() < 1e-10f)
|
||||
return;
|
||||
n.normalise();
|
||||
|
||||
int base = (int)out.getVertices().size();
|
||||
const Ogre::Vector3 *pp[3] = { &p0, &p1, &p2 };
|
||||
const Ogre::Vector2 *uu[3] = { &uv0, &uv1, &uv2 };
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
Procedural::TriangleBuffer::Vertex v;
|
||||
v.mPosition = *pp[i];
|
||||
v.mNormal = n;
|
||||
v.mUV = *uu[i];
|
||||
out.getVertices().push_back(v);
|
||||
out.getIndices().push_back(base + i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a set of center-surface triangles into a solid slab (spec
|
||||
* section 8).
|
||||
*
|
||||
* Every triangle is emitted twice: offset by +halfThick along Y and
|
||||
* offset by -halfThick, with the winding chosen so the top normal
|
||||
* points up (auto-oriented by the source triangle's normal Y sign —
|
||||
* the published keep-winding rule flips the top face down for the
|
||||
* section 7 band triangle order). Boundary edges (undirected edges
|
||||
* used by exactly one triangle — requires centerSurf to share vertices
|
||||
* along interior edges) grow vertical skirt quads whose normals point
|
||||
* away from the center surface's centroid. @p skirtFilter may reject
|
||||
* specific boundary edges (e.g. a segment's far end, which meets the
|
||||
* neighbour node's piece exactly).
|
||||
*/
|
||||
void RoadSystem::extrudeToSlab(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer ¢erSurf,
|
||||
float roadThickness,
|
||||
const SkirtFilter &skirtFilter)
|
||||
{
|
||||
float halfThick = std::max(0.01f, roadThickness) * 0.5f;
|
||||
Ogre::Vector3 up(0.0f, halfThick, 0.0f);
|
||||
|
||||
const auto &verts = centerSurf.getVertices();
|
||||
const auto &indices = centerSurf.getIndices();
|
||||
|
||||
/* Centroid: interior reference for skirt orientation. */
|
||||
Ogre::Vector3 refPoint = Ogre::Vector3::ZERO;
|
||||
for (const auto &v : verts)
|
||||
refPoint += v.mPosition;
|
||||
if (!verts.empty())
|
||||
refPoint /= (float)verts.size();
|
||||
|
||||
/* Top and bottom. */
|
||||
for (size_t t = 0; t + 2 < indices.size(); t += 3) {
|
||||
const auto &v0 = verts[(size_t)indices[t]];
|
||||
const auto &v1 = verts[(size_t)indices[t + 1]];
|
||||
const auto &v2 = verts[(size_t)indices[t + 2]];
|
||||
Ogre::Vector3 n = (v1.mPosition - v0.mPosition)
|
||||
.crossProduct(v2.mPosition - v0.mPosition);
|
||||
if (n.squaredLength() < 1e-10f)
|
||||
continue;
|
||||
int i1 = n.y >= 0.0f ? 1 : 2;
|
||||
int i2 = n.y >= 0.0f ? 2 : 1;
|
||||
const Procedural::TriangleBuffer::Vertex *vv[3] = { &v0, &v1,
|
||||
&v2 };
|
||||
emitTri(out, vv[0]->mPosition + up, vv[i1]->mPosition + up,
|
||||
vv[i2]->mPosition + up, vv[0]->mUV, vv[i1]->mUV,
|
||||
vv[i2]->mUV);
|
||||
emitTri(out, vv[0]->mPosition - up, vv[i2]->mPosition - up,
|
||||
vv[i1]->mPosition - up, vv[0]->mUV, vv[i2]->mUV,
|
||||
vv[i1]->mUV);
|
||||
}
|
||||
|
||||
/* Boundary-edge detection by index counting. */
|
||||
struct EdgeUse {
|
||||
int count = 0;
|
||||
int a = 0, b = 0; /* directed, from the first use */
|
||||
};
|
||||
std::map<std::pair<int, int>, EdgeUse> edgeUse;
|
||||
for (size_t t = 0; t + 2 < indices.size(); t += 3) {
|
||||
int tri[3] = { indices[t], indices[t + 1], indices[t + 2] };
|
||||
for (int e = 0; e < 3; ++e) {
|
||||
int a = tri[e], b = tri[(e + 1) % 3];
|
||||
auto &eu = edgeUse[std::minmax(a, b)];
|
||||
if (eu.count == 0) {
|
||||
eu.a = a;
|
||||
eu.b = b;
|
||||
}
|
||||
++eu.count;
|
||||
}
|
||||
}
|
||||
|
||||
float thickness = 2.0f * halfThick;
|
||||
for (const auto &kv : edgeUse) {
|
||||
const EdgeUse &eu = kv.second;
|
||||
if (eu.count != 1)
|
||||
continue;
|
||||
const auto &v0 = verts[(size_t)eu.a];
|
||||
const auto &v1 = verts[(size_t)eu.b];
|
||||
if (skirtFilter &&
|
||||
!skirtFilter(v0.mPosition, v1.mPosition))
|
||||
continue;
|
||||
|
||||
Ogre::Vector3 t0 = v0.mPosition + up;
|
||||
Ogre::Vector3 t1 = v1.mPosition + up;
|
||||
Ogre::Vector3 b0 = v0.mPosition - up;
|
||||
Ogre::Vector3 b1 = v1.mPosition - up;
|
||||
Ogre::Vector2 uvB0(v0.mUV.x, v0.mUV.y - thickness);
|
||||
Ogre::Vector2 uvB1(v1.mUV.x, v1.mUV.y - thickness);
|
||||
|
||||
Ogre::Vector3 n = (t1 - t0).crossProduct(b0 - t0);
|
||||
if (n.squaredLength() < 1e-10f)
|
||||
continue;
|
||||
Ogre::Vector3 mid = (t0 + t1 + b0 + b1) * 0.25f;
|
||||
bool outward = n.dotProduct(mid - refPoint) >= 0.0f;
|
||||
if (outward) {
|
||||
emitTri(out, t0, t1, b1, v0.mUV, v1.mUV, uvB1);
|
||||
emitTri(out, t0, b1, b0, v0.mUV, uvB1, uvB0);
|
||||
} else {
|
||||
emitTri(out, t0, b1, t1, v0.mUV, uvB1, v1.mUV);
|
||||
emitTri(out, t0, b0, b1, v0.mUV, uvB0, uvB1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Road surface level offset shared by every wedge seeded at one node.
|
||||
*
|
||||
* The level is the mean of the incident edges' roadLevelAtNode values,
|
||||
* so all wedge pieces meeting at the node use the same center height
|
||||
* and no cracks open between adjacent pieces when per-edge road levels
|
||||
* differ.
|
||||
*/
|
||||
static float nodeRoadLevel(const RoadGraph &graph, int nodeId)
|
||||
{
|
||||
float sum = 0.0f;
|
||||
int count = 0;
|
||||
for (const RoadEdge &e : graph.edges) {
|
||||
if (e.nodeA == nodeId) {
|
||||
sum += e.roadLevelA;
|
||||
++count;
|
||||
} else if (e.nodeB == nodeId) {
|
||||
sum += e.roadLevelB;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count > 0 ? sum / (float)count : 0.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1 (spec section 4): straight strip of N concatenated template
|
||||
* copies along -Z. After this the strip occupies X in [0,1],
|
||||
* Y in [-thick/2, +thick/2], Z in [-N, 0]. All template faces are
|
||||
* kept.
|
||||
*/
|
||||
void RoadSystem::buildConcatenatedStrip(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
int N)
|
||||
{
|
||||
out.getVertices().clear();
|
||||
out.getIndices().clear();
|
||||
for (int i = 0; i < N; ++i) {
|
||||
int base = (int)out.getVertices().size();
|
||||
for (const auto &v : templ.getVertices()) {
|
||||
Procedural::TriangleBuffer::Vertex cv = v;
|
||||
cv.mPosition.z -= (float)i;
|
||||
out.getVertices().push_back(cv);
|
||||
}
|
||||
for (int idx : templ.getIndices())
|
||||
out.getIndices().push_back(base + idx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append one template copy shifted to z -= @p zOff, clamping the path
|
||||
* distance of every vertex to @p clampD.
|
||||
*
|
||||
* Template faces lying fully in a copy-boundary Z plane are dropped:
|
||||
* those are the template caps, which would otherwise stack coplanar
|
||||
* faces at every copy join and at the edge midpoints where the
|
||||
* neighbour node's piece meets this one (z-fighting). The template's
|
||||
* X = 0 wall is dropped as well: it runs along the centerline shared
|
||||
* with the adjacent wedge and is interior to the joined road body.
|
||||
*/
|
||||
static void appendTemplateCopy(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
float zOff, float clampD)
|
||||
{
|
||||
const auto &tverts = templ.getVertices();
|
||||
const auto &tidx = templ.getIndices();
|
||||
|
||||
int base = (int)out.getVertices().size();
|
||||
for (const auto &v : tverts) {
|
||||
Procedural::TriangleBuffer::Vertex cv = v;
|
||||
cv.mPosition.z -= zOff;
|
||||
if (-cv.mPosition.z > clampD)
|
||||
cv.mPosition.z = -clampD;
|
||||
out.getVertices().push_back(cv);
|
||||
}
|
||||
|
||||
for (size_t t = 0; t + 2 < tidx.size(); t += 3) {
|
||||
const Ogre::Vector3 &a = tverts[(size_t)tidx[t]].mPosition;
|
||||
const Ogre::Vector3 &b = tverts[(size_t)tidx[t + 1]].mPosition;
|
||||
const Ogre::Vector3 &c = tverts[(size_t)tidx[t + 2]].mPosition;
|
||||
|
||||
bool cap0 = std::fabs(a.z) < 1e-6f &&
|
||||
std::fabs(b.z) < 1e-6f && std::fabs(c.z) < 1e-6f;
|
||||
bool cap1 = std::fabs(a.z + 1.0f) < 1e-6f &&
|
||||
std::fabs(b.z + 1.0f) < 1e-6f &&
|
||||
std::fabs(c.z + 1.0f) < 1e-6f;
|
||||
if (cap0 || cap1)
|
||||
continue; /* open joins at copy boundaries/midpoints */
|
||||
|
||||
bool wall0 = std::fabs(a.x) < 1e-6f &&
|
||||
std::fabs(b.x) < 1e-6f &&
|
||||
std::fabs(c.x) < 1e-6f;
|
||||
const Ogre::Vector3 &n = tverts[(size_t)tidx[t]].mNormal;
|
||||
if (wall0 && std::fabs(n.x) > 0.9f)
|
||||
continue; /* interior centerline wall */
|
||||
|
||||
out.getIndices().push_back(base + tidx[t]);
|
||||
out.getIndices().push_back(base + tidx[t + 1]);
|
||||
out.getIndices().push_back(base + tidx[t + 2]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Two-run concatenated strip for one wedge (spec correction C3).
|
||||
*
|
||||
* Run 1 covers d in [0, L1] with ceil(L1) uniform copies from d = 0,
|
||||
* run 2 covers [L1, L1+L2] with ceil(L2) copies from d = L1; vertices
|
||||
* past each run's end are clamped onto it. Vertex layers therefore
|
||||
* land exactly on the corner distance L1 and on the strip end — the
|
||||
* miter corner is always sampled, which the published uniform
|
||||
* N = ceil(L) layout cannot guarantee for fractional half-lengths
|
||||
* (e.g. L1 = 5.5).
|
||||
*/
|
||||
static void buildWedgeStrip(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
float L1, float L2)
|
||||
{
|
||||
out.getVertices().clear();
|
||||
out.getIndices().clear();
|
||||
int k1 = std::max(1, (int)std::ceil(L1));
|
||||
int k2 = std::max(1, (int)std::ceil(L2));
|
||||
for (int i = 0; i < k1; ++i)
|
||||
appendTemplateCopy(out, templ, (float)i, L1);
|
||||
for (int j = 0; j < k2; ++j)
|
||||
appendTemplateCopy(out, templ, L1 + (float)j, L1 + L2);
|
||||
}
|
||||
|
||||
Ogre::Vector3 RoadSystem::computeCurbOffset(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
float d)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(wedge.nodeId);
|
||||
if (!node)
|
||||
return Ogre::Vector3::ZERO;
|
||||
|
||||
const RoadHalfEdge &h1 = wedge.first;
|
||||
const RoadHalfEdge &h2 = wedge.second;
|
||||
Ogre::Vector3 dir1 = h1.direction;
|
||||
Ogre::Vector3 dir2 = h2.direction;
|
||||
Ogre::Vector3 r1 = roadRightVec(dir1);
|
||||
Ogre::Vector3 r2 = roadRightVec(dir2);
|
||||
float lw = graph.config.laneWidth;
|
||||
float L1 = h1.halfLength;
|
||||
float L2 = h2.halfLength;
|
||||
|
||||
Ogre::Vector3 offA = r1 * (h1.lanesOut * lw); /* H1-side curb */
|
||||
Ogre::Vector3 offB = r2 * (-h2.lanesIn * lw); /* H2-side curb */
|
||||
|
||||
/* Narrow symmetric blend zone around the node (spec 5.2). */
|
||||
float W = std::min(ROAD_SEAM_OVERLAP * 4.0f,
|
||||
std::min(L1 * 0.5f, L2 * 0.5f));
|
||||
if (L1 < ROAD_SEAM_OVERLAP || L2 < ROAD_SEAM_OVERLAP)
|
||||
W = 0.0f;
|
||||
|
||||
/*
|
||||
* Miter corner: intersection of the two constant-width curb
|
||||
* lines, used as the blend anchor at the node so the curb passes
|
||||
* exactly through the outer corner (spec correction C1 — the
|
||||
* published 50/50 vector lerp cut the corner and left a hole at
|
||||
* every outer intersection corner). Near-straight wedges
|
||||
* (|det| < 0.05, curb lines almost collinear) drop the corner
|
||||
* and blend directly between the two side offsets.
|
||||
*/
|
||||
bool hasCorner = false;
|
||||
Ogre::Vector3 cornerOff;
|
||||
float det = dir1.z * dir2.x - dir1.x * dir2.z;
|
||||
if (std::fabs(det) >= 0.05f) {
|
||||
Ogre::Vector3 rhs = offB - offA;
|
||||
float t1x = (-rhs.x * dir2.z + dir2.x * rhs.z) / det;
|
||||
cornerOff = offA + dir1 * t1x;
|
||||
hasCorner = true;
|
||||
}
|
||||
|
||||
if (W <= 0.0f) {
|
||||
if (d < L1)
|
||||
return offA;
|
||||
if (d > L1)
|
||||
return offB;
|
||||
return hasCorner ? cornerOff : (offA + offB) * 0.5f;
|
||||
}
|
||||
|
||||
if (d <= L1 - W)
|
||||
return offA;
|
||||
if (d >= L1 + W)
|
||||
return offB;
|
||||
if (!hasCorner) {
|
||||
float t = (d - (L1 - W)) / (2.0f * W);
|
||||
return offA + (offB - offA) * t;
|
||||
}
|
||||
if (d <= L1) {
|
||||
float t = (d - (L1 - W)) / W;
|
||||
return offA + (cornerOff - offA) * t;
|
||||
}
|
||||
float t = (d - L1) / W;
|
||||
return cornerOff + (offB - cornerOff) * t;
|
||||
}
|
||||
|
||||
void RoadSystem::transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(wedge.nodeId);
|
||||
if (!node)
|
||||
return;
|
||||
|
||||
const RoadHalfEdge &h1 = wedge.first;
|
||||
const RoadHalfEdge &h2 = wedge.second;
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
Ogre::Vector3 dir1 = h1.direction;
|
||||
Ogre::Vector3 dir2 = h2.direction;
|
||||
float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f;
|
||||
float L2 = h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f;
|
||||
float L = L1 + L2;
|
||||
float in1 = h1.lanesIn * graph.config.laneWidth;
|
||||
|
||||
Ogre::Vector3 MA = O + dir1 * L1;
|
||||
Ogre::Vector3 MB = O + dir2 * L2;
|
||||
|
||||
/* One shared road level for the whole d = L1 vertex layer so
|
||||
* adjacent wedge pieces cannot crack at the node. */
|
||||
float yO = O.y + nodeRoadLevel(graph, wedge.nodeId);
|
||||
|
||||
for (auto &v : strip.getVertices()) {
|
||||
float d = -v.mPosition.z;
|
||||
if (d < 0.0f)
|
||||
d = 0.0f;
|
||||
if (d > L)
|
||||
d = L;
|
||||
|
||||
/* Centerline position (polyline M_A -> O -> M_B). */
|
||||
Ogre::Vector3 center;
|
||||
if (d <= L1)
|
||||
center = MA + (O - MA) * (d / L1);
|
||||
else
|
||||
center = O + (MB - O) * ((d - L1) / L2);
|
||||
|
||||
/*
|
||||
* World position: template X maps along the curb offset
|
||||
* (direction AND magnitude — the offset itself widens
|
||||
* through the miter corner), template Y maps directly to
|
||||
* the vertical offset from the road surface.
|
||||
*/
|
||||
Ogre::Vector3 off = computeCurbOffset(wedge, graph, d);
|
||||
Ogre::Vector3 worldXZ = center + off * v.mPosition.x;
|
||||
|
||||
/*
|
||||
* Surface height: half-edge profiles, with the shared
|
||||
* node level at the d = L1 break layer. The published
|
||||
* formulas passed the wedge-start distance d to the H1
|
||||
* helpers, inverting the profile along the first
|
||||
* half-edge; the helpers expect the distance from the
|
||||
* seed node, i.e. L1 - d (spec correction C2).
|
||||
*/
|
||||
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;
|
||||
float worldY = surfY + v.mPosition.y;
|
||||
|
||||
/* UV: phase-continuous longitudinal u; lateral v scaled
|
||||
* by the local width with the +in1 continuity offset. */
|
||||
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;
|
||||
|
||||
/*
|
||||
* Normal: rotate template-forward (-Z) to the segment
|
||||
* direction by the SIGNED angle around Y (the published
|
||||
* unsigned angleBetween rotated the wrong way for half
|
||||
* the possible directions — spec correction C5).
|
||||
*/
|
||||
const Ogre::Vector3 &segDir = (d <= L1) ? dir1 : dir2;
|
||||
float theta = std::atan2(-segDir.x, -segDir.z);
|
||||
Ogre::Quaternion q(Ogre::Radian(theta),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
Ogre::Vector3 n = q * v.mNormal;
|
||||
|
||||
v.mPosition = Ogre::Vector3(worldXZ.x, worldY, worldXZ.z);
|
||||
v.mNormal = n;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3 (spec section 6): shift centerline-side vertices near the
|
||||
* node slightly past it so adjacent wedges overlap at the center
|
||||
* junction. Only needed for nodes with > 2 neighbors.
|
||||
*/
|
||||
void RoadSystem::shiftSeamVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph)
|
||||
{
|
||||
std::vector<int> nids = graph.getNeighborIds(wedge.nodeId);
|
||||
if (nids.size() <= 2)
|
||||
return; /* straight-through or endpoint */
|
||||
|
||||
const RoadNode *node = graph.findNodeById(wedge.nodeId);
|
||||
if (!node)
|
||||
return;
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
|
||||
for (auto &v : strip.getVertices()) {
|
||||
Ogre::Vector3 toNode(v.mPosition.x - O.x, 0,
|
||||
v.mPosition.z - O.z);
|
||||
float distToNode = toNode.length();
|
||||
if (distToNode >= ROAD_SEAM_OVERLAP)
|
||||
continue;
|
||||
|
||||
Ogre::Vector3 radial = toNode.normalisedCopy();
|
||||
if (radial.isZeroLength())
|
||||
continue;
|
||||
|
||||
float push = ROAD_SEAM_OVERLAP - distToNode +
|
||||
ROAD_SEAM_OVERLAP;
|
||||
v.mPosition.x += radial.x * push;
|
||||
v.mPosition.z += radial.z * push;
|
||||
}
|
||||
}
|
||||
|
||||
bool RoadSystem::buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
Procedural::TriangleBuffer fb =
|
||||
makeFallbackTemplate(graph.config.roadThickness);
|
||||
return buildWedgeGeometry(wedge, graph, fb, out);
|
||||
return RoadGeometryLib::buildWedgeGeometry(wedge, graph, out);
|
||||
}
|
||||
|
||||
bool RoadSystem::buildWedgeGeometry(const RoadWedge &wedge,
|
||||
@@ -1687,117 +952,14 @@ bool RoadSystem::buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
if (wedge.degenerate) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadSystem: skipping degenerate road wedge at node " +
|
||||
Ogre::StringConverter::toString(wedge.nodeId));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Phase 1: concatenated strip. */
|
||||
Procedural::TriangleBuffer strip;
|
||||
buildWedgeStrip(strip, templ, wedge.first.halfLength,
|
||||
wedge.second.halfLength);
|
||||
|
||||
/* Phase 2: bend into wedge shape. */
|
||||
transformWedgeVertices(strip, wedge, graph);
|
||||
|
||||
/* Phase 3: close center seam. */
|
||||
shiftSeamVertices(strip, wedge, graph);
|
||||
|
||||
/* Slab extrusion: turn the center surface into a closed solid.
|
||||
* The centerline edges (template X=0) and cap faces (template Z=0,
|
||||
* Z=-1) were dropped by appendTemplateCopy, leaving only the outer
|
||||
* curb wall and road top/bottom in the strip index buffer. Those
|
||||
* remaining faces form the center surface — extrudeToSlab detects
|
||||
* boundary edges from the center-surface triangle soup and adds
|
||||
* top, bottom, and skirt geometry. */
|
||||
extrudeToSlab(out, strip, graph.config.roadThickness);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Center-surface band of a dead-end straight segment (spec section 7):
|
||||
* the full road width s in [-inW, +outW] along the single half-edge
|
||||
* with a small overlap past the node. c[0]/c[3] are at the node end,
|
||||
* c[1]/c[2] at the edge midpoint. Heights are absolute road surface
|
||||
* heights. Retained because complyTerrain uses the four corners.
|
||||
*/
|
||||
static bool computeSegmentBand(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph, Ogre::Vector3 c[4],
|
||||
Ogre::Vector2 uvc[4])
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(segment.nodeId);
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
const RoadHalfEdge &he = segment.halfEdge;
|
||||
if (he.lanesIn + he.lanesOut < 1)
|
||||
return false;
|
||||
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
Ogre::Vector3 d = he.direction;
|
||||
Ogre::Vector3 r = roadRightVec(d);
|
||||
float lw = graph.config.laneWidth;
|
||||
float inW = he.lanesIn * lw;
|
||||
float outW = he.lanesOut * lw;
|
||||
float L = he.halfLength;
|
||||
float t0 = -ROAD_SEAM_OVERLAP;
|
||||
|
||||
c[0] = O + t0 * d - inW * r;
|
||||
c[1] = O + L * d - inW * r;
|
||||
c[2] = O + L * d + outW * r;
|
||||
c[3] = O + t0 * d + outW * r;
|
||||
float y0 = halfEdgeHeightAt(he, graph, t0);
|
||||
float yL = halfEdgeHeightAt(he, graph, L);
|
||||
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);
|
||||
return true;
|
||||
return RoadGeometryLib::buildWedgeGeometry(wedge, graph, templ, out);
|
||||
}
|
||||
|
||||
bool RoadSystem::buildSegmentGeometry(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
Ogre::Vector3 c[4];
|
||||
Ogre::Vector2 uvc[4];
|
||||
if (!computeSegmentBand(segment, graph, c, uvc))
|
||||
return false;
|
||||
|
||||
/* Build center-surface as a TriangleBuffer. */
|
||||
Procedural::TriangleBuffer centerSurf;
|
||||
int base = (int)centerSurf.getVertices().size();
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
Procedural::TriangleBuffer::Vertex v;
|
||||
v.mPosition = c[i];
|
||||
v.mNormal = Ogre::Vector3::UNIT_Y;
|
||||
v.mUV = uvc[i];
|
||||
centerSurf.getVertices().push_back(v);
|
||||
}
|
||||
centerSurf.getIndices().push_back(base + 0);
|
||||
centerSurf.getIndices().push_back(base + 1);
|
||||
centerSurf.getIndices().push_back(base + 2);
|
||||
centerSurf.getIndices().push_back(base + 0);
|
||||
centerSurf.getIndices().push_back(base + 2);
|
||||
centerSurf.getIndices().push_back(base + 3);
|
||||
|
||||
/* Extrude to slab, keeping the far-end edge open (it meets the
|
||||
* neighbor node's piece). */
|
||||
auto skirtFilter = [&](const Ogre::Vector3 &p0,
|
||||
const Ogre::Vector3 &p1) -> bool {
|
||||
/* The far end is the edge (c1, c2) — exclude it. */
|
||||
float d1 = p0.distance(c[1]) + p1.distance(c[2]);
|
||||
float d2 = p0.distance(c[2]) + p1.distance(c[1]);
|
||||
return (d1 > 0.001f && d2 > 0.001f);
|
||||
};
|
||||
extrudeToSlab(out, centerSurf, graph.config.roadThickness,
|
||||
skirtFilter);
|
||||
return true;
|
||||
return RoadGeometryLib::buildSegmentGeometry(segment, graph, out);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -1897,16 +1059,9 @@ void RoadSystem::complyTerrain(TerrainSystem *terrainSystem,
|
||||
continue;
|
||||
|
||||
/* Write fixups from the generated top-surface
|
||||
* vertices. The top-surface Y after slab
|
||||
* extrusion is at +halfThick; the road-surface Y
|
||||
* is topY - halfThick, so the fixup target is
|
||||
* topY - halfThick - roadThickness =
|
||||
* topY - halfThick*2 - halfThick =
|
||||
* topY - roadThickness - halfThick.
|
||||
*
|
||||
* Simpler: sample the Y of vertices whose normal
|
||||
* vertices: sample the Y of vertices whose normal
|
||||
* points up and write target = Y - roadThickness
|
||||
* underneath them. */
|
||||
* underneath them (the slab bottom). */
|
||||
const auto &verts = tmp.getVertices();
|
||||
for (const auto &v : verts) {
|
||||
if (v.mNormal.y <= 0.5f)
|
||||
@@ -1920,7 +1075,7 @@ void RoadSystem::complyTerrain(TerrainSystem *terrainSystem,
|
||||
for (const RoadStraightSegment &seg : pg.segments) {
|
||||
Ogre::Vector3 c[4];
|
||||
Ogre::Vector2 uvc[4];
|
||||
if (!computeSegmentBand(seg, rg, c, uvc))
|
||||
if (!RoadGeometryLib::computeSegmentBand(seg, rg, c, uvc))
|
||||
continue;
|
||||
|
||||
/* Write fixups at the band corners. */
|
||||
|
||||
@@ -156,8 +156,10 @@ public:
|
||||
* The wedge piece is built by the three-phase pipeline: a strip of
|
||||
* concatenated template copies is bent along the wedge's 2-segment
|
||||
* centerline polyline (edge midpoint -> node -> edge midpoint) with
|
||||
* the outer-curb offset interpolated through the miter corner, so
|
||||
* the road keeps its exact width through turns with no gaps or
|
||||
* the outer-curb offset following the mitered curb chain — pinned
|
||||
* at the miter corner for inner wedges so cross-sections cannot
|
||||
* fold, blended through the miter corner for outer wedges — so the
|
||||
* road keeps its exact width through turns with no gaps or
|
||||
* overlaps. The template supplies the slab thickness (top and
|
||||
* bottom at +/- roadThickness/2); template cap faces and the
|
||||
* centerline wall are dropped because they are interior to the
|
||||
@@ -168,6 +170,10 @@ public:
|
||||
* via getRoadTemplate()). Static so headless tests can call them
|
||||
* without a scene. Returns false when the primitive is degenerate
|
||||
* and nothing was emitted (e.g. a wedge wider than ~360 degrees).
|
||||
*
|
||||
* All of these forward to RoadGeometryLib
|
||||
* (roadlib/RoadGeometryLib.cpp), which holds the single
|
||||
* implementation.
|
||||
*/
|
||||
static bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
@@ -180,56 +186,6 @@ public:
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
/**
|
||||
* Pipeline phases (ProceduralRoadGeometry.md section 10).
|
||||
*
|
||||
* Exposed as public statics so headless tests can exercise the key
|
||||
* math without a scene.
|
||||
*/
|
||||
/** Phase 1: straight strip of N concatenated template copies
|
||||
* along -Z (all faces kept). */
|
||||
static void buildConcatenatedStrip(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
int N);
|
||||
/** Phase 2: bend the strip into the wedge shape, in place. */
|
||||
static void transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph);
|
||||
/** Phase 3: push centerline-side vertices near the node slightly
|
||||
* past it so adjacent wedges overlap at the center junction. */
|
||||
static void shiftSeamVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph);
|
||||
/**
|
||||
* Outer-curb offset at path distance @p d from the wedge start.
|
||||
*
|
||||
* The vector from the centerline to the outer curb; it anchors at
|
||||
* w1*r1 on the first half-edge, passes exactly through the miter
|
||||
* corner at the node (no corner holes), and ends at -w2*r2 on the
|
||||
* second half-edge, interpolated through the narrow blend zone.
|
||||
*/
|
||||
static Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
float d);
|
||||
|
||||
/** Returns false for a boundary edge that must stay open. */
|
||||
using SkirtFilter = std::function<bool(const Ogre::Vector3 &p0,
|
||||
const Ogre::Vector3 &p1)>;
|
||||
|
||||
/**
|
||||
* Turn a center-surface triangle set into a solid slab (spec
|
||||
* section 8): top and bottom at +/- roadThickness/2 (winding
|
||||
* auto-oriented by normal Y sign) plus vertical skirts on
|
||||
* boundary edges (edges used by exactly one triangle; centerSurf
|
||||
* must share vertices along interior edges). @p skirtFilter can
|
||||
* exclude specific boundary edges (e.g. the segment far end,
|
||||
* which meets the neighbor node's piece).
|
||||
*/
|
||||
static void extrudeToSlab(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer ¢erSurf,
|
||||
float roadThickness,
|
||||
const SkirtFilter &skirtFilter = nullptr);
|
||||
|
||||
/**
|
||||
* Create a unit-box template for headless tests and fallback.
|
||||
*
|
||||
@@ -332,9 +288,6 @@ private:
|
||||
/* Roadside prefab spawning (M5.11). */
|
||||
void spawnSidePrefabs(RoadPageGeometry &pg);
|
||||
|
||||
bool loadTemplateFromMesh(const std::string &meshName);
|
||||
void buildFallbackTemplate(float roadThickness);
|
||||
|
||||
Ogre::Vector3 getNodePosition(int nodeId) const;
|
||||
bool getEdgePositions(int edgeIndex, Ogre::Vector3 &outA,
|
||||
Ogre::Vector3 &outB) const;
|
||||
|
||||
@@ -1941,6 +1941,190 @@ bool TerrainTestRunner::testRoadPageAssignment(EditorApp &app,
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Road slab self-intersection analysis */
|
||||
/* */
|
||||
/* Counts COPLANAR pairs (two triangles on the same plane overlapping */
|
||||
/* in area — z-fighting duplicates) and CROSSING pairs (a triangle */
|
||||
/* edge properly piercing another triangle's interior — */
|
||||
/* interpenetrating sheets). Triangles sharing vertices or edges are */
|
||||
/* not counted. Used by testRoadWedgeGeometry. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
namespace {
|
||||
|
||||
typedef std::pair<double, double> P2; /* (x, z) */
|
||||
|
||||
double cross2d(const P2 &a, const P2 &b)
|
||||
{
|
||||
return a.first * b.second - a.second * b.first;
|
||||
}
|
||||
|
||||
std::vector<P2> clipHalfPlane(const std::vector<P2> &poly, const P2 &a,
|
||||
const P2 &b)
|
||||
{
|
||||
std::vector<P2> out;
|
||||
if (poly.empty())
|
||||
return out;
|
||||
P2 edge(b.first - a.first, b.second - a.second);
|
||||
auto inside = [&](const P2 &p) {
|
||||
P2 rel(p.first - a.first, p.second - a.second);
|
||||
return cross2d(edge, rel) >= 0.0;
|
||||
};
|
||||
auto intersect = [&](const P2 &p0, const P2 &p1) {
|
||||
P2 e0(p0.first - a.first, p0.second - a.second);
|
||||
P2 e1(p1.first - a.first, p1.second - a.second);
|
||||
double d0 = cross2d(edge, e0);
|
||||
double d1 = cross2d(edge, e1);
|
||||
double t = d0 / (d0 - d1);
|
||||
return P2(p0.first + (p1.first - p0.first) * t,
|
||||
p0.second + (p1.second - p0.second) * t);
|
||||
};
|
||||
for (size_t i = 0; i < poly.size(); ++i) {
|
||||
const P2 &cur = poly[i];
|
||||
const P2 &prv = poly[(i + poly.size() - 1) % poly.size()];
|
||||
bool inCur = inside(cur), inPrv = inside(prv);
|
||||
if (inCur) {
|
||||
if (!inPrv)
|
||||
out.push_back(intersect(prv, cur));
|
||||
out.push_back(cur);
|
||||
} else if (inPrv) {
|
||||
out.push_back(intersect(prv, cur));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
double polyArea(const std::vector<P2> &poly)
|
||||
{
|
||||
if (poly.size() < 3)
|
||||
return 0.0;
|
||||
double s = 0.0;
|
||||
for (size_t i = 0; i < poly.size(); ++i) {
|
||||
const P2 &p = poly[i];
|
||||
const P2 &q = poly[(i + 1) % poly.size()];
|
||||
s += p.first * q.second - q.first * p.second;
|
||||
}
|
||||
return 0.5 * s;
|
||||
}
|
||||
|
||||
double triOverlapAreaXZ(const Ogre::Vector3 t0[3], const Ogre::Vector3 t1[3])
|
||||
{
|
||||
std::vector<P2> subject;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
subject.push_back(P2(t0[i].x, t0[i].z));
|
||||
std::vector<P2> clip;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
clip.push_back(P2(t1[i].x, t1[i].z));
|
||||
if (polyArea(clip) < 0.0)
|
||||
std::reverse(clip.begin(), clip.end());
|
||||
std::vector<P2> poly = subject;
|
||||
for (int e = 0; e < 3 && !poly.empty(); ++e)
|
||||
poly = clipHalfPlane(poly, clip[e], clip[(e + 1) % 3]);
|
||||
return std::fabs(polyArea(poly));
|
||||
}
|
||||
|
||||
/* Barycentric coords of p in triangle (a,b,c); false if degenerate. */
|
||||
bool bary(const Ogre::Vector3 &p, const Ogre::Vector3 &a,
|
||||
const Ogre::Vector3 &b, const Ogre::Vector3 &c, float &u, float &v,
|
||||
float &w)
|
||||
{
|
||||
Ogre::Vector3 v0 = b - a, v1 = c - a, v2 = p - a;
|
||||
float d00 = v0.dotProduct(v0);
|
||||
float d01 = v0.dotProduct(v1);
|
||||
float d11 = v1.dotProduct(v1);
|
||||
float d20 = v2.dotProduct(v0);
|
||||
float d21 = v2.dotProduct(v1);
|
||||
float denom = d00 * d11 - d01 * d01;
|
||||
if (std::fabs(denom) < 1e-12f)
|
||||
return false;
|
||||
v = (d11 * d20 - d01 * d21) / denom;
|
||||
w = (d00 * d21 - d01 * d20) / denom;
|
||||
u = 1.0f - v - w;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Segment (p0,p1) vs triangle (a,b,c): proper interior piercing test.
|
||||
* The intersection must be strictly inside the triangle and strictly
|
||||
* inside the segment (shared vertices/edges do not count). */
|
||||
bool segTriPierce(const Ogre::Vector3 &p0, const Ogre::Vector3 &p1,
|
||||
const Ogre::Vector3 &a, const Ogre::Vector3 &b,
|
||||
const Ogre::Vector3 &c)
|
||||
{
|
||||
Ogre::Vector3 n = (b - a).crossProduct(c - a);
|
||||
float len = n.length();
|
||||
if (len < 1e-8f)
|
||||
return false;
|
||||
n /= len;
|
||||
float d0 = n.dotProduct(p0 - a);
|
||||
float d1 = n.dotProduct(p1 - a);
|
||||
if (d0 * d1 >= 0.0f)
|
||||
return false; /* same side or touching */
|
||||
float t = d0 / (d0 - d1);
|
||||
if (t < 1e-4f || t > 1.0f - 1e-4f)
|
||||
return false;
|
||||
Ogre::Vector3 p = p0 + (p1 - p0) * t;
|
||||
float u, v, w;
|
||||
if (!bary(p, a, b, c, u, v, w))
|
||||
return false;
|
||||
if (u < 1e-4f || v < 1e-4f || w < 1e-4f)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void countSlabOverlaps(const Procedural::TriangleBuffer &buf, int &coplanar,
|
||||
int &crossing)
|
||||
{
|
||||
coplanar = 0;
|
||||
crossing = 0;
|
||||
const auto &verts = buf.getVertices();
|
||||
const auto &indices = buf.getIndices();
|
||||
size_t nTri = indices.size() / 3;
|
||||
|
||||
for (size_t i = 0; i < nTri; ++i) {
|
||||
Ogre::Vector3 t0[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
t0[k] = verts[(size_t)indices[i * 3 + k]].mPosition;
|
||||
Ogre::Vector3 n0 = (t0[1] - t0[0]).crossProduct(t0[2] - t0[0]);
|
||||
float l0 = n0.length();
|
||||
if (l0 > 1e-8f)
|
||||
n0 /= l0;
|
||||
for (size_t j = i + 1; j < nTri; ++j) {
|
||||
Ogre::Vector3 t1[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
t1[k] = verts[(size_t)indices[j * 3 + k]]
|
||||
.mPosition;
|
||||
Ogre::Vector3 n1 =
|
||||
(t1[1] - t1[0]).crossProduct(t1[2] - t1[0]);
|
||||
float l1 = n1.length();
|
||||
if (l1 > 1e-8f)
|
||||
n1 /= l1;
|
||||
|
||||
if (std::fabs(n0.dotProduct(n1)) > 0.9999f) {
|
||||
/* Parallel planes: coplanar z-fight check. */
|
||||
float dist =
|
||||
std::fabs(n0.dotProduct(t1[0] - t0[0]));
|
||||
if (dist < 1e-3f &&
|
||||
triOverlapAreaXZ(t0, t1) > 1e-3) {
|
||||
++coplanar;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
bool pierce = false;
|
||||
for (int e = 0; e < 3 && !pierce; ++e)
|
||||
pierce = segTriPierce(t0[e], t0[(e + 1) % 3],
|
||||
t1[0], t1[1], t1[2]);
|
||||
for (int e = 0; e < 3 && !pierce; ++e)
|
||||
pierce = segTriPierce(t1[e], t1[(e + 1) % 3],
|
||||
t0[0], t0[1], t0[2]);
|
||||
if (pierce)
|
||||
++crossing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
TerrainSystem *ts)
|
||||
{
|
||||
@@ -2143,6 +2327,12 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
if (s.min.x < -0.06f || s.min.z < -0.06f ||
|
||||
s.max.x > 10.05f || s.max.z > 10.05f)
|
||||
return fail("90 deg wedge overshoots the L-shape");
|
||||
/* The template supplies the slab thickness: top and bottom
|
||||
* at +/- roadThickness/2, no second extrusion on top. */
|
||||
if (s.min.y < -0.16f || s.min.y > -0.14f ||
|
||||
s.max.y < 0.14f || s.max.y > 0.16f)
|
||||
return fail("90 deg wedge slab thickness wrong "
|
||||
"(double extrusion?)");
|
||||
|
||||
bool sawCorner = false;
|
||||
bool sawNode = false;
|
||||
@@ -2190,6 +2380,64 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
"(-3,-3)");
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 3b: a converging (inner, sweep < 180 deg) wedge must not
|
||||
* self-intersect — no coplanar duplicate sheets and no piercing
|
||||
* triangles — flat or with a height difference at the corner
|
||||
* node. Regression test for the miter-corner fold: the curb is
|
||||
* pinned at the miter corner K through the whole corner zone so
|
||||
* consecutive cross-sections cannot fold over each other.
|
||||
*/
|
||||
{
|
||||
const float nodeYs[2] = { 0.0f, 5.0f };
|
||||
for (int iter = 0; iter < 2; ++iter) {
|
||||
RoadGraph rg;
|
||||
int a3 = rg.addNode(Ogre::Vector3(-10, 0, 0));
|
||||
int b3 = rg.addNode(
|
||||
Ogre::Vector3(0, nodeYs[iter], 0));
|
||||
int c3 = rg.addNode(Ogre::Vector3(10, 0, 10));
|
||||
rg.addEdge(a3, b3);
|
||||
rg.addEdge(b3, c3);
|
||||
|
||||
std::vector<RoadWedge> wedges;
|
||||
std::vector<RoadStraightSegment> segs;
|
||||
enumerateWedges(rg, wedges, segs);
|
||||
|
||||
bool saw135 = false;
|
||||
for (const auto &w : wedges) {
|
||||
if (w.nodeId != b3 || w.degenerate)
|
||||
continue;
|
||||
if (fabsf(w.sweptAngleDeg - 135.0f) < 0.1f)
|
||||
saw135 = true;
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildWedgeGeometry(w, rg,
|
||||
buf))
|
||||
return fail("135 deg corner wedge "
|
||||
"build failed");
|
||||
int cop = 0, cro = 0;
|
||||
countSlabOverlaps(buf, cop, cro);
|
||||
if (cop != 0 || cro != 0)
|
||||
return fail("converging wedge "
|
||||
"self-intersects");
|
||||
}
|
||||
if (!saw135)
|
||||
return fail("135 deg wedge not found");
|
||||
|
||||
for (const auto &sg : segs) {
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildSegmentGeometry(sg, rg,
|
||||
buf))
|
||||
return fail("segment build failed "
|
||||
"(case 3b)");
|
||||
int cop = 0, cro = 0;
|
||||
countSlabOverlaps(buf, cop, cro);
|
||||
if (cop != 0 || cro != 0)
|
||||
return fail("segment self-intersects "
|
||||
"(case 3b)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 4: a nearly-collinear (~360 degree) wedge is
|
||||
* degenerate and emits nothing.
|
||||
|
||||
Reference in New Issue
Block a user