Procedural road geometry fixes

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