Skip to content

Commit 8399d2d

Browse files
mergify[bot]eringrammarkschlosseratbentleyben-polinskyhl662
authored
Support geometry collection for reality meshes with glTF tiles (backport #9015) [release/5.6.x] (#9035)
Co-authored-by: Erin Ingram <47707444+eringram@users.noreply.github.com> Co-authored-by: Mark Schlosser <47000437+markschlosseratbentley@users.noreply.github.com> Co-authored-by: Ben Polinsky <78756012+ben-polinsky@users.noreply.github.com> Co-authored-by: Nam Le <50554904+hl662@users.noreply.github.com>
1 parent b5d0edc commit 8399d2d

File tree

5 files changed

+176
-16
lines changed

5 files changed

+176
-16
lines changed

common/api/core-frontend.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4119,7 +4119,7 @@ export abstract class GltfReader {
41194119
// (undocumented)
41204120
protected readFeatureIndices(_json: any): number[] | undefined;
41214121
// (undocumented)
4122-
readGltfAndCreateGeometry(transformToRoot?: Transform, needNormals?: boolean, needParams?: boolean): RealityTileGeometry;
4122+
readGltfAndCreateGeometry(transformToRoot?: Transform, needNormals?: boolean, needParams?: boolean): Promise<RealityTileGeometry>;
41234123
// (undocumented)
41244124
protected readGltfAndCreateGraphics(isLeaf: boolean, featureTable: FeatureTable | undefined, contentRange: ElementAlignedBox3d | undefined, transformToRoot?: Transform, pseudoRtcBias?: Vector3d, instances?: InstancedGraphicParams): GltfReaderResult;
41254125
// (undocumented)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@itwin/core-frontend",
5+
"comment": "Support geometry collection for reality meshes with glTF tiles",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@itwin/core-frontend"
10+
}

core/frontend/src/internal/tile/RealityTileLoader.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,43 @@ export abstract class RealityTileLoader {
8080

8181
public async loadGeometryFromStream(tile: RealityTile, streamBuffer: ByteStream, system: RenderSystem): Promise<RealityTileContent> {
8282
const format = this._getFormat(streamBuffer);
83-
if (format !== TileFormat.B3dm)
83+
if (format !== TileFormat.B3dm && format !== TileFormat.Gltf) {
8484
return {};
85+
}
8586

8687
const { is3d, yAxisUp, iModel, modelId } = tile.realityRoot;
87-
const reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, undefined, this.getBatchIdMap());
88-
if (reader)
89-
reader.defaultWrapMode = GltfWrapMode.ClampToEdge;
88+
let reader: GltfReader | undefined;
9089

90+
// Create final transform from tree's iModelTransform and transformToRoot
9191
let transform = tile.tree.iModelTransform;
9292
if (tile.transformToRoot) {
9393
transform = transform.multiplyTransformTransform(tile.transformToRoot);
9494
}
9595

96-
const geom = reader?.readGltfAndCreateGeometry(transform);
96+
switch (format) {
97+
case TileFormat.Gltf:
98+
const props = createReaderPropsWithBaseUrl(streamBuffer, yAxisUp, tile.tree.baseUrl);
99+
100+
if (props) {
101+
reader = new GltfGraphicsReader(props, {
102+
iModel,
103+
gltf: props.glTF,
104+
contentRange: tile.contentRange,
105+
transform: tile.transformToRoot,
106+
hasChildren: !tile.isLeaf,
107+
pickableOptions: { id: modelId },
108+
idMap: this.getBatchIdMap()
109+
});
110+
}
111+
break;
112+
case TileFormat.B3dm:
113+
reader = B3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, tile.center, tile.transformToRoot, undefined, this.getBatchIdMap());
114+
if (reader)
115+
reader.defaultWrapMode = GltfWrapMode.ClampToEdge;
116+
break;
117+
}
118+
const geom = await reader?.readGltfAndCreateGeometry(transform);
119+
97120
// See RealityTileTree.reprojectAndResolveChildren for how reprojectionTransform is calculated
98121
const xForm = tile.reprojectionTransform;
99122
if (tile.tree.reprojectGeometry && geom?.polyfaces && xForm) {

core/frontend/src/test/tile/RealityTile.test.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
import { afterEach, beforeEach, describe, expect, it, MockInstance, vi } from "vitest";
77
import { ByteStream } from "@itwin/core-bentley";
8-
import { TileFormat } from "@itwin/core-common";
8+
import { GltfV2ChunkTypes, GltfVersions, TileFormat } from "@itwin/core-common";
99
import { Point3d, PolyfaceBuilder, Range3d, StrokeOptions, Transform } from "@itwin/core-geometry";
1010
import { IModelConnection } from "../../IModelConnection";
1111
import { IModelApp } from "../../IModelApp";
1212
import { MockRender } from "../../internal/render/MockRender";
1313
import { RenderMemory } from "../../render/RenderMemory";
1414
import {
15-
B3dmReader, RealityTile, RealityTileGeometry, RealityTileLoader, RealityTileTree,
15+
B3dmReader, GltfGraphicsReader, RealityTile, RealityTileGeometry, RealityTileLoader, RealityTileTree,
1616
Tile, TileDrawArgs, TileLoadPriority, TileRequest, TileRequestChannel
1717
} from "../../tile/internal";
1818
import { createBlankConnection } from "../createBlankConnection";
@@ -153,7 +153,7 @@ class TestRealityTree extends RealityTileTree {
153153
}
154154

155155
class TestB3dmReader extends B3dmReader {
156-
public override readGltfAndCreateGeometry(_transformToRoot?: Transform, _needNormals?: boolean, _needParams?: boolean): RealityTileGeometry {
156+
public override async readGltfAndCreateGeometry(_transformToRoot?: Transform, _needNormals?: boolean, _needParams?: boolean): Promise<RealityTileGeometry> {
157157
// Create mock geometry data with a simple polyface
158158
const options = StrokeOptions.createForFacets();
159159
const polyBuilder = PolyfaceBuilder.create(options);
@@ -168,6 +168,26 @@ class TestB3dmReader extends B3dmReader {
168168
}
169169
}
170170

171+
/** Creates a minimal valid GLB (binary glTF) for testing. */
172+
function createMinimalGlb(): Uint8Array {
173+
const json = JSON.stringify({ asset: { version: "2.0" }, meshes: [] });
174+
const jsonBytes = new TextEncoder().encode(json.padEnd(Math.ceil(json.length / 4) * 4));
175+
const numBytes = 12 + 8 + jsonBytes.length; // header + JSON chunk header + JSON data
176+
177+
const glb = new Uint8Array(numBytes);
178+
const view = new DataView(glb.buffer);
179+
180+
view.setUint32(0, TileFormat.Gltf, true);
181+
view.setUint32(4, GltfVersions.Version2, true);
182+
view.setUint32(8, numBytes, true);
183+
184+
view.setUint32(12, jsonBytes.length, true);
185+
view.setUint32(16, GltfV2ChunkTypes.JSON, true);
186+
glb.set(jsonBytes, 20);
187+
188+
return glb;
189+
}
190+
171191
function expectPointToEqual(point: Point3d, x: number, y: number, z: number) {
172192
expect(point.x).toEqual(x);
173193
expect(point.y).toEqual(y);
@@ -321,4 +341,34 @@ describe("RealityTileLoader", () => {
321341
const geometryTransform = createGeometrySpy.mock.calls[0][0];
322342
expect(geometryTransform).toEqual(expectedTransform);
323343
});
344+
345+
it("should load geometry from tiles in glTF format", async () => {
346+
const gltfStreamBuffer = ByteStream.fromUint8Array(createMinimalGlb());
347+
348+
const mockPolyface = PolyfaceBuilder.create(StrokeOptions.createForFacets()).claimPolyface();
349+
vi.spyOn(GltfGraphicsReader.prototype, "readGltfAndCreateGeometry")
350+
.mockResolvedValue({ polyfaces: [mockPolyface] });
351+
352+
const tree = new TestRealityTree(0, imodel, reader, false);
353+
const tile = tree.rootTile;
354+
355+
const result = await reader.loadGeometryFromStream(tile, gltfStreamBuffer, IModelApp.renderSystem);
356+
357+
expect(result.geometry).to.not.be.undefined;
358+
expect(result.geometry?.polyfaces).to.have.length(1);
359+
});
360+
361+
it("should return empty content for unsupported tile format", async () => {
362+
const buffer = new Uint8Array(16);
363+
const view = new DataView(buffer.buffer);
364+
view.setUint32(0, 0x12345678, true);
365+
const invalidStreamBuffer = ByteStream.fromUint8Array(buffer);
366+
367+
const tree = new TestRealityTree(0, imodel, reader, false);
368+
const tile = tree.rootTile;
369+
370+
const result = await reader.loadGeometryFromStream(tile, invalidStreamBuffer, IModelApp.renderSystem);
371+
372+
expect(result.geometry).to.be.undefined;
373+
});
324374
});

core/frontend/src/tile/GltfReader.ts

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { IModelApp } from "../IModelApp";
2222
import { InstancedGraphicParams } from "../common/render/InstancedGraphicParams";
2323
import { RealityMeshParams } from "../render/RealityMeshParams";
2424
import { Mesh } from "../common/internal/render/MeshPrimitives";
25-
import { Triangle } from "../common/internal/render/Primitives";
25+
import { Triangle, TriangleList } from "../common/internal/render/Primitives";
2626
import { RenderGraphic } from "../render/RenderGraphic";
2727
import { RenderSystem } from "../render/RenderSystem";
2828
import { BatchedTileIdMap, decodeMeshoptBuffer, RealityTileGeometry,TileContent } from "./internal";
@@ -31,7 +31,7 @@ import { CreateRenderMaterialArgs } from "../render/CreateRenderMaterialArgs";
3131
import { DisplayParams } from "../common/internal/render/DisplayParams";
3232
import { FrontendLoggerCategory } from "../common/FrontendLoggerCategory";
3333
import { getImageSourceFormatForMimeType, imageBitmapFromImageSource, imageElementFromImageSource, tryImageElementFromUrl } from "../common/ImageUtil";
34-
import { MeshPrimitiveType } from "../common/internal/render/MeshPrimitive";
34+
import { MeshPointList, MeshPrimitiveType } from "../common/internal/render/MeshPrimitive";
3535
import { PointCloudArgs } from "../common/internal/render/PointCloudPrimitive";
3636
import { TextureImageSource } from "../common/render/TextureParams";
3737
import {
@@ -258,7 +258,7 @@ export class GltfReaderProps {
258258

259259
/** The GltfMeshData contains the raw GLTF mesh data. If the data is suitable to create a [[RealityMesh]] directly, basically in the quantized format produced by
260260
* ContextCapture, then a RealityMesh is created directly from this data. Otherwise, the mesh primitive is populated from the raw data and a MeshPrimitive
261-
* is generated. The MeshPrimitve path is much less efficient but should be rarely used.
261+
* is generated. The MeshPrimitive path is much less efficient but should be rarely used.
262262
*
263263
* @internal
264264
*/
@@ -664,7 +664,8 @@ export abstract class GltfReader {
664664
};
665665
}
666666

667-
public readGltfAndCreateGeometry(transformToRoot?: Transform, needNormals = false, needParams = false): RealityTileGeometry {
667+
public async readGltfAndCreateGeometry(transformToRoot?: Transform, needNormals = false, needParams = false): Promise<RealityTileGeometry> {
668+
await this.resolveResources();
668669
const transformStack = new TransformStack(this.getTileTransform(transformToRoot));
669670
const polyfaces: IndexedPolyface[] = [];
670671
for (const nodeKey of this._sceneNodes) {
@@ -949,11 +950,31 @@ export abstract class GltfReader {
949950
}
950951

951952
private polyfaceFromGltfMesh(mesh: GltfMeshData, transform: Transform | undefined , needNormals: boolean, needParams: boolean): IndexedPolyface | undefined {
952-
if (!mesh.pointQParams || !mesh.points || !mesh.indices)
953-
return undefined;
953+
if (mesh.pointQParams && mesh.points && mesh.indices)
954+
return this.polyfaceFromQuantizedData(mesh.pointQParams, mesh.points, mesh.indices, mesh.normals, mesh.uvQParams, mesh.uvs, transform, needNormals, needParams);
954955

955-
const { points, pointQParams, normals, uvs, uvQParams, indices } = mesh;
956+
const meshPrim = mesh.primitive;
957+
const triangles = meshPrim.triangles;
958+
const points = meshPrim.points;
959+
if (!triangles || triangles.isEmpty || points.length === 0)
960+
return undefined;
956961

962+
// This will likely only be the case for Draco-compressed meshes-- see where readDracoMeshPrimitive is called within readMeshPrimitive
963+
// That is the only case where mesh.primitive is populated but mesh.pointQParams, mesh.points, & mesh.indices are not
964+
return this.polyfaceFromMeshPrimitive(triangles, points, meshPrim.normals, meshPrim.uvParams, transform, needNormals, needParams);
965+
}
966+
967+
private polyfaceFromQuantizedData(
968+
pointQParams: QParams3d,
969+
points: Uint16Array,
970+
indices: Uint8Array | Uint16Array | Uint32Array,
971+
normals: Uint16Array | undefined,
972+
uvQParams: QParams2d | undefined,
973+
uvs: Uint16Array | undefined,
974+
transform: Transform | undefined,
975+
needNormals: boolean,
976+
needParams: boolean
977+
): IndexedPolyface {
957978
const includeNormals = needNormals && undefined !== normals;
958979
const includeParams = needParams && undefined !== uvQParams && undefined !== uvs;
959980

@@ -990,6 +1011,62 @@ export abstract class GltfReader {
9901011
return polyface;
9911012
}
9921013

1014+
private polyfaceFromMeshPrimitive(
1015+
triangles: TriangleList,
1016+
points: MeshPointList,
1017+
normals: OctEncodedNormal[],
1018+
uvParams: Point2d[],
1019+
transform: Transform | undefined,
1020+
needNormals: boolean,
1021+
needParams: boolean
1022+
): IndexedPolyface {
1023+
const includeNormals = needNormals && normals.length > 0;
1024+
const includeParams = needParams && uvParams.length > 0;
1025+
1026+
const polyface = IndexedPolyface.create(includeNormals, includeParams);
1027+
1028+
if (points instanceof QPoint3dList) {
1029+
for (let i = 0; i < points.length; i++) {
1030+
const point = points.unquantize(i);
1031+
if (transform)
1032+
transform.multiplyPoint3d(point, point);
1033+
polyface.addPoint(point);
1034+
}
1035+
} else {
1036+
const center = points.range.center;
1037+
for (const pt of points) {
1038+
const point = pt.plus(center);
1039+
if (transform)
1040+
transform.multiplyPoint3d(point, point);
1041+
polyface.addPoint(point);
1042+
}
1043+
}
1044+
1045+
if (includeNormals)
1046+
for (const normal of normals)
1047+
polyface.addNormal(OctEncodedNormal.decodeValue(normal.value));
1048+
1049+
if (includeParams)
1050+
for (const uv of uvParams)
1051+
polyface.addParam(uv);
1052+
1053+
const indices = triangles.indices;
1054+
let j = 0;
1055+
for (const index of indices) {
1056+
polyface.addPointIndex(index);
1057+
if (includeNormals)
1058+
polyface.addNormalIndex(index);
1059+
1060+
if (includeParams)
1061+
polyface.addParamIndex(index);
1062+
1063+
if (0 === (++j % 3))
1064+
polyface.terminateFacet();
1065+
}
1066+
1067+
return polyface;
1068+
}
1069+
9931070
// ###TODO what is the actual type of `json`?
9941071
public getBufferView(json: { [k: string]: any }, accessorName: string): GltfBufferView | undefined {
9951072
try {

0 commit comments

Comments
 (0)