Compare commits
3 Commits
a97efbe237
...
e82190ec6d
| Author | SHA1 | Date | |
|---|---|---|---|
| e82190ec6d | |||
| 33f76a1e54 | |||
| c48ff9f4e2 |
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.13.0)
|
||||
project(world2)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
enable_testing()
|
||||
set(BLENDER "${CMAKE_SOURCE_DIR}/../../blender-bin/bin/blender" CACHE STRING "Blender path")
|
||||
set(CREATE_DIRECTORIES
|
||||
${CMAKE_BINARY_DIR}/assets/blender/shapes/male
|
||||
|
||||
@@ -402,6 +402,7 @@ target_link_libraries(editSceneEditor
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
PackageArchive
|
||||
RoadGeometryLib
|
||||
lua
|
||||
SDL2::SDL2
|
||||
)
|
||||
@@ -629,6 +630,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,627 @@
|
||||
# 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
|
||||
follows the mitered curb chain — pinned at the miter corner K
|
||||
for inner wedges (sweep < 180°, so cross-sections cannot fold
|
||||
over each other), blended through K over a narrow zone for
|
||||
outer wedges — so the cross-section direction varies
|
||||
continuously: no gaps, no folds.
|
||||
|
||||
Phase 3: shiftSeamVertices(strip, wedge, graph)
|
||||
→ Shift centerline-side vertices near the node slightly past O
|
||||
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.
|
||||
|
||||
The single implementation lives in `roadlib/RoadGeometryLib.cpp`
|
||||
(namespace `RoadGeometryLib`); the public `RoadSystem` statics forward
|
||||
to it. The transformed wedge strip is already a closed tube (the
|
||||
template supplies top, bottom and curb faces), so it is appended to
|
||||
the output verbatim — slab extrusion (§8) applies to straight
|
||||
segments only. `RoadGeometryLib` also provides
|
||||
`loadTemplateFromMesh()` (template loading per §2) and
|
||||
`makeFallbackTemplate()`.
|
||||
|
||||
## 4. Phase 1 — Concatenated Strip
|
||||
|
||||
**Function**: `static void buildConcatenatedStrip(Procedural::TriangleBuffer &out, const Procedural::TriangleBuffer &templ, int N)`
|
||||
|
||||
```
|
||||
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 Corner Regimes and Blend Zone
|
||||
|
||||
The two constant-width curb lines
|
||||
|
||||
```
|
||||
A(s) = O + offA + dir1 * s s in [0, L1]
|
||||
B(s) = O + offB + dir2 * s s in [0, L2]
|
||||
|
||||
offA = w1 * r1 // H1-side curb end at the node
|
||||
offB = -w2 * r2 // H2-side curb end at the node
|
||||
```
|
||||
|
||||
meet at the miter corner K. Expressed as parameters along each
|
||||
direction from the node-side curb ends:
|
||||
|
||||
```
|
||||
det = dir1.z * dir2.x - dir1.x * dir2.z
|
||||
rhs = offB - offA
|
||||
t1 = (dir2.x * rhs.z - dir2.z * rhs.x) / det
|
||||
t2 = (dir1.x * rhs.z - dir1.z * rhs.x) / det
|
||||
|
||||
cornerOff = offA + dir1 * t1 == offB + dir2 * t2
|
||||
K = O + cornerOff
|
||||
```
|
||||
|
||||
`|det| < 0.05` means the curb lines are (nearly) collinear —
|
||||
near-straight wedges drop the corner entirely.
|
||||
|
||||
* **Inner corner (converging, sweep < 180°): `t1 > 0` and `t2 > 0`** —
|
||||
the curb lines meet ahead of the node, inside the wedge. When K
|
||||
lies on both curb segments (`t1 <= L1` and `t2 <= L2`) the curb is
|
||||
**pinned at K** over the whole corner zone `[L1 - t1, L1 + t2]` (see
|
||||
§5.4): the curb arc around an inner corner is shorter than the
|
||||
centerline arc, so blending parallel cross-sections through the zone
|
||||
would fold them over each other — overlapping geometry when flat,
|
||||
turning into grossly intersecting ramp sheets once the two ends
|
||||
differ in height.
|
||||
* **Outer corner (diverging, sweep > 180°): `t1 < 0`** — the curb
|
||||
lines meet behind the node; cross-sections fan out and cannot fold.
|
||||
The offset blends through K over a narrow symmetric zone around the
|
||||
node:
|
||||
|
||||
```
|
||||
W = min(ROAD_SEAM_OVERLAP * 4, // ~0.2 units — keeps corners sharp
|
||||
L1 * 0.5f, L2 * 0.5f) // clamped for very short edges
|
||||
```
|
||||
|
||||
If L1 < ROAD_SEAM_OVERLAP or L2 < ROAD_SEAM_OVERLAP, W is set to 0
|
||||
(no blending needed — both segment ends are at nearly the same point).
|
||||
* **Notch fallback** — an inner corner whose K falls outside either
|
||||
curb segment (`t1 > L1` or `t2 > L2`; a very wide road on very short
|
||||
edges) uses the same narrow blend as an outer corner.
|
||||
|
||||
### 5.3 Centerline Position
|
||||
|
||||
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. Its behaviour depends on the corner
|
||||
regime (§5.2).
|
||||
|
||||
**Inner corner — curb pinned at the miter corner K:**
|
||||
|
||||
```
|
||||
zoneStart = L1 - t1
|
||||
zoneEnd = L1 + t2
|
||||
|
||||
if d <= zoneStart: offset(d) = offA
|
||||
if d >= zoneEnd: offset(d) = offB
|
||||
else: offset(d) = K - center(d)
|
||||
```
|
||||
|
||||
Inside the zone every cross-section aims its outer-curb end exactly at
|
||||
K, so consecutive sections share the endpoint K and cannot cross each
|
||||
other. The rule is C0-continuous: at `zoneStart` it equals offA
|
||||
exactly (K lies on curb line A) and at `zoneEnd` it equals offB. The
|
||||
outer curb wall collapses to the vertical line at K inside the zone
|
||||
(zero-area quads) — the geometrically correct miter joint.
|
||||
|
||||
**Outer corner, notch fallback, or no corner — narrow blend:**
|
||||
|
||||
```
|
||||
if W == 0 or d <= L1 - W:
|
||||
offset(d) = offA
|
||||
elif d >= L1 + W:
|
||||
offset(d) = offB
|
||||
elif no corner (|det| < 0.05):
|
||||
t = (d - (L1 - W)) / (2 * W) // 0 → 1 across blend zone
|
||||
offset(d) = lerp(offA, offB, t)
|
||||
elif d <= L1:
|
||||
t = (d - (L1 - W)) / W
|
||||
offset(d) = lerp(offA, cornerOff, t)
|
||||
else:
|
||||
t = (d - L1) / W
|
||||
offset(d) = lerp(cornerOff, offB, t)
|
||||
```
|
||||
|
||||
The blend passes exactly through the miter corner at the node, so no
|
||||
hole opens at the outer corner. Both `offA` and `offB` point into the
|
||||
wedge interior, so the interpolated vector never passes through zero
|
||||
for non-degenerate wedges.
|
||||
|
||||
### 5.5 Effective Road Width
|
||||
|
||||
The scalar road half-width at distance d is the offset magnitude:
|
||||
|
||||
```
|
||||
width(d) = |offset(d)|
|
||||
```
|
||||
|
||||
It equals w1 on the first half-edge and w2 on the second half-edge,
|
||||
widens through outer miter corners, and shrinks toward the pinned
|
||||
corner K for inner wedges. It is used for the lateral UV scale only
|
||||
(§5.7).
|
||||
|
||||
### 5.6 Surface Height
|
||||
|
||||
```
|
||||
roadSurfaceY(d):
|
||||
if d <= L1: return halfEdgeHeightAt(H1, graph, L1 - d)
|
||||
else: return halfEdgeHeightAt(H2, graph, d - L1)
|
||||
```
|
||||
|
||||
`halfEdgeHeightAt(he, graph, t)` returns the absolute world Y of the
|
||||
road surface at distance t **from the seed node** O. d is the distance
|
||||
from the wedge start M_A, so the distance from O along H1 is L1 - d
|
||||
(back toward the midpoint), while the distance from O along H2 is
|
||||
d - L1 (forward toward M_B). Passing d directly to H1 inverts the
|
||||
height profile along the first half-edge.
|
||||
|
||||
### 5.7 Per-Vertex Transform
|
||||
|
||||
For each vertex `v` at template position (vx, vy, vz):
|
||||
|
||||
```
|
||||
d = -vz // guaranteed to be in [0, L]
|
||||
|
||||
// Template X maps along the curb offset (direction AND magnitude —
|
||||
// the offset itself widens through outer miter corners and aims at
|
||||
// the pinned corner K for inner wedges):
|
||||
worldXZ = center(d) + offset(d) * vx
|
||||
worldY = roadSurfaceY(d) + vy
|
||||
|
||||
v.position = Vector3(worldXZ.x, worldY, worldXZ.z)
|
||||
|
||||
// UV — longitudinal U from halfEdgeU (phase-continuous), lateral V
|
||||
// scaled by the effective width:
|
||||
v.uv.x = (d <= L1) ? halfEdgeU(H1, graph, L1 - d)
|
||||
: halfEdgeU(H2, graph, d - L1)
|
||||
v.uv.y = v.uv.y * width(d) + in1
|
||||
|
||||
// Normal — rotate template-forward (-Z) to segment direction by the
|
||||
// SIGNED angle around Y:
|
||||
segDir = (d <= L1) ? dir1 : dir2
|
||||
theta = atan2(-segDir.x, -segDir.z)
|
||||
Ogre::Quaternion q(Ogre::Radian(theta), 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**: no extrusion. After Phase 2+3 the strip is already a
|
||||
closed tube around the road body — the template supplies top, bottom
|
||||
and curb faces, and `appendTemplateCopy` drops only the template
|
||||
caps and the centerline wall (the open ends butt exactly against the
|
||||
neighbouring pieces at the edge midpoints, the open centerline side
|
||||
against the adjacent wedge). The transformed strip is appended to
|
||||
the output verbatim. (Re-extruding it additionally stacked coplanar
|
||||
sheets at the strip's center surface and doubled the slab
|
||||
thickness.)
|
||||
- **Segment**: the center-surface band (§7) is flat, so it is passed
|
||||
to `extrudeToSlab`, keeping the far-end edge open (it meets the
|
||||
neighbour node's piece exactly).
|
||||
|
||||
In the segment case the boundary edges are: the outer curb chain, the
|
||||
start cap, and the end cap. Centerline edges (O→M_A, O→M_B) are
|
||||
interior and get no skirts — they meet adjacent road pieces.
|
||||
|
||||
## 9. Seam Suppression Summary
|
||||
|
||||
| Mechanism | What it fixes | Where |
|
||||
|-----------|--------------|-------|
|
||||
| Continuous curb offset (§5.4) | Outer-corner gap where H1 and H2 diverge | Phase 2 |
|
||||
| Curb pinned at miter corner K (§5.4) | Inner-corner cross-section fold (overlapping geometry) | Phase 2 |
|
||||
| ROAD_SEAM_OVERLAP on segments (§7) | Center gap for dead-end nodes | Segment band |
|
||||
| Center seam shifting (§6) | Center hole where >2 wedges meet | Phase 3 |
|
||||
| Slab extrusion (§8) | Road must be a closed solid | Segments |
|
||||
|
||||
## 10. Internal Functions (Testable)
|
||||
|
||||
All of these live in namespace `RoadGeometryLib`
|
||||
(`roadlib/RoadGeometryLib.cpp`); the public `RoadSystem` statics for
|
||||
the entry points forward to them.
|
||||
|
||||
```cpp
|
||||
// Phase 1
|
||||
static void buildConcatenatedStrip(Procedural::TriangleBuffer &out,
|
||||
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) | (3,0,3) — pinned at K | (3,0,0) |
|
||||
| 270° wedge, w1=w2=3 | dir1=+Z, dir2=+X | (-3,0,0) | (-3,0,-3) — blend through K | (0,0,-3) |
|
||||
| 180° straight, w1=w2=3 | dir1=+X, dir2=-X | (0,0,3) | (0,0,3) | (0,0,3) |
|
||||
| Asymmetric w1=6,w2=3 | 90° | (0,0,6) | (3,0,6) — pinned at K | (3,0,0) |
|
||||
| Blend zone continuity | Any | offset varies with d | no discontinuity at L1 | — |
|
||||
|
||||
For converging (inner) wedges the offset at the zone boundaries is
|
||||
exactly offA (at d = L1 - t1) and offB (at d = L1 + t2); inside the
|
||||
zone it is `K - center(d)`.
|
||||
|
||||
### 11.2 Integration Tests (matching existing `testRoadWedgeGeometry`)
|
||||
|
||||
| Test | Expected |
|
||||
|------|----------|
|
||||
| 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 at (3,y,3), node vertex at (0,y,0), Y∈[-0.15,+0.15] (no second extrusion) |
|
||||
| 135° converging corner, flat and with corner node raised | No coplanar-overlapping or piercing triangle pairs in any wedge or segment (fold regression) |
|
||||
| 270° wrap | XZ∈[-3,10]², outer corner near (-3,y,-3) |
|
||||
| 180° straight-through | Two rectangular halves, Z∈[0,3] and [-3,0], no bowing |
|
||||
| Degenerate (>270°) | Returns false, empty output |
|
||||
| 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.
|
||||
|
||||
## 14. Standalone Demo (RoadGeometryDemo)
|
||||
|
||||
Target `RoadGeometryDemo` (`road_demo/main.cpp`) is a small OGRE +
|
||||
ImGui application for interactive inspection of the wedge pipeline.
|
||||
It links only `RoadGeometryLib` — no ECS, terrain or physics — so it
|
||||
builds and starts fast.
|
||||
|
||||
- Three world-space points A, B, C define two road edges A–B and B–C.
|
||||
Sliders adjust every point coordinate (including Y, for height
|
||||
differences at the corner node); the two wedges at node B (smaller
|
||||
and larger sweep) are rebuilt live via
|
||||
`RoadGeometryLib::buildWedgeGeometry`.
|
||||
- Road configuration sliders: lane width, lanes per direction, road
|
||||
thickness.
|
||||
- "Wedge to display" radio: Smaller / Larger / Both.
|
||||
- "Template Mesh" panel: type an OGRE `.mesh` resource name and press
|
||||
**Load** — the mesh is loaded through
|
||||
`RoadGeometryLib::loadTemplateFromMesh` and normalised into template
|
||||
space per §2 (convention violations are logged but the mesh is still
|
||||
used). Enable **Use custom template** to build the wedges with it
|
||||
instead of the generated fallback box (§2); a status line shows the
|
||||
current template state.
|
||||
- The generated slab is drawn solid plus a wireframe overlay, with
|
||||
visual aids for the nodes, edge midpoints and edge lines.
|
||||
- Camera: right-drag to orbit, mouse wheel to zoom, ESC to exit.
|
||||
|
||||
Build and run:
|
||||
|
||||
```bash
|
||||
cmake --build <build-dir> --target RoadGeometryDemo
|
||||
./RoadGeometryDemo
|
||||
```
|
||||
@@ -279,34 +279,39 @@ interactive mode (with display).
|
||||
These are the concrete code changes needed to close all gaps. Each item is
|
||||
ordered by dependency.
|
||||
|
||||
| # | Item | Files to modify | Depends on |
|
||||
|---|------|-----------------|------------|
|
||||
| W1 | M5.10 perpendicular falloff in `complyTerrain()` | `RoadSystem.cpp` | — |
|
||||
| W2 | Helper `computeComplianceHeight()` + unit test | `RoadSystem.cpp`, `TerrainTests.cpp` | W1 |
|
||||
| W3 | `testTerrainCompliance` (automated) | `TerrainTests.cpp`, `TerrainTests.hpp` | W1 |
|
||||
| W4 | `testRoadColliderInteraction` (automated) | `TerrainTests.cpp`, `TerrainTests.hpp` | — |
|
||||
| # | Item | Files to modify | Status |
|
||||
|---|------|-----------------|--------|
|
||||
| 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 |
|
||||
| W4 | `testRoadColliderInteraction` (automated) | `TerrainTests.cpp`, `TerrainTests.hpp` | ✅ DONE |
|
||||
| W5 | Road collider debug draw toggle (M5.9.6) | `RoadSystem.hpp/.cpp`, `TerrainSystem.hpp/.cpp`, `TerrainEditor.hpp` | — |
|
||||
| W6 | Test prefab fixture `tiny_cube.prefab` | `src/features/editScene/tests/prefabs/tiny_cube.prefab` (new) | — |
|
||||
| W7 | `testRoadSidePrefabs` (automated) | `TerrainTests.cpp`, `TerrainTests.hpp` | W6 |
|
||||
| W8 | Register new tests in `TerrainTestRunner::run()` | `TerrainTests.cpp` | W3, W4, W7 |
|
||||
| W6 | Test prefab fixture `tiny_cube.prefab` | `src/features/editScene/tests/prefabs/tiny_cube.prefab` (new) | ✅ DONE |
|
||||
| W7 | `testRoadSidePrefabs` (automated) | `TerrainTests.cpp`, `TerrainTests.hpp` | ✅ DONE |
|
||||
| W8 | Register new tests in `TerrainTestRunner::run()` | `TerrainTests.cpp` | ✅ DONE |
|
||||
|
||||
## 5. Summary
|
||||
|
||||
| Area | Status |
|
||||
|------|--------|
|
||||
| M5.1–M5.8 automated coverage | ✅ Adequate (8/8 sub-items have tests) |
|
||||
| M5.9 automated coverage | ⚠️ Partial → W4 adds raycast+rebuild verification |
|
||||
| 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 | ❌ Missing from implementation → W1+W2 |
|
||||
| M5.10 automated coverage | ❌ None → W3 |
|
||||
| M5.11 automated coverage | ❌ None → W6+W7 |
|
||||
| M5.10 perpendicular falloff | ✅ Implemented → W1+W2 |
|
||||
| M5.10 automated coverage | ✅ W3 covers falloff + save/load |
|
||||
| M5.11 automated coverage | ✅ W6+W7 cover prefab spawn + teardown |
|
||||
| M5.12 automated coverage | ✅ Adequate |
|
||||
| Manual verification steps | 📋 Defined (sections 3.1–3.7) |
|
||||
| Open questions | ✅ All resolved (section 0) |
|
||||
|
||||
**Exit criteria** — Milestone 5 is fully verified when:
|
||||
- [ ] All 8 work items (W1–W8) are implemented.
|
||||
- [ ] `./editSceneEditor --headless --run-terrain-tests=1` passes with all
|
||||
existing + new M5 tests green (expect 22–23 tests per iteration).
|
||||
- [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).
|
||||
- [ ] Manual verification walkthroughs 3.1–3.7 are executed and pass.
|
||||
- [ ] `ctest -R editSceneTerrainTest` passes in CI.
|
||||
- [ ] W5 (road collider debug draw toggle) implemented.
|
||||
|
||||
@@ -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 | template-strip polyline sweep in `RoadGeometryLib` (curb pinned at the miter corner for inner wedges), forwarded from `RoadSystem`, `roadWedgeGeometry` test |
|
||||
| M5.7 Edge length constraint | ✅ complete | `snapToIntegerLength()` + `ROAD_MIN_EDGE_LENGTH`; `splitEdge` snaps, `joinNodes` warns, `validate` rejects short edges; `roadEdgeLength` test green |
|
||||
| M5.8 Mesh assembly per page | ✅ complete | page entities with `TriangleBufferComponent(proceduralContent)` + `RenderableComponent` + `NavMeshGeometrySource` + `LodComponent`, `roadPageMeshes` test |
|
||||
| M5.9 Road physics colliders | ✅ complete | `createPageCollider`/`destroyPageCollider` in `RoadSystem`, asserted in `roadPageMeshes` |
|
||||
@@ -2646,42 +2646,95 @@ 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).
|
||||
**Status update (2026-08-09): inner-corner fold and double extrusion
|
||||
fixed; implementation consolidated in `RoadGeometryLib`.** Two defects
|
||||
were found with the standalone demo (`road_demo/main.cpp`, target
|
||||
`RoadGeometryDemo`; see ProceduralRoadGeometry.md §14):
|
||||
|
||||
1. Inner corner wedges (sweep < 180°) self-intersected: blending
|
||||
cross-sections through the miter corner folded consecutive sections
|
||||
over each other near the corner (coplanar z-fighting sheets when
|
||||
flat, grossly intersecting ramps once the two ends differed in
|
||||
height). The curb is now **pinned at the miter corner K** over the
|
||||
whole corner zone `[L1 - t1, L1 + t2]` (t1/t2 are the corner
|
||||
parameters along each curb line — ProceduralRoadGeometry.md
|
||||
§5.2/§5.4); outer wedges keep the narrow blend through K.
|
||||
2. Wedge strips were extruded a second time (`extrudeToSlab` on top of
|
||||
the already closed template tube), doubling the slab thickness and
|
||||
stacking coplanar sheets. The transformed strip is now appended
|
||||
verbatim; `extrudeToSlab` remains for the flat segment bands only.
|
||||
|
||||
The geometry pipeline now lives once in `roadlib/RoadGeometryLib.cpp`
|
||||
(namespace `RoadGeometryLib`) — including `loadTemplateFromMesh()` —
|
||||
and the `RoadSystem` statics forward to it, replacing the duplicated
|
||||
copy in `RoadSystem.cpp`. The `roadWedgeGeometry` headless test gained
|
||||
a self-intersection checker (coplanar-overlap + piercing triangle
|
||||
pairs) with a 135° converging-corner regression case (flat and with a
|
||||
raised corner node) plus wedge slab-thickness bounds; all 22 headless
|
||||
tests pass.
|
||||
|
||||
An earlier radial-sweep attempt (2026-07-31, reverted 2026-08-02) swept a
|
||||
constant-width band along the outer-curb polyline: it left holes at every
|
||||
node center, collapsed > 180° wedges to a diagonal band across the node,
|
||||
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, wedge slab-thickness bounds, and a
|
||||
self-intersection scan — coplanar-overlap + piercing triangle pairs — on
|
||||
a 135° converging corner, flat and with a raised corner node).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
/*
|
||||
* 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; a custom
|
||||
* template mesh (OGRE .mesh resource name) can be loaded to preview
|
||||
* user road cross-sections instead of the generated fallback box.
|
||||
*
|
||||
* Build: cmake --build <build> --target RoadGeometryDemo
|
||||
* Run: ./RoadGeometryDemo
|
||||
*/
|
||||
|
||||
#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. */
|
||||
bool m_dirty = true;
|
||||
|
||||
/* 0 = smaller-angle wedge, 1 = larger-angle wedge, 2 = both */
|
||||
int m_wedgeMode = 2;
|
||||
|
||||
/* Custom template mesh selection. */
|
||||
char m_templateName[256] = {};
|
||||
bool m_useCustomTemplate = false;
|
||||
bool m_customTemplateLoaded = false;
|
||||
std::string m_templateStatus = "no custom template loaded";
|
||||
Procedural::TriangleBuffer m_customTemplate;
|
||||
};
|
||||
|
||||
DemoApp::DemoApp()
|
||||
: 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);
|
||||
|
||||
/* Rebuild if any parameter changed. */
|
||||
if (m_dirty) {
|
||||
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;
|
||||
|
||||
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;
|
||||
bool built;
|
||||
if (m_useCustomTemplate && m_customTemplateLoaded)
|
||||
built = RoadGeometryLib::buildWedgeGeometry(
|
||||
w, graph, m_customTemplate, tmp);
|
||||
else
|
||||
built = RoadGeometryLib::buildWedgeGeometry(w, graph,
|
||||
tmp);
|
||||
if (!built)
|
||||
return false;
|
||||
int base = (int)buf.getVertices().size();
|
||||
for (const auto &v : tmp.getVertices()) {
|
||||
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(
|
||||
"BaseWhite",
|
||||
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.y##A", &m_pointA.y, -10.0f, 10.0f);
|
||||
changed |= ImGui::SliderFloat("A.z##A", &m_pointA.z, -20.0f, 20.0f);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextColored(ImVec4(1, 0.5f, 0, 1), "Point B (orange, seed)");
|
||||
changed |= ImGui::SliderFloat("B.x##B", &m_pointB.x, -20.0f, 20.0f);
|
||||
changed |= ImGui::SliderFloat("B.y##B", &m_pointB.y, -10.0f, 10.0f);
|
||||
changed |= ImGui::SliderFloat("B.z##B", &m_pointB.z, -20.0f, 20.0f);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), "Point C (red)");
|
||||
changed |= ImGui::SliderFloat("C.x##C", &m_pointC.x, -20.0f, 20.0f);
|
||||
changed |= ImGui::SliderFloat("C.y##C", &m_pointC.y, -10.0f, 10.0f);
|
||||
changed |= ImGui::SliderFloat("C.z##C", &m_pointC.z, -20.0f, 20.0f);
|
||||
|
||||
ImGui::Separator();
|
||||
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();
|
||||
ImGui::Text("Template Mesh (optional)");
|
||||
ImGui::InputText("Mesh name", m_templateName, sizeof(m_templateName));
|
||||
if (ImGui::Button("Load")) {
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (RoadGeometryLib::loadTemplateFromMesh(m_templateName,
|
||||
buf)) {
|
||||
m_customTemplate = buf;
|
||||
m_customTemplateLoaded = true;
|
||||
m_templateStatus =
|
||||
std::string("loaded: ") + m_templateName;
|
||||
} else {
|
||||
m_customTemplateLoaded = false;
|
||||
m_useCustomTemplate = false;
|
||||
m_templateStatus = std::string("load failed: ") +
|
||||
m_templateName;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
ImGui::TextDisabled("%s", m_templateStatus.c_str());
|
||||
if (m_customTemplateLoaded &&
|
||||
ImGui::Checkbox("Use custom template", &m_useCustomTemplate))
|
||||
changed = true;
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
/* Derived info. */
|
||||
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,870 @@
|
||||
/*
|
||||
* 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
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/** Centerline position (polyline MA -> O -> MB) at path distance d. */
|
||||
static Ogre::Vector3 wedgeCenterAt(const RoadWedge &wedge,
|
||||
const RoadGraph &graph, float d)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(wedge.nodeId);
|
||||
if (!node)
|
||||
return Ogre::Vector3::ZERO;
|
||||
|
||||
const RoadHalfEdge &h1 = wedge.first;
|
||||
const RoadHalfEdge &h2 = wedge.second;
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
float L1 = h1.halfLength > 1e-4f ? h1.halfLength : 1e-4f;
|
||||
float L2 = h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f;
|
||||
|
||||
Ogre::Vector3 MA = O + h1.direction * L1;
|
||||
Ogre::Vector3 MB = O + h2.direction * L2;
|
||||
|
||||
if (d <= L1)
|
||||
return MA + (O - MA) * (d / L1);
|
||||
return O + (MB - O) * ((d - L1) / L2);
|
||||
}
|
||||
|
||||
Ogre::Vector3 computeCurbOffset(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
float d)
|
||||
{
|
||||
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);
|
||||
|
||||
/*
|
||||
* Miter corner: intersection of the two constant-width curb
|
||||
* lines, expressed as parameters t1/t2 along each direction from
|
||||
* the node-side curb ends:
|
||||
*
|
||||
* offA + dir1 * t1 == offB + dir2 * t2 == cornerOff
|
||||
*
|
||||
* t1 > 0 (and t2 > 0) means the curb lines converge ahead of the
|
||||
* node: the wedge is an inner corner (sweep < 180 deg).
|
||||
*/
|
||||
bool hasCorner = false;
|
||||
bool converging = false;
|
||||
float t1 = 0.0f;
|
||||
float t2 = 0.0f;
|
||||
Ogre::Vector3 cornerOff;
|
||||
float det = dir1.z * dir2.x - dir1.x * dir2.z;
|
||||
if (std::fabs(det) >= 0.05f) {
|
||||
Ogre::Vector3 rhs = offB - offA;
|
||||
t1 = (dir2.x * rhs.z - dir2.z * rhs.x) / det;
|
||||
t2 = (dir1.x * rhs.z - dir1.z * rhs.x) / det;
|
||||
cornerOff = offA + dir1 * t1;
|
||||
hasCorner = true;
|
||||
converging = t1 > 0.0f && t2 > 0.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* Inner corner with the miter corner K lying on both curb
|
||||
* segments: pin the curb at K over the whole corner zone
|
||||
* [L1 - t1, L1 + t2]. Rows before the zone use offA, rows after
|
||||
* use offB, rows inside aim straight at K (offset K - center(d)).
|
||||
* A blend through K would fold the cross-sections over each
|
||||
* other, because the curb arc around the inner corner is shorter
|
||||
* than the centerline arc; pinned rows share the endpoint K and
|
||||
* cannot cross. When K falls outside either curb segment (wide
|
||||
* road on short edges) the old blend is kept instead.
|
||||
*/
|
||||
if (converging && t1 <= L1 && t2 <= L2) {
|
||||
float zoneStart = L1 - t1;
|
||||
float zoneEnd = L1 + t2;
|
||||
if (d <= zoneStart)
|
||||
return offA;
|
||||
if (d >= zoneEnd)
|
||||
return offB;
|
||||
Ogre::Vector3 K = node->position + cornerOff;
|
||||
return K - wedgeCenterAt(wedge, graph, d);
|
||||
}
|
||||
|
||||
/* Blend zone width. */
|
||||
float W = std::min(SEAM_OVERLAP * 4.0f,
|
||||
std::min(L1 * 0.5f, L2 * 0.5f));
|
||||
if (L1 < SEAM_OVERLAP || L2 < SEAM_OVERLAP)
|
||||
W = 0.0f;
|
||||
|
||||
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 L = L1 + (h2.halfLength > 1e-4f ? h2.halfLength : 1e-4f);
|
||||
float in1 = h1.lanesIn * graph.config.laneWidth;
|
||||
|
||||
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 = wedgeCenterAt(wedge, graph, d);
|
||||
|
||||
/* 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);
|
||||
|
||||
/*
|
||||
* The transformed strip is already a closed tube around the road
|
||||
* body (the template supplies top, bottom and curb faces; the
|
||||
* template caps and centerline wall are dropped by
|
||||
* appendTemplateCopy and butt exactly against the neighbouring
|
||||
* pieces), so it is appended verbatim. Re-extruding it into a
|
||||
* slab would double the road thickness and stack coplanar sheets
|
||||
* at the strip's center surface.
|
||||
*/
|
||||
int base = (int)out.getVertices().size();
|
||||
for (const auto &v : strip.getVertices())
|
||||
out.getVertices().push_back(v);
|
||||
for (int idx : strip.getIndices())
|
||||
out.getIndices().push_back(base + idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Template mesh loading
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
bool loadTemplateFromMesh(const std::string &meshName,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
if (meshName.empty())
|
||||
return false;
|
||||
|
||||
Ogre::MeshPtr mesh;
|
||||
try {
|
||||
mesh = Ogre::MeshManager::getSingleton().load(
|
||||
meshName,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadGeometryLib: road mesh template '" + meshName +
|
||||
"' unavailable (" + e.what() +
|
||||
"), using fallback box");
|
||||
return false;
|
||||
}
|
||||
if (!mesh || mesh->getNumSubMeshes() == 0)
|
||||
return false;
|
||||
|
||||
Procedural::TriangleBuffer tb;
|
||||
|
||||
for (unsigned si = 0; si < mesh->getNumSubMeshes(); ++si) {
|
||||
Ogre::SubMesh *sub = mesh->getSubMesh(si);
|
||||
Ogre::VertexData *vd = sub->useSharedVertices ?
|
||||
mesh->sharedVertexData :
|
||||
sub->vertexData;
|
||||
if (!vd || !sub->indexData || !sub->indexData->indexBuffer)
|
||||
continue;
|
||||
|
||||
const Ogre::VertexElement *posElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_POSITION);
|
||||
if (!posElem)
|
||||
continue;
|
||||
const Ogre::VertexElement *normElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_NORMAL);
|
||||
const Ogre::VertexElement *uvElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_TEXTURE_COORDINATES, 0);
|
||||
|
||||
int base = (int)tb.getVertices().size();
|
||||
tb.getVertices().reserve(base + vd->vertexCount);
|
||||
|
||||
/* Positions (required). */
|
||||
{
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
posElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
posElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
Procedural::TriangleBuffer::Vertex tv;
|
||||
tv.mPosition =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
tv.mNormal = Ogre::Vector3::UNIT_Y;
|
||||
/* Convention fallback: UVs span the X/Z
|
||||
* extents. */
|
||||
tv.mUV = Ogre::Vector2(p[0], p[2]);
|
||||
tb.getVertices().push_back(tv);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Normals (optional). */
|
||||
if (normElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
normElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
normElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mNormal =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* UVs (optional). */
|
||||
if (uvElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
uvElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
uvElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mUV =
|
||||
Ogre::Vector2(p[0], p[1]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Indices (16- or 32-bit). */
|
||||
Ogre::HardwareIndexBufferSharedPtr ibuf =
|
||||
sub->indexData->indexBuffer;
|
||||
size_t start = sub->indexData->indexStart;
|
||||
size_t count = sub->indexData->indexCount;
|
||||
tb.getIndices().reserve(tb.getIndices().size() + count);
|
||||
if (ibuf->getType() == Ogre::HardwareIndexBuffer::IT_16BIT) {
|
||||
const uint16_t *p = static_cast<const uint16_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
} else {
|
||||
const uint32_t *p = static_cast<const uint32_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (tb.getVertices().empty() || tb.getIndices().empty())
|
||||
return false;
|
||||
|
||||
/*
|
||||
* Normalize into template space (ProceduralRoadGeometry.md
|
||||
* section 2): X in [0, span] with X=0 at the centerline side,
|
||||
* Z in [-span, 0] with 0 at the wedge start. A mesh spanning
|
||||
* roughly 1 unit on both axes is conforming; violations are
|
||||
* warned about but the mesh is still used as-is.
|
||||
*/
|
||||
Ogre::Vector3 mn = tb.getVertices()[0].mPosition;
|
||||
Ogre::Vector3 mx = mn;
|
||||
for (const auto &v : tb.getVertices()) {
|
||||
mn.makeFloor(v.mPosition);
|
||||
mx.makeCeil(v.mPosition);
|
||||
}
|
||||
Ogre::Vector3 span = mx - mn;
|
||||
for (auto &v : tb.getVertices()) {
|
||||
v.mPosition.x -= mn.x;
|
||||
v.mPosition.z -= mx.z;
|
||||
}
|
||||
if (span.x < 0.5f || span.x > 2.0f || span.z < 0.5f ||
|
||||
span.z > 2.0f) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadGeometryLib: road mesh template '" + meshName +
|
||||
"' violates the template conventions "
|
||||
"(X [0,1] lateral, Z [-1,0] longitudinal, unit "
|
||||
"extents); spans are (" +
|
||||
Ogre::StringConverter::toString(span) +
|
||||
"), using it anyway");
|
||||
}
|
||||
|
||||
out = tb;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace RoadGeometryLib
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 the
|
||||
* outer-curb offset following the mitered curb chain (pinned at the
|
||||
* miter corner for inner wedges so cross-sections cannot fold).
|
||||
* The transformed strip is already a closed tube around the road
|
||||
* body (the template supplies top/bottom/curb faces), so it is
|
||||
* appended verbatim — no second extrusion is applied.
|
||||
*
|
||||
* @return false when the wedge is degenerate and nothing was emitted.
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* Load a road cross-section template from an OGRE mesh: the mesh
|
||||
* triangles are read verbatim and normalised into template space
|
||||
* (X in [0, span] with X=0 at the centerline side, Z in [-span, 0]
|
||||
* with 0 at the wedge start). Meshes violating the unit-extent
|
||||
* template conventions are warned about but still used. Returns
|
||||
* false when the mesh could not be loaded or has no usable
|
||||
* geometry. Used by RoadSystem and by the road demo for
|
||||
* user-selected template meshes.
|
||||
*/
|
||||
bool loadTemplateFromMesh(const std::string &meshName,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* Utility helpers
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
/** 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
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "../components/Lod.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../physics/physics.h"
|
||||
#include "../roadlib/RoadGeometryLib.hpp"
|
||||
#include "PrefabSystem.hpp"
|
||||
#include <OgreTerrainGroup.h>
|
||||
#include <OgreMaterialManager.h>
|
||||
@@ -915,619 +916,50 @@ RoadSystem::getRoadTemplate(const RoadConfig &cfg)
|
||||
m_templateThickness = cfg.roadThickness;
|
||||
m_templateBuffer = Procedural::TriangleBuffer();
|
||||
|
||||
if (!loadTemplateFromMesh(cfg.roadMeshTemplate))
|
||||
buildFallbackTemplate(cfg.roadThickness);
|
||||
if (!RoadGeometryLib::loadTemplateFromMesh(cfg.roadMeshTemplate,
|
||||
m_templateBuffer))
|
||||
m_templateBuffer =
|
||||
RoadGeometryLib::makeFallbackTemplate(cfg.roadThickness);
|
||||
|
||||
return m_templateBuffer;
|
||||
}
|
||||
|
||||
bool RoadSystem::loadTemplateFromMesh(const std::string &meshName)
|
||||
Procedural::TriangleBuffer
|
||||
RoadSystem::makeFallbackTemplate(float roadThickness)
|
||||
{
|
||||
if (meshName.empty())
|
||||
return false;
|
||||
|
||||
Ogre::MeshPtr mesh;
|
||||
try {
|
||||
mesh = Ogre::MeshManager::getSingleton().load(
|
||||
meshName,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadSystem: road mesh template '" + meshName +
|
||||
"' unavailable (" + e.what() +
|
||||
"), using fallback box");
|
||||
return false;
|
||||
}
|
||||
if (!mesh || mesh->getNumSubMeshes() == 0)
|
||||
return false;
|
||||
|
||||
Procedural::TriangleBuffer tb;
|
||||
|
||||
for (unsigned si = 0; si < mesh->getNumSubMeshes(); ++si) {
|
||||
Ogre::SubMesh *sub = mesh->getSubMesh(si);
|
||||
Ogre::VertexData *vd = sub->useSharedVertices ?
|
||||
mesh->sharedVertexData :
|
||||
sub->vertexData;
|
||||
if (!vd || !sub->indexData || !sub->indexData->indexBuffer)
|
||||
continue;
|
||||
|
||||
const Ogre::VertexElement *posElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_POSITION);
|
||||
if (!posElem)
|
||||
continue;
|
||||
const Ogre::VertexElement *normElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_NORMAL);
|
||||
const Ogre::VertexElement *uvElem =
|
||||
vd->vertexDeclaration->findElementBySemantic(
|
||||
Ogre::VES_TEXTURE_COORDINATES, 0);
|
||||
|
||||
int base = (int)tb.getVertices().size();
|
||||
tb.getVertices().reserve(base + vd->vertexCount);
|
||||
|
||||
/* Positions (required). */
|
||||
{
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
posElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
posElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
Procedural::TriangleBuffer::Vertex tv;
|
||||
tv.mPosition =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
tv.mNormal = Ogre::Vector3::UNIT_Y;
|
||||
/* Convention fallback: UVs span the X/Z
|
||||
* extents. */
|
||||
tv.mUV = Ogre::Vector2(p[0], p[2]);
|
||||
tb.getVertices().push_back(tv);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Normals (optional). */
|
||||
if (normElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
normElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
normElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mNormal =
|
||||
Ogre::Vector3(p[0], p[1], p[2]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* UVs (optional). */
|
||||
if (uvElem) {
|
||||
Ogre::HardwareVertexBufferSharedPtr vbuf =
|
||||
vd->vertexBufferBinding->getBuffer(
|
||||
uvElem->getSource());
|
||||
unsigned char *data = static_cast<unsigned char *>(
|
||||
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t v = 0; v < vd->vertexCount; ++v) {
|
||||
float *p;
|
||||
uvElem->baseVertexPointerToElement(
|
||||
data + v * vbuf->getVertexSize(), &p);
|
||||
tb.getVertices()[base + v].mUV =
|
||||
Ogre::Vector2(p[0], p[1]);
|
||||
}
|
||||
vbuf->unlock();
|
||||
}
|
||||
|
||||
/* Indices (16- or 32-bit). */
|
||||
Ogre::HardwareIndexBufferSharedPtr ibuf =
|
||||
sub->indexData->indexBuffer;
|
||||
size_t start = sub->indexData->indexStart;
|
||||
size_t count = sub->indexData->indexCount;
|
||||
tb.getIndices().reserve(tb.getIndices().size() + count);
|
||||
if (ibuf->getType() == Ogre::HardwareIndexBuffer::IT_16BIT) {
|
||||
const uint16_t *p = static_cast<const uint16_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
} else {
|
||||
const uint32_t *p = static_cast<const uint32_t *>(
|
||||
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
|
||||
for (size_t i = start; i < start + count; ++i)
|
||||
tb.getIndices().push_back(base + (int)p[i]);
|
||||
ibuf->unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (tb.getVertices().empty() || tb.getIndices().empty())
|
||||
return false;
|
||||
|
||||
/* Convention check: X within [0,1] and roughly 1 unit long,
|
||||
* Z within [-1,1] and roughly 1 unit wide. Violations are warned
|
||||
* about but the mesh is still used as-is. */
|
||||
Ogre::Vector3 mn = tb.getVertices()[0].mPosition;
|
||||
Ogre::Vector3 mx = mn;
|
||||
for (const auto &v : tb.getVertices()) {
|
||||
mn.makeFloor(v.mPosition);
|
||||
mx.makeCeil(v.mPosition);
|
||||
}
|
||||
if (mn.x < -0.001f || mx.x > 1.001f || (mx.x - mn.x) < 0.5f ||
|
||||
mn.z < -1.001f || mx.z > 1.001f || (mx.z - mn.z) < 0.5f) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadSystem: road mesh template '" + meshName +
|
||||
"' violates the template conventions "
|
||||
"(X [0,1], Z [-1,1], unit extents); bounds are (" +
|
||||
Ogre::StringConverter::toString(mn) + ") .. (" +
|
||||
Ogre::StringConverter::toString(mx) +
|
||||
"), using it anyway");
|
||||
}
|
||||
|
||||
m_templateBuffer = tb;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RoadSystem::buildFallbackTemplate(float roadThickness)
|
||||
{
|
||||
float h = std::max(0.01f, roadThickness) * 0.5f;
|
||||
|
||||
m_templateBuffer = Procedural::TriangleBuffer();
|
||||
auto &verts = m_templateBuffer.getVertices();
|
||||
auto &indices = m_templateBuffer.getIndices();
|
||||
verts.reserve(24);
|
||||
indices.reserve(36);
|
||||
|
||||
struct Corner {
|
||||
Ogre::Vector3 p;
|
||||
Ogre::Vector2 uv;
|
||||
};
|
||||
|
||||
/* One quad face: 4 vertices, 2 triangles, counter-clockwise
|
||||
* seen from outside (Ogre front face). Top/bottom faces map UV to
|
||||
* (x, z); side faces map u along their horizontal extent and v
|
||||
* along Y so every face spans (0,0)-(1,1). */
|
||||
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): Z in [0,1], X in [0,1]. */
|
||||
addFace({ { 0, h, 0 }, { 0, 0 } }, { { 0, h, 1 }, { 0, 1 } },
|
||||
{ { 1, h, 1 }, { 1, 1 } }, { { 1, h, 0 }, { 1, 0 } },
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
/* Bottom (-Y). */
|
||||
addFace({ { 0, -h, 0 }, { 0, 0 } }, { { 1, -h, 0 }, { 1, 0 } },
|
||||
{ { 1, -h, 1 }, { 1, 1 } }, { { 0, -h, 1 }, { 0, 1 } },
|
||||
Ogre::Vector3::NEGATIVE_UNIT_Y);
|
||||
/* Front (+Z). */
|
||||
addFace({ { 0, -h, 1 }, { 0, 0 } }, { { 1, -h, 1 }, { 1, 0 } },
|
||||
{ { 1, h, 1 }, { 1, 1 } }, { { 0, h, 1 }, { 0, 1 } },
|
||||
Ogre::Vector3::UNIT_Z);
|
||||
/* Back (-Z). */
|
||||
addFace({ { 1, -h, 0 }, { 0, 0 } }, { { 0, -h, 0 }, { 1, 0 } },
|
||||
{ { 0, h, 0 }, { 1, 1 } }, { { 1, h, 0 }, { 0, 1 } },
|
||||
Ogre::Vector3::NEGATIVE_UNIT_Z);
|
||||
/* Right (+X). */
|
||||
addFace({ { 1, -h, 1 }, { 0, 0 } }, { { 1, -h, 0 }, { 1, 0 } },
|
||||
{ { 1, h, 0 }, { 1, 1 } }, { { 1, h, 1 }, { 0, 1 } },
|
||||
Ogre::Vector3::UNIT_X);
|
||||
/* Left (-X). */
|
||||
addFace({ { 0, -h, 0 }, { 0, 0 } }, { { 0, -h, 1 }, { 1, 0 } },
|
||||
{ { 0, h, 1 }, { 1, 1 } }, { { 0, h, 0 }, { 0, 1 } },
|
||||
Ogre::Vector3::NEGATIVE_UNIT_X);
|
||||
return RoadGeometryLib::makeFallbackTemplate(roadThickness);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Wedge / segment geometry generation (M5.6) */
|
||||
/* */
|
||||
/* All geometry generation lives in RoadGeometryLib */
|
||||
/* (roadlib/RoadGeometryLib.cpp); the RoadSystem methods below */
|
||||
/* forward to it so headless tests can keep calling the RoadSystem */
|
||||
/* statics. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Right-of-travel direction for a horizontal road direction @p d.
|
||||
*
|
||||
* Template convention: +X forward x +Y up = +Z right, so for any
|
||||
* normalized horizontal direction the right side is d x UNIT_Y. This is
|
||||
* also the direction of increasing atan2(z, x) angle, i.e. the side a
|
||||
* wedge sweeps toward from its first half-edge.
|
||||
*/
|
||||
static Ogre::Vector3 roadRightVec(const Ogre::Vector3 &d)
|
||||
{
|
||||
return d.crossProduct(Ogre::Vector3::UNIT_Y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute road surface heights at both ends of a half-edge.
|
||||
*
|
||||
* yNode is the surface height at the seed node; yMid is the surface
|
||||
* height at the edge midpoint, averaged between the linearly
|
||||
* interpolated heights of both edge ends.
|
||||
*/
|
||||
static void halfEdgeHeights(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float &yNode, float &yMid)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(he.nodeId);
|
||||
const RoadNode *neighbor = graph.findNodeById(he.neighborId);
|
||||
float nodeY = node ? node->position.y : 0.0f;
|
||||
float neighborY = neighbor ? neighbor->position.y : nodeY;
|
||||
|
||||
yNode = nodeY + he.roadLevelAtNode;
|
||||
yMid = 0.5f * (yNode + neighborY + he.roadLevelAtNeighbor);
|
||||
}
|
||||
|
||||
/** Road surface height at distance @p t along a half-edge. */
|
||||
static float halfEdgeHeightAt(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float t)
|
||||
{
|
||||
float yNode, yMid;
|
||||
halfEdgeHeights(he, graph, yNode, yMid);
|
||||
float l = he.halfLength > 1e-4f ? he.halfLength : 1e-4f;
|
||||
return yNode + (yMid - yNode) * (t / l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Along-road texture coordinate at distance @p t from the seed node.
|
||||
*
|
||||
* For the nodeA half of an edge this is simply t; for the nodeB half it
|
||||
* is 2*halfLength - t, so u stays phase-continuous across the edge
|
||||
* midpoint where the two halves meet.
|
||||
*/
|
||||
static float halfEdgeU(const RoadHalfEdge &he, const RoadGraph &graph,
|
||||
float t)
|
||||
{
|
||||
if (he.edgeIndex >= 0 && he.edgeIndex < (int)graph.edges.size() &&
|
||||
graph.edges[he.edgeIndex].nodeB == he.nodeId)
|
||||
return 2.0f * he.halfLength - t;
|
||||
return t;
|
||||
}
|
||||
|
||||
/** One center-surface triangle with UVs, input to emitSlab(). */
|
||||
struct RoadSurfTri {
|
||||
Ogre::Vector3 p[3];
|
||||
Ogre::Vector2 uv[3];
|
||||
};
|
||||
|
||||
/** One exposed center-surface boundary edge that gets a side skirt. */
|
||||
struct RoadSkirtEdge {
|
||||
Ogre::Vector3 p0, p1;
|
||||
Ogre::Vector2 uv0, uv1;
|
||||
};
|
||||
|
||||
/** Append one triangle; degenerate (zero-area) triangles are skipped. */
|
||||
static void emitTri(Procedural::TriangleBuffer &out, const Ogre::Vector3 &p0,
|
||||
const Ogre::Vector3 &p1, const Ogre::Vector3 &p2,
|
||||
const Ogre::Vector2 &uv0, const Ogre::Vector2 &uv1,
|
||||
const Ogre::Vector2 &uv2)
|
||||
{
|
||||
Ogre::Vector3 n = (p1 - p0).crossProduct(p2 - p0);
|
||||
if (n.squaredLength() < 1e-10f)
|
||||
return;
|
||||
n.normalise();
|
||||
|
||||
int base = (int)out.getVertices().size();
|
||||
const Ogre::Vector3 *pp[3] = { &p0, &p1, &p2 };
|
||||
const Ogre::Vector2 *uu[3] = { &uv0, &uv1, &uv2 };
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
Procedural::TriangleBuffer::Vertex v;
|
||||
v.mPosition = *pp[i];
|
||||
v.mNormal = n;
|
||||
v.mUV = *uu[i];
|
||||
out.getVertices().push_back(v);
|
||||
out.getIndices().push_back(base + i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a set of center-surface triangles into a solid slab.
|
||||
*
|
||||
* Every triangle is emitted twice: offset by +halfThick along Y with the
|
||||
* winding chosen so the normal points up, and offset by -halfThick with
|
||||
* the opposite winding. Each listed boundary edge grows a vertical
|
||||
* skirt quad whose normal points away from @p refPoint (an interior
|
||||
* reference, e.g. the primitive's centroid).
|
||||
*/
|
||||
static void emitSlab(Procedural::TriangleBuffer &out,
|
||||
const std::vector<RoadSurfTri> &tris,
|
||||
const std::vector<RoadSkirtEdge> &skirts, float halfThick,
|
||||
const Ogre::Vector3 &refPoint)
|
||||
{
|
||||
Ogre::Vector3 up(0.0f, halfThick, 0.0f);
|
||||
|
||||
for (const RoadSurfTri &t : tris) {
|
||||
Ogre::Vector3 n =
|
||||
(t.p[1] - t.p[0]).crossProduct(t.p[2] - t.p[0]);
|
||||
if (n.squaredLength() < 1e-10f)
|
||||
continue;
|
||||
int i1 = n.y >= 0.0f ? 1 : 2;
|
||||
int i2 = n.y >= 0.0f ? 2 : 1;
|
||||
|
||||
/* Top surface. */
|
||||
emitTri(out, t.p[0] + up, t.p[i1] + up, t.p[i2] + up,
|
||||
t.uv[0], t.uv[i1], t.uv[i2]);
|
||||
/* Bottom surface, flipped. */
|
||||
emitTri(out, t.p[0] - up, t.p[i2] - up, t.p[i1] - up,
|
||||
t.uv[0], t.uv[i2], t.uv[i1]);
|
||||
}
|
||||
|
||||
float thickness = 2.0f * halfThick;
|
||||
for (const RoadSkirtEdge &e : skirts) {
|
||||
Ogre::Vector3 t0 = e.p0 + up;
|
||||
Ogre::Vector3 t1 = e.p1 + up;
|
||||
Ogre::Vector3 b0 = e.p0 - up;
|
||||
Ogre::Vector3 b1 = e.p1 - up;
|
||||
Ogre::Vector2 uvB0(e.uv0.x, e.uv0.y - thickness);
|
||||
Ogre::Vector2 uvB1(e.uv1.x, e.uv1.y - thickness);
|
||||
|
||||
Ogre::Vector3 n = (t1 - t0).crossProduct(b0 - t0);
|
||||
if (n.squaredLength() < 1e-10f)
|
||||
continue;
|
||||
Ogre::Vector3 mid = 0.5f * (t0 + t1);
|
||||
bool outward = n.dotProduct(mid - refPoint) >= 0.0f;
|
||||
if (outward) {
|
||||
emitTri(out, t0, t1, b1, e.uv0, e.uv1, uvB1);
|
||||
emitTri(out, t0, b1, b0, e.uv0, uvB1, uvB0);
|
||||
} else {
|
||||
emitTri(out, t0, b1, t1, e.uv0, uvB1, e.uv1);
|
||||
emitTri(out, t0, b0, b1, e.uv0, uvB0, uvB1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Overlap of strip quads behind the node so no seam gap shows. */
|
||||
static const float ROAD_SEAM_OVERLAP = 0.05f;
|
||||
|
||||
bool RoadSystem::buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
if (wedge.degenerate) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"RoadSystem: skipping degenerate road wedge at node " +
|
||||
Ogre::StringConverter::toString(wedge.nodeId));
|
||||
return false;
|
||||
}
|
||||
return RoadGeometryLib::buildWedgeGeometry(wedge, graph, out);
|
||||
}
|
||||
|
||||
const RoadNode *node = graph.findNodeById(wedge.nodeId);
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
const RoadHalfEdge &h1 = wedge.first;
|
||||
const RoadHalfEdge &h2 = wedge.second;
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
Ogre::Vector3 d1 = h1.direction;
|
||||
Ogre::Vector3 d2 = h2.direction;
|
||||
Ogre::Vector3 r1 = roadRightVec(d1);
|
||||
Ogre::Vector3 r2 = roadRightVec(d2);
|
||||
float lw = graph.config.laneWidth;
|
||||
float halfThick =
|
||||
std::max(0.01f, graph.config.roadThickness) * 0.5f;
|
||||
float out1 = h1.lanesOut * lw;
|
||||
float in1 = h1.lanesIn * lw;
|
||||
float in2 = h2.lanesIn * lw;
|
||||
float L1 = h1.halfLength;
|
||||
float L2 = h2.halfLength;
|
||||
|
||||
/*
|
||||
* The wedge region is the union of two one-sided band strips:
|
||||
* strip1 = O + t*d1 + s*r1, t in [0, L1], s in [0, out1] (h1's
|
||||
* outbound side) and strip2 = O + t*d2 + s*r2, t in [0, L2],
|
||||
* s in [-in2, 0] (h2's inbound side). For swept angles up to
|
||||
* 180 degrees the union is an L-shaped hexagon with a single
|
||||
* outer corner X where the two outer curbs intersect; emit it as
|
||||
* a triangle fan around X. Otherwise fall back to two
|
||||
* independent strip quads with their own curb and cap skirts.
|
||||
*/
|
||||
|
||||
/* Outer corner: X = O + t1*d1 + out1*r1 = O + t2*d2 - in2*r2. */
|
||||
Ogre::Vector3 rhs = -in2 * r2 - out1 * r1;
|
||||
float det = d1.z * d2.x - d1.x * d2.z;
|
||||
float t1 = 0.0f, t2 = 0.0f;
|
||||
bool cornerOk = false;
|
||||
if (std::fabs(det) >= 0.05f) {
|
||||
t1 = (-rhs.x * d2.z + d2.x * rhs.z) / det;
|
||||
t2 = (d1.x * rhs.z - rhs.x * d1.z) / det;
|
||||
cornerOk = t1 >= 0.0f && t1 <= L1 && t2 >= 0.0f &&
|
||||
t2 <= L2;
|
||||
}
|
||||
|
||||
if (cornerOk && wedge.sweptAngleDeg <= 180.0f) {
|
||||
float yNode1, yMid1, yNode2, yMid2;
|
||||
halfEdgeHeights(h1, graph, yNode1, yMid1);
|
||||
halfEdgeHeights(h2, graph, yNode2, yMid2);
|
||||
|
||||
Ogre::Vector3 vX = O + t1 * d1 + out1 * r1;
|
||||
float yO = 0.5f * (yNode1 + yNode2);
|
||||
float yX = 0.5f * (halfEdgeHeightAt(h1, graph, t1) +
|
||||
halfEdgeHeightAt(h2, graph, t2));
|
||||
|
||||
Ogre::Vector3 poly[6] = {
|
||||
O, O + L1 * d1, O + L1 * d1 + out1 * r1,
|
||||
vX, O + L2 * d2 - in2 * r2, O + L2 * d2
|
||||
};
|
||||
float polyY[6] = { yO, yMid1, yMid1, yX, yMid2, yMid2 };
|
||||
|
||||
/* Planar UVs in the (d1, r1) frame. */
|
||||
Ogre::Vector2 polyUV[6];
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
Ogre::Vector3 rel = poly[i] - O;
|
||||
polyUV[i] = Ogre::Vector2(rel.dotProduct(d1),
|
||||
rel.dotProduct(r1) + in1);
|
||||
}
|
||||
|
||||
std::vector<RoadSurfTri> tris;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
int j = (i + 1) % 6;
|
||||
RoadSurfTri tri;
|
||||
tri.p[0] = Ogre::Vector3(vX.x, yX, vX.z);
|
||||
tri.p[1] =
|
||||
Ogre::Vector3(poly[i].x, polyY[i], poly[i].z);
|
||||
tri.p[2] =
|
||||
Ogre::Vector3(poly[j].x, polyY[j], poly[j].z);
|
||||
tri.uv[0] = polyUV[3];
|
||||
tri.uv[1] = polyUV[i];
|
||||
tri.uv[2] = polyUV[j];
|
||||
tris.push_back(tri);
|
||||
}
|
||||
|
||||
/*
|
||||
* The fan closes the whole L-shape; the only exposed
|
||||
* boundary is the two outer curbs meeting at X.
|
||||
*/
|
||||
std::vector<RoadSkirtEdge> skirts;
|
||||
RoadSkirtEdge curb1;
|
||||
curb1.p0 = Ogre::Vector3(poly[2].x, polyY[2], poly[2].z);
|
||||
curb1.p1 = Ogre::Vector3(vX.x, yX, vX.z);
|
||||
curb1.uv0 = polyUV[2];
|
||||
curb1.uv1 = polyUV[3];
|
||||
skirts.push_back(curb1);
|
||||
RoadSkirtEdge curb2;
|
||||
curb2.p0 = Ogre::Vector3(vX.x, yX, vX.z);
|
||||
curb2.p1 = Ogre::Vector3(poly[4].x, polyY[4], poly[4].z);
|
||||
curb2.uv0 = polyUV[3];
|
||||
curb2.uv1 = polyUV[4];
|
||||
skirts.push_back(curb2);
|
||||
|
||||
emitSlab(out, tris, skirts, halfThick, O);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Fallback: two independent one-sided strip quads. */
|
||||
float t0 = -ROAD_SEAM_OVERLAP;
|
||||
|
||||
struct StripDef {
|
||||
const RoadHalfEdge &he;
|
||||
const Ogre::Vector3 &d;
|
||||
const Ogre::Vector3 &r;
|
||||
float s0, s1; /* lateral range along r */
|
||||
float L;
|
||||
};
|
||||
StripDef strips[2] = { { h1, d1, r1, 0.0f, out1, L1 },
|
||||
{ h2, d2, r2, -in2, 0.0f, L2 } };
|
||||
|
||||
for (const StripDef &sd : strips) {
|
||||
Ogre::Vector3 c00 = O + t0 * sd.d + sd.s0 * sd.r;
|
||||
Ogre::Vector3 c10 = O + sd.L * sd.d + sd.s0 * sd.r;
|
||||
Ogre::Vector3 c11 = O + sd.L * sd.d + sd.s1 * sd.r;
|
||||
Ogre::Vector3 c01 = O + t0 * sd.d + sd.s1 * sd.r;
|
||||
float y0 = halfEdgeHeightAt(sd.he, graph, t0);
|
||||
float yL = halfEdgeHeightAt(sd.he, graph, sd.L);
|
||||
c00.y = c01.y = y0;
|
||||
c10.y = c11.y = yL;
|
||||
|
||||
float inW = sd.he.lanesIn * lw;
|
||||
Ogre::Vector2 uv00(halfEdgeU(sd.he, graph, t0), sd.s0 + inW);
|
||||
Ogre::Vector2 uv10(halfEdgeU(sd.he, graph, sd.L),
|
||||
sd.s0 + inW);
|
||||
Ogre::Vector2 uv11(halfEdgeU(sd.he, graph, sd.L),
|
||||
sd.s1 + inW);
|
||||
Ogre::Vector2 uv01(halfEdgeU(sd.he, graph, t0), sd.s1 + inW);
|
||||
|
||||
std::vector<RoadSurfTri> tris;
|
||||
RoadSurfTri tA;
|
||||
tA.p[0] = c00; tA.p[1] = c10; tA.p[2] = c11;
|
||||
tA.uv[0] = uv00; tA.uv[1] = uv10; tA.uv[2] = uv11;
|
||||
tris.push_back(tA);
|
||||
RoadSurfTri tB;
|
||||
tB.p[0] = c00; tB.p[1] = c11; tB.p[2] = c01;
|
||||
tB.uv[0] = uv00; tB.uv[1] = uv11; tB.uv[2] = uv01;
|
||||
tris.push_back(tB);
|
||||
|
||||
std::vector<RoadSkirtEdge> skirts;
|
||||
RoadSkirtEdge curb;
|
||||
curb.p0 = c01; curb.p1 = c11;
|
||||
curb.uv0 = uv01; curb.uv1 = uv11;
|
||||
skirts.push_back(curb);
|
||||
RoadSkirtEdge cap;
|
||||
cap.p0 = c00; cap.p1 = c01;
|
||||
cap.uv0 = uv00; cap.uv1 = uv01;
|
||||
skirts.push_back(cap);
|
||||
|
||||
Ogre::Vector3 ref = 0.25f * (c00 + c10 + c11 + c01);
|
||||
emitSlab(out, tris, skirts, halfThick, ref);
|
||||
}
|
||||
|
||||
return true;
|
||||
bool RoadSystem::buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
const Procedural::TriangleBuffer &templ,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
return RoadGeometryLib::buildWedgeGeometry(wedge, graph, templ, out);
|
||||
}
|
||||
|
||||
bool RoadSystem::buildSegmentGeometry(const RoadStraightSegment &segment,
|
||||
const RoadGraph &graph,
|
||||
Procedural::TriangleBuffer &out)
|
||||
{
|
||||
const RoadNode *node = graph.findNodeById(segment.nodeId);
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
const RoadHalfEdge &he = segment.halfEdge;
|
||||
const Ogre::Vector3 &O = node->position;
|
||||
Ogre::Vector3 d = he.direction;
|
||||
Ogre::Vector3 r = roadRightVec(d);
|
||||
float lw = graph.config.laneWidth;
|
||||
float halfThick =
|
||||
std::max(0.01f, graph.config.roadThickness) * 0.5f;
|
||||
float inW = he.lanesIn * lw;
|
||||
float outW = he.lanesOut * lw;
|
||||
float L = he.halfLength;
|
||||
float t0 = -ROAD_SEAM_OVERLAP;
|
||||
|
||||
/* Full-width band: s in [-inW, +outW], t in [t0, L]. */
|
||||
Ogre::Vector3 c00 = O + t0 * d - inW * r;
|
||||
Ogre::Vector3 c10 = O + L * d - inW * r;
|
||||
Ogre::Vector3 c11 = O + L * d + outW * r;
|
||||
Ogre::Vector3 c01 = O + t0 * d + outW * r;
|
||||
float y0 = halfEdgeHeightAt(he, graph, t0);
|
||||
float yL = halfEdgeHeightAt(he, graph, L);
|
||||
c00.y = c01.y = y0;
|
||||
c10.y = c11.y = yL;
|
||||
|
||||
Ogre::Vector2 uv00(halfEdgeU(he, graph, t0), 0.0f);
|
||||
Ogre::Vector2 uv10(halfEdgeU(he, graph, L), 0.0f);
|
||||
Ogre::Vector2 uv11(halfEdgeU(he, graph, L), inW + outW);
|
||||
Ogre::Vector2 uv01(halfEdgeU(he, graph, t0), inW + outW);
|
||||
|
||||
std::vector<RoadSurfTri> tris;
|
||||
RoadSurfTri tA;
|
||||
tA.p[0] = c00; tA.p[1] = c10; tA.p[2] = c11;
|
||||
tA.uv[0] = uv00; tA.uv[1] = uv10; tA.uv[2] = uv11;
|
||||
tris.push_back(tA);
|
||||
RoadSurfTri tB;
|
||||
tB.p[0] = c00; tB.p[1] = c11; tB.p[2] = c01;
|
||||
tB.uv[0] = uv00; tB.uv[1] = uv11; tB.uv[2] = uv01;
|
||||
tris.push_back(tB);
|
||||
|
||||
/* Skirts: node-end cap plus both curbs (not the far end). */
|
||||
std::vector<RoadSkirtEdge> skirts;
|
||||
RoadSkirtEdge cap;
|
||||
cap.p0 = c00; cap.p1 = c01;
|
||||
cap.uv0 = uv00; cap.uv1 = uv01;
|
||||
skirts.push_back(cap);
|
||||
RoadSkirtEdge curbIn;
|
||||
curbIn.p0 = c00; curbIn.p1 = c10;
|
||||
curbIn.uv0 = uv00; curbIn.uv1 = uv10;
|
||||
skirts.push_back(curbIn);
|
||||
RoadSkirtEdge curbOut;
|
||||
curbOut.p0 = c01; curbOut.p1 = c11;
|
||||
curbOut.uv0 = uv01; curbOut.uv1 = uv11;
|
||||
skirts.push_back(curbOut);
|
||||
|
||||
Ogre::Vector3 ref = 0.25f * (c00 + c10 + c11 + c01);
|
||||
emitSlab(out, tris, skirts, halfThick, ref);
|
||||
return true;
|
||||
return RoadGeometryLib::buildSegmentGeometry(segment, graph, out);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -1604,45 +1036,74 @@ void RoadSystem::complyTerrain(TerrainSystem *terrainSystem,
|
||||
if (!terrainSystem || !m_terrainGroup)
|
||||
return;
|
||||
|
||||
/* Walk every loaded page's wedge/segment geometry and write fixup
|
||||
* values at the vertices of the generated road mesh. The fixup
|
||||
* target is the road underside: vertex.y - roadThickness. */
|
||||
flecs::entity terrain = getTerrainEntity();
|
||||
if (!terrain.is_alive() || !terrain.has<TerrainComponent>())
|
||||
return;
|
||||
const RoadGraph &rg = terrain.get<TerrainComponent>().roadGraph;
|
||||
float halfThick = std::max(0.01f, roadThickness) * 0.5f;
|
||||
Procedural::TriangleBuffer fb =
|
||||
makeFallbackTemplate(rg.config.roadThickness);
|
||||
|
||||
/*
|
||||
* Walk every loaded page's wedges and segments, generate road
|
||||
* geometry into a temp buffer, then write fixup values under every
|
||||
* top-surface vertex. The fixup target is the road underside:
|
||||
* surfaceY - roadThickness.
|
||||
*/
|
||||
for (auto &kv : m_pageGeometry) {
|
||||
RoadPageGeometry &pg = kv.second;
|
||||
|
||||
for (const RoadWedge &wedge : pg.wedges) {
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!buildWedgeGeometry(wedge, m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph, buf))
|
||||
Procedural::TriangleBuffer tmp;
|
||||
if (!buildWedgeGeometry(wedge, rg, fb, tmp))
|
||||
continue;
|
||||
|
||||
const auto &verts = buf.getVertices();
|
||||
/* Write fixups from the generated top-surface
|
||||
* vertices: sample the Y of vertices whose normal
|
||||
* points up and write target = Y - roadThickness
|
||||
* underneath them (the slab bottom). */
|
||||
const auto &verts = tmp.getVertices();
|
||||
for (const auto &v : verts) {
|
||||
const Ogre::Vector3 &p = v.mPosition;
|
||||
/* Only write for top-surface vertices
|
||||
* (Y near +roadThickness/2). */
|
||||
if (p.y < 0.0f)
|
||||
if (v.mNormal.y <= 0.5f)
|
||||
continue;
|
||||
|
||||
float targetY = p.y - roadThickness;
|
||||
terrainSystem->writeFixup(p.x, p.z,
|
||||
targetY);
|
||||
terrainSystem->writeFixup(
|
||||
v.mPosition.x, v.mPosition.z,
|
||||
v.mPosition.y - roadThickness);
|
||||
}
|
||||
}
|
||||
|
||||
for (const RoadStraightSegment &seg : pg.segments) {
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!buildSegmentGeometry(seg, m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph, buf))
|
||||
Ogre::Vector3 c[4];
|
||||
Ogre::Vector2 uvc[4];
|
||||
if (!RoadGeometryLib::computeSegmentBand(seg, rg, c, uvc))
|
||||
continue;
|
||||
|
||||
const auto &verts = buf.getVertices();
|
||||
for (const auto &v : verts) {
|
||||
const Ogre::Vector3 &p = v.mPosition;
|
||||
if (p.y < 0.0f)
|
||||
continue;
|
||||
/* Write fixups at the band corners. */
|
||||
for (int i = 0; i < 4; ++i)
|
||||
terrainSystem->writeFixup(
|
||||
c[i].x, c[i].z,
|
||||
c[i].y - roadThickness);
|
||||
|
||||
float targetY = p.y - roadThickness;
|
||||
terrainSystem->writeFixup(p.x, p.z,
|
||||
targetY);
|
||||
/* Sample intermediate points along the band edges
|
||||
* and interior for smooth compliance. */
|
||||
Ogre::Vector3 d10 = c[1] - c[0];
|
||||
Ogre::Vector3 d32 = c[2] - c[3];
|
||||
Ogre::Vector3 d30 = c[3] - c[0];
|
||||
float edgeLen = d10.length();
|
||||
float widthLen = d30.length();
|
||||
int nSteps = std::max(1, (int)std::ceil(edgeLen));
|
||||
int nWidth = std::max(1, (int)std::ceil(widthLen));
|
||||
for (int s = 1; s < nSteps; ++s) {
|
||||
float t = (float)s / (float)nSteps;
|
||||
Ogre::Vector3 p0 = c[0] + d10 * t;
|
||||
Ogre::Vector3 p1 = c[3] + d32 * t;
|
||||
for (int w = 0; w <= nWidth; ++w) {
|
||||
float wt = (float)w / (float)nWidth;
|
||||
Ogre::Vector3 pos = p0 + (p1 - p0) * wt;
|
||||
terrainSystem->writeFixup(
|
||||
pos.x, pos.z,
|
||||
pos.y - roadThickness);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,38 +133,69 @@ 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 following the mitered curb chain — pinned
|
||||
* at the miter corner for inner wedges so cross-sections cannot
|
||||
* fold, blended through the miter corner for outer wedges — so the
|
||||
* road keeps its exact width through turns with no gaps or
|
||||
* overlaps. The template supplies the slab thickness (top and
|
||||
* bottom at +/- roadThickness/2); template cap faces and the
|
||||
* centerline wall are dropped because they are interior to the
|
||||
* 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).
|
||||
*
|
||||
* All of these forward to RoadGeometryLib
|
||||
* (roadlib/RoadGeometryLib.cpp), which holds the single
|
||||
* implementation.
|
||||
*/
|
||||
static bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
const RoadGraph &graph,
|
||||
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,
|
||||
Procedural::TriangleBuffer &out);
|
||||
|
||||
/**
|
||||
* Create a unit-box template for headless tests and fallback.
|
||||
*
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Bind the terrain group used for page tracking (M5.4).
|
||||
*
|
||||
@@ -256,9 +288,6 @@ private:
|
||||
/* Roadside prefab spawning (M5.11). */
|
||||
void spawnSidePrefabs(RoadPageGeometry &pg);
|
||||
|
||||
bool loadTemplateFromMesh(const std::string &meshName);
|
||||
void buildFallbackTemplate(float roadThickness);
|
||||
|
||||
Ogre::Vector3 getNodePosition(int nodeId) const;
|
||||
bool getEdgePositions(int edgeIndex, Ogre::Vector3 &outA,
|
||||
Ogre::Vector3 &outB) const;
|
||||
|
||||
@@ -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;
|
||||
@@ -1941,6 +1941,190 @@ bool TerrainTestRunner::testRoadPageAssignment(EditorApp &app,
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Road slab self-intersection analysis */
|
||||
/* */
|
||||
/* Counts COPLANAR pairs (two triangles on the same plane overlapping */
|
||||
/* in area — z-fighting duplicates) and CROSSING pairs (a triangle */
|
||||
/* edge properly piercing another triangle's interior — */
|
||||
/* interpenetrating sheets). Triangles sharing vertices or edges are */
|
||||
/* not counted. Used by testRoadWedgeGeometry. */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
namespace {
|
||||
|
||||
typedef std::pair<double, double> P2; /* (x, z) */
|
||||
|
||||
double cross2d(const P2 &a, const P2 &b)
|
||||
{
|
||||
return a.first * b.second - a.second * b.first;
|
||||
}
|
||||
|
||||
std::vector<P2> clipHalfPlane(const std::vector<P2> &poly, const P2 &a,
|
||||
const P2 &b)
|
||||
{
|
||||
std::vector<P2> out;
|
||||
if (poly.empty())
|
||||
return out;
|
||||
P2 edge(b.first - a.first, b.second - a.second);
|
||||
auto inside = [&](const P2 &p) {
|
||||
P2 rel(p.first - a.first, p.second - a.second);
|
||||
return cross2d(edge, rel) >= 0.0;
|
||||
};
|
||||
auto intersect = [&](const P2 &p0, const P2 &p1) {
|
||||
P2 e0(p0.first - a.first, p0.second - a.second);
|
||||
P2 e1(p1.first - a.first, p1.second - a.second);
|
||||
double d0 = cross2d(edge, e0);
|
||||
double d1 = cross2d(edge, e1);
|
||||
double t = d0 / (d0 - d1);
|
||||
return P2(p0.first + (p1.first - p0.first) * t,
|
||||
p0.second + (p1.second - p0.second) * t);
|
||||
};
|
||||
for (size_t i = 0; i < poly.size(); ++i) {
|
||||
const P2 &cur = poly[i];
|
||||
const P2 &prv = poly[(i + poly.size() - 1) % poly.size()];
|
||||
bool inCur = inside(cur), inPrv = inside(prv);
|
||||
if (inCur) {
|
||||
if (!inPrv)
|
||||
out.push_back(intersect(prv, cur));
|
||||
out.push_back(cur);
|
||||
} else if (inPrv) {
|
||||
out.push_back(intersect(prv, cur));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
double polyArea(const std::vector<P2> &poly)
|
||||
{
|
||||
if (poly.size() < 3)
|
||||
return 0.0;
|
||||
double s = 0.0;
|
||||
for (size_t i = 0; i < poly.size(); ++i) {
|
||||
const P2 &p = poly[i];
|
||||
const P2 &q = poly[(i + 1) % poly.size()];
|
||||
s += p.first * q.second - q.first * p.second;
|
||||
}
|
||||
return 0.5 * s;
|
||||
}
|
||||
|
||||
double triOverlapAreaXZ(const Ogre::Vector3 t0[3], const Ogre::Vector3 t1[3])
|
||||
{
|
||||
std::vector<P2> subject;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
subject.push_back(P2(t0[i].x, t0[i].z));
|
||||
std::vector<P2> clip;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
clip.push_back(P2(t1[i].x, t1[i].z));
|
||||
if (polyArea(clip) < 0.0)
|
||||
std::reverse(clip.begin(), clip.end());
|
||||
std::vector<P2> poly = subject;
|
||||
for (int e = 0; e < 3 && !poly.empty(); ++e)
|
||||
poly = clipHalfPlane(poly, clip[e], clip[(e + 1) % 3]);
|
||||
return std::fabs(polyArea(poly));
|
||||
}
|
||||
|
||||
/* Barycentric coords of p in triangle (a,b,c); false if degenerate. */
|
||||
bool bary(const Ogre::Vector3 &p, const Ogre::Vector3 &a,
|
||||
const Ogre::Vector3 &b, const Ogre::Vector3 &c, float &u, float &v,
|
||||
float &w)
|
||||
{
|
||||
Ogre::Vector3 v0 = b - a, v1 = c - a, v2 = p - a;
|
||||
float d00 = v0.dotProduct(v0);
|
||||
float d01 = v0.dotProduct(v1);
|
||||
float d11 = v1.dotProduct(v1);
|
||||
float d20 = v2.dotProduct(v0);
|
||||
float d21 = v2.dotProduct(v1);
|
||||
float denom = d00 * d11 - d01 * d01;
|
||||
if (std::fabs(denom) < 1e-12f)
|
||||
return false;
|
||||
v = (d11 * d20 - d01 * d21) / denom;
|
||||
w = (d00 * d21 - d01 * d20) / denom;
|
||||
u = 1.0f - v - w;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Segment (p0,p1) vs triangle (a,b,c): proper interior piercing test.
|
||||
* The intersection must be strictly inside the triangle and strictly
|
||||
* inside the segment (shared vertices/edges do not count). */
|
||||
bool segTriPierce(const Ogre::Vector3 &p0, const Ogre::Vector3 &p1,
|
||||
const Ogre::Vector3 &a, const Ogre::Vector3 &b,
|
||||
const Ogre::Vector3 &c)
|
||||
{
|
||||
Ogre::Vector3 n = (b - a).crossProduct(c - a);
|
||||
float len = n.length();
|
||||
if (len < 1e-8f)
|
||||
return false;
|
||||
n /= len;
|
||||
float d0 = n.dotProduct(p0 - a);
|
||||
float d1 = n.dotProduct(p1 - a);
|
||||
if (d0 * d1 >= 0.0f)
|
||||
return false; /* same side or touching */
|
||||
float t = d0 / (d0 - d1);
|
||||
if (t < 1e-4f || t > 1.0f - 1e-4f)
|
||||
return false;
|
||||
Ogre::Vector3 p = p0 + (p1 - p0) * t;
|
||||
float u, v, w;
|
||||
if (!bary(p, a, b, c, u, v, w))
|
||||
return false;
|
||||
if (u < 1e-4f || v < 1e-4f || w < 1e-4f)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void countSlabOverlaps(const Procedural::TriangleBuffer &buf, int &coplanar,
|
||||
int &crossing)
|
||||
{
|
||||
coplanar = 0;
|
||||
crossing = 0;
|
||||
const auto &verts = buf.getVertices();
|
||||
const auto &indices = buf.getIndices();
|
||||
size_t nTri = indices.size() / 3;
|
||||
|
||||
for (size_t i = 0; i < nTri; ++i) {
|
||||
Ogre::Vector3 t0[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
t0[k] = verts[(size_t)indices[i * 3 + k]].mPosition;
|
||||
Ogre::Vector3 n0 = (t0[1] - t0[0]).crossProduct(t0[2] - t0[0]);
|
||||
float l0 = n0.length();
|
||||
if (l0 > 1e-8f)
|
||||
n0 /= l0;
|
||||
for (size_t j = i + 1; j < nTri; ++j) {
|
||||
Ogre::Vector3 t1[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
t1[k] = verts[(size_t)indices[j * 3 + k]]
|
||||
.mPosition;
|
||||
Ogre::Vector3 n1 =
|
||||
(t1[1] - t1[0]).crossProduct(t1[2] - t1[0]);
|
||||
float l1 = n1.length();
|
||||
if (l1 > 1e-8f)
|
||||
n1 /= l1;
|
||||
|
||||
if (std::fabs(n0.dotProduct(n1)) > 0.9999f) {
|
||||
/* Parallel planes: coplanar z-fight check. */
|
||||
float dist =
|
||||
std::fabs(n0.dotProduct(t1[0] - t0[0]));
|
||||
if (dist < 1e-3f &&
|
||||
triOverlapAreaXZ(t0, t1) > 1e-3) {
|
||||
++coplanar;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
bool pierce = false;
|
||||
for (int e = 0; e < 3 && !pierce; ++e)
|
||||
pierce = segTriPierce(t0[e], t0[(e + 1) % 3],
|
||||
t1[0], t1[1], t1[2]);
|
||||
for (int e = 0; e < 3 && !pierce; ++e)
|
||||
pierce = segTriPierce(t1[e], t1[(e + 1) % 3],
|
||||
t0[0], t0[1], t0[2]);
|
||||
if (pierce)
|
||||
++crossing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
TerrainSystem *ts)
|
||||
{
|
||||
@@ -2032,6 +2216,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.
|
||||
@@ -2068,10 +2288,12 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 3: 90-degree wedge fans around the outer corner without
|
||||
* overshooting the L-shape. Corner at origin, neighbors at +X
|
||||
* and +Z, default 1+1 lanes -> outer corner at (3, y, 3), all
|
||||
* vertices inside [0, 10]^2 in XZ.
|
||||
* 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;
|
||||
@@ -2105,58 +2327,201 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
|
||||
if (s.min.x < -0.06f || s.min.z < -0.06f ||
|
||||
s.max.x > 10.05f || s.max.z > 10.05f)
|
||||
return fail("90 deg wedge overshoots the L-shape");
|
||||
/* The template supplies the slab thickness: top and bottom
|
||||
* at +/- roadThickness/2, no second extrusion on top. */
|
||||
if (s.min.y < -0.16f || s.min.y > -0.14f ||
|
||||
s.max.y < 0.14f || s.max.y > 0.16f)
|
||||
return fail("90 deg wedge slab thickness wrong "
|
||||
"(double extrusion?)");
|
||||
|
||||
bool sawCorner = false;
|
||||
bool sawNode = false;
|
||||
for (const auto &v : buf.getVertices()) {
|
||||
if (fabsf(v.mPosition.x - 3.0f) < 0.05f &&
|
||||
fabsf(v.mPosition.z - 3.0f) < 0.05f) {
|
||||
fabsf(v.mPosition.z - 3.0f) < 0.05f)
|
||||
sawCorner = true;
|
||||
break;
|
||||
}
|
||||
if (fabsf(v.mPosition.x) < 0.05f &&
|
||||
fabsf(v.mPosition.z) < 0.05f)
|
||||
sawNode = true;
|
||||
}
|
||||
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 takes the two-strip fallback path. */
|
||||
/*
|
||||
* 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, 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.
|
||||
*/
|
||||
{
|
||||
/*
|
||||
* Case 3b: a converging (inner, sweep < 180 deg) wedge must not
|
||||
* self-intersect — no coplanar duplicate sheets and no piercing
|
||||
* triangles — flat or with a height difference at the corner
|
||||
* node. Regression test for the miter-corner fold: the curb is
|
||||
* pinned at the miter corner K through the whole corner zone so
|
||||
* consecutive cross-sections cannot fold over each other.
|
||||
*/
|
||||
{
|
||||
const float nodeYs[2] = { 0.0f, 5.0f };
|
||||
for (int iter = 0; iter < 2; ++iter) {
|
||||
RoadGraph rg;
|
||||
int 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);
|
||||
int a3 = rg.addNode(Ogre::Vector3(-10, 0, 0));
|
||||
int b3 = rg.addNode(
|
||||
Ogre::Vector3(0, nodeYs[iter], 0));
|
||||
int c3 = rg.addNode(Ogre::Vector3(10, 0, 10));
|
||||
rg.addEdge(a3, b3);
|
||||
rg.addEdge(b3, c3);
|
||||
|
||||
std::vector<RoadWedge> wedges;
|
||||
std::vector<RoadStraightSegment> segs;
|
||||
enumerateWedges(rg, wedges, segs);
|
||||
|
||||
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");
|
||||
bool saw135 = false;
|
||||
for (const auto &w : wedges) {
|
||||
if (w.nodeId != b3 || w.degenerate)
|
||||
continue;
|
||||
if (fabsf(w.sweptAngleDeg - 135.0f) < 0.1f)
|
||||
saw135 = true;
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildWedgeGeometry(w, rg,
|
||||
buf))
|
||||
return fail("135 deg corner wedge "
|
||||
"build failed");
|
||||
int cop = 0, cro = 0;
|
||||
countSlabOverlaps(buf, cop, cro);
|
||||
if (cop != 0 || cro != 0)
|
||||
return fail("converging wedge "
|
||||
"self-intersects");
|
||||
}
|
||||
if (!saw135)
|
||||
return fail("135 deg wedge not found");
|
||||
|
||||
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");
|
||||
for (const auto &sg : segs) {
|
||||
Procedural::TriangleBuffer buf;
|
||||
if (!RoadSystem::buildSegmentGeometry(sg, rg,
|
||||
buf))
|
||||
return fail("segment build failed "
|
||||
"(case 3b)");
|
||||
int cop = 0, cro = 0;
|
||||
countSlabOverlaps(buf, cop, cro);
|
||||
if (cop != 0 || cro != 0)
|
||||
return fail("segment self-intersects "
|
||||
"(case 3b)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Case 4: a nearly-collinear (~360 degree) wedge is
|
||||
* degenerate and emits nothing.
|
||||
*/
|
||||
{
|
||||
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);
|
||||
|
||||
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, 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