Added demo for road geometry
This commit is contained in:
@@ -629,6 +629,46 @@ target_include_directories(save_load_lua_test PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Road Geometry Library — standalone wedge/segment generation (M5)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Extracted from RoadSystem.cpp; depends only on Ogre, OgreProcedural, and
|
||||
# RoadGraph.hpp. No ECS, physics, or terrain dependency.
|
||||
add_library(RoadGeometryLib STATIC
|
||||
roadlib/RoadGeometryLib.cpp
|
||||
roadlib/RoadGeometryLib.hpp
|
||||
)
|
||||
|
||||
target_include_directories(RoadGeometryLib PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(RoadGeometryLib PUBLIC
|
||||
OgreMain
|
||||
OgreProcedural::OgreProcedural
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Road Geometry Demo — standalone OGRE + ImGui app for debugging
|
||||
# ---------------------------------------------------------------------------
|
||||
# Takes 3 world-space points ABC and renders the wedge formed at node B
|
||||
# (midpoint AB → B → midpoint BC). ImGui sliders adjust points in real-time.
|
||||
add_executable(RoadGeometryDemo
|
||||
road_demo/main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(RoadGeometryDemo
|
||||
RoadGeometryLib
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMain
|
||||
OgreProcedural::OgreProcedural
|
||||
)
|
||||
|
||||
target_include_directories(RoadGeometryDemo PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Package Archive Library
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,489 @@
|
||||
# Procedural Road Wedge Geometry — Specification
|
||||
|
||||
## 1. Terms
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Wedge** | A road piece covering the V-shaped region from midpoint of edge AB, through node B, to midpoint of edge BC. Produced by `enumerateWedges()` — consecutive pairs of angle-sorted half-edges at a node. Wedges at a node tile the full 360°. |
|
||||
| **Half-edge** | One directional segment from a node to the midpoint of one of its incident edges. Fields: `direction`, `halfLength`, `lanesOut`, `lanesIn`, `roadLevelAtNode`, `roadLevelAtNeighbor`. |
|
||||
| **Centerline** | The 2-segment polyline M_A → O → M_B. O is the seed node; M_A, M_B are edge midpoints. Lengths: L1 = first half-edge, L2 = second, total L = L1 + L2. |
|
||||
| **Template** | An Ogre mesh (or generated fallback box) returned by `RoadSystem::getRoadTemplate(cfg)`. X∈[0,1], Y∈[-thick/2,+thick/2], Z∈[-1,0], UV∈[0,1]². |
|
||||
| **Template space** | Coordinate system of the concatenated strip: X = lateral from centerline toward outer curb, Y = vertical from road surface, Z = longitudinal (0 at wedge start, negative toward end). |
|
||||
| **Outer curb** | The exposed boundary of the wedge — the edge farthest from the node, opposite the centerline. Defined by the continuous curve C(d) = center(d) + offset(d). |
|
||||
|
||||
## 2. Template Mesh Conventions
|
||||
|
||||
The template from `getRoadTemplate(cfg)`:
|
||||
|
||||
- **X**: ∈ [0, 1]. Template +X maps toward the **outer curb** of the wedge. X=0 is the centerline.
|
||||
- **Y**: ∈ [-roadThickness/2, +roadThickness/2]. Maps directly to world vertical offset from the road surface at that position.
|
||||
- **Z**: ∈ [-1, 0] (fallback box; loaded files may differ but must span exactly 1 unit of distance along the road).
|
||||
- **UVs**: span (0,0)–(1,1) over X/Z extents on each face.
|
||||
- **Normals**: preserved through rigid rotation during transformation.
|
||||
|
||||
If the template file is missing, the fallback is a 6-face unit box (24 verts, 36 indices, X∈[0,1], Y∈[-thick/2,+thick/2], Z∈[-1,0]).
|
||||
|
||||
## 3. Algorithm Overview
|
||||
|
||||
Three phases, each a pure function on `Procedural::TriangleBuffer`:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
Phase 3: shiftSeamVertices(strip, wedge, graph)
|
||||
→ Shift centerline-side vertices near the node slightly past O
|
||||
so adjacent wedges overlap and close the center junction.
|
||||
Essential for nodes with > 2 neighbors.
|
||||
```
|
||||
|
||||
These are chained by the public entry points:
|
||||
|
||||
```cpp
|
||||
static bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out);
|
||||
static bool buildSegmentGeometry(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out);
|
||||
```
|
||||
|
||||
Degenerate wedges (sweptAngleDeg > 270°, flagged by `enumerateWedges()`) return false and emit nothing.
|
||||
|
||||
## 4. Phase 1 — Concatenated Strip
|
||||
|
||||
**Function**: `static void buildConcatenatedStrip(Procedural::TriangleBuffer &out, const Procedural::TriangleBuffer &templ, int N)`
|
||||
|
||||
```
|
||||
out.clear()
|
||||
for i = 0 to N-1:
|
||||
base = out.vertexCount()
|
||||
for each vertex v in templ:
|
||||
v' = copy of v
|
||||
v'.position.z -= i // shift 1 unit back per copy
|
||||
out.addVertex(v')
|
||||
for each index idx in templ:
|
||||
out.addIndex(base + idx)
|
||||
```
|
||||
|
||||
After Phase 1 the strip occupies X∈[0,1], Y∈[-thick/2,+thick/2], Z∈[-N,0].
|
||||
|
||||
`N = (int)ceil(L1 + L2)`. Vertices with d = |z| > L after Phase 2 are removed.
|
||||
|
||||
## 5. Phase 2 — Vertex Transformation (No Gaps)
|
||||
|
||||
**Function**: `static void transformWedgeVertices(Procedural::TriangleBuffer &strip, const RoadWedge &wedge, const RoadGraph &graph)`
|
||||
|
||||
### 5.1 Derived Values
|
||||
|
||||
```
|
||||
O = graph.nodes[seedNode].position
|
||||
M_A = O + H1.direction * L1
|
||||
M_B = O + H2.direction * L2
|
||||
|
||||
L1 = H1.halfLength
|
||||
L2 = H2.halfLength
|
||||
L = L1 + L2
|
||||
|
||||
dir1 = H1.direction // normalized, y=0, points from O toward neighbor
|
||||
dir2 = H2.direction
|
||||
|
||||
r1 = dir1.crossProduct(UNIT_Y) // right vector, segment 1
|
||||
r2 = dir2.crossProduct(UNIT_Y) // right vector, segment 2
|
||||
|
||||
lw = graph.config.laneWidth
|
||||
w1 = H1.lanesOut * lw // road half-width on H1 side of centerline
|
||||
w2 = H2.lanesIn * lw // road half-width on H2 side of centerline
|
||||
in1 = H1.lanesIn * lw // UV lateral offset for continuity
|
||||
|
||||
yO = O.y + nodeRoadLevel(graph, seedNode)
|
||||
```
|
||||
|
||||
### 5.2 Blend Zone
|
||||
|
||||
A narrow symmetric zone around the node where the outer-curb offset
|
||||
transitions continuously from `w1 * r1` to `-w2 * r2`:
|
||||
|
||||
```
|
||||
W = min(ROAD_SEAM_OVERLAP * 4, // ~0.2 units — tight, 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).
|
||||
|
||||
### 5.3 Centerline Position
|
||||
|
||||
For a vertex at distance `d = -v.position.z` from the wedge start:
|
||||
|
||||
```
|
||||
// d is guaranteed to be in [0, L] by Phase 1 construction
|
||||
if d <= L1:
|
||||
t = d / L1
|
||||
center(d) = lerp(M_A, O, t)
|
||||
else:
|
||||
t = (d - L1) / L2
|
||||
center(d) = lerp(O, M_B, t)
|
||||
```
|
||||
|
||||
`center(d)` has a sharp corner at O — the centerline is a polyline.
|
||||
This is correct: road intersections have sharp bends. The corner
|
||||
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:
|
||||
|
||||
```
|
||||
if W == 0 or d <= L1 - W:
|
||||
offset(d) = w1 * r1
|
||||
elif d >= L1 + W:
|
||||
offset(d) = -w2 * r2
|
||||
else:
|
||||
t = (d - (L1 - W)) / (2 * W) // 0 → 1 across blend zone
|
||||
offset(d) = lerp(w1 * r1, -w2 * r2, 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.
|
||||
|
||||
### 5.5 Road Width Interpolation
|
||||
|
||||
The scalar road half-width varies linearly across the wedge:
|
||||
|
||||
```
|
||||
width(d) = w1 + (w2 - w1) * (d / L)
|
||||
```
|
||||
|
||||
### 5.6 Surface Height
|
||||
|
||||
```
|
||||
roadSurfaceY(d):
|
||||
if d <= L1: return halfEdgeHeightAt(H1, graph, 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.
|
||||
|
||||
### 5.7 Per-Vertex Transform
|
||||
|
||||
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
|
||||
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)
|
||||
: halfEdgeU(H2, graph, d - L1)
|
||||
v.uv.y = v.uv.y * localWidth + in1
|
||||
|
||||
// Normal — rotate template-forward (-Z) to segment direction:
|
||||
segDir = (d <= L1) ? dir1 : dir2
|
||||
Ogre::Quaternion q(segDir.angleBetween(Ogre::Vector3::NEGATIVE_UNIT_Z),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
v.normal = q * v.normal;
|
||||
```
|
||||
|
||||
Since Phase 1 guarantees d ∈ [0, L] (we use exactly ceil(L) copies and
|
||||
the template spans exactly 1 Z-unit), there are no out-of-range vertices.
|
||||
|
||||
### 5.8 Why This Is Continuous (No Gaps)
|
||||
|
||||
At every distance d, the cross-section extends from `center(d)` along
|
||||
`lateralDir(d) = normalize(offset(d))`. Since `offset(d)` is continuous
|
||||
(even across the node!), `lateralDir(d)` varies continuously. Vertices
|
||||
at adjacent distances d and d+ε map to adjacent world positions. **No gap
|
||||
opens at the outer corner.**
|
||||
|
||||
The travel direction `dir(d)` is piecewise (dir1 → dir2 at the node), but
|
||||
`dir(d)` only affects the normal rotation and UV computation — it does
|
||||
not affect vertex positions. The cross-section orientation is driven
|
||||
entirely by the continuous `offset(d)`.
|
||||
|
||||
The centerline has a sharp corner at O, but the centerline edge is the
|
||||
**inside** of the bend, shared with adjacent wedges. No fill is needed
|
||||
there.
|
||||
|
||||
## 6. Phase 3 — Center Seam Shifting
|
||||
|
||||
**Function**: `static void shiftSeamVertices(Procedural::TriangleBuffer &strip, const RoadWedge &wedge, const RoadGraph &graph)`
|
||||
|
||||
For nodes with > 2 neighbors, the inner edges of all incident wedges may
|
||||
not meet at a perfect point, leaving a sub-pixel hole at the exact center.
|
||||
This is closed by shifting centerline-side vertices near O slightly past
|
||||
the node:
|
||||
|
||||
```
|
||||
ROAD_SEAM_OVERLAP = 0.05f
|
||||
|
||||
// Only needed for nodes with > 2 neighbors
|
||||
if graph.getNeighborIds(seedNode).size() <= 2:
|
||||
return // straight-through or endpoint, center is continuous
|
||||
|
||||
for each vertex v in strip:
|
||||
// Check if vertex is on the centerline side (small lateral offset)
|
||||
Ogre::Vector3 toNode(v.position.x - O.x, 0, v.position.z - O.z);
|
||||
float distToNode = toNode.length();
|
||||
|
||||
if distToNode < ROAD_SEAM_OVERLAP:
|
||||
Ogre::Vector3 radial = toNode.normalisedCopy();
|
||||
if radial.isZeroLength():
|
||||
continue // exactly at O, should not happen
|
||||
v.position += radial * (ROAD_SEAM_OVERLAP - distToNode + ROAD_SEAM_OVERLAP);
|
||||
```
|
||||
|
||||
This creates ~0.05 units of overlap at the center junction where >2
|
||||
wedges meet. For nodes with exactly 2 neighbors (straight-through roads)
|
||||
or 1 neighbor (endpoints), the centerline is continuous and no shifting
|
||||
is needed.
|
||||
|
||||
## 7. Straight Segments (Dead-End Nodes)
|
||||
|
||||
A `RoadStraightSegment` covers a single half-edge from an endpoint node.
|
||||
No bend, no blend zone — a simple rectangular band:
|
||||
|
||||
```
|
||||
d = HE.direction
|
||||
r = d.crossProduct(UNIT_Y)
|
||||
inW = HE.lanesIn * laneWidth
|
||||
outW= HE.lanesOut * laneWidth
|
||||
L = HE.halfLength
|
||||
|
||||
// Four corners of the center-surface band:
|
||||
c[0] = O + (-ROAD_SEAM_OVERLAP) * d - inW * r // node end, inbound curb
|
||||
c[1] = O + L * d - inW * r // far end, inbound curb
|
||||
c[2] = O + L * d + outW * r // far end, outbound curb
|
||||
c[3] = O + (-ROAD_SEAM_OVERLAP) * d + outW * r // node end, outbound curb
|
||||
|
||||
// Heights:
|
||||
c[0].y = c[3].y = halfEdgeHeightAt(HE, graph, -ROAD_SEAM_OVERLAP)
|
||||
c[1].y = c[2].y = halfEdgeHeightAt(HE, graph, L)
|
||||
|
||||
// UV:
|
||||
uvc[0] = (halfEdgeU(HE, graph, -ROAD_SEAM_OVERLAP), 0)
|
||||
uvc[1] = (halfEdgeU(HE, graph, L), 0)
|
||||
uvc[2] = (halfEdgeU(HE, graph, L), inW + outW)
|
||||
uvc[3] = (halfEdgeU(HE, graph, -ROAD_SEAM_OVERLAP), inW + outW)
|
||||
```
|
||||
|
||||
The `-ROAD_SEAM_OVERLAP` extends the inner end past the node so it overlaps
|
||||
adjacent wedge pieces.
|
||||
|
||||
Triangulation: two center-surface triangles (c0,c1,c2) and (c0,c2,c3).
|
||||
Extruded to slab with skirts on c[0]→c[3] (node-end cap), c[0]→c[1]
|
||||
(inbound curb), c[3]→c[2] (outbound curb). The far end c[1]→c[2] is open
|
||||
(it meets the neighbor's geometry at the edge midpoint).
|
||||
|
||||
**Function**: `static bool buildSegmentGeometry(const RoadStraightSegment &seg, const RoadGraph &graph, Procedural::TriangleBuffer &out)`
|
||||
|
||||
## 8. Slab Extrusion
|
||||
|
||||
**Function**: `static void extrudeToSlab(Procedural::TriangleBuffer &out, const Procedural::TriangleBuffer ¢erSurf, float roadThickness)`
|
||||
|
||||
### 8.1 Top and Bottom
|
||||
|
||||
For each triangle (p0, p1, p2, uv0, uv1, uv2) in centerSurf:
|
||||
|
||||
```
|
||||
h = roadThickness * 0.5f
|
||||
up = (0, h, 0)
|
||||
|
||||
// Top surface (keep winding)
|
||||
emitTri(out, p0+up, p1+up, p2+up, uv0, uv1, uv2)
|
||||
|
||||
// Bottom surface (flip winding)
|
||||
emitTri(out, p0-up, p2-up, p1-up, uv0, uv2, uv1)
|
||||
```
|
||||
|
||||
### 8.2 Side Skirts
|
||||
|
||||
Only on **boundary edges** — edges appearing in exactly one triangle.
|
||||
Detection: scan all triangle edges; an edge (minIdx, maxIdx) seen once
|
||||
is a boundary edge.
|
||||
|
||||
For each boundary edge (p0, p1, uv0, uv1):
|
||||
|
||||
```
|
||||
thick = 2 * h
|
||||
t0 = p0 + up; t1 = p1 + up
|
||||
b0 = p0 - up; b1 = p1 - up
|
||||
|
||||
// Orient the skirt so its normal points outward.
|
||||
Ogre::Vector3 n = (t1 - t0).crossProduct(b0 - t0);
|
||||
Ogre::Vector3 mid = (t0 + t1 + b0 + b1) * 0.25f;
|
||||
bool outward = n.dotProduct(mid - refPoint) >= 0;
|
||||
|
||||
if outward:
|
||||
emitTri(out, t0, t1, b1, uv0, uv1, UV(uv1.x, uv1.y - thick));
|
||||
emitTri(out, t0, b1, b0, uv0, UV(uv1.x, uv1.y - thick), UV(uv0.x, uv0.y - thick));
|
||||
else:
|
||||
emitTri(out, t0, b1, t1, uv0, UV(uv1.x, uv1.y - thick), uv1);
|
||||
emitTri(out, t0, b0, b1, uv0, UV(uv0.x, uv0.y - thick), UV(uv1.x, uv1.y - thick));
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
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.
|
||||
|
||||
## 9. Seam Suppression Summary
|
||||
|
||||
| Mechanism | What it fixes | Where |
|
||||
|-----------|--------------|-------|
|
||||
| Continuous curb offset (§5.4) | Outer-corner gap where H1 and H2 diverge | 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 |
|
||||
|
||||
## 10. Internal Functions (Testable)
|
||||
|
||||
```cpp
|
||||
// Phase 1
|
||||
static void buildConcatenatedStrip(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
int N);
|
||||
|
||||
// Phase 2 — transforms vertices in-place, continuous across the node
|
||||
static void transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph);
|
||||
|
||||
// Phase 3 — shifts centerline vertices in-place past the node
|
||||
static void shiftSeamVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph);
|
||||
|
||||
// Slab: adds top+bottom+skirts to output from center surface
|
||||
static void extrudeToSlab(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer ¢erSurf,
|
||||
float roadThickness);
|
||||
|
||||
// The key math — independently testable, no scene required
|
||||
static Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
float d);
|
||||
```
|
||||
|
||||
## 11. Test Specification
|
||||
|
||||
### 11.1 `computeCurbOffset` Unit Tests
|
||||
|
||||
| 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) |
|
||||
| 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) |
|
||||
| Blend zone continuity | Any | offset varies with d | no discontinuity at L1 | — |
|
||||
|
||||
### 11.2 Integration Tests (matching existing `testRoadWedgeGeometry`)
|
||||
|
||||
| Test | Expected |
|
||||
|------|----------|
|
||||
| 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) |
|
||||
| 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 |
|
||||
| Template usage | Vertex count ∝ templateVertCount × ceil(L) |
|
||||
|
||||
### 11.3 `extrudeToSlab` Tests
|
||||
|
||||
| Test | Expected |
|
||||
|------|----------|
|
||||
| Single triangle, thick=0.3 | 6 tris (top+bottom+3 skirts), Y∈[-0.15,0.15] |
|
||||
| Two adjacent triangles | 10 tris (shared edge has no skirt) |
|
||||
|
||||
## 12. Migration from Current Implementation
|
||||
|
||||
### Kept unchanged
|
||||
|
||||
- `roadRightVec(d)` — cross with UNIT_Y
|
||||
- `halfEdgeHeights(he, graph, yNode, yMid)`
|
||||
- `halfEdgeHeightAt(he, graph, t)`
|
||||
- `halfEdgeU(he, graph, t)` — phase-continuous UV
|
||||
- `nodeRoadLevel(graph, nodeId)`
|
||||
- `emitTri(out, p0, p1, p2, uv0, uv1, uv2)` — degenerate-skip
|
||||
- `ROAD_SEAM_OVERLAP = 0.05f`
|
||||
- Helper structs: `RoadSurfTri`, `RoadSkirtEdge`
|
||||
|
||||
### Replaced
|
||||
|
||||
| Old | New | Reason |
|
||||
|-----|-----|--------|
|
||||
| `computeWedgeOutline()` (~80 lines) | `computeCurbOffset()` (~30 lines) | Interpolate offsets, not build polygon |
|
||||
| `RoadWedgeOutline` struct | Not needed | No explicit polygon |
|
||||
| `triangulateOutline()` (~90 lines) | Template index buffer | Template already has triangulation |
|
||||
| `emitSlab()` (~45 lines) | `extrudeToSlab()` (~40 lines) | Same logic, cleaner signature |
|
||||
|
||||
### Removed
|
||||
|
||||
- `RoadWedgeOutline` struct — no polygon built
|
||||
- `triangulateOutline()` — ear-clipping no longer needed
|
||||
- Gap-fill triangle fan at outer corner — continuous offset eliminates the gap
|
||||
- `computeMiterCorner()` — no miter corner needed
|
||||
|
||||
## 13. Rationale
|
||||
|
||||
### Why continuous curb offset instead of segment classification + miter fill?
|
||||
|
||||
The continuous-interpolation approach is what the user's original
|
||||
implementation did: every vertex's Z coordinate maps uniquely to a
|
||||
position+orientation along the path. No gaps appear because the
|
||||
cross-section orientation varies continuously.
|
||||
|
||||
The alternative (classify each vertex as "segment 1" or "segment 2"
|
||||
and apply a different transform) creates a discontinuity at the node
|
||||
where the two segments' outer curbs diverge. This requires extra
|
||||
geometry (miter corner triangles) to fill — an unnecessary complication.
|
||||
|
||||
### Why linear interpolation of offset vectors?
|
||||
|
||||
Both `w1*r1` and `-w2*r2` point into the wedge interior. Linear
|
||||
vector interpolation stays within the wedge for all sweep angles.
|
||||
Angular interpolation (slerp) would add complexity with no visible
|
||||
benefit for the narrow blend zone (W ≈ 0.2 units).
|
||||
|
||||
### Why is the travel direction still piecewise?
|
||||
|
||||
`dir(d)` is piecewise (dir1 for d≤L1, dir2 for d>L1) because the
|
||||
centerline is a polyline with a sharp corner. This is correct for
|
||||
road intersections. `dir(d)` only affects normal rotation and UV
|
||||
lookup — vertex positions are driven by the continuous `offset(d)`.
|
||||
|
||||
### Template mesh is finally used
|
||||
|
||||
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.
|
||||
@@ -281,7 +281,7 @@ ordered by dependency.
|
||||
|
||||
| # | Item | Files to modify | Status |
|
||||
|---|------|-----------------|--------|
|
||||
| W0 | Sweep-based wedge geometry (M5.6 gaps + overlaps) | `RoadSystem.cpp`, `RoadSystem.hpp`, `TerrainTests.cpp` | ✅ DONE (2026-07-31) |
|
||||
| W0 | Sweep-based wedge geometry (M5.6 gaps + overlaps) — **superseded 2026-08-02**: the radial curb sweep left node-center holes, diagonal > 180° bands, bowed through-roads and double-height segments; replaced by the mitered polyline sweep (`computeWedgeOutline`/`triangulateOutline` + `emitSlab`) per user direction | `RoadSystem.cpp`, `RoadSystem.hpp`, `TerrainTests.cpp` | ✅ DONE (2026-08-02, reworked) |
|
||||
| W1 | M5.10 perpendicular falloff in `complyTerrain()` | `RoadSystem.cpp` | ✅ DONE |
|
||||
| W2 | Helper `computeComplianceHeight()` + unit test | `RoadSystem.cpp`, `TerrainTests.cpp` | ✅ DONE |
|
||||
| W3 | `testTerrainCompliance` (automated) | `TerrainTests.cpp`, `TerrainTests.hpp` | ✅ DONE |
|
||||
@@ -296,7 +296,7 @@ ordered by dependency.
|
||||
| Area | Status |
|
||||
|------|--------|
|
||||
| M5.1–M5.8 automated coverage | ✅ Adequate (8/8 sub-items have tests) |
|
||||
| M5.6 sweep-based wedge geometry | ✅ Implemented (W0, 2026-07-31) — replaces fan/strip approach |
|
||||
| M5.6 wedge geometry | ✅ Mitered polyline sweep (2026-08-02) — replaces the broken radial curb sweep (W0 rework): wedge = one mesh bent along the 2-segment centerline polyline, exact width at corners, no node holes/overlaps |
|
||||
| M5.9 automated coverage | ✅ W4 adds raycast+rebuild verification |
|
||||
| M5.9.6 road collider debug toggle | ❌ Not implemented → W5 |
|
||||
| M5.10 perpendicular falloff | ✅ Implemented → W1+W2 |
|
||||
@@ -307,7 +307,8 @@ ordered by dependency.
|
||||
| Open questions | ✅ All resolved (section 0) |
|
||||
|
||||
**Exit criteria** — Milestone 5 is fully verified when:
|
||||
- [x] W0 (sweep-based wedge geometry) implemented and tested.
|
||||
- [x] W0 (wedge geometry) implemented and tested — mitered polyline sweep
|
||||
(2026-08-02 rework; radial curb sweep attempt reverted).
|
||||
- [x] W1–W4, W6–W8 implemented.
|
||||
- [x] `./editSceneEditor --headless --run-terrain-tests=1` passes with all
|
||||
22 tests green per iteration (verified 2026-07-31).
|
||||
|
||||
@@ -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 | `buildWedgeGeometry`/`buildSegmentGeometry` + `emitSlab` in `RoadSystem`, `roadWedgeGeometry` test |
|
||||
| M5.6 Wedge geometry | ✅ complete | mitered polyline sweep: `computeWedgeOutline`/`triangulateOutline` + `emitSlab` in `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` |
|
||||
@@ -2646,42 +2646,66 @@ region defined by its two half-edges.
|
||||
- No wedge blending is needed.
|
||||
- Apply the same lane-count/asymmetric-lane rules as for a wedge side.
|
||||
|
||||
**Status (2026-07-26): ✅ complete**, with a deliberate deviation from the
|
||||
literal template-copy sweep described above. The naive chord sweep was
|
||||
rejected: it overshoots non-road corner triangles and ignores per-side lane
|
||||
widths. What is implemented in `RoadSystem::buildWedgeGeometry()` /
|
||||
`buildSegmentGeometry()` instead:
|
||||
**Status (2026-08-02): ✅ complete — mitered polyline sweep.** Each wedge
|
||||
piece is generated as ONE whole mesh bent along the wedge's 2-segment
|
||||
centerline polyline `M_A → O → M_B` (edge midpoint → node → edge midpoint)
|
||||
— the same transform as bending a mesh along a spline, but with a
|
||||
2-segment polyline. Cross-sections run from the centerline (shared
|
||||
exactly with the neighbouring wedge) out to the curb; at the node the
|
||||
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.
|
||||
|
||||
- The wedge region is the exact union of two one-sided lane band strips
|
||||
(H1's outbound side `s ∈ [0, L_out·laneWidth]`, H2's inbound side
|
||||
`s ∈ [-L_in·laneWidth, 0]`; `roadRightVec(d) = d × UNIT_Y`).
|
||||
- Swept ≤ 180° with a valid outer corner `X` (curb intersection, Cramer
|
||||
solve with `|det| ≥ 0.05`, `t` inside both half-lengths): single L-shaped
|
||||
hexagon emitted as a fan around `X` (4 triangles; degenerate ones skipped).
|
||||
Only the two outer curbs get skirts.
|
||||
- Otherwise (swept > 180°, near-parallel, or corner outside the half-edges):
|
||||
two independent strip quads with outer curb + node-end cap skirts and a
|
||||
0.05 seam overlap at the node (replaces the "center-gap filling" above).
|
||||
- Straight segments emit the full band `s ∈ [-L_in·w, +L_out·w]` with cap +
|
||||
both curb skirts; the far end has no skirt (meets the neighbor's geometry).
|
||||
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,
|
||||
bowed straight-through roads toward the node, and double-counted the node
|
||||
height in dead-end segments. The two-strip fallback it had replaced had
|
||||
its own overlap/gap issues (inner-turn overlaps, outer-turn center
|
||||
splits). The mitered outline below is the geometrically exact solution
|
||||
for every swept angle:
|
||||
|
||||
- Outline polygon (`computeWedgeOutline()`):
|
||||
`O → M_A → curbA → [X →] curbB → M_B`, where `curbA = M_A + w_A·r1`,
|
||||
`curbB = M_B - w_B·r2`, `w_A = H1.lanesOut·laneWidth`,
|
||||
`w_B = H2.lanesIn·laneWidth`, `r = roadRightVec(d) = d × UNIT_Y`.
|
||||
- `X` is the intersection of the two curb lines (Cramer solve,
|
||||
`|det| ≥ 0.05`) with NO parameter-range restriction — a corner behind
|
||||
the node (swept > 180°) is exactly what closes the wrap-around piece.
|
||||
Near-straight wedges (`|det| < 0.05`, curb lines collinear) drop the
|
||||
corner: the outer edge is a straight line between the two midpoint curb
|
||||
points, producing a clean rectangle for 180° through-roads.
|
||||
- Triangulation by ear-clipping in the XZ projection
|
||||
(`triangulateOutline()`), which handles concave outlines from oversized
|
||||
miter corners on short edges; fan fallback as a numeric safety net.
|
||||
- Straight segments (dead-end nodes) emit the full band
|
||||
`s ∈ [-L_in·w, +L_out·w]` with node-end cap + both curb skirts; the far
|
||||
end has no skirt (meets the neighbor's geometry).
|
||||
- All primitives go through `emitSlab()`: center-surface triangles are
|
||||
duplicated at `±roadThickness/2` with auto-flipped winding (normals from
|
||||
cross products), boundary edges grow vertical skirts oriented away from an
|
||||
interior reference point. This gives closed solids suitable for physics
|
||||
mesh shapes (M5.9) instead of open surfaces.
|
||||
- Heights interpolate `roadLevelA/B` from node to edge midpoint; lateral
|
||||
direction stays flat. UVs: `u` along the road (phase-continuous across
|
||||
the edge midpoint), `v` across the band (continuous at the center line);
|
||||
wedge fans use a planar projection in the first half-edge's frame. UV
|
||||
scaling replaces the physical 1-unit template repeat of M5.7's snapping
|
||||
scheme.
|
||||
cross products); only the exposed outer curb chain grows vertical skirts
|
||||
(centerline rays and midpoint caps are shared with neighbouring pieces).
|
||||
Closed solids suitable for physics mesh shapes (M5.9).
|
||||
- Heights: one shared road level per node (`nodeRoadLevel()` = node Y +
|
||||
mean incident `roadLevelAtNode`) so adjacent wedge pieces cannot crack;
|
||||
half-edge profiles (`roadLevelA/B` interpolation) at midpoints and curb
|
||||
ends; averaged profile heights at the miter corner. Segment heights are
|
||||
absolute surface heights (the radial sweep's double-counted node Y is
|
||||
fixed and covered by a regression test).
|
||||
- UVs: planar projection in the first half-edge's frame with the `+in1`
|
||||
offset, keeping `v` continuous with the piece covering the other side of
|
||||
the same half-edge; segments use `halfEdgeU()` (phase-continuous across
|
||||
the edge midpoint). UV scaling replaces the physical 1-unit template
|
||||
repeat of M5.7's snapping scheme.
|
||||
- Degenerate wedges (> 270°) are logged and skipped (`false`).
|
||||
|
||||
`getRoadTemplate()` (M5.3) is retained: its space conventions define the
|
||||
sign math above, and custom mesh templates may still be honored later.
|
||||
Headless coverage: `roadWedgeGeometry` test (segment extents incl. top and
|
||||
bottom surfaces, asymmetric lanes, 90° fan without overshoot + outer corner
|
||||
vertex, 270° fallback path, degenerate wedge rejection).
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,611 @@
|
||||
/*
|
||||
* RoadGeometryDemo — standalone OGRE + ImGui app for debugging
|
||||
* procedural road wedge geometry.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Build: cmake --build <build> --target RoadGeometryDemo
|
||||
* Run: ./RoadGeometryDemo
|
||||
*/
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <OgreApplicationContext.h>
|
||||
#include <OgreCameraMan.h>
|
||||
#include <OgreImGuiOverlay.h>
|
||||
#include <OgreImGuiInputListener.h>
|
||||
#include <OgreOverlaySystem.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../components/RoadGraph.hpp"
|
||||
#include "../roadlib/RoadGeometryLib.hpp"
|
||||
|
||||
/* =========================================================================
|
||||
* Render target listener that wraps ImGui around each viewport update.
|
||||
* ========================================================================= */
|
||||
|
||||
class ImGuiFrameListener : public Ogre::RenderTargetListener {
|
||||
public:
|
||||
ImGuiFrameListener(std::function<void()> renderFn)
|
||||
: m_renderFn(std::move(renderFn))
|
||||
{
|
||||
}
|
||||
|
||||
void preViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override
|
||||
{
|
||||
(void)evt;
|
||||
if (m_shuttingDown)
|
||||
return;
|
||||
Ogre::ImGuiOverlay::NewFrame();
|
||||
if (m_renderFn)
|
||||
m_renderFn();
|
||||
}
|
||||
|
||||
void postViewportUpdate(const Ogre::RenderTargetViewportEvent &evt) override
|
||||
{
|
||||
(void)evt;
|
||||
if (m_shuttingDown)
|
||||
return;
|
||||
ImGui::EndFrame();
|
||||
}
|
||||
|
||||
void setShuttingDown(bool v) { m_shuttingDown = v; }
|
||||
|
||||
private:
|
||||
std::function<void()> m_renderFn;
|
||||
bool m_shuttingDown = false;
|
||||
};
|
||||
|
||||
/* =========================================================================
|
||||
* DemoApp
|
||||
* ========================================================================= */
|
||||
|
||||
class DemoApp : public OgreBites::ApplicationContext,
|
||||
public OgreBites::InputListener {
|
||||
public:
|
||||
DemoApp();
|
||||
~DemoApp();
|
||||
|
||||
void setup() override;
|
||||
void shutdown() override;
|
||||
bool frameStarted(const Ogre::FrameEvent &evt) override;
|
||||
|
||||
/* InputListener — forward camera events. */
|
||||
bool keyPressed(const OgreBites::KeyboardEvent &evt) override;
|
||||
bool mouseMoved(const OgreBites::MouseMotionEvent &evt) override;
|
||||
bool mousePressed(const OgreBites::MouseButtonEvent &evt) override;
|
||||
bool mouseReleased(const OgreBites::MouseButtonEvent &evt) override;
|
||||
bool mouseWheelRolled(const OgreBites::MouseWheelEvent &evt) override;
|
||||
|
||||
private:
|
||||
void rebuildWedgeGeometry();
|
||||
void renderImGui();
|
||||
|
||||
Ogre::SceneManager *m_sceneMgr = nullptr;
|
||||
Ogre::ImGuiOverlay *m_imguiOverlay = nullptr;
|
||||
std::unique_ptr<ImGuiFrameListener> m_frameListener;
|
||||
|
||||
Ogre::SceneNode *m_wedgeNode = nullptr;
|
||||
Ogre::ManualObject *m_wedgeTriangles = nullptr;
|
||||
Ogre::ManualObject *m_wedgeWireframe = nullptr;
|
||||
Ogre::ManualObject *m_visualAids = nullptr;
|
||||
|
||||
Ogre::SceneNode *m_camNode = nullptr;
|
||||
std::unique_ptr<OgreBites::CameraMan> m_cameraMan;
|
||||
|
||||
/* Points in world space. */
|
||||
Ogre::Vector3 m_pointA = Ogre::Vector3(-5, 0, 0);
|
||||
Ogre::Vector3 m_pointB = Ogre::Vector3(0, 0, 0);
|
||||
Ogre::Vector3 m_pointC = Ogre::Vector3(5, 0, 5);
|
||||
|
||||
/* Road config. */
|
||||
float m_laneWidth = 3.0f;
|
||||
int m_lanesPerDirection = 1;
|
||||
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;
|
||||
};
|
||||
|
||||
DemoApp::DemoApp()
|
||||
: OgreBites::ApplicationContext("RoadGeometryDemo")
|
||||
{
|
||||
}
|
||||
|
||||
DemoApp::~DemoApp()
|
||||
{
|
||||
}
|
||||
|
||||
void DemoApp::shutdown()
|
||||
{
|
||||
/* Tell the frame listener to stop issuing ImGui calls before the
|
||||
* base class tears down the ImGui overlay. */
|
||||
if (m_frameListener)
|
||||
m_frameListener->setShuttingDown(true);
|
||||
|
||||
/* Remove the listener from the render window so it doesn't fire
|
||||
* during the remaining frames of the shutdown sequence. */
|
||||
if (m_frameListener)
|
||||
getRenderWindow()->removeListener(m_frameListener.get());
|
||||
|
||||
OgreBites::ApplicationContext::shutdown();
|
||||
}
|
||||
|
||||
void DemoApp::setup()
|
||||
{
|
||||
OgreBites::ApplicationContext::setup();
|
||||
|
||||
m_sceneMgr = getRoot()->createSceneManager();
|
||||
m_sceneMgr->setAmbientLight(Ogre::ColourValue(0.5f, 0.5f, 0.5f));
|
||||
|
||||
/* RTSS integration. */
|
||||
Ogre::RTShader::ShaderGenerator *shadergen =
|
||||
Ogre::RTShader::ShaderGenerator::getSingletonPtr();
|
||||
shadergen->addSceneManager(m_sceneMgr);
|
||||
|
||||
/* Overlay system (needed by ImGui). */
|
||||
Ogre::OverlaySystem *overlaySys = getOverlaySystem();
|
||||
m_sceneMgr->addRenderQueueListener(overlaySys);
|
||||
|
||||
/* ImGui overlay via ApplicationContext helper. */
|
||||
m_imguiOverlay = initialiseImGui();
|
||||
m_imguiOverlay->setZOrder(300);
|
||||
m_imguiOverlay->show();
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
/* Camera. */
|
||||
Ogre::SceneNode *targetNode =
|
||||
m_sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
targetNode->setPosition(0, 0, 2);
|
||||
|
||||
m_camNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
m_camNode->setPosition(0, 15, 20);
|
||||
m_camNode->lookAt(Ogre::Vector3(0, 0, 2), Ogre::Node::TS_WORLD);
|
||||
|
||||
Ogre::Camera *cam = m_sceneMgr->createCamera("MainCam");
|
||||
cam->setNearClipDistance(0.1f);
|
||||
cam->setFarClipDistance(1000.0f);
|
||||
cam->setAutoAspectRatio(true);
|
||||
m_camNode->attachObject(cam);
|
||||
|
||||
m_cameraMan = std::make_unique<OgreBites::CameraMan>(m_camNode);
|
||||
m_cameraMan->setStyle(OgreBites::CS_ORBIT);
|
||||
m_cameraMan->setTarget(targetNode);
|
||||
|
||||
/* Viewport. */
|
||||
getRenderWindow()->addViewport(cam);
|
||||
|
||||
/* Render target listener for ImGui frame management. */
|
||||
m_frameListener = std::make_unique<ImGuiFrameListener>(
|
||||
[this]() { renderImGui(); });
|
||||
getRenderWindow()->addListener(m_frameListener.get());
|
||||
|
||||
/* Input listeners — ImGui goes first so it gets first dibs. */
|
||||
addInputListener(getImGuiInputListener());
|
||||
addInputListener(this);
|
||||
|
||||
/* Lighting. */
|
||||
m_sceneMgr->setShadowTechnique(Ogre::SHADOWTYPE_NONE);
|
||||
|
||||
Ogre::Light *dLight = m_sceneMgr->createLight("DirLight");
|
||||
dLight->setType(Ogre::Light::LT_DIRECTIONAL);
|
||||
dLight->setDiffuseColour(Ogre::ColourValue(0.8f, 0.8f, 0.7f));
|
||||
dLight->setSpecularColour(Ogre::ColourValue(0.3f, 0.3f, 0.3f));
|
||||
/* Direction is set via scene node. */
|
||||
Ogre::SceneNode *dNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
dNode->attachObject(dLight);
|
||||
dNode->setDirection(Ogre::Vector3(0.5f, -1, 0.3f).normalisedCopy());
|
||||
|
||||
/* Wedge geometry node. */
|
||||
m_wedgeNode = m_sceneMgr->getRootSceneNode()->createChildSceneNode();
|
||||
|
||||
m_wedgeTriangles = m_sceneMgr->createManualObject("WedgeTriangles");
|
||||
m_wedgeWireframe = m_sceneMgr->createManualObject("WedgeWireframe");
|
||||
m_visualAids = m_sceneMgr->createManualObject("VisualAids");
|
||||
|
||||
m_wedgeNode->attachObject(m_wedgeTriangles);
|
||||
m_wedgeNode->attachObject(m_wedgeWireframe);
|
||||
m_wedgeNode->attachObject(m_visualAids);
|
||||
m_wedgeWireframe->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
m_visualAids->setRenderQueueGroup(Ogre::RENDER_QUEUE_OVERLAY);
|
||||
|
||||
/* Grid for orientation. */
|
||||
Ogre::ManualObject *grid = m_sceneMgr->createManualObject("Grid");
|
||||
grid->begin("BaseWhiteNoLighting", Ogre::RenderOperation::OT_LINE_LIST);
|
||||
Ogre::ColourValue gridCol(0.4f, 0.4f, 0.4f, 0.5f);
|
||||
for (int i = -20; i <= 20; ++i) {
|
||||
grid->position(i, 0, -20);
|
||||
grid->colour(gridCol);
|
||||
grid->position(i, 0, 20);
|
||||
grid->colour(gridCol);
|
||||
grid->position(-20, 0, i);
|
||||
grid->colour(gridCol);
|
||||
grid->position(20, 0, i);
|
||||
grid->colour(gridCol);
|
||||
}
|
||||
grid->end();
|
||||
m_wedgeNode->attachObject(grid);
|
||||
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
bool DemoApp::frameStarted(const Ogre::FrameEvent &evt)
|
||||
{
|
||||
/* Base class pumps SDL events (keyboard, mouse, window close).
|
||||
* Without this no input reaches the ImGui or camera handlers. */
|
||||
OgreBites::ApplicationContextBase::frameStarted(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;
|
||||
m_dirty = false;
|
||||
rebuildWedgeGeometry();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* Input forwarding
|
||||
* ========================================================================= */
|
||||
|
||||
bool DemoApp::keyPressed(const OgreBites::KeyboardEvent &evt)
|
||||
{
|
||||
/* ESC always exits, even if ImGui is active. */
|
||||
if (evt.keysym.sym == OgreBites::SDLK_ESCAPE) {
|
||||
getRoot()->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
|
||||
/* When ImGui wants the keyboard, don't forward to the camera. */
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
if (io.WantCaptureKeyboard)
|
||||
return false;
|
||||
|
||||
m_cameraMan->keyPressed(evt);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DemoApp::mouseMoved(const OgreBites::MouseMotionEvent &evt)
|
||||
{
|
||||
m_cameraMan->mouseMoved(evt);
|
||||
return true;
|
||||
}
|
||||
bool DemoApp::mousePressed(const OgreBites::MouseButtonEvent &evt)
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
if (io.WantCaptureMouse)
|
||||
return false;
|
||||
m_cameraMan->mousePressed(evt);
|
||||
return true;
|
||||
}
|
||||
bool DemoApp::mouseReleased(const OgreBites::MouseButtonEvent &evt)
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
if (io.WantCaptureMouse)
|
||||
return false;
|
||||
m_cameraMan->mouseReleased(evt);
|
||||
return true;
|
||||
}
|
||||
bool DemoApp::mouseWheelRolled(const OgreBites::MouseWheelEvent &evt)
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
if (io.WantCaptureMouse)
|
||||
return false;
|
||||
m_cameraMan->mouseWheelRolled(evt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* Geometry rebuild
|
||||
* ========================================================================= */
|
||||
|
||||
void DemoApp::rebuildWedgeGeometry()
|
||||
{
|
||||
/* Build graph: 3 nodes, 2 edges. */
|
||||
RoadGraph graph;
|
||||
graph.config.laneWidth = m_laneWidth;
|
||||
graph.config.lanesPerDirection = m_lanesPerDirection;
|
||||
graph.config.roadThickness = m_roadThickness;
|
||||
graph.config.roadMaterialName = "WedgeDebug";
|
||||
|
||||
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);
|
||||
int idC = graph.addNode(posC, 0.0f);
|
||||
graph.addEdge(idA, idB);
|
||||
graph.addEdge(idB, idC);
|
||||
|
||||
/* Enumerate wedges and find the interior one at node B. */
|
||||
std::vector<RoadWedge> wedges;
|
||||
std::vector<RoadStraightSegment> segments;
|
||||
enumerateWedges(graph, wedges, segments);
|
||||
|
||||
/* Collect both wedges at node B. With two incident edges there
|
||||
* are exactly two wedges: one sweeps the smaller angle (typically
|
||||
* the road interior for bends ≤ 180°) and the other sweeps the
|
||||
* larger complement. */
|
||||
RoadWedge *wedgeSmall = nullptr, *wedgeLarge = nullptr;
|
||||
for (auto &w : wedges) {
|
||||
if (w.nodeId != idB || w.degenerate)
|
||||
continue;
|
||||
if (!wedgeSmall || w.sweptAngleDeg < wedgeSmall->sweptAngleDeg)
|
||||
wedgeSmall = &w;
|
||||
if (!wedgeLarge || w.sweptAngleDeg > wedgeLarge->sweptAngleDeg)
|
||||
wedgeLarge = &w;
|
||||
}
|
||||
|
||||
/* Generate geometry for selected wedge mode. */
|
||||
Procedural::TriangleBuffer tb;
|
||||
bool ok = false;
|
||||
|
||||
auto buildWedge = [&](const RoadWedge &w,
|
||||
Procedural::TriangleBuffer &buf) -> bool {
|
||||
Procedural::TriangleBuffer tmp;
|
||||
if (!RoadGeometryLib::buildWedgeGeometry(w, graph, tmp))
|
||||
return false;
|
||||
int base = (int)buf.getVertices().size();
|
||||
for (const auto &v : tmp.getVertices()) {
|
||||
buf.getVertices().push_back(v);
|
||||
}
|
||||
for (int idx : tmp.getIndices())
|
||||
buf.getIndices().push_back(base + idx);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (m_wedgeMode == 0 && wedgeSmall)
|
||||
ok = buildWedge(*wedgeSmall, tb);
|
||||
else if (m_wedgeMode == 1 && wedgeLarge)
|
||||
ok = buildWedge(*wedgeLarge, tb);
|
||||
else if (m_wedgeMode == 2) {
|
||||
if (wedgeSmall)
|
||||
ok |= buildWedge(*wedgeSmall, tb);
|
||||
if (wedgeLarge)
|
||||
ok |= buildWedge(*wedgeLarge, tb);
|
||||
}
|
||||
|
||||
/* ---- Visual aids ---- */
|
||||
m_visualAids->clear();
|
||||
m_visualAids->begin("BaseWhiteNoLighting",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
|
||||
Ogre::Vector3 midAB = (posA + posB) * 0.5f;
|
||||
Ogre::Vector3 midBC = (posB + posC) * 0.5f;
|
||||
|
||||
auto addBox = [&](const Ogre::Vector3 &c, float half,
|
||||
const Ogre::ColourValue &col) {
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
int a1 = (axis + 1) % 3;
|
||||
int a2 = (axis + 2) % 3;
|
||||
for (int s1 = -1; s1 <= 1; s1 += 2)
|
||||
for (int s2 = -1; s2 <= 1; s2 += 2) {
|
||||
Ogre::Vector3 p = c;
|
||||
p[a1] += s1 * half;
|
||||
p[a2] += s2 * half;
|
||||
Ogre::Vector3 q = p;
|
||||
q[axis] += 2.0f * half;
|
||||
m_visualAids->position(p);
|
||||
m_visualAids->colour(col);
|
||||
m_visualAids->position(q);
|
||||
m_visualAids->colour(col);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addBox(posA, 0.2f, Ogre::ColourValue(0, 1, 0));
|
||||
addBox(posB, 0.3f, Ogre::ColourValue(1, 0.5f, 0));
|
||||
addBox(posC, 0.2f, Ogre::ColourValue(1, 0, 0));
|
||||
|
||||
/* Edge lines. */
|
||||
m_visualAids->colour(Ogre::ColourValue(0, 1, 1));
|
||||
m_visualAids->position(posA);
|
||||
m_visualAids->position(posB);
|
||||
m_visualAids->position(posB);
|
||||
m_visualAids->position(posC);
|
||||
|
||||
/* Midpoint crosses. */
|
||||
auto addCross = [&](const Ogre::Vector3 &p, float sz,
|
||||
const Ogre::ColourValue &col) {
|
||||
m_visualAids->colour(col);
|
||||
m_visualAids->position(p + Ogre::Vector3(-sz, 0, 0));
|
||||
m_visualAids->position(p + Ogre::Vector3(sz, 0, 0));
|
||||
m_visualAids->position(p + Ogre::Vector3(0, 0, -sz));
|
||||
m_visualAids->position(p + Ogre::Vector3(0, 0, sz));
|
||||
};
|
||||
addCross(midAB, 0.5f, Ogre::ColourValue(0.5f, 1, 0.5f));
|
||||
addCross(midBC, 0.5f, Ogre::ColourValue(1, 0.5f, 0.5f));
|
||||
|
||||
/* Lines from B to midpoints. */
|
||||
m_visualAids->colour(Ogre::ColourValue(1, 1, 0, 0.5f));
|
||||
m_visualAids->position(posB);
|
||||
m_visualAids->position(midAB);
|
||||
m_visualAids->position(posB);
|
||||
m_visualAids->position(midBC);
|
||||
|
||||
m_visualAids->end();
|
||||
|
||||
/* ---- Wedge mesh ---- */
|
||||
m_wedgeTriangles->clear();
|
||||
m_wedgeWireframe->clear();
|
||||
|
||||
if (ok && !tb.getVertices().empty()) {
|
||||
m_wedgeTriangles->setVisible(true);
|
||||
m_wedgeWireframe->setVisible(true);
|
||||
|
||||
m_wedgeTriangles->begin(
|
||||
"BaseWhiteNoLighting",
|
||||
Ogre::RenderOperation::OT_TRIANGLE_LIST);
|
||||
for (const auto &v : tb.getVertices()) {
|
||||
m_wedgeTriangles->position(v.mPosition);
|
||||
m_wedgeTriangles->normal(v.mNormal);
|
||||
m_wedgeTriangles->colour(
|
||||
Ogre::ColourValue(1, 0.3f, 0.3f, 0.6f));
|
||||
}
|
||||
for (int idx : tb.getIndices())
|
||||
m_wedgeTriangles->index(idx);
|
||||
m_wedgeTriangles->end();
|
||||
|
||||
m_wedgeWireframe->begin(
|
||||
"BaseWhiteNoLighting",
|
||||
Ogre::RenderOperation::OT_LINE_LIST);
|
||||
for (size_t t = 0; t + 2 < tb.getIndices().size(); t += 3) {
|
||||
int i0 = tb.getIndices()[t];
|
||||
int i1 = tb.getIndices()[t + 1];
|
||||
int i2 = tb.getIndices()[t + 2];
|
||||
const auto &a = tb.getVertices()[i0].mPosition;
|
||||
const auto &b = tb.getVertices()[i1].mPosition;
|
||||
const auto &c = tb.getVertices()[i2].mPosition;
|
||||
Ogre::ColourValue wc(1, 1, 0, 0.9f);
|
||||
m_wedgeWireframe->position(a);
|
||||
m_wedgeWireframe->colour(wc);
|
||||
m_wedgeWireframe->position(b);
|
||||
m_wedgeWireframe->colour(wc);
|
||||
m_wedgeWireframe->position(b);
|
||||
m_wedgeWireframe->colour(wc);
|
||||
m_wedgeWireframe->position(c);
|
||||
m_wedgeWireframe->colour(wc);
|
||||
m_wedgeWireframe->position(c);
|
||||
m_wedgeWireframe->colour(wc);
|
||||
m_wedgeWireframe->position(a);
|
||||
m_wedgeWireframe->colour(wc);
|
||||
}
|
||||
m_wedgeWireframe->end();
|
||||
} else {
|
||||
m_wedgeTriangles->setVisible(false);
|
||||
m_wedgeWireframe->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* ImGui panel
|
||||
* ========================================================================= */
|
||||
|
||||
void DemoApp::renderImGui()
|
||||
{
|
||||
ImGui::SetNextWindowPos(ImVec2(10, 10), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(420, 520), ImGuiCond_FirstUseEver);
|
||||
|
||||
if (!ImGui::Begin("Wedge Geometry Debug", nullptr, 0)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Wedge: midpoint AB -> B -> midpoint BC");
|
||||
ImGui::Separator();
|
||||
|
||||
bool changed = false;
|
||||
|
||||
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.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.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.z##C", &m_pointC.z, -20.0f, 20.0f);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Road Configuration");
|
||||
changed |=
|
||||
ImGui::SliderFloat("Lane Width", &m_laneWidth, 1.0f, 10.0f);
|
||||
changed |= ImGui::SliderInt("Lanes per Direction",
|
||||
&m_lanesPerDirection, 1, 4);
|
||||
changed |= ImGui::SliderFloat("Road Thickness", &m_roadThickness,
|
||||
0.05f, 2.0f);
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Derived info. */
|
||||
Ogre::Vector3 dirAB = m_pointB - m_pointA;
|
||||
dirAB.y = 0;
|
||||
float lenAB = dirAB.length();
|
||||
Ogre::Vector3 dirBC = m_pointC - m_pointB;
|
||||
dirBC.y = 0;
|
||||
float lenBC = dirBC.length();
|
||||
|
||||
if (lenAB > 0.001f && lenBC > 0.001f) {
|
||||
dirAB.normalise();
|
||||
dirBC.normalise();
|
||||
float dot = dirAB.dotProduct(dirBC);
|
||||
float interiorDeg =
|
||||
std::acos(std::max(-1.0f, std::min(1.0f, dot))) *
|
||||
(180.0f / M_PI);
|
||||
float exteriorDeg = 360.0f - interiorDeg;
|
||||
|
||||
ImGui::Text("Edge A-B length: %.2f", lenAB);
|
||||
ImGui::Text("Edge B-C length: %.2f", lenBC);
|
||||
ImGui::Text("Interior angle B: %.1f deg", interiorDeg);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Wedge to display:");
|
||||
int prevMode = m_wedgeMode;
|
||||
ImGui::RadioButton("Smaller", &m_wedgeMode, 0);
|
||||
ImGui::SameLine();
|
||||
float smallDeg = (interiorDeg <= 180.0f) ? interiorDeg : exteriorDeg;
|
||||
ImGui::TextDisabled("~%.1f deg", smallDeg);
|
||||
ImGui::RadioButton("Larger", &m_wedgeMode, 1);
|
||||
ImGui::SameLine();
|
||||
float largeDeg = (interiorDeg > 180.0f) ? interiorDeg : exteriorDeg;
|
||||
ImGui::TextDisabled("~%.1f deg", largeDeg);
|
||||
ImGui::RadioButton("Both", &m_wedgeMode, 2);
|
||||
if (m_wedgeMode != prevMode)
|
||||
changed = true;
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Text("Camera: Right-drag to orbit, Wheel to zoom");
|
||||
ImGui::Text("Press ESC to exit");
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if (changed)
|
||||
m_dirty = true;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
* main
|
||||
* ========================================================================= */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
DemoApp app;
|
||||
app.initApp();
|
||||
app.getRoot()->startRendering();
|
||||
app.closeApp();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
/*
|
||||
* RoadGeometryLib — implementation of road wedge/segment geometry.
|
||||
*
|
||||
* Extracted from RoadSystem.cpp; no dependency on Flecs, TerrainSystem
|
||||
* or any ECS components. Only Ogre, OgreProcedural, and RoadGraph.hpp.
|
||||
*/
|
||||
|
||||
#include "RoadGeometryLib.hpp"
|
||||
|
||||
#include <OgreLogManager.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace RoadGeometryLib {
|
||||
|
||||
const float SEAM_OVERLAP = 0.05f;
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Utility helpers
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
Ogre::Vector3 roadRightVec(const Ogre::Vector3 &d)
|
||||
{
|
||||
return d.crossProduct(Ogre::Vector3::UNIT_Y);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Template helpers
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
Procedural::TriangleBuffer 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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Phase 1 — Concatenated Strip
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
void 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 (z-fighting).
|
||||
* The centerline wall (template X=0) is dropped as well — it 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;
|
||||
|
||||
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;
|
||||
|
||||
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.
|
||||
*
|
||||
* 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, so the miter corner at d=L1
|
||||
* is always sampled.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Phase 2 — Vertex Transformation
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
Ogre::Vector3 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);
|
||||
Ogre::Vector3 offB = r2 * (-h2.lanesIn * lw);
|
||||
|
||||
/* 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;
|
||||
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 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;
|
||||
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 (polyline MA -> O -> MB). */
|
||||
Ogre::Vector3 center;
|
||||
if (d <= L1)
|
||||
center = MA + (O - MA) * (d / L1);
|
||||
else
|
||||
center = O + (MB - O) * ((d - L1) / L2);
|
||||
|
||||
/* World position from curb offset. */
|
||||
Ogre::Vector3 off = computeCurbOffset(wedge, graph, d);
|
||||
Ogre::Vector3 worldXZ = center + off * v.mPosition.x;
|
||||
|
||||
/* Surface height. */
|
||||
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;
|
||||
|
||||
/* 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;
|
||||
|
||||
/* Normal rotation. */
|
||||
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 — Center Seam Shifting
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
void shiftSeamVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph)
|
||||
{
|
||||
std::vector<int> nids = graph.getNeighborIds(wedge.nodeId);
|
||||
if (nids.size() <= 2)
|
||||
return;
|
||||
|
||||
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 >= SEAM_OVERLAP)
|
||||
continue;
|
||||
|
||||
Ogre::Vector3 radial = toNode.normalisedCopy();
|
||||
if (radial.isZeroLength())
|
||||
continue;
|
||||
|
||||
float push = SEAM_OVERLAP - distToNode + SEAM_OVERLAP;
|
||||
v.mPosition.x += radial.x * push;
|
||||
v.mPosition.z += radial.z * push;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Slab extrusion
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
void 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();
|
||||
|
||||
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;
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Entry points — wedge and segment geometry
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
Procedural::TriangleBuffer fb =
|
||||
makeFallbackTemplate(graph.config.roadThickness);
|
||||
return buildWedgeGeometry(wedge, graph, fb, out);
|
||||
}
|
||||
|
||||
bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
if (wedge.degenerate) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadGeometryLib: skipping degenerate wedge at node " +
|
||||
std::to_string(wedge.nodeId));
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Phase 1. */
|
||||
Procedural::TriangleBuffer strip;
|
||||
buildWedgeStrip(strip, templ, wedge.first.halfLength,
|
||||
wedge.second.halfLength);
|
||||
|
||||
/* Phase 2. */
|
||||
transformWedgeVertices(strip, wedge, graph);
|
||||
|
||||
/* Phase 3. */
|
||||
shiftSeamVertices(strip, wedge, graph);
|
||||
|
||||
/* Slab extrusion. */
|
||||
extrudeToSlab(out, strip, graph.config.roadThickness);
|
||||
return true;
|
||||
}
|
||||
|
||||
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 = -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;
|
||||
}
|
||||
|
||||
bool 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;
|
||||
|
||||
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);
|
||||
|
||||
auto skirtFilter = [&](const Ogre::Vector3 &p0,
|
||||
const Ogre::Vector3 &p1) -> bool {
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace RoadGeometryLib
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* RoadGeometryLib — standalone road wedge/segment geometry generation.
|
||||
*
|
||||
* This library contains the pure geometry functions extracted from
|
||||
* RoadSystem.cpp. It has no dependency on Flecs, TerrainSystem, Jolt,
|
||||
* or any ECS component. Only Ogre (Vector3/Quaternion), OgreProcedural
|
||||
* (Procedural::TriangleBuffer), and RoadGraph.hpp are needed.
|
||||
*
|
||||
* All functions are declared in namespace RoadGeometryLib.
|
||||
*/
|
||||
|
||||
#ifndef ROAD_GEOMETRY_LIB_HPP
|
||||
#define ROAD_GEOMETRY_LIB_HPP
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <ProceduralTriangleBuffer.h>
|
||||
|
||||
#include "../components/RoadGraph.hpp"
|
||||
|
||||
namespace RoadGeometryLib {
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Public entry points
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Build the world-space road slab for one wedge and append to @p out.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @return false when the wedge is degenerate and nothing was emitted.
|
||||
*/
|
||||
bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
/**
|
||||
* Build the world-space road slab for a straight segment (dead-end
|
||||
* node) and append to @p out.
|
||||
*/
|
||||
bool buildSegmentGeometry(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Pipeline phases (exposed for testing)
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/** Phase 1: concatenate N template copies into a straight strip along -Z. */
|
||||
void buildConcatenatedStrip(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
int N);
|
||||
|
||||
/** Phase 2: bend the strip into wedge shape, in place. */
|
||||
void transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph);
|
||||
|
||||
/** Phase 3: shift centerline vertices past the node for overlap. */
|
||||
void shiftSeamVertices(Procedural::TriangleBuffer &strip,
|
||||
const RoadWedge &wedge,
|
||||
const RoadGraph &graph);
|
||||
|
||||
/**
|
||||
* Compute the curb offset vector at path distance @p d.
|
||||
*
|
||||
* The vector points from the centerline to the outer curb; it varies
|
||||
* continuously through the node so no gaps open at the corner.
|
||||
*/
|
||||
Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
float d);
|
||||
|
||||
/**
|
||||
* Turn a center-surface triangle set into a closed solid slab.
|
||||
*
|
||||
* Adds top/bottom faces (at +/- roadThickness/2) plus vertical skirts
|
||||
* on boundary edges. @p skirtFilter can exclude specific edges.
|
||||
*/
|
||||
using SkirtFilter = std::function<bool(const Ogre::Vector3 &p0,
|
||||
const Ogre::Vector3 &p1)>;
|
||||
|
||||
void extrudeToSlab(Procedural::TriangleBuffer &out,
|
||||
const Procedural::TriangleBuffer ¢erSurf,
|
||||
float roadThickness,
|
||||
const SkirtFilter &skirtFilter = nullptr);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Template mesh helpers
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a unit-box fallback template in template space:
|
||||
* X in [0,1] lateral, Y in [-thick/2, +thick/2], Z in [-1, 0].
|
||||
*/
|
||||
Procedural::TriangleBuffer makeFallbackTemplate(float roadThickness);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Utility helpers
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/** Right-of-travel vector for a horizontal direction. */
|
||||
Ogre::Vector3 roadRightVec(const Ogre::Vector3 &d);
|
||||
|
||||
/** Absolute road surface Y at distance @p t from the seed node. */
|
||||
float halfEdgeHeightAt(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float t);
|
||||
|
||||
/** Phase-continuous along-road UV coordinate. */
|
||||
float halfEdgeU(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float t);
|
||||
|
||||
/** Shared road level at a node (mean of incident edge levels). */
|
||||
float nodeRoadLevel(const RoadGraph &graph, int nodeId);
|
||||
|
||||
/** Append one triangle; degenerate (zero-area) triangles are skipped. */
|
||||
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);
|
||||
|
||||
/** Seam overlap constant (0.05 units). */
|
||||
extern const float SEAM_OVERLAP;
|
||||
|
||||
/**
|
||||
* Compute the four corners of a segment's center-surface band.
|
||||
*
|
||||
* c[0..3] are the band corners; uvc[0..3] their UVs.
|
||||
* @return false if the segment is invalid.
|
||||
*/
|
||||
bool computeSegmentBand(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph,
|
||||
Ogre::Vector3 c[4], Ogre::Vector2 uvc[4]);
|
||||
|
||||
} // namespace RoadGeometryLib
|
||||
|
||||
#endif // ROAD_GEOMETRY_LIB_HPP
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
#include <Jolt/Jolt.h>
|
||||
#include <Jolt/Physics/Body/BodyID.h>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -132,45 +133,109 @@ public:
|
||||
* Road mesh template (M5.3).
|
||||
*
|
||||
* Returns the template road segment as a Procedural::TriangleBuffer in
|
||||
* template space: X in [0, 1] along the edge, Y in
|
||||
* [-roadThickness/2, +roadThickness/2], Z in [0, 1] across the road
|
||||
* (+Z = right of the A->B travel direction), UVs spanning (0,0)-(1,1)
|
||||
* over the X/Z extents.
|
||||
* template space (ProceduralRoadGeometry.md section 2): X in [0, 1]
|
||||
* lateral (X=0 at the centerline, X=1 at the outer curb), Y in
|
||||
* [-roadThickness/2, +roadThickness/2], Z in [-1, 0] longitudinal
|
||||
* (0 at the wedge start, -1 one unit along the road), UVs spanning
|
||||
* (0,0)-(1,1) with u along Z and v along X.
|
||||
*
|
||||
* The template is loaded from cfg.roadMeshTemplate (General resource
|
||||
* group). A missing or empty mesh falls back to a generated unit box
|
||||
* (the supported prototyping path — no asset required). The buffer is
|
||||
* cached and rebuilt only when cfg.roadMeshTemplate or
|
||||
* cfg.roadThickness changes.
|
||||
* group) and normalized into this space. A missing or empty mesh
|
||||
* falls back to a generated unit box (the supported prototyping
|
||||
* path — no asset required). The buffer is cached and rebuilt only
|
||||
* when cfg.roadMeshTemplate or cfg.roadThickness changes.
|
||||
*/
|
||||
const Procedural::TriangleBuffer &getRoadTemplate(const RoadConfig &cfg);
|
||||
|
||||
/**
|
||||
* Geometry generation (M5.6).
|
||||
* Geometry generation (M5.6, ProceduralRoadGeometry.md).
|
||||
*
|
||||
* Builds the world-space road slab for one wedge or one straight
|
||||
* segment and appends it to @ out. The slab has top and bottom
|
||||
* surfaces at +/- roadThickness/2 around the interpolated road level
|
||||
* and side skirts along exposed edges (curbs and endpoint caps).
|
||||
* segment and appends it to @p out.
|
||||
*
|
||||
* 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 270 degrees).
|
||||
* 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
|
||||
* 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
|
||||
* joined road body.
|
||||
*
|
||||
* The 3-argument overload uses the generated fallback box template;
|
||||
* the 4-argument overload takes an explicit template (runtime path
|
||||
* 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).
|
||||
*/
|
||||
static bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
const Procedural::TriangleBuffer &tmpl,
|
||||
Procedural::TriangleBuffer &out);
|
||||
static bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
Procedural::TriangleBuffer &out);
|
||||
static bool buildSegmentGeometry(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph,
|
||||
const Procedural::TriangleBuffer &tmpl,
|
||||
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.
|
||||
*
|
||||
* Returns a unit box occupying X=[0,1], Z=[0,1],
|
||||
* Y=[-thick/2, +thick/2] with 6 faces, 24 vertices, 36 indices.
|
||||
* Returns a unit box occupying X=[0,1] (lateral, 0=centerline),
|
||||
* Y=[-thick/2, +thick/2], Z=[-1,0] (longitudinal, 0=wedge start)
|
||||
* with 6 faces, 24 vertices, 36 indices.
|
||||
*/
|
||||
static Procedural::TriangleBuffer
|
||||
makeFallbackTemplate(float roadThickness);
|
||||
|
||||
@@ -1466,7 +1466,7 @@ bool TerrainTestRunner::testRoadTemplate(EditorApp &app, TerrainSystem *ts)
|
||||
v.mPosition.x <= 1.0f + 1e-4f &&
|
||||
v.mPosition.y >= -h - 1e-4f &&
|
||||
v.mPosition.y <= h + 1e-4f &&
|
||||
v.mPosition.z >= -1e-4f &&
|
||||
v.mPosition.z >= -1.0f - 1e-4f &&
|
||||
v.mPosition.z <= 1.0f + 1e-4f &&
|
||||
v.mUV.x >= -1e-4f && v.mUV.x <= 1.0f + 1e-4f &&
|
||||
v.mUV.y >= -1e-4f && v.mUV.y <= 1.0f + 1e-4f;
|
||||
@@ -2018,7 +2018,7 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
return fail("no straight segment for node A");
|
||||
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildSegmentGeometry(*segA, rg, RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
|
||||
if (!RoadSystem::buildSegmentGeometry(*segA, rg, buf))
|
||||
return fail("buildSegmentGeometry returned false");
|
||||
|
||||
Scan s = scan(buf);
|
||||
@@ -2032,6 +2032,42 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
return fail("segment slab missing top/bottom surface");
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 1b: elevated nodes (y = 10). Regression test: the road
|
||||
* surface must sit at node height (+/- roadThickness/2), NOT at
|
||||
* double the node height (absolute heights must not be added on
|
||||
* top of the node position).
|
||||
*/
|
||||
{
|
||||
RoadGraph rg;
|
||||
int a = rg.addNode(Ogre::Vector3(0, 10, 0));
|
||||
int b = rg.addNode(Ogre::Vector3(20, 10, 0));
|
||||
rg.addEdge(a, b);
|
||||
|
||||
std::vector<RoadWedge> wedges;
|
||||
std::vector<RoadStraightSegment> segs;
|
||||
enumerateWedges(rg, wedges, segs);
|
||||
|
||||
const RoadStraightSegment *segA = nullptr;
|
||||
for (const auto &s : segs)
|
||||
if (s.nodeId == a)
|
||||
segA = &s;
|
||||
if (!segA)
|
||||
return fail("no straight segment for elevated node A");
|
||||
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildSegmentGeometry(*segA, rg, buf))
|
||||
return fail("buildSegmentGeometry failed (elevated)");
|
||||
|
||||
Scan s = scan(buf);
|
||||
if (!s.ok)
|
||||
return fail("elevated segment buffer invalid");
|
||||
if (s.max.y < 10.14f || s.max.y > 10.16f ||
|
||||
s.min.y < 9.84f || s.min.y > 9.86f)
|
||||
return fail("elevated segment at wrong height "
|
||||
"(double-counted node Y?)");
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 2: asymmetric lanes (2 out, 1 in) shift the band to
|
||||
* z in [-3, +6] on the +right side of the A->B direction.
|
||||
@@ -2056,7 +2092,7 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
return fail("no straight segment for node A (asym)");
|
||||
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildSegmentGeometry(*segA, rg, RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
|
||||
if (!RoadSystem::buildSegmentGeometry(*segA, rg, buf))
|
||||
return fail("buildSegmentGeometry failed (asym)");
|
||||
|
||||
Scan s = scan(buf);
|
||||
@@ -2068,11 +2104,12 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 3: 90-degree wedge (sweep-based, M5.6).
|
||||
* Corner at origin, neighbors at +X and +Z, default 1+1 lanes.
|
||||
* Polyline: P1=(10,0,3) → X=(3,0,3) → P2=(3,0,10).
|
||||
* The sweep distributes vertices along the polyline with
|
||||
* seamless curved transition at the outer corner.
|
||||
* Case 3: 90-degree wedge is one mesh bent along the 2-segment
|
||||
* centerline polyline with a mitered outer corner. Corner node at
|
||||
* origin, neighbors at +X and +Z, default 1+1 lanes -> L-shaped
|
||||
* hexagon with outer corner at (3, y, 3), all vertices inside
|
||||
* [0, 10]^2 in XZ. The 270-degree wedge wraps around the node
|
||||
* with its miter corner behind it at (-3, y, -3).
|
||||
*/
|
||||
{
|
||||
RoadGraph rg;
|
||||
@@ -2097,95 +2134,146 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
return fail("L-corner wedges not found");
|
||||
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildWedgeGeometry(*w90, rg,
|
||||
RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
|
||||
if (!RoadSystem::buildWedgeGeometry(*w90, rg, buf))
|
||||
return fail("buildWedgeGeometry failed for 90 deg");
|
||||
|
||||
Scan s = scan(buf);
|
||||
if (!s.ok)
|
||||
return fail("wedge buffer has NaN or bad indices");
|
||||
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");
|
||||
|
||||
/* Seam overlap may push vertices slightly beyond the
|
||||
* ideal L-shape; allow generous bounds. */
|
||||
if (s.min.x < -1.0f || s.min.z < -1.0f ||
|
||||
s.max.x > 12.0f || s.max.z > 12.0f)
|
||||
return fail("90 deg wedge overshoots expected bounds");
|
||||
|
||||
/* Verify top/bottom surfaces present. */
|
||||
float halfThick = 0.5f * std::max(0.01f,
|
||||
rg.config.roadThickness);
|
||||
if (s.min.y > -halfThick || s.max.y < halfThick ||
|
||||
s.min.y < -halfThick * 1.1f ||
|
||||
s.max.y > halfThick * 1.1f)
|
||||
return fail("wedge missing top/bottom surface");
|
||||
|
||||
/* Vertices should exist near the node O (inner
|
||||
* edge convergence) and near both polyline segments. */
|
||||
bool sawNearNode = false;
|
||||
bool sawNearXseg = false;
|
||||
bool sawNearZseg = false;
|
||||
bool sawCorner = false;
|
||||
bool sawNode = false;
|
||||
for (const auto &v : buf.getVertices()) {
|
||||
const Ogre::Vector3 &p = v.mPosition;
|
||||
float d2 = p.x * p.x + p.z * p.z;
|
||||
if (d2 < 4.0f * 4.0f)
|
||||
sawNearNode = true;
|
||||
/* Near the +X polyline segment (z≈3, x in [3,10]). */
|
||||
if (fabsf(p.z - 3.0f) < 0.2f && p.x >= 2.8f &&
|
||||
p.x <= 10.2f)
|
||||
sawNearXseg = true;
|
||||
/* Near the +Z polyline segment (x≈3, z in [3,10]). */
|
||||
if (fabsf(p.x - 3.0f) < 0.2f && p.z >= 2.8f &&
|
||||
p.z <= 10.2f)
|
||||
sawNearZseg = true;
|
||||
if (fabsf(v.mPosition.x - 3.0f) < 0.05f &&
|
||||
fabsf(v.mPosition.z - 3.0f) < 0.05f)
|
||||
sawCorner = true;
|
||||
if (fabsf(v.mPosition.x) < 0.05f &&
|
||||
fabsf(v.mPosition.z) < 0.05f)
|
||||
sawNode = true;
|
||||
}
|
||||
if (!sawNearNode)
|
||||
return fail("90 deg wedge missing inner vertices near O");
|
||||
if (!sawNearXseg)
|
||||
return fail("90 deg wedge missing vertices near +X curb");
|
||||
if (!sawNearZseg)
|
||||
return fail("90 deg wedge missing vertices near +Z curb");
|
||||
if (!sawCorner)
|
||||
return fail("90 deg wedge missing outer corner (3,3)");
|
||||
if (!sawNode)
|
||||
return fail("90 deg wedge missing node vertex (0,0)");
|
||||
|
||||
/* The 270-degree wedge is also sweep-generated. */
|
||||
/*
|
||||
* The 270-degree wedge takes the mitered wrap-around path:
|
||||
* hexagon O, (0,10), (-3,10), (-3,-3), (10,-3), (10,0) with
|
||||
* the outer corner behind the node. It must cover the
|
||||
* other three quadrants exactly: bounds x/z in [-3, 10].
|
||||
*/
|
||||
Procedural::TriangleBuffer buf270;
|
||||
if (!RoadSystem::buildWedgeGeometry(*w270, rg,
|
||||
RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf270))
|
||||
if (!RoadSystem::buildWedgeGeometry(*w270, rg, buf270))
|
||||
return fail("buildWedgeGeometry failed for 270 deg");
|
||||
Scan s270 = scan(buf270);
|
||||
if (!s270.ok)
|
||||
return fail("270 deg wedge buffer invalid");
|
||||
if (s270.min.x < -3.05f || s270.min.z < -3.05f ||
|
||||
s270.max.x > 10.05f || s270.max.z > 10.05f ||
|
||||
s270.min.x > -2.95f || s270.min.z > -2.95f ||
|
||||
s270.max.x < 9.95f || s270.max.z < 9.95f)
|
||||
return fail("270 deg wedge bounds wrong");
|
||||
|
||||
bool sawBackCorner = false;
|
||||
for (const auto &v : buf270.getVertices()) {
|
||||
if (fabsf(v.mPosition.x + 3.0f) < 0.05f &&
|
||||
fabsf(v.mPosition.z + 3.0f) < 0.05f) {
|
||||
sawBackCorner = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!sawBackCorner)
|
||||
return fail("270 deg wedge missing miter corner "
|
||||
"(-3,-3)");
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 4: a nearly-collinear (~360 degree) wedge is
|
||||
/*
|
||||
* Case 4: a nearly-collinear (~360 degree) wedge is
|
||||
* degenerate and emits nothing.
|
||||
*/
|
||||
{
|
||||
RoadGraph rg;
|
||||
int c = rg.addNode(Ogre::Vector3(0, 0, 0));
|
||||
int a2 = rg.addNode(Ogre::Vector3(10, 0, 0));
|
||||
int b2 = rg.addNode(Ogre::Vector3(10, 0, 0.001f));
|
||||
rg.addEdge(c, a2);
|
||||
rg.addEdge(c, b2);
|
||||
/*
|
||||
* Case 4: a nearly-collinear (~360 degree) wedge is
|
||||
* degenerate and emits nothing.
|
||||
*/
|
||||
{
|
||||
RoadGraph rg;
|
||||
int c = rg.addNode(Ogre::Vector3(0, 0, 0));
|
||||
int a2 = rg.addNode(Ogre::Vector3(10, 0, 0));
|
||||
int b2 = rg.addNode(Ogre::Vector3(10, 0, 0.001f));
|
||||
rg.addEdge(c, a2);
|
||||
rg.addEdge(c, b2);
|
||||
|
||||
std::vector<RoadWedge> wedges;
|
||||
std::vector<RoadStraightSegment> segs;
|
||||
enumerateWedges(rg, wedges, segs);
|
||||
std::vector<RoadWedge> wedges;
|
||||
std::vector<RoadStraightSegment> segs;
|
||||
enumerateWedges(rg, wedges, segs);
|
||||
|
||||
const RoadWedge *wDeg = nullptr;
|
||||
for (const auto &w : wedges)
|
||||
if (w.degenerate)
|
||||
wDeg = &w;
|
||||
if (!wDeg)
|
||||
return fail("near-360 deg wedge not marked degenerate");
|
||||
const RoadWedge *wDeg = nullptr;
|
||||
for (const auto &w : wedges)
|
||||
if (w.degenerate)
|
||||
wDeg = &w;
|
||||
if (!wDeg)
|
||||
return fail("near-360 deg wedge not marked degenerate");
|
||||
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (RoadSystem::buildWedgeGeometry(*wDeg, rg, RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
|
||||
return fail("degenerate wedge not rejected");
|
||||
if (!buf.getVertices().empty() || !buf.getIndices().empty())
|
||||
return fail("degenerate wedge emitted geometry");
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (RoadSystem::buildWedgeGeometry(*wDeg, rg, buf))
|
||||
return fail("degenerate wedge not rejected");
|
||||
if (!buf.getVertices().empty() || !buf.getIndices().empty())
|
||||
return fail("degenerate wedge emitted geometry");
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 5: straight-through node (180-degree wedges). Three nodes
|
||||
* on a line; the middle node's two wedges must be straight
|
||||
* rectangular halves of the through-road — z in [0, 3] and
|
||||
* z in [-3, 0] — with no bowing toward the node and no overlap.
|
||||
*/
|
||||
{
|
||||
RoadGraph rg;
|
||||
int nx = rg.addNode(Ogre::Vector3(-20, 0, 0));
|
||||
int c = rg.addNode(Ogre::Vector3(0, 0, 0));
|
||||
int px = rg.addNode(Ogre::Vector3(20, 0, 0));
|
||||
rg.addEdge(nx, c);
|
||||
rg.addEdge(c, px);
|
||||
|
||||
std::vector<RoadWedge> wedges;
|
||||
std::vector<RoadStraightSegment> segs;
|
||||
enumerateWedges(rg, wedges, segs);
|
||||
|
||||
const RoadWedge *wPlusZ = nullptr, *wMinusZ = nullptr;
|
||||
for (const auto &w : wedges) {
|
||||
if (w.nodeId != c)
|
||||
continue;
|
||||
if (fabsf(w.sweptAngleDeg - 180.0f) > 0.1f)
|
||||
return fail("straight node wedge not 180 deg");
|
||||
if (w.first.direction.x > 0.0f)
|
||||
wPlusZ = &w;
|
||||
else
|
||||
wMinusZ = &w;
|
||||
}
|
||||
if (!wPlusZ || !wMinusZ)
|
||||
return fail("straight node wedges not found");
|
||||
|
||||
Procedural::TriangleBuffer bufUp;
|
||||
if (!RoadSystem::buildWedgeGeometry(*wPlusZ, rg, bufUp))
|
||||
return fail("buildWedgeGeometry failed (+Z half)");
|
||||
Scan sUp = scan(bufUp);
|
||||
if (!sUp.ok)
|
||||
return fail("+Z half buffer invalid");
|
||||
if (sUp.min.z < -0.001f || sUp.max.z > 3.05f ||
|
||||
sUp.max.z < 2.95f ||
|
||||
sUp.min.x < -10.05f || sUp.max.x > 10.05f)
|
||||
return fail("+Z half is not a straight rectangle");
|
||||
|
||||
Procedural::TriangleBuffer bufDn;
|
||||
if (!RoadSystem::buildWedgeGeometry(*wMinusZ, rg, bufDn))
|
||||
return fail("buildWedgeGeometry failed (-Z half)");
|
||||
Scan sDn = scan(bufDn);
|
||||
if (!sDn.ok)
|
||||
return fail("-Z half buffer invalid");
|
||||
if (sDn.max.z > 0.001f || sDn.min.z < -3.05f ||
|
||||
sDn.min.z > -2.95f ||
|
||||
sDn.min.x < -10.05f || sDn.max.x > 10.05f)
|
||||
return fail("-Z half is not a straight rectangle");
|
||||
}
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainTests: road wedge geometry test passed");
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user