Skip to content

Commit 0174692

Browse files
committed
refactor(vis): centralize triangle routing logic in TriangleRouter for shared use across geometry encoders
1 parent 5b1d756 commit 0174692

4 files changed

Lines changed: 167 additions & 167 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Stuttgart University of Applied Sciences <https://www.hft-stuttgart.de>
4+
*/
5+
6+
package org.citydb.vis.encoder;
7+
8+
import org.citydb.vis.geometry.TriangleMesh;
9+
import org.citydb.vis.geometry.VertexWelder;
10+
import org.citydb.vis.styling.DefaultObjectStyle;
11+
import org.citydb.vis.styling.ObjectStyleRegistry;
12+
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
16+
/**
17+
* Single source of truth for the format-neutral routing facts of a node's
18+
* valid triangles, shared by the I3S and 3D Tiles geometry encoders.
19+
* <p>
20+
* Both encoders must answer the same per-triangle questions before they can
21+
* emit geometry — "does it carry a texture?", "is it X3DMaterial-colored?",
22+
* "which {@link DefaultObjectStyle} does its surface type resolve to?", and
23+
* "which face range (property-table row) does it belong to?". Each encoder
24+
* used to compute these independently, which let the routing decision drift
25+
* between formats — a recurring source of mixed-texture / white-pixel bugs.
26+
* {@link #route} centralizes those facts; each encoder then applies its own
27+
* format policy (multi-primitive vs single buffer, atlas pages, color baking)
28+
* on top of an identical fact base.
29+
* <p>
30+
* The returned list is in triangle-soup order — index {@code i} corresponds to
31+
* {@link VertexWelder.WeldResult#validTriIndices()} position {@code i}, so a
32+
* per-vertex iteration at output vertex {@code idx} reads {@code route(...).get(idx / 3)}.
33+
*/
34+
public final class TriangleRouter {
35+
36+
private TriangleRouter() {
37+
}
38+
39+
/**
40+
* Format-neutral routing facts for one valid triangle.
41+
*
42+
* @param triangleIndex index into {@link TriangleMesh#getTriangles()}
43+
* @param faceIndex face-range (property-table row) index this triangle
44+
* belongs to, aligned with
45+
* {@link VertexWelder.WeldResult#faceRanges()}
46+
* @param textureId mesh texture id, or {@code -1} when the triangle
47+
* carries no texture reference; encoders map a
48+
* non-negative id to an atlas page themselves
49+
* @param colored whether the triangle came from an X3DMaterial-colored
50+
* polygon ({@link TriangleMesh#isTriangleColored(int)})
51+
* @param style the {@link DefaultObjectStyle} its source surface
52+
* type resolves to (never {@code null}; falls back to
53+
* the registry's default style)
54+
*/
55+
public record RoutedTriangle(int triangleIndex, int faceIndex,
56+
int textureId, boolean colored,
57+
DefaultObjectStyle style) {
58+
/** Whether the triangle carries a real texture reference. */
59+
public boolean textured() {
60+
return textureId >= 0;
61+
}
62+
}
63+
64+
/**
65+
* Classify every valid triangle of {@code weld} into its format-neutral
66+
* routing facts, walking face ranges once to assign each triangle its
67+
* property-table row.
68+
* <p>
69+
* Must be called only when {@code weld} is non-empty — the caller's
70+
* empty-mesh early-return guarantees {@code weld.faceRanges()} is non-empty.
71+
*/
72+
public static List<RoutedTriangle> route(TriangleMesh mesh,
73+
VertexWelder.WeldResult weld,
74+
ObjectStyleRegistry styleRegistry) {
75+
List<Integer> validTriIndices = weld.validTriIndices();
76+
List<int[]> faceRanges = weld.faceRanges();
77+
// Texture ids are only meaningful when the mesh declares UVs; an
78+
// untextured mesh has no per-triangle texture lane, so treat every
79+
// triangle as texture-less.
80+
List<Integer> triTexIds = mesh.hasTexCoords() ? mesh.getTriangleTextureIds() : null;
81+
82+
List<RoutedTriangle> routed = new ArrayList<>(validTriIndices.size());
83+
int rangeIdx = 0;
84+
int[] currentRange = faceRanges.get(0);
85+
for (int i = 0; i < validTriIndices.size(); i++) {
86+
while (i > currentRange[1]) {
87+
currentRange = faceRanges.get(++rangeIdx);
88+
}
89+
int ti = validTriIndices.get(i);
90+
int textureId = triTexIds != null ? triTexIds.get(ti) : -1;
91+
boolean colored = mesh.isTriangleColored(ti);
92+
DefaultObjectStyle style = styleRegistry.resolve(mesh.getTriangleSurfaceType(ti));
93+
routed.add(new RoutedTriangle(ti, rangeIdx, textureId, colored, style));
94+
}
95+
return routed;
96+
}
97+
}

citydb-vis/src/main/java/org/citydb/vis/encoder/i3s/I3SGeometryEncoder.java

Lines changed: 26 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
package org.citydb.vis.encoder.i3s;
77

8+
import org.citydb.vis.encoder.TriangleRouter;
9+
import org.citydb.vis.encoder.TriangleRouter.RoutedTriangle;
810
import org.citydb.vis.geometry.TriangleMesh;
911
import org.citydb.vis.geometry.VertexWelder;
1012
import org.citydb.vis.scene.BoundingVolume;
@@ -152,17 +154,22 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
152154
return null;
153155
}
154156

157+
// Format-neutral per-triangle routing facts (texture id, X3DMaterial-
158+
// colored flag, resolved style, face-range row) computed once and
159+
// shared with the 3D Tiles encoder. The I3S single-buffer / one-
160+
// material-per-node policy is applied on top of these facts below.
161+
List<RoutedTriangle> routed = TriangleRouter.route(mesh, weld, styleRegistry);
162+
155163
// Detect intra-feature mixed: textured node whose mesh still has
156164
// some triangles flagged untextured (texId < 0). The atlas builder
157165
// routes these via a reserved white-pixel sentinel region so they
158166
// ride through the textured material as flat white. Without
159167
// per-vertex bake the styling/default colour is dropped for those
160168
// triangles — the textured slot has no baseColorFactor.
161-
List<Integer> triTexIds = hasTexCoords ? mesh.getTriangleTextureIds() : null;
162169
boolean intraFeatureMixed = false;
163170
if (hasTexCoords) {
164-
for (int ti : weld.validTriIndices()) {
165-
if (triTexIds.get(ti) < 0) {
171+
for (RoutedTriangle rt : routed) {
172+
if (!rt.textured()) {
166173
intraFeatureMixed = true;
167174
break;
168175
}
@@ -192,17 +199,16 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
192199
// Identity check `s != defaultStyle` works because
193200
// ObjectStyleRegistry's cache returns the same instance for every
194201
// type that resolves to the default style.
195-
DefaultObjectStyle[] perTriangleStyle = null;
196202
boolean anyTypeOverride = false;
197203
boolean anyNonWhiteStyle = false;
198204
boolean buildStyles = (!hasTexCoords && (styleRegistry.hasOverrides()
199205
|| (meshHasColors && styleRegistry.defaultStyle().hasNonDefaultColor())))
200206
|| (hasTexCoords && intraFeatureMixed && (styleRegistry.hasOverrides()
201207
|| styleRegistry.defaultStyle().hasNonDefaultColor()));
202208
if (buildStyles) {
203-
perTriangleStyle = resolveTriangleStyles(weld, mesh, styleRegistry);
204209
DefaultObjectStyle defaultStyle = styleRegistry.defaultStyle();
205-
for (DefaultObjectStyle s : perTriangleStyle) {
210+
for (RoutedTriangle rt : routed) {
211+
DefaultObjectStyle s = rt.style();
206212
if (s != defaultStyle) {
207213
anyTypeOverride = true;
208214
}
@@ -213,13 +219,13 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
213219
break;
214220
}
215221
}
216-
if (!anyNonWhiteStyle) {
217-
// Every triangle resolves to opaque white — no point
218-
// paying the per-vertex COLOR_0 cost; fall back to
219-
// WHITE_RGBA in the iterator.
220-
perTriangleStyle = null;
221-
}
222222
}
223+
// Per-vertex COLOR_0 baking of resolved styles is worth its cost only
224+
// when the node carries a non-white style somewhere; otherwise every
225+
// triangle resolves to opaque white and the iterator falls back to
226+
// WHITE_RGBA. The per-triangle style itself is read straight off the
227+
// shared routing facts when baking.
228+
boolean bakeStyles = buildStyles && anyNonWhiteStyle;
223229
// Styled-colored slot (NORMAL + COLOR_0 + alphaMode=blend) is used
224230
// only for pure-plain nodes that hit a per-type override. Mixed
225231
// nodes stay on the X3DMaterial colored slot 2/3 (one-material-per-
@@ -287,14 +293,11 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
287293
? GeoTransform.EnuBasis.at(centerX, centerY) : null;
288294

289295
boolean[] anyAlphaBelowOne = new boolean[1];
290-
DefaultObjectStyle[] styles = perTriangleStyle;
291-
List<Integer> validTriIndices = weld.validTriIndices();
292-
List<Integer> triTexIdsFinal = triTexIds;
293296
VertexWelder.iterateOutputVertices(weld, mesh, (idx, weldedPos, srcIdx) -> {
294297
outPositions[idx] = weldedPos;
298+
RoutedTriangle rt = routed.get(idx / 3);
295299
if (outNormals != null) {
296-
int normalTriIdx = validTriIndices.get(idx / 3);
297-
boolean trulyTextured = hasTexCoords && triTexIdsFinal.get(normalTriIdx) >= 0;
300+
boolean trulyTextured = hasTexCoords && rt.textured();
298301
if (trulyTextured) {
299302
enu.fillUpInEcef(outNormals[idx]);
300303
} else {
@@ -303,12 +306,10 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
303306
}
304307
outUVs[idx] = hasTexCoords ? mesh.getTexCoords().get(srcIdx) : ZERO_UV;
305308
if (outColors != null) {
306-
int triLocalIdx = idx / 3;
307-
int srcTriIdx = validTriIndices.get(triLocalIdx);
308-
boolean whitePixelTri = hasTexCoords && triTexIdsFinal.get(srcTriIdx) < 0;
309-
boolean x3dColored = mesh.isTriangleColored(srcTriIdx);
309+
boolean whitePixelTri = hasTexCoords && !rt.textured();
310+
boolean x3dColored = rt.colored();
310311
float[] c;
311-
if (whitePixelTri && !x3dColored && styles != null) {
312+
if (whitePixelTri && !x3dColored && bakeStyles) {
312313
// Intra-feature mixed: this triangle has no real texture
313314
// (atlas builder pointed its UVs at the white-pixel
314315
// sentinel). Bake the resolved styling / default colour
@@ -317,7 +318,7 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
317318
// Skipped when the triangle is also X3D-authored: the
318319
// next branch returns the authored RGBA, which wins
319320
// over styling per X3DMaterial precedence.
320-
c = styles[triLocalIdx].color();
321+
c = rt.style().color();
321322
} else if (meshHasColors && (hasTexCoords || x3dColored)) {
322323
// mesh.getColors() returns the authored X3DMaterial RGBA
323324
// for X3D-authored vertices and WHITE_RGBA padding (set
@@ -329,7 +330,7 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
329330
// X3D-authored triangles enter this branch — non-X3D
330331
// triangles fall through to styling or default white.
331332
c = mesh.getColors().get(srcIdx);
332-
} else if (!hasTexCoords && styles != null) {
333+
} else if (!hasTexCoords && bakeStyles) {
333334
// Non-textured + non-X3D triangle on a styled node:
334335
// bake the resolved style colour. Source is the
335336
// matching --feature-type-style override when present,
@@ -339,7 +340,7 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
339340
// textured triangle on a textured node — only the
340341
// dedicated white-pixel branch above applies styling
341342
// through a textured material.
342-
c = styles[triLocalIdx].color();
343+
c = rt.style().color();
343344
} else {
344345
c = WHITE_RGBA;
345346
}
@@ -379,24 +380,6 @@ public NodeGeometryResult writeNodeGeometry(Path layerDir, SceneNode node,
379380
return new NodeGeometryResult(rangeFeatureIds, featureAabbs);
380381
}
381382

382-
/**
383-
* Resolve each valid triangle's source surface type to a
384-
* {@link DefaultObjectStyle} via the registry. Returned array is
385-
* aligned with {@link VertexWelder.WeldResult#validTriIndices()} —
386-
* index {@code i / 3} on a per-vertex iteration locates the style for
387-
* the triangle owning vertex {@code i}.
388-
*/
389-
private static DefaultObjectStyle[] resolveTriangleStyles(VertexWelder.WeldResult weld,
390-
TriangleMesh mesh,
391-
ObjectStyleRegistry registry) {
392-
List<Integer> validTriIndices = weld.validTriIndices();
393-
DefaultObjectStyle[] styles = new DefaultObjectStyle[validTriIndices.size()];
394-
for (int i = 0; i < validTriIndices.size(); i++) {
395-
styles[i] = registry.resolve(mesh.getTriangleSurfaceType(validTriIndices.get(i)));
396-
}
397-
return styles;
398-
}
399-
400383
/**
401384
* Compute per-feature AABBs from welded (center-relative) positions,
402385
* converted back to absolute geographic coordinates. Returns a list

0 commit comments

Comments
 (0)