Fixed normals for road geometry
This commit is contained in:
@@ -19,7 +19,10 @@ The template from `getRoadTemplate(cfg)`:
|
||||
- **Y**: ∈ [-roadThickness/2, +roadThickness/2]. Maps directly to world vertical offset from the road surface at that position.
|
||||
- **Z**: ∈ [-1, 0] (fallback box; loaded files may differ but must span exactly 1 unit of distance along the road).
|
||||
- **UVs**: span (0,0)–(1,1) over X/Z extents on each face.
|
||||
- **Normals**: preserved through rigid rotation during transformation.
|
||||
- **Normals**: the template axes are mapped explicitly during the bend
|
||||
(+X → outer-curb direction, +Y → up, -Z → travel), because the wedge
|
||||
bend is a *reflection* of the template (see §5.7) and therefore cannot
|
||||
be represented by a single rotation around Y.
|
||||
|
||||
If the template file is missing, the fallback is a 6-face unit box (24 verts, 36 indices, X∈[0,1], Y∈[-thick/2,+thick/2], Z∈[-1,0]).
|
||||
|
||||
@@ -62,8 +65,8 @@ 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
|
||||
the output with winding reversed (§5.9) — slab extrusion (§8) applies
|
||||
to straight segments only. `RoadGeometryLib` also provides
|
||||
`loadTemplateFromMesh()` (template loading per §2) and
|
||||
`makeFallbackTemplate()`.
|
||||
|
||||
@@ -283,12 +286,16 @@ v.uv.x = (d <= L1) ? halfEdgeU(H1, graph, L1 - d)
|
||||
: halfEdgeU(H2, graph, d - L1)
|
||||
v.uv.y = v.uv.y * width(d) + in1
|
||||
|
||||
// Normal — rotate template-forward (-Z) to segment direction by the
|
||||
// SIGNED angle around Y:
|
||||
segDir = (d <= L1) ? dir1 : dir2
|
||||
theta = atan2(-segDir.x, -segDir.z)
|
||||
Ogre::Quaternion q(Ogre::Radian(theta), Ogre::Vector3::UNIT_Y);
|
||||
v.normal = q * v.normal;
|
||||
// Normal — map the template axes onto the bent world frame explicitly.
|
||||
// The bend is a REFLECTION of the template: travel is -dir1 on the first
|
||||
// half-edge and the outer curb runs along +offset(d), which is
|
||||
// -roadRight(dir2) on the second half-edge. A single rotation around Y
|
||||
// would invert those axes, so the template axes are mapped one by one:
|
||||
lateral = normalize(offset(d)) // +X -> outer-curb direction
|
||||
travel = (d <= L1) ? -dir1 : dir2 // -Z -> travel direction
|
||||
v.normal = lateral * v.normal.x
|
||||
+ Vector3(0, v.normal.y, 0)
|
||||
+ travel * (-v.normal.z);
|
||||
```
|
||||
|
||||
Since Phase 1 guarantees d ∈ [0, L] (we use exactly ceil(L) copies and
|
||||
@@ -303,14 +310,42 @@ at adjacent distances d and d+ε map to adjacent world positions. **No gap
|
||||
opens at the outer corner.**
|
||||
|
||||
The travel direction `dir(d)` is piecewise (dir1 → dir2 at the node), but
|
||||
`dir(d)` only affects the normal rotation and UV computation — it does
|
||||
not affect vertex positions. The cross-section orientation is driven
|
||||
`dir(d)` only affects the normal mapping (§5.7) and UV computation — it
|
||||
does not affect vertex positions. The cross-section orientation is driven
|
||||
entirely by the continuous `offset(d)`.
|
||||
|
||||
The centerline has a sharp corner at O, but the centerline edge is the
|
||||
**inside** of the bend, shared with adjacent wedges. No fill is needed
|
||||
there.
|
||||
|
||||
### 5.9 The Bend Is a Reflection (Normals and Winding)
|
||||
|
||||
The template's local frame — X = outer-curb lateral, Y = up, Z =
|
||||
longitudinal with travel along -Z — is **left-handed** (X × Y = +Z =
|
||||
-forward), while the world road frame (outer-curb lateral, up, travel)
|
||||
is **right-handed** (lateral × up = +travel). The position mapping
|
||||
|
||||
```
|
||||
world = center(d) + offset(d) * x + (0, y, 0)
|
||||
```
|
||||
|
||||
is therefore a *reflection* of the template, not a rotation. That has
|
||||
two consequences, both handled explicitly:
|
||||
|
||||
* **Normals** cannot be recovered by a single rotation around Y. The
|
||||
template-forward -Z does not map to `dir1`/`dir2`: travel is -dir1 on
|
||||
the first half-edge (midpoint → node) and the outer curb runs along
|
||||
`offset(d)`, which equals -roadRight(dir2) on the second half-edge.
|
||||
`transformWedgeVertices` therefore maps the template axes one by one
|
||||
(§5.7): +X → normalize(offset(d)), +Y → +Y, -Z → travel.
|
||||
|
||||
* **Winding** comes out inverted — every triangle's front face flips to
|
||||
the back. `buildWedgeGeometry` reverses each triangle's index order
|
||||
before appending the strip (§8.3) so the closed solid is front-facing
|
||||
outward; otherwise the top surface would be culled by back-face
|
||||
culling and the underside (with downward normals) would show through —
|
||||
the "inverted normals" symptom reported when nodes are repositioned.
|
||||
|
||||
## 6. Phase 3 — Center Seam Shifting
|
||||
|
||||
**Function**: `static void shiftSeamVertices(Procedural::TriangleBuffer &strip, const RoadWedge &wedge, const RoadGraph &graph)`
|
||||
@@ -437,10 +472,12 @@ Where `refPoint` is the centroid of `centerSurf`.
|
||||
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.)
|
||||
against the adjacent wedge). Because the bend is a reflection of the
|
||||
template, each triangle's winding is reversed before the strip is
|
||||
appended, so the closed solid is front-facing outward (its top
|
||||
surface survives back-face culling). (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).
|
||||
@@ -458,6 +495,8 @@ interior and get no skirts — they meet adjacent road pieces.
|
||||
| ROAD_SEAM_OVERLAP on segments (§7) | Center gap for dead-end nodes | Segment band |
|
||||
| Center seam shifting (§6) | Center hole where >2 wedges meet | Phase 3 |
|
||||
| Slab extrusion (§8) | Road must be a closed solid | Segments |
|
||||
| Explicit normal mapping (§5.7) | Outer-curb wall normal inverted on the second half-edge | Phase 2 |
|
||||
| Winding reversal (§5.9, §8.3) | Inside-out wedge (top surface culled; "inverted normals") | buildWedgeGeometry |
|
||||
|
||||
## 10. Internal Functions (Testable)
|
||||
|
||||
@@ -535,8 +574,10 @@ zone it is `K - center(d)`.
|
||||
`road_geometry_overlap_test`, CTest `roadGeometryOverlapTest`) builds
|
||||
the demo's A–B–C graph headlessly — flat, corner node raised/lowered,
|
||||
endpoints raised — and fails when any wedge or segment slab contains
|
||||
coplanar-overlapping or piercing triangle pairs, or when the flat
|
||||
wedge's slab thickness exceeds roadThickness/2. Optional arguments
|
||||
coplanar-overlapping or piercing triangle pairs, when the flat wedge's
|
||||
slab thickness exceeds roadThickness/2, or when any triangle's stored
|
||||
vertex normal disagrees with its winding (`dot(geometric, stored) < 0`),
|
||||
which indicates an inside-out (reflected) face. Optional arguments
|
||||
`Ax Ay Az Bx By Bz Cx Cy Cz` analyse a single custom configuration
|
||||
(useful when debugging geometry reported by the demo).
|
||||
|
||||
@@ -594,9 +635,21 @@ benefit for the narrow blend zone (W ≈ 0.2 units).
|
||||
|
||||
`dir(d)` is piecewise (dir1 for d≤L1, dir2 for d>L1) because the
|
||||
centerline is a polyline with a sharp corner. This is correct for
|
||||
road intersections. `dir(d)` only affects normal rotation and UV
|
||||
road intersections. `dir(d)` only affects the normal mapping and UV
|
||||
lookup — vertex positions are driven by the continuous `offset(d)`.
|
||||
|
||||
### Why map the template axes instead of rotating normals?
|
||||
|
||||
The template frame (X = outer-curb lateral, Y = up, -Z = forward) is
|
||||
left-handed while the bent world frame (lateral, up, travel) is
|
||||
right-handed, so the bend is a reflection and no rotation around Y can
|
||||
map the template normals onto the surface. Rotating the normal toward
|
||||
`dir(d)` inverted the outer-curb wall normal on the second half-edge
|
||||
(where the curb runs along -roadRight(dir2)) and the travel axis on the
|
||||
first half-edge (travel = -dir1), producing inside-out faces. Mapping
|
||||
the axes one by one (§5.7) and reversing the winding (§5.9) restores a
|
||||
correctly oriented closed solid.
|
||||
|
||||
### Template mesh is finally used
|
||||
|
||||
The current implementation ignores the template from M5.3. This
|
||||
|
||||
@@ -432,12 +432,27 @@ void transformWedgeVertices(Procedural::TriangleBuffer &strip,
|
||||
: halfEdgeU(h2, graph, d - L1);
|
||||
v.mUV.y = v.mUV.y * widthD + in1;
|
||||
|
||||
/* Normal rotation. */
|
||||
const Ogre::Vector3 &segDir = (d <= L1) ? dir1 : dir2;
|
||||
float theta = std::atan2(-segDir.x, -segDir.z);
|
||||
Ogre::Quaternion q(Ogre::Radian(theta),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
Ogre::Vector3 n = q * v.mNormal;
|
||||
/*
|
||||
* Normal. The wedge bend is a reflection of the template:
|
||||
* the outer curb runs along +offset(d), which is
|
||||
* -roadRight(dir2) on the second half-edge, and travel is
|
||||
* -dir1 on the first half-edge. A single rotation around Y
|
||||
* therefore cannot map the template normals onto the bent
|
||||
* surface (it would invert the outer-curb wall normal on the
|
||||
* second half-edge and the longitudinal axis on the first).
|
||||
* Map the template axes explicitly instead: +X -> outer-curb
|
||||
* direction, +Y -> up, -Z -> travel.
|
||||
*/
|
||||
Ogre::Vector3 lateral = off;
|
||||
if (lateral.length() < 1e-4f)
|
||||
lateral = (d <= L1) ? roadRightVec(dir1)
|
||||
: -roadRightVec(dir2);
|
||||
else
|
||||
lateral.normalise();
|
||||
Ogre::Vector3 travel = (d <= L1) ? -dir1 : dir2;
|
||||
Ogre::Vector3 n = lateral * v.mNormal.x +
|
||||
Ogre::Vector3(0, v.mNormal.y, 0) +
|
||||
travel * (-v.mNormal.z);
|
||||
|
||||
v.mPosition = Ogre::Vector3(worldXZ.x, worldY, worldXZ.z);
|
||||
v.mNormal = n;
|
||||
@@ -618,15 +633,28 @@ bool buildWedgeGeometry(const RoadWedge &wedge,
|
||||
* 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
|
||||
* pieces), so it is appended as-is. 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);
|
||||
|
||||
/*
|
||||
* The wedge bend is a reflection of the template (the outer curb
|
||||
* runs along -roadRight on the second half-edge), so the template
|
||||
* winding comes out inverted. Reverse each triangle so the closed
|
||||
* road solid is front-facing outward (and back-face culling keeps
|
||||
* the top surface).
|
||||
*/
|
||||
const std::vector<int> &si = strip.getIndices();
|
||||
out.getIndices().reserve(out.getIndices().size() + si.size());
|
||||
for (size_t t = 0; t + 2 < si.size(); t += 3) {
|
||||
out.getIndices().push_back(base + si[t]);
|
||||
out.getIndices().push_back(base + si[t + 2]);
|
||||
out.getIndices().push_back(base + si[t + 1]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -164,6 +164,7 @@ static bool segTriPierce(const Vector3 &p0, const Vector3 &p1,
|
||||
struct Analysis {
|
||||
int coplanar = 0;
|
||||
int crossing = 0;
|
||||
int invertedNormal = 0;
|
||||
bool nan = false;
|
||||
Vector3 mn = Vector3(FLT_MAX, FLT_MAX, FLT_MAX);
|
||||
Vector3 mx = Vector3(-FLT_MAX, -FLT_MAX, -FLT_MAX);
|
||||
@@ -197,6 +198,17 @@ static Analysis analyze(const Procedural::TriangleBuffer &buf,
|
||||
float l0 = n0.length();
|
||||
if (l0 > 1e-8f)
|
||||
n0 /= l0;
|
||||
/* The stored vertex normal must agree with the winding: a
|
||||
* reflection in the wedge bend would otherwise flip the face
|
||||
* inside-out (inverted shading / back-face culling). */
|
||||
const Vector3 &sn = verts[(size_t)indices[i * 3]].mNormal;
|
||||
if (sn.squaredLength() > 1e-6f && n0.dotProduct(sn) < 0.0f) {
|
||||
if (res.invertedNormal < 20)
|
||||
printf(" INVERTED-NORMAL tri %zu near "
|
||||
"(%.2f,%.2f,%.2f)\n",
|
||||
i, t0[0].x, t0[0].y, t0[0].z);
|
||||
++res.invertedNormal;
|
||||
}
|
||||
for (size_t j = i + 1; j < nTri; ++j) {
|
||||
Vector3 t1[3];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
@@ -245,8 +257,8 @@ static Analysis analyze(const Procedural::TriangleBuffer &buf,
|
||||
}
|
||||
}
|
||||
}
|
||||
printf(" coplanar-overlap pairs: %d crossing pairs: %d\n",
|
||||
res.coplanar, res.crossing);
|
||||
printf(" coplanar-overlap pairs: %d crossing pairs: %d inverted-normal faces: %d\n",
|
||||
res.coplanar, res.crossing, res.invertedNormal);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -261,7 +273,8 @@ static bool checkPiece(bool built, const char *name,
|
||||
Analysis a = analyze(buf, name);
|
||||
if (out)
|
||||
*out = a;
|
||||
bool ok = !a.nan && a.coplanar == 0 && a.crossing == 0;
|
||||
bool ok = !a.nan && a.coplanar == 0 && a.crossing == 0 &&
|
||||
a.invertedNormal == 0;
|
||||
if (!ok)
|
||||
printf(" *** %s FAILED ***\n", name);
|
||||
return ok;
|
||||
|
||||
Reference in New Issue
Block a user