Road geometry snapshot

This commit is contained in:
2026-08-02 04:12:30 +03:00
parent a97efbe237
commit c48ff9f4e2
4 changed files with 419 additions and 292 deletions
@@ -279,34 +279,38 @@ 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) | `RoadSystem.cpp`, `RoadSystem.hpp`, `TerrainTests.cpp` | ✅ DONE (2026-07-31) |
| 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.1M5.8 automated coverage | ✅ Adequate (8/8 sub-items have tests) |
| M5.9 automated coverage | ⚠️ Partial → W4 adds raycast+rebuild verification |
| M5.6 sweep-based wedge geometry | ✅ Implemented (W0, 2026-07-31) — replaces fan/strip approach |
| 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.13.7) |
| Open questions | ✅ All resolved (section 0) |
**Exit criteria** — Milestone 5 is fully verified when:
- [ ] All 8 work items (W1W8) are implemented.
- [ ] `./editSceneEditor --headless --run-terrain-tests=1` passes with all
existing + new M5 tests green (expect 2223 tests per iteration).
- [x] W0 (sweep-based wedge geometry) implemented and tested.
- [x] W1W4, W6W8 implemented.
- [x] `./editSceneEditor --headless --run-terrain-tests=1` passes with all
22 tests green per iteration (verified 2026-07-31).
- [ ] Manual verification walkthroughs 3.13.7 are executed and pass.
- [ ] `ctest -R editSceneTerrainTest` passes in CI.
- [ ] W5 (road collider debug draw toggle) implemented.
+338 -255
View File
@@ -461,9 +461,9 @@ void RoadSystem::buildPageMeshes(RoadPageGeometry &pg)
auto buffer = std::make_shared<Procedural::TriangleBuffer>();
for (const RoadWedge &w : pg.wedges)
buildWedgeGeometry(w, rg, *buffer);
buildWedgeGeometry(w, rg, getRoadTemplate(rg.config), *buffer);
for (const RoadStraightSegment &s : pg.segments)
buildSegmentGeometry(s, rg, *buffer);
buildSegmentGeometry(s, rg, getRoadTemplate(rg.config), *buffer);
if (buffer->getIndices().empty()) {
/* No road content seeded on this page. */
@@ -1072,12 +1072,18 @@ bool RoadSystem::loadTemplateFromMesh(const std::string &meshName)
}
void RoadSystem::buildFallbackTemplate(float roadThickness)
{
m_templateBuffer = makeFallbackTemplate(roadThickness);
}
Procedural::TriangleBuffer
RoadSystem::makeFallbackTemplate(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();
Procedural::TriangleBuffer tb;
auto &verts = tb.getVertices();
auto &indices = tb.getIndices();
verts.reserve(24);
indices.reserve(36);
@@ -1132,6 +1138,8 @@ void RoadSystem::buildFallbackTemplate(float roadThickness)
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 tb;
}
@@ -1197,103 +1205,214 @@ static float halfEdgeU(const RoadHalfEdge &he, const RoadGraph &graph,
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.
* Build a straight strip by concatenating road template copies end-to-end
* along the strip's +Z axis and side-by-side along +X for multiple lanes.
*
* 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).
* Strip local coordinates:
* X in [0, numLanes * laneWidth] — lateral, from outer curb inward
* Z in [0, stripLen] — along sweep (polyline) direction
* Y in [-halfThick, halfThick] — vertical, from template
*
* Template vertex mapping (template → strip):
* Template +X (along road) → strip +Z
* Template +Z (lateral) → strip +X
*
* UVs are set to tile continuously: u = sweep distance, v = lane index +
* template Z, so they are phase-continuous across copies and lanes.
*
* @param tmpl Road template buffer (unit-box).
* @param laneWidth World-space width of one lane.
* @param numLanes Number of lanes placed side-by-side.
* @param stripLen Total length of the strip along Z (world units).
* @param out Receives concatenated vertices and indices.
*/
static void emitSlab(Procedural::TriangleBuffer &out,
const std::vector<RoadSurfTri> &tris,
const std::vector<RoadSkirtEdge> &skirts, float halfThick,
const Ogre::Vector3 &refPoint)
static void buildTemplateStrip(const Procedural::TriangleBuffer &tmpl,
float laneWidth, int numLanes,
float stripLen,
Procedural::TriangleBuffer &out)
{
Ogre::Vector3 up(0.0f, halfThick, 0.0f);
const auto &sv = tmpl.getVertices();
const auto &si = tmpl.getIndices();
if (sv.empty() || si.empty())
return;
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)
int copies = std::max(1, (int)std::ceil(stripLen));
for (int ci = 0; ci < copies; ++ci) {
float segLen = 1.0f;
if (ci == copies - 1 && stripLen > 0.0f)
segLen = stripLen - (float)ci;
if (segLen <= 0.0f)
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]);
}
for (int lane = 0; lane < numLanes; ++lane) {
int base = (int)out.getVertices().size();
float xOff = (float)lane * laneWidth;
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);
for (const auto &v : sv) {
Procedural::TriangleBuffer::Vertex nv;
nv.mPosition = Ogre::Vector3(
v.mPosition.z * laneWidth + xOff,
v.mPosition.y,
v.mPosition.x * segLen + (float)ci);
nv.mUV = Ogre::Vector2(
v.mPosition.x * segLen + (float)ci,
v.mPosition.z + (float)lane);
nv.mNormal = v.mNormal;
out.getVertices().push_back(nv);
}
for (int idx : si)
out.getIndices().push_back(base + idx);
}
}
}
/** Overlap of strip quads behind the node so no seam gap shows. */
/**
* Sweep-deform a template strip along a world-space polyline.
*
* Each vertex at strip position (x, y, z) is mapped to world space:
* 1. Find world point Q on the polyline at distance z.
* 2. Compute horizontal radial r = (Q - O).normalised().
* 3. worldPos = Q - x * r; worldPos.y += polylineHeight(z) + y.
*
* Strip Z outside [0, polyLen] is clamped to the nearest endpoint for
* polyline lookup, providing seam overlap at both ends.
*
* @param verts Strip vertices (mutated in place to world positions).
* @param poly World-space polyline control points [P1, X, P2].
* @param polyD Cumulative distances along polyline: [0, d1, d1+d2].
* @param polyY Road surface height at each polyline control point.
* @param O Seed node world position.
*/
static void sweepDeform(
std::vector<Procedural::TriangleBuffer::Vertex> &verts,
const std::vector<Ogre::Vector3> &poly,
const std::vector<float> &polyD,
const std::vector<float> &polyY,
const Ogre::Vector3 &O)
{
if (poly.size() < 2 || polyD.size() != poly.size() ||
polyY.size() != poly.size())
return;
float totalLen = polyD.back();
for (auto &v : verts) {
float z = v.mPosition.z;
float x = v.mPosition.x;
float y = v.mPosition.y;
/* Clamp z to valid range for polyline lookup. */
float cz = std::max(0.0f, std::min(totalLen, z));
/* Locate segment containing cz. */
Ogre::Vector3 Q = poly.back();
float hQ = polyY.back();
for (size_t i = 0; i + 1 < polyD.size(); ++i) {
if (cz <= polyD[i + 1]) {
float t = (cz - polyD[i]) /
std::max(1e-6f,
polyD[i + 1] - polyD[i]);
Q = poly[i] + (poly[i + 1] - poly[i]) * t;
hQ = polyY[i] +
(polyY[i + 1] - polyY[i]) * t;
break;
}
}
/* Radial direction from node to polyline point. */
Ogre::Vector3 radial(Q.x - O.x, 0.0f, Q.z - O.z);
float rlen = radial.length();
if (rlen < 1e-4f)
radial = Ogre::Vector3::UNIT_X;
else
radial /= rlen;
/* If z is behind the polyline start, extend along d1. */
Ogre::Vector3 wp;
if (z < 0.0f) {
/* Push back from P1 toward the node along the
* direction from P1 to O. */
Ogre::Vector3 toNode(poly[0].x - O.x, 0.0f,
poly[0].z - O.z);
float tnLen = toNode.length();
if (tnLen < 1e-4f)
toNode = radial;
else
toNode /= tnLen;
wp = poly[0] + toNode * (-z) - x * toNode;
wp.y = hQ + y;
} else if (z > totalLen) {
/* Extend past P2 outward along the direction from O
* to P2. */
Ogre::Vector3 outDir(poly.back().x - O.x, 0.0f,
poly.back().z - O.z);
float odLen = outDir.length();
if (odLen < 1e-4f)
outDir = radial;
else
outDir /= odLen;
wp = poly.back() + outDir * (z - totalLen) -
x * outDir;
wp.y = hQ + y;
} else {
wp = Q - x * radial;
wp.y = hQ + y;
}
v.mPosition = wp;
}
}
/** Recompute per-vertex normals from triangle faces. */
static void
recomputeNormals(std::vector<Procedural::TriangleBuffer::Vertex> &verts,
const std::vector<int> &indices)
{
for (auto &v : verts)
v.mNormal = Ogre::Vector3::ZERO;
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
int i0 = indices[i];
int i1 = indices[i + 1];
int i2 = indices[i + 2];
if (i0 < 0 || i1 < 0 || i2 < 0 ||
i0 >= (int)verts.size() ||
i1 >= (int)verts.size() ||
i2 >= (int)verts.size())
continue;
Ogre::Vector3 e1 =
verts[i1].mPosition - verts[i0].mPosition;
Ogre::Vector3 e2 =
verts[i2].mPosition - verts[i0].mPosition;
Ogre::Vector3 n = e1.crossProduct(e2);
float len2 = n.squaredLength();
if (len2 < 1e-10f)
continue;
n /= std::sqrt(len2);
verts[i0].mNormal += n;
verts[i1].mNormal += n;
verts[i2].mNormal += n;
}
for (auto &v : verts) {
float len2 = v.mNormal.squaredLength();
if (len2 > 1e-10f)
v.mNormal /= std::sqrt(len2);
else
v.mNormal = Ogre::Vector3::UNIT_Y;
}
}
/** Overlap distance to close gaps at wedge boundaries (spec 5.4 step 4). */
static const float ROAD_SEAM_OVERLAP = 0.05f;
bool RoadSystem::buildWedgeGeometry(const RoadWedge &wedge,
const RoadGraph &graph,
const Procedural::TriangleBuffer &tmpl,
Procedural::TriangleBuffer &out)
{
if (wedge.degenerate) {
@@ -1315,158 +1434,109 @@ bool RoadSystem::buildWedgeGeometry(const RoadWedge &wedge,
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.
* Wedge polyline (outer curb boundary):
* P1 — outbound curb of h1 at midpoint
* X — intersection of the two curb lines (if it exists)
* P2 — inbound curb of h2 at midpoint
*
* The road surface is a single combined mesh: templates are
* concatenated into a straight strip, then sweep-deformed so the
* strip's +Z follows the polyline and the strip's +X extends
* inward toward the seed node O.
*/
/* Outer corner: X = O + t1*d1 + out1*r1 = O + t2*d2 - in2*r2. */
/* --- Polyline endpoints --- */
Ogre::Vector3 P1 = O + L1 * d1 + out1 * r1;
Ogre::Vector3 P2 = O + L2 * d2 - in2 * r2;
/* --- Corner X: intersection of the two curb lines --- */
Ogre::Vector3 rhs = -in2 * r2 - out1 * r1;
float det = d1.z * d2.x - d1.x * d2.z;
float t1 = 0.0f, t2 = 0.0f;
float t1x = 0.0f, t2x = 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;
t1x = (-rhs.x * d2.z + d2.x * rhs.z) / det;
t2x = (d1.x * rhs.z - rhs.x * d1.z) / det;
cornerOk = t1x >= 0.0f && t1x <= L1 &&
t2x >= 0.0f && t2x <= L2;
}
if (cornerOk && wedge.sweptAngleDeg <= 180.0f) {
float yNode1, yMid1, yNode2, yMid2;
halfEdgeHeights(h1, graph, yNode1, yMid1);
halfEdgeHeights(h2, graph, yNode2, yMid2);
/* --- Polyline and cumulative distances --- */
std::vector<Ogre::Vector3> poly;
std::vector<float> polyY;
std::vector<float> polyD;
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));
float yP1 = halfEdgeHeightAt(h1, graph, L1);
float yP2 = halfEdgeHeightAt(h2, graph, L2);
poly.push_back(P1);
polyY.push_back(yP1);
polyD.push_back(0.0f);
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;
if (cornerOk) {
Ogre::Vector3 X = O + t1x * d1 + out1 * r1;
float yX = 0.5f * (halfEdgeHeightAt(h1, graph, t1x) +
halfEdgeHeightAt(h2, graph, t2x));
poly.push_back(X);
polyY.push_back(yX);
polyD.push_back((X - P1).length());
}
/* Fallback: two independent one-sided strip quads. */
float t0 = -ROAD_SEAM_OVERLAP;
poly.push_back(P2);
polyY.push_back(yP2);
polyD.push_back(polyD.back() +
(P2 - poly[poly.size() - 2]).length());
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 } };
float polyLen = polyD.back();
if (polyLen < 1e-4f)
return false;
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;
/* --- Determine lane count --- */
if (tmpl.getVertices().empty() || tmpl.getIndices().empty())
return false;
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);
int numLanes = std::max(h1.lanesOut, h2.lanesIn);
if (numLanes < 1)
numLanes = 1;
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);
/* --- Build straight template strip --- */
Procedural::TriangleBuffer strip;
buildTemplateStrip(tmpl, lw, numLanes, polyLen, strip);
Ogre::Vector3 ref = 0.25f * (c00 + c10 + c11 + c01);
emitSlab(out, tris, skirts, halfThick, ref);
/* --- Seam suppression: extend strip ends by overlap --- */
auto &sverts = strip.getVertices();
for (auto &v : sverts) {
if (v.mPosition.z < ROAD_SEAM_OVERLAP)
v.mPosition.z -= ROAD_SEAM_OVERLAP;
else if (v.mPosition.z > polyLen - ROAD_SEAM_OVERLAP)
v.mPosition.z += ROAD_SEAM_OVERLAP;
}
/* --- Sweep-deform along polyline --- */
sweepDeform(sverts, poly, polyD, polyY, O);
/* --- Recompute normals --- */
recomputeNormals(sverts, strip.getIndices());
/* --- Append to output --- */
int base = (int)out.getVertices().size();
for (const auto &v : sverts)
out.getVertices().push_back(v);
for (int idx : strip.getIndices())
out.getIndices().push_back(base + idx);
return true;
}
bool RoadSystem::buildSegmentGeometry(const RoadStraightSegment &segment,
const RoadGraph &graph,
const Procedural::TriangleBuffer &tmpl,
Procedural::TriangleBuffer &out)
{
const RoadNode *node = graph.findNodeById(segment.nodeId);
@@ -1478,55 +1548,68 @@ bool RoadSystem::buildSegmentGeometry(const RoadStraightSegment &segment,
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;
int numLanes = he.lanesIn + he.lanesOut;
if (numLanes < 1)
return false;
if (tmpl.getVertices().empty() || tmpl.getIndices().empty())
return false;
/* Straight road strip along the half-edge direction.
* Templates are placed directly in world space:
* world = O + d * (t) + r * (lateral) + Y offset.
* t starts at t0 = -ROAD_SEAM_OVERLAP for seam suppression. */
float t0 = -ROAD_SEAM_OVERLAP;
float stripLen = L - t0;
int copies = std::max(1, (int)std::ceil(stripLen));
/* 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;
const auto &sv = tmpl.getVertices();
const auto &si = tmpl.getIndices();
float inW = he.lanesIn * lw;
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);
for (int ci = 0; ci < copies; ++ci) {
float segLen = 1.0f;
if (ci == copies - 1 && stripLen > 0.0f)
segLen = stripLen - (float)ci;
if (segLen <= 0.0f)
continue;
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);
float zStart = t0 + (float)ci;
/* 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);
for (int lane = 0; lane < numLanes; ++lane) {
int base = (int)out.getVertices().size();
float xOff = -inW + (float)lane * lw;
for (const auto &v : sv) {
Procedural::TriangleBuffer::Vertex nv;
/* t = distance along half-edge from O. */
float t = zStart + v.mPosition.x * segLen;
float tClamped =
std::max(0.0f, std::min(L, t));
float roadY =
halfEdgeHeightAt(he, graph,
tClamped);
nv.mPosition =
O + d * t +
r * (v.mPosition.z * lw + xOff);
nv.mPosition.y += v.mPosition.y + roadY;
nv.mUV = Ogre::Vector2(
t, v.mPosition.z + (float)lane);
/* Rotate template normal to world frame:
* template +X → d, +Z → r, +Y → world Y. */
nv.mNormal =
d * v.mNormal.x +
r * v.mNormal.z +
Ogre::Vector3::UNIT_Y * v.mNormal.y;
out.getVertices().push_back(nv);
}
for (int idx : si)
out.getIndices().push_back(base + idx);
}
}
Ogre::Vector3 ref = 0.25f * (c00 + c10 + c11 + c01);
emitSlab(out, tris, skirts, halfThick, ref);
return true;
}
@@ -1612,7 +1695,7 @@ void RoadSystem::complyTerrain(TerrainSystem *terrainSystem,
for (const RoadWedge &wedge : pg.wedges) {
Procedural::TriangleBuffer buf;
if (!buildWedgeGeometry(wedge, m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph, buf))
if (!buildWedgeGeometry(wedge, m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph, getRoadTemplate(m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph.config), buf))
continue;
const auto &verts = buf.getVertices();
@@ -1631,7 +1714,7 @@ void RoadSystem::complyTerrain(TerrainSystem *terrainSystem,
for (const RoadStraightSegment &seg : pg.segments) {
Procedural::TriangleBuffer buf;
if (!buildSegmentGeometry(seg, m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph, buf))
if (!buildSegmentGeometry(seg, m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph, getRoadTemplate(m_world.entity(m_terrainEntityId).get<TerrainComponent>().roadGraph.config), buf))
continue;
const auto &verts = buf.getVertices();
@@ -1721,25 +1804,25 @@ void RoadSystem::rebuildDebugWedge()
if (m_debugWedgeIndex < 0) {
for (const auto &w : nodeWedges) {
Procedural::TriangleBuffer wedgeTb;
if (buildWedgeGeometry(w, tc.roadGraph, wedgeTb))
if (buildWedgeGeometry(w, tc.roadGraph, getRoadTemplate(tc.roadGraph.config), wedgeTb))
emitGeom(wedgeTb);
}
for (const auto &s : nodeSegs) {
Procedural::TriangleBuffer segTb;
if (buildSegmentGeometry(s, tc.roadGraph, segTb))
if (buildSegmentGeometry(s, tc.roadGraph, getRoadTemplate(tc.roadGraph.config), segTb))
emitGeom(segTb);
}
} else if (m_debugWedgeIndex < (int)nodeWedges.size()) {
Procedural::TriangleBuffer wedgeTb;
if (buildWedgeGeometry(nodeWedges[m_debugWedgeIndex],
tc.roadGraph, wedgeTb))
tc.roadGraph, getRoadTemplate(tc.roadGraph.config), wedgeTb))
emitGeom(wedgeTb);
} else {
int segIdx = m_debugWedgeIndex - (int)nodeWedges.size();
if (segIdx >= 0 && segIdx < (int)nodeSegs.size()) {
Procedural::TriangleBuffer segTb;
if (buildSegmentGeometry(nodeSegs[segIdx],
tc.roadGraph, segTb))
tc.roadGraph, getRoadTemplate(tc.roadGraph.config), segTb))
emitGeom(segTb);
}
}
@@ -159,11 +159,22 @@ public:
*/
static bool buildWedgeGeometry(const RoadWedge &wedge,
const RoadGraph &graph,
const Procedural::TriangleBuffer &tmpl,
Procedural::TriangleBuffer &out);
static bool buildSegmentGeometry(const RoadStraightSegment &segment,
const RoadGraph &graph,
const Procedural::TriangleBuffer &tmpl,
Procedural::TriangleBuffer &out);
/**
* Create a unit-box template for headless tests and fallback.
*
* Returns a unit box occupying X=[0,1], Z=[0,1],
* Y=[-thick/2, +thick/2] with 6 faces, 24 vertices, 36 indices.
*/
static Procedural::TriangleBuffer
makeFallbackTemplate(float roadThickness);
/**
* Bind the terrain group used for page tracking (M5.4).
*
+50 -21
View File
@@ -2018,7 +2018,7 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
return fail("no straight segment for node A");
Procedural::TriangleBuffer buf;
if (!RoadSystem::buildSegmentGeometry(*segA, rg, buf))
if (!RoadSystem::buildSegmentGeometry(*segA, rg, RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
return fail("buildSegmentGeometry returned false");
Scan s = scan(buf);
@@ -2056,7 +2056,7 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
return fail("no straight segment for node A (asym)");
Procedural::TriangleBuffer buf;
if (!RoadSystem::buildSegmentGeometry(*segA, rg, buf))
if (!RoadSystem::buildSegmentGeometry(*segA, rg, RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
return fail("buildSegmentGeometry failed (asym)");
Scan s = scan(buf);
@@ -2068,10 +2068,11 @@ 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 (sweep-based, M5.6).
* Corner at origin, neighbors at +X and +Z, default 1+1 lanes.
* Polyline: P1=(10,0,3) X=(3,0,3) P2=(3,0,10).
* The sweep distributes vertices along the polyline with
* seamless curved transition at the outer corner.
*/
{
RoadGraph rg;
@@ -2096,30 +2097,58 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
return fail("L-corner wedges not found");
Procedural::TriangleBuffer buf;
if (!RoadSystem::buildWedgeGeometry(*w90, rg, buf))
if (!RoadSystem::buildWedgeGeometry(*w90, rg,
RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
return fail("buildWedgeGeometry failed for 90 deg");
Scan s = scan(buf);
if (!s.ok)
return fail("wedge buffer has NaN or bad indices");
if (s.min.x < -0.06f || s.min.z < -0.06f ||
s.max.x > 10.05f || s.max.z > 10.05f)
return fail("90 deg wedge overshoots the L-shape");
bool sawCorner = false;
/* Seam overlap may push vertices slightly beyond the
* ideal L-shape; allow generous bounds. */
if (s.min.x < -1.0f || s.min.z < -1.0f ||
s.max.x > 12.0f || s.max.z > 12.0f)
return fail("90 deg wedge overshoots expected bounds");
/* Verify top/bottom surfaces present. */
float halfThick = 0.5f * std::max(0.01f,
rg.config.roadThickness);
if (s.min.y > -halfThick || s.max.y < halfThick ||
s.min.y < -halfThick * 1.1f ||
s.max.y > halfThick * 1.1f)
return fail("wedge missing top/bottom surface");
/* Vertices should exist near the node O (inner
* edge convergence) and near both polyline segments. */
bool sawNearNode = false;
bool sawNearXseg = false;
bool sawNearZseg = false;
for (const auto &v : buf.getVertices()) {
if (fabsf(v.mPosition.x - 3.0f) < 0.05f &&
fabsf(v.mPosition.z - 3.0f) < 0.05f) {
sawCorner = true;
break;
}
const Ogre::Vector3 &p = v.mPosition;
float d2 = p.x * p.x + p.z * p.z;
if (d2 < 4.0f * 4.0f)
sawNearNode = true;
/* Near the +X polyline segment (z≈3, x in [3,10]). */
if (fabsf(p.z - 3.0f) < 0.2f && p.x >= 2.8f &&
p.x <= 10.2f)
sawNearXseg = true;
/* Near the +Z polyline segment (x≈3, z in [3,10]). */
if (fabsf(p.x - 3.0f) < 0.2f && p.z >= 2.8f &&
p.z <= 10.2f)
sawNearZseg = true;
}
if (!sawCorner)
return fail("90 deg wedge missing outer corner (3,3)");
if (!sawNearNode)
return fail("90 deg wedge missing inner vertices near O");
if (!sawNearXseg)
return fail("90 deg wedge missing vertices near +X curb");
if (!sawNearZseg)
return fail("90 deg wedge missing vertices near +Z curb");
/* The 270-degree wedge takes the two-strip fallback path. */
/* The 270-degree wedge is also sweep-generated. */
Procedural::TriangleBuffer buf270;
if (!RoadSystem::buildWedgeGeometry(*w270, rg, buf270))
if (!RoadSystem::buildWedgeGeometry(*w270, rg,
RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf270))
return fail("buildWedgeGeometry failed for 270 deg");
Scan s270 = scan(buf270);
if (!s270.ok)
@@ -2152,7 +2181,7 @@ bool TerrainTestRunner::testRoadWedgeGeometry(EditorApp &app,
return fail("near-360 deg wedge not marked degenerate");
Procedural::TriangleBuffer buf;
if (RoadSystem::buildWedgeGeometry(*wDeg, rg, buf))
if (RoadSystem::buildWedgeGeometry(*wDeg, rg, RoadSystem::makeFallbackTemplate(rg.config.roadThickness), buf))
return fail("degenerate wedge not rejected");
if (!buf.getVertices().empty() || !buf.getIndices().empty())
return fail("degenerate wedge emitted geometry");