diff --git a/README.md b/README.md index 1b6da81b..cd89bef0 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,13 @@ The following options will cause the validator to _exclude_ tileset files (i.e. "excludeContentTypes": [ "CONTENT_TYPE_TILESET" ] } ``` +The following options will cause the validator to validate that each bounding volume contains all points and vertices of the content of the respective tile and all its descendants: +```JSON +{ + "validateBoundingVolumeContainment": true +} +``` +Note that this form of validation may be computationally expensive, and is therefore disabled by default. The options can also be part of a configuration file, as described in the next section. diff --git a/package.json b/package.json index 3942b993..41881ffe 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@gltf-transform/core": "^3.2.1", "@gltf-transform/extensions": "^3.2.1", "@gltf-transform/functions": "^3.2.1", - "3d-tiles-tools": "0.5.0", + "3d-tiles-tools": "0.5.3", "cesium": "^1.97.0", "gltf-validator": "^2.0.0-dev.3.9", "minimatch": "^5.1.0", diff --git a/specs/BoundingVolumeContainmentValidationSpec.ts b/specs/BoundingVolumeContainmentValidationSpec.ts new file mode 100644 index 00000000..d61b6c7e --- /dev/null +++ b/specs/BoundingVolumeContainmentValidationSpec.ts @@ -0,0 +1,312 @@ +import { ValidationOptions } from "../src/validation/ValidationOptions"; +import { Validators } from "../src/validation/Validators"; + +describe("Bounding volume containment validation", function () { + //========================================================================== + // Valid basic: + + it("detects no issues in basic/validWithB3dm", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithB3dm.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithB3dmRtc", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithB3dmRtc.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithCmpt", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithCmpt.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithExternal", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithExternal.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbBasic", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbBasic.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbContentBox", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbContentBox.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbNested", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbNested.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbRegion", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbRegion.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbRotation", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbRotation.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbScale", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbScale.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbSphereTRS", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbSphereTRS.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithGlbTranslation", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithGlbTranslation.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithI3dm", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithI3dm.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithPnts", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithPnts.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + it("detects no issues in basic/validWithPntsRtc", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/validWithPntsRtc.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + //========================================================================== + // Invalid basic: + + it("detects issues in basic/withExternalInvalidBox", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/withExternalInvalidBox.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("EXTERNAL_TILESET_VALIDATION_ERROR"); + }); + + it("detects issues in basic/withGlbInvalidContentBox", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/withGlbInvalidContentBox.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + }); + + it("detects issues in basic/withGlbInvalidContentBox", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/withGlbInvalidContentBox.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + }); + + it("detects issues in basic/withGlbInvalidRegion", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/withGlbInvalidRegion.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + }); + + it("detects issues in basic/withGlbNestedInvalidBoxInner", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxInner.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + }); + + it("detects issues in basic/withGlbNestedInvalidBoxRoot", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxRoot.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + }); + + it("detects issues in basic/withGlbTRSInvalidSphere", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/basic/withGlbTRSInvalidSphere.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + }); + + //========================================================================== + // Valid ImplicitOctree: + + // Omitted - see https://github.com/CesiumGS/cesium/issues/13195#issuecomment-3915058716 + xit("detects no issues in ImplicitOctree/tilesetWithMetadataBoundingVolume", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/ImplicitOctree/tilesetWithMetadataBoundingVolume.json", + validationOptions + ); + expect(result.length).toEqual(0); + }); + + //========================================================================== + // Invalid ImplicitOctree: + + it("detects issues in ImplicitOctree/tilesetWithInvalidBoundingVolume", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidBoundingVolume.json", + validationOptions + ); + + // There are 14 errors, because the whole hierarchy is invalid when + // the root bounding volume is invalid: + // '/root' for content 'content/content_0__0_0_0.glb' + // '/root' for content 'content/content_1__0_0_1.glb' + // '/root' for content 'content/content_2__0_3_3.glb' + // '/root' for content 'content/content_3__7_7_7.glb' + // '/root/at[0][0,0,0]' for content 'content/content_0__0_0_0.glb' + // '/root/at[0][0,0,0]' for content 'content/content_3__7_7_7.glb' + // '/root/at[0][0,0,0]' for content 'content/content_2__0_3_3.glb' + // '/root/at[0][0,0,0]' for content 'content/content_1__0_0_1.glb' + // '/root/at[1][0,0,1]' for content 'content/content_1__0_0_1.glb' + // '/root/at[1][0,1,1]' for content 'content/content_2__0_3_3.glb' + // '/root/at[1][1,1,1]' for content 'content/content_3__7_7_7.glb' + // '/root/at[2][0,3,3]' for content 'content/content_2__0_3_3.glb' + // '/root/at[2][3,3,3]' for content 'content/content_3__7_7_7.glb' + // '/root/at[3][7,7,7]' for content 'content/content_3__7_7_7.glb' + expect(result.length).toEqual(14); + for (let i = 0; i < 14; i++) { + expect(result.get(i).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + } + }); + + // Omitted - see https://github.com/CesiumGS/cesium/issues/13195#issuecomment-3915058716 + xit("detects issues in ImplicitOctree/tilesetWithInvalidMetadataBoundingVolume", async function () { + const validationOptions = new ValidationOptions(); + validationOptions.validateBoundingVolumeContainment = true; + const result = await Validators.validateTilesetFile( + "specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidMetadataBoundingVolume.json", + validationOptions + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual( + "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME" + ); + }); +}); diff --git a/specs/TilesetValidationSpec.ts b/specs/TilesetValidationSpec.ts index 2dcc5f4d..ad2b7b57 100644 --- a/specs/TilesetValidationSpec.ts +++ b/specs/TilesetValidationSpec.ts @@ -990,6 +990,13 @@ describe("Tileset validation", function () { expect(result.length).toEqual(0); }); + it("detects no issues in validTilesetWithValidGlb", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/validTilesetWithValidGlb.json" + ); + expect(result.length).toEqual(0); + }); + it("detects no issues in validTilesetWithValidSchemaFromUri", async function () { const result = await Validators.validateTilesetFile( "specs/data/tilesets/validTilesetWithValidSchemaFromUri.json" diff --git a/specs/data/boundingVolumes/ImplicitOctree/README.md b/specs/data/boundingVolumes/ImplicitOctree/README.md new file mode 100644 index 00000000..97335596 --- /dev/null +++ b/specs/data/boundingVolumes/ImplicitOctree/README.md @@ -0,0 +1,8 @@ + +Test data for the bounding volume containment validation. + +This is a simple implicit octree that contains GLB files in +the "lower half" (z=0 to z=0.5) of the octree, which +requires the metadata `TILE_BOUNDING_BOX` to be defined as +`[ 0.5, -0.5, 0.25, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.25 ]` +to be valid. \ No newline at end of file diff --git a/specs/data/boundingVolumes/ImplicitOctree/content/content_0__0_0_0.glb b/specs/data/boundingVolumes/ImplicitOctree/content/content_0__0_0_0.glb new file mode 100644 index 00000000..95503dd8 Binary files /dev/null and b/specs/data/boundingVolumes/ImplicitOctree/content/content_0__0_0_0.glb differ diff --git a/specs/data/boundingVolumes/ImplicitOctree/content/content_1__0_0_1.glb b/specs/data/boundingVolumes/ImplicitOctree/content/content_1__0_0_1.glb new file mode 100644 index 00000000..7b6afb9e Binary files /dev/null and b/specs/data/boundingVolumes/ImplicitOctree/content/content_1__0_0_1.glb differ diff --git a/specs/data/boundingVolumes/ImplicitOctree/content/content_2__0_3_3.glb b/specs/data/boundingVolumes/ImplicitOctree/content/content_2__0_3_3.glb new file mode 100644 index 00000000..b424cb87 Binary files /dev/null and b/specs/data/boundingVolumes/ImplicitOctree/content/content_2__0_3_3.glb differ diff --git a/specs/data/boundingVolumes/ImplicitOctree/content/content_3__7_7_7.glb b/specs/data/boundingVolumes/ImplicitOctree/content/content_3__7_7_7.glb new file mode 100644 index 00000000..14478b63 Binary files /dev/null and b/specs/data/boundingVolumes/ImplicitOctree/content/content_3__7_7_7.glb differ diff --git a/specs/data/boundingVolumes/ImplicitOctree/subtrees/0.0.0.0.subtree b/specs/data/boundingVolumes/ImplicitOctree/subtrees/0.0.0.0.subtree new file mode 100644 index 00000000..5276443f Binary files /dev/null and b/specs/data/boundingVolumes/ImplicitOctree/subtrees/0.0.0.0.subtree differ diff --git a/specs/data/boundingVolumes/ImplicitOctree/subtrees/2.0.3.3.subtree b/specs/data/boundingVolumes/ImplicitOctree/subtrees/2.0.3.3.subtree new file mode 100644 index 00000000..3b76d360 Binary files /dev/null and b/specs/data/boundingVolumes/ImplicitOctree/subtrees/2.0.3.3.subtree differ diff --git a/specs/data/boundingVolumes/ImplicitOctree/subtrees/2.3.3.3.subtree b/specs/data/boundingVolumes/ImplicitOctree/subtrees/2.3.3.3.subtree new file mode 100644 index 00000000..6c230a7e Binary files /dev/null and b/specs/data/boundingVolumes/ImplicitOctree/subtrees/2.3.3.3.subtree differ diff --git a/specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidBoundingVolume.json b/specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidBoundingVolume.json new file mode 100644 index 00000000..6869e05d --- /dev/null +++ b/specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidBoundingVolume.json @@ -0,0 +1,27 @@ +{ + "asset" : { + "extras" : { + "generator" : "J3DTiles-0.0.1-SNAPSHOT" + }, + "version" : "1.1" + }, + "geometricError" : 1048576.0, + "root" : { + "boundingVolume" : { + "box" : [ 0.6, -0.6, 0.6, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1024.0, + "refine" : "REPLACE", + "content" : { + "uri" : "content/content_{level}__{x}_{y}_{z}.glb" + }, + "implicitTiling" : { + "subdivisionScheme" : "OCTREE", + "subtreeLevels" : 2, + "availableLevels" : 4, + "subtrees" : { + "uri" : "subtrees/{level}.{x}.{y}.{z}.subtree" + } + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidMetadataBoundingVolume.json b/specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidMetadataBoundingVolume.json new file mode 100644 index 00000000..e7a79b6f --- /dev/null +++ b/specs/data/boundingVolumes/ImplicitOctree/tilesetWithInvalidMetadataBoundingVolume.json @@ -0,0 +1,49 @@ +{ + "asset" : { + "extras" : { + "generator" : "J3DTiles-0.0.1-SNAPSHOT" + }, + "version" : "1.1" + }, + "schema": { + "id": "validator_test_data_schema", + "classes": { + "tile": { + "properties": { + "tightBoundingBox": { + "type": "SCALAR", + "componentType": "FLOAT64", + "array": true, + "count": 12, + "semantic": "TILE_BOUNDING_BOX" + } + } + } + } + }, + "geometricError" : 1048576.0, + "root" : { + "boundingVolume" : { + "box" : [ 0.5, -0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "metadata": { + "class": "tile", + "properties": { + "tightBoundingBox": [ 0.6, -0.6, 0.3, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.25 ] + } + }, + "geometricError" : 1024.0, + "refine" : "REPLACE", + "content" : { + "uri" : "content/content_{level}__{x}_{y}_{z}.glb" + }, + "implicitTiling" : { + "subdivisionScheme" : "OCTREE", + "subtreeLevels" : 2, + "availableLevels" : 4, + "subtrees" : { + "uri" : "subtrees/{level}.{x}.{y}.{z}.subtree" + } + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/ImplicitOctree/tilesetWithMetadataBoundingVolume.json b/specs/data/boundingVolumes/ImplicitOctree/tilesetWithMetadataBoundingVolume.json new file mode 100644 index 00000000..b1c2ac46 --- /dev/null +++ b/specs/data/boundingVolumes/ImplicitOctree/tilesetWithMetadataBoundingVolume.json @@ -0,0 +1,49 @@ +{ + "asset" : { + "extras" : { + "generator" : "J3DTiles-0.0.1-SNAPSHOT" + }, + "version" : "1.1" + }, + "schema": { + "id": "validator_test_data_schema", + "classes": { + "tile": { + "properties": { + "tightBoundingBox": { + "type": "SCALAR", + "componentType": "FLOAT64", + "array": true, + "count": 12, + "semantic": "TILE_BOUNDING_BOX" + } + } + } + } + }, + "geometricError" : 1048576.0, + "root" : { + "boundingVolume" : { + "box" : [ 0.5, -0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "metadata": { + "class": "tile", + "properties": { + "tightBoundingBox": [ 0.5, -0.5, 0.25, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.25 ] + } + }, + "geometricError" : 1024.0, + "refine" : "REPLACE", + "content" : { + "uri" : "content/content_{level}__{x}_{y}_{z}.glb" + }, + "implicitTiling" : { + "subdivisionScheme" : "OCTREE", + "subtreeLevels" : 2, + "availableLevels" : 4, + "subtrees" : { + "uri" : "subtrees/{level}.{x}.{y}.{z}.subtree" + } + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/basic/README.md b/specs/data/boundingVolumes/basic/README.md new file mode 100644 index 00000000..b08fafd6 --- /dev/null +++ b/specs/data/boundingVolumes/basic/README.md @@ -0,0 +1,21 @@ + +Test data for the bounding volume containment validation. + +The content files: + +- `unitCube.b3dm` - A unit cube in a B3DM. Hence the name. +- `unitCube.glb` - A unit cube. In a GLB this time. +- `unitCube.pnts` - A unit cube of 8x8x8 points +- `unitCube-RTC_1_2_3.b3dm` - A unit cube with an `RTC_CENTER` of (1,2,3) +- `unitCube-RTC_1_2_3.pnts` - A unit cube of 8x8x8 points with an `RTC_CENTER` of (1,2,3) +- `unitCubes.i3dm` - Unit cubes with instance positions (0,0,0), (2,0,0), (0,2,0), and (2,2,0) + +The tileset files refer to these contents, as indicated by their name. + +For B3DM, PNTS, and I3DM content, only simple positive test cases (without +tile transforms, and using bounding boxes) are defined. + +Other bounding volume types, non-trivial tile transforms, and negative +test cases are covered with the GLB-based tests. + + diff --git a/specs/data/boundingVolumes/basic/unitCube-RTC_1_2_3.b3dm b/specs/data/boundingVolumes/basic/unitCube-RTC_1_2_3.b3dm new file mode 100644 index 00000000..0f0689d6 Binary files /dev/null and b/specs/data/boundingVolumes/basic/unitCube-RTC_1_2_3.b3dm differ diff --git a/specs/data/boundingVolumes/basic/unitCube-RTC_1_2_3.pnts b/specs/data/boundingVolumes/basic/unitCube-RTC_1_2_3.pnts new file mode 100644 index 00000000..b61d148d Binary files /dev/null and b/specs/data/boundingVolumes/basic/unitCube-RTC_1_2_3.pnts differ diff --git a/specs/data/boundingVolumes/basic/unitCube.b3dm b/specs/data/boundingVolumes/basic/unitCube.b3dm new file mode 100644 index 00000000..73c4d113 Binary files /dev/null and b/specs/data/boundingVolumes/basic/unitCube.b3dm differ diff --git a/specs/data/boundingVolumes/basic/unitCube.cmpt b/specs/data/boundingVolumes/basic/unitCube.cmpt new file mode 100644 index 00000000..8ff0d760 Binary files /dev/null and b/specs/data/boundingVolumes/basic/unitCube.cmpt differ diff --git a/specs/data/boundingVolumes/basic/unitCube.glb b/specs/data/boundingVolumes/basic/unitCube.glb new file mode 100644 index 00000000..e27b197c Binary files /dev/null and b/specs/data/boundingVolumes/basic/unitCube.glb differ diff --git a/specs/data/boundingVolumes/basic/unitCube.pnts b/specs/data/boundingVolumes/basic/unitCube.pnts new file mode 100644 index 00000000..487736c4 Binary files /dev/null and b/specs/data/boundingVolumes/basic/unitCube.pnts differ diff --git a/specs/data/boundingVolumes/basic/unitCubes.i3dm b/specs/data/boundingVolumes/basic/unitCubes.i3dm new file mode 100644 index 00000000..4eadfe36 Binary files /dev/null and b/specs/data/boundingVolumes/basic/unitCubes.i3dm differ diff --git a/specs/data/boundingVolumes/basic/validWithB3dm.json b/specs/data/boundingVolumes/basic/validWithB3dm.json new file mode 100644 index 00000000..93905ebf --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithB3dm.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, -0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1.0, + "content": { + "uri": "unitCube.b3dm" + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/basic/validWithB3dmRtc.json b/specs/data/boundingVolumes/basic/validWithB3dmRtc.json new file mode 100644 index 00000000..948ca916 --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithB3dmRtc.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 1.5, 1.5, 3.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1.0, + "content": { + "uri": "unitCube-RTC_1_2_3.b3dm" + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/basic/validWithCmpt.json b/specs/data/boundingVolumes/basic/validWithCmpt.json new file mode 100644 index 00000000..1688f241 --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithCmpt.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, -0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1.0, + "content": { + "uri": "unitCube.cmpt" + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/basic/validWithExternal.json b/specs/data/boundingVolumes/basic/validWithExternal.json new file mode 100644 index 00000000..6b4d3bb4 --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithExternal.json @@ -0,0 +1,51 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A root tile with a translation of (100, 0, 0) and a child tile with a rotation of 45 degrees around the Z-axis, referring to an external tileset" + } + }, + "geometricError": 50.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 100, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.70710678, 0, 0.5, + 0.70710678, 0, 0, + 0, 0.70710678, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 10, + "children": [ + { + "transform": [ + 0.70710678, 0.70710678, 0, 0, + -0.70710678, 0.70710678, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 5, + "content": { + "uri": "validWithExternal_external.json" + } + } + ] + } +} diff --git a/specs/data/boundingVolumes/basic/validWithExternal_external.json b/specs/data/boundingVolumes/basic/validWithExternal_external.json new file mode 100644 index 00000000..c0eeb2de --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithExternal_external.json @@ -0,0 +1,26 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A single unit cube at the origin with no explicit transform, used as an external tileset" + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbBasic.json b/specs/data/boundingVolumes/basic/validWithGlbBasic.json new file mode 100644 index 00000000..0158e29d --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbBasic.json @@ -0,0 +1,26 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A single unit cube at the origin with no explicit transform." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbContentBox.json b/specs/data/boundingVolumes/basic/validWithGlbContentBox.json new file mode 100644 index 00000000..96fa14af --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbContentBox.json @@ -0,0 +1,34 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A single unit cube at the origin with no explicit transform." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb", + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + } + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbNested.json b/specs/data/boundingVolumes/basic/validWithGlbNested.json new file mode 100644 index 00000000..6e44f9b9 --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbNested.json @@ -0,0 +1,51 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A root tile with a translation of (100, 0, 0) and a child tile with a rotation of 45 degrees around the Z-axis." + } + }, + "geometricError": 50.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 100, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.70710678, 0, 0.5, + 0.70710678, 0, 0, + 0, 0.70710678, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 10, + "children": [ + { + "transform": [ + 0.70710678, 0.70710678, 0, 0, + -0.70710678, 0.70710678, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } + ] + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbRegion.json b/specs/data/boundingVolumes/basic/validWithGlbRegion.json new file mode 100644 index 00000000..7ce56083 --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbRegion.json @@ -0,0 +1,26 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A unit cube placed in an East-North-Up reference frame. The boundingRegion is defined in WGS84 and encloses the transformed cube." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "transform": [0.9666108723721498, 0.2562487490934376, 0, 0, -0.1645317863982543, 0.6206399607647399, 0.766638983072568, 0, 0.19645028041861062, -0.7410415762222726, 0.6420783983544834, 0, 1254723.361032366, -4733015.270841262, 4073486.511929942, 1], + "boundingVolume": { + "region": [ + -1.3116569604021266, 0.6972060827730874, + -1.3116567561748709, 0.697206239961853, + 20, 21 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbRotation.json b/specs/data/boundingVolumes/basic/validWithGlbRotation.json new file mode 100644 index 00000000..28098fdf --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbRotation.json @@ -0,0 +1,32 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A unit cube rotated by 45 degrees around the Z-axis." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "transform": [ + 0.70710678, 0.70710678, 0, 0, + -0.70710678, 0.70710678, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbScale.json b/specs/data/boundingVolumes/basic/validWithGlbScale.json new file mode 100644 index 00000000..c1cbf47c --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbScale.json @@ -0,0 +1,32 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A unit cube scaled by (2, 3, 4)." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "transform": [ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + 0, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbSphereTRS.json b/specs/data/boundingVolumes/basic/validWithGlbSphereTRS.json new file mode 100644 index 00000000..70ef7aad --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbSphereTRS.json @@ -0,0 +1,30 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A unit cube that is scaled, rotated, and then translated. Enclosed in a bounding sphere." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1.41421356, -2.12132034, 0, 0, + 1.41421356, 2.12132034, 0, 0, + 0, 0, 4, 0, + 10, 20, 30, 1 + ], + "boundingVolume": { + "sphere": [ + 0.5, -0.5, 0.5, + 0.8660254 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithGlbTranslation.json b/specs/data/boundingVolumes/basic/validWithGlbTranslation.json new file mode 100644 index 00000000..20e7be87 --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithGlbTranslation.json @@ -0,0 +1,32 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A unit cube translated by (10, 20, 30)." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 10, 20, 30, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/validWithI3dm.json b/specs/data/boundingVolumes/basic/validWithI3dm.json new file mode 100644 index 00000000..b79dc975 --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithI3dm.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 1.5, 0.5, 0.5, 1.5, 0.0, 0.0, 0.0, 1.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1.0, + "content": { + "uri": "unitCubes.i3dm" + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/basic/validWithPnts.json b/specs/data/boundingVolumes/basic/validWithPnts.json new file mode 100644 index 00000000..c1ce4f4a --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithPnts.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1.0, + "content": { + "uri": "unitCube.pnts" + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/basic/validWithPntsRtc.json b/specs/data/boundingVolumes/basic/validWithPntsRtc.json new file mode 100644 index 00000000..57e2242e --- /dev/null +++ b/specs/data/boundingVolumes/basic/validWithPntsRtc.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 1.5, 2.5, 3.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1.0, + "content": { + "uri": "unitCube-RTC_1_2_3.pnts" + } + } +} \ No newline at end of file diff --git a/specs/data/boundingVolumes/basic/withExternalInvalidBox.json b/specs/data/boundingVolumes/basic/withExternalInvalidBox.json new file mode 100644 index 00000000..6fb52fb5 --- /dev/null +++ b/specs/data/boundingVolumes/basic/withExternalInvalidBox.json @@ -0,0 +1,51 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A root tile with a translation of (100, 0, 0) and a child tile with a rotation of 45 degrees around the Z-axis, referring to an external tileset with an invalid box" + } + }, + "geometricError": 50.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 100, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.70710678, 0, 0.6, + 0.70710678, 0, 0, + 0, 0.70710678, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 10, + "children": [ + { + "transform": [ + 0.70710678, 0.70710678, 0, 0, + -0.70710678, 0.70710678, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 5, + "content": { + "uri": "withExternalInvalidBox_external.json" + } + } + ] + } +} diff --git a/specs/data/boundingVolumes/basic/withExternalInvalidBox_external.json b/specs/data/boundingVolumes/basic/withExternalInvalidBox_external.json new file mode 100644 index 00000000..87240eb7 --- /dev/null +++ b/specs/data/boundingVolumes/basic/withExternalInvalidBox_external.json @@ -0,0 +1,26 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A single unit cube at the origin, with an invalid bounding box, used as an external tileset" + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.6, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/withGlbInvalidContentBox.json b/specs/data/boundingVolumes/basic/withGlbInvalidContentBox.json new file mode 100644 index 00000000..c7f7b171 --- /dev/null +++ b/specs/data/boundingVolumes/basic/withGlbInvalidContentBox.json @@ -0,0 +1,34 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A single unit cube at the origin with no explicit transform and an invalid content bounding volume" + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 1.0 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb", + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.6, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + } + } + } +} diff --git a/specs/data/boundingVolumes/basic/withGlbInvalidRegion.json b/specs/data/boundingVolumes/basic/withGlbInvalidRegion.json new file mode 100644 index 00000000..3d47b7e8 --- /dev/null +++ b/specs/data/boundingVolumes/basic/withGlbInvalidRegion.json @@ -0,0 +1,26 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A unit cube placed in an East-North-Up reference frame. The boundingRegion is defined in WGS84 and does not fully enclose the transformed cube." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "transform": [0.9666108723721498, 0.2562487490934376, 0, 0, -0.1645317863982543, 0.6206399607647399, 0.766638983072568, 0, 0.19645028041861062, -0.7410415762222726, 0.6420783983544834, 0, 1254723.361032366, -4733015.270841262, 4073486.511929942, 1], + "boundingVolume": { + "region": [ + -1.3116569604021266, 0.6972060827730874, + -1.3116567561748709, 0.697206239961853, + 20, 20.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxInner.json b/specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxInner.json new file mode 100644 index 00000000..c69c909f --- /dev/null +++ b/specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxInner.json @@ -0,0 +1,51 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A root tile with a translation of (100, 0, 0) and a child tile with a rotation of 45 degrees around the Z-axis. The inner tile bounding volume does not fully contain the content." + } + }, + "geometricError": 50.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 100, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.70710678, 0, 0.5, + 0.70710678, 0, 0, + 0, 0.70710678, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 10, + "children": [ + { + "transform": [ + 0.70710678, 0.70710678, 0, 0, + -0.70710678, 0.70710678, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.6, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } + ] + } +} diff --git a/specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxRoot.json b/specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxRoot.json new file mode 100644 index 00000000..56e53a8d --- /dev/null +++ b/specs/data/boundingVolumes/basic/withGlbNestedInvalidBoxRoot.json @@ -0,0 +1,51 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A root tile with a translation of (100, 0, 0) and a child tile with a rotation of 45 degrees around the Z-axis. The root bounding volume does not fully contain the content data." + } + }, + "geometricError": 50.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 100, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.70710678, 0, 0.6, + 0.70710678, 0, 0, + 0, 0.70710678, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 10, + "children": [ + { + "transform": [ + 0.70710678, 0.70710678, 0, 0, + -0.70710678, 0.70710678, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ], + "boundingVolume": { + "box": [ + 0.5, -0.5, 0.5, + 0.5, 0, 0, + 0, 0.5, 0, + 0, 0, 0.5 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } + ] + } +} diff --git a/specs/data/boundingVolumes/basic/withGlbTRSInvalidSphere.json b/specs/data/boundingVolumes/basic/withGlbTRSInvalidSphere.json new file mode 100644 index 00000000..b1ad24c4 --- /dev/null +++ b/specs/data/boundingVolumes/basic/withGlbTRSInvalidSphere.json @@ -0,0 +1,30 @@ +{ + "asset": { + "version": "1.1" + }, + "extras": { + "info": { + "description": "A unit cube that is scaled, rotated, and then translated. The bounding sphere does not fully enclose the vertices." + } + }, + "geometricError": 10.0, + "root": { + "refine": "REPLACE", + "transform": [ + 1.41421356, -2.12132034, 0, 0, + 1.41421356, 2.12132034, 0, 0, + 0, 0, 4, 0, + 10, 20, 30, 1 + ], + "boundingVolume": { + "sphere": [ + 0.5, -0.5, 1.0, + 0.8660254 + ] + }, + "geometricError": 0.0, + "content": { + "uri": "unitCube.glb" + } + } +} diff --git a/specs/data/tilesets/tiles/glTF/TriangleGlb/Triangle.glb b/specs/data/tilesets/tiles/glTF/TriangleGlb/Triangle.glb new file mode 100644 index 00000000..07e6195e Binary files /dev/null and b/specs/data/tilesets/tiles/glTF/TriangleGlb/Triangle.glb differ diff --git a/specs/data/tilesets/validTilesetWithValidGlb.json b/specs/data/tilesets/validTilesetWithValidGlb.json new file mode 100644 index 00000000..18977b11 --- /dev/null +++ b/specs/data/tilesets/validTilesetWithValidGlb.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "boundingVolume" : { + "box" : [ 0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5 ] + }, + "geometricError" : 1.0, + "content": { + "uri": "tiles/glTF/TriangleGlb/Triangle.glb" + } + } +} \ No newline at end of file diff --git a/src/issues/ContentDataValidationIssues.ts b/src/issues/ContentDataValidationIssues.ts new file mode 100644 index 00000000..444b1018 --- /dev/null +++ b/src/issues/ContentDataValidationIssues.ts @@ -0,0 +1,26 @@ +import { ValidationIssue } from "../validation/ValidationIssue"; +import { ValidationIssueSeverity } from "../validation/ValidationIssueSeverity"; + +/** + * Methods to create `ValidationIssue` instances that describe + * issues related to content data. + */ +export class ContentDataValidationIssues { + /** + * Indicates that tile content was not fully enclosed by a + * bounding volume. + * + * @param path - The path for the `ValidationIssue` + * @param message - The message for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME( + path: string, + message: string + ) { + const type = "CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME"; + const severity = ValidationIssueSeverity.ERROR; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } +} diff --git a/src/validation/ContentDataBoundingVolumeValidator.ts b/src/validation/ContentDataBoundingVolumeValidator.ts new file mode 100644 index 00000000..1e38cd67 --- /dev/null +++ b/src/validation/ContentDataBoundingVolumeValidator.ts @@ -0,0 +1,594 @@ +import { BoundingVolume } from "3d-tiles-tools"; +import { BoundingVolumesContainment } from "3d-tiles-tools"; +import { ResourceResolver } from "3d-tiles-tools"; +import { Tiles } from "3d-tiles-tools"; +import { Tileset } from "3d-tiles-tools"; +import { TilesetTraverser } from "3d-tiles-tools"; +import { TraversedTile } from "3d-tiles-tools"; +import { VertexProcessing } from "3d-tiles-tools"; + +import { BoundingSphere } from "cesium"; +import { Matrix3 } from "cesium"; +import { Matrix4 } from "cesium"; +import { OrientedBoundingBox } from "cesium"; + +import { ValidationContext } from "./ValidationContext"; +import { ValidationState } from "./ValidationState"; + +import { IoValidationIssues } from "../issues/IoValidationIssue"; +import { ValidationIssues } from "../issues/ValidationIssues"; +import { ContentDataValidationIssues } from "../issues/ContentDataValidationIssues"; + +/** + * A class for validating that content data is fully enclosed in the + * tile bounding volumes. + * + * @internal + */ +export class ContentDataBoundingVolumeValidator { + /** + * An epsilon for containment checks + */ + private static readonly CONTAINMENT_EPSILON = 1e-5; + + /** + * Validates the the bounding volume containment for the given tileset. + * + * This will traverse the tileset, and check, for each content, whether + * all vertices of that content are fully contained in the bounding + * volume of the containing tile and the bounding volumes of all + * ancestors of that tile. + * + * @param tilesetPath - The path for validation issues + * @param tileset - The `Tileset` + * @param validationState - The `ValidationState` + * @param context - The `TraversalContext` + * @returns A promise that resolves when the validation is finished + */ + static async validateBoundingVolumeContainment( + tilesetPath: string, + tileset: Tileset, + validationState: ValidationState, + context: ValidationContext + ): Promise { + let allTraversedTilesValid = true; + + // Traverse the tileset, and validate each traversed tile + // (i.e. its content against the bounding volumes of the + // tile and all its ancestors) + + // Note: This will re-compute some elements repeatedly, + // for example, the global transforms and the transformed + // bounding volumes of the tiles: For each traversed tile, + // it will go UP to the root, and gather the transformed + // bounding volumes for each ancestor. This could be + // optimized by doing a manual traversal here, and + // maintaining the "stack" of bounding volumes alongside + // the stack of traversed tiles. Given that the majority + // of the time is spent in the geometry/vertex processing, + // the relative performance gains should be low. + const resourceResolver = context.getResourceResolver(); + const tilesetTraverser = new TilesetTraverser(".", resourceResolver, { + depthFirst: true, + traverseExternalTilesets: true, + }); + try { + const schema = validationState.schemaState.validatedElement; + await tilesetTraverser.traverseWithSchema( + tileset, + schema, + async (traversedTile: TraversedTile) => { + if (traversedTile.isImplicitTilesetRoot()) { + return true; + } + const valid = + await ContentDataBoundingVolumeValidator.validateTraversedTile( + traversedTile, + resourceResolver, + context + ); + allTraversedTilesValid = allTraversedTilesValid && valid; + return true; + } + ); + } catch (error) { + const message = `Internal error while traversing tileset: ${error}`; + const issue = ValidationIssues.INTERNAL_ERROR(tilesetPath, message); + context.addIssue(issue); + return false; + } + return allTraversedTilesValid; + } + + /** + * Validate the given traversed tile. + * + * This will check whether the vertices of the contents of the given + * tile are all contained in the bounding volume of the tile and + * the bounding volumes of all its ancestors. + * + * @param traversedTile - The traversed tile + * @param resourceResolver - The resource resolver for resolving the + * tile content from the content URI + * @param context - The context for validation issues + * @returns Whether the tile was valid + */ + private static async validateTraversedTile( + traversedTile: TraversedTile, + resourceResolver: ResourceResolver, + context: ValidationContext + ): Promise { + //console.log("Have to validate " + traversedTile); + + // Compute the mapping from 'traversedTile.path' strings + // to the bounding volume of the respective tile, each + // transformed using the global transform of the tile + const transformedTileBoundingVolumes = + ContentDataBoundingVolumeValidator.computeTransformedTileBoundingVolumes( + traversedTile + ); + const globalTileTransform = + ContentDataBoundingVolumeValidator.computeGlobalTransform(traversedTile); + + // Validate all contents against all bounding volumes + const result = + await ContentDataBoundingVolumeValidator.validateAllContentsAllBoundingVolumes( + traversedTile, + transformedTileBoundingVolumes, + globalTileTransform, + resourceResolver, + context + ); + return result; + } + + /** + * Validate the contents of the given traversed tile against the given + * bounding volumes. + * + * This will read each content, transform its vertices with the given + * transform, and check that the resulting vertex is contained in + * each of the given bounding volumes (and in the content bounding + * volume of that content, if it is defined). + * + * @param contentUris - The content URIs + * @param transformedTileBoundingVolumes - The transformed bounding volumes + * @param globalTileTransform - The global transform of the containing + * tile, used to transform the vertices of the content + * @param resourceResolver - The resolver for resolving the content data + * based on the URIs + * @param context - The context for validation issues + * @returns Whether the contents have been valid + */ + private static async validateAllContentsAllBoundingVolumes( + traversedTile: TraversedTile, + transformedTileBoundingVolumes: Map, + globalTileTransform: number[], + resourceResolver: ResourceResolver, + context: ValidationContext + ): Promise { + let allValid = true; + + const finalTile = traversedTile.asFinalTile(); + const contents = Tiles.getContents(finalTile); + for (const content of contents) { + // Obtain the optional content bounding volume, and transform + // it with the global tile transform + let transformedContentBoundingVolume = undefined; + const contentBoundingVolume = content.boundingVolume; + if (contentBoundingVolume) { + transformedContentBoundingVolume = + ContentDataBoundingVolumeValidator.computeTransformedBoundingVolume( + contentBoundingVolume, + globalTileTransform + ); + } + + // Validate the data from the content URI against all transformed + // tile bounding volumes and the transformed content bounding volume + const contentUri = content.uri; + const valid = + await ContentDataBoundingVolumeValidator.validateSingleContentAllBoundingVolumes( + contentUri, + transformedTileBoundingVolumes, + transformedContentBoundingVolume, + globalTileTransform, + resourceResolver, + context + ); + allValid = allValid && valid; + } + return allValid; + } + + /** + * Validate the specified content against the given bounding volumes. + * + * This will read the content, transform its vertices with the given + * transform, and check that the resulting vertex is contained in + * each of the given bounding volumes. + * + * @param contentUri - The content URI + * @param transformedTileBoundingVolumes - The transformed bounding volumes + * @param transformedContentBoundingVolume - The optional transformed + * bounding volume of the content + * @param globalTileTransform - The global transform of the containing + * tile, used to transform the vertices of the content + * @param resourceResolver - The resolver for resolving the content data + * based on the URI + * @param context - The context for validation issues + * @returns Whether the content was valid + */ + private static async validateSingleContentAllBoundingVolumes( + contentUri: string, + transformedTileBoundingVolumes: Map, + transformedContentBoundingVolume: BoundingVolume | undefined, + globalTileTransform: number[], + resourceResolver: ResourceResolver, + context: ValidationContext + ): Promise { + const data = await resourceResolver.resolveData(contentUri); + if (!data) { + const message = `Could not resolve content data from ${contentUri}`; + const issue = IoValidationIssues.IO_ERROR(contentUri, message); + context.addIssue(issue); + return false; + } + + const externalGlbResolver = (uri: string) => { + return resourceResolver.resolveData(uri); + }; + const result = + await ContentDataBoundingVolumeValidator.validateSingleContentDataAllBoundingVolumes( + contentUri, + data, + externalGlbResolver, + transformedTileBoundingVolumes, + transformedContentBoundingVolume, + globalTileTransform, + context + ); + return result; + } + + /** + * Validate the given content data against the given bounding volumes. + * + * This will transform the vertices of the given content with the given + * transform, and check that the resulting vertex is contained in + * each of the given bounding volumes. + * + * @param contentUri - The content URI + * @param data - The content data + * @param externalGlbResolver - The function for resolving external GLB + * files from I3DM content + * @param transformedTileBoundingVolumes - The transformed bounding volumes + * @param transformedContentBoundingVolume - The optional transformed + * bounding volume of the content + * @param globalTileTransform - The global transform of the containing + * tile, used to transform the vertices of the content + * @param context - The context for validation issues + * @returns Whether the content was valid + */ + private static async validateSingleContentDataAllBoundingVolumes( + contentUri: string, + data: Buffer, + externalGlbResolver: (glbUri: string) => Promise, + transformedTileBoundingVolumes: Map, + transformedContentBoundingVolume: BoundingVolume | undefined, + globalTileTransform: number[], + context: ValidationContext + ): Promise { + const transformedPoint = Array(3); + + // The counter for the total number of vertices + let vertexCounter = 0; + + // The counters that map the keys of the transformed bounding + // volumes (which are the 'traversedTile.path' strings) to + // the number of vertices that have NOT been contained in + // the bounding volume of the respective tile + const nonContainedInTileBvVertexCounters = new Map(); + let nonContainedInContentBvVertexCounter = 0; + + // The consumer that will receive all vertices of the content + const consumer = (p: number[]) => { + // Transform the point with the global tile transform + ContentDataBoundingVolumeValidator.transformPoint3D( + globalTileTransform, + p, + transformedPoint + ); + + // Check each transformed bounding volume to see whether it contains + // the transformed point, and count the number of points that are + // not contained for each of them + for (const [ + tilePath, + transformedTileBoundingVolume, + ] of transformedTileBoundingVolumes.entries()) { + const containedInTileBv = BoundingVolumesContainment.contains( + transformedTileBoundingVolume, + transformedPoint, + ContentDataBoundingVolumeValidator.CONTAINMENT_EPSILON + ); + + //console.log("check if vertex ", p); + //console.log(" transformed ", transformedPoint); + //console.log(" is contained in ", transformedTileBoundingVolume); + //console.log(" results in ", containedInTileBv); + + if (!containedInTileBv) { + const nonContainedVertexCounter = + nonContainedInTileBvVertexCounters.get(tilePath) ?? 0; + nonContainedInTileBvVertexCounters.set( + tilePath, + nonContainedVertexCounter + 1 + ); + } + } + + // If a content bounding volume was defined, check for containment + // in the content bounding volume + if (transformedContentBoundingVolume) { + const containedInContentBv = BoundingVolumesContainment.contains( + transformedContentBoundingVolume, + transformedPoint, + ContentDataBoundingVolumeValidator.CONTAINMENT_EPSILON + ); + if (!containedInContentBv) { + nonContainedInContentBvVertexCounter++; + } + } + + vertexCounter++; + }; + + // Process the vertices, passing each vertex to the consumer + const processInstancePoints = true; + await VertexProcessing.fromContent( + contentUri, + data, + externalGlbResolver, + processInstancePoints, + consumer + ); + + // Generate validation issues for each tile bounding volume that + // did not contain all vertices + let allValid = true; + for (const [ + tilePath, + nonContainedCounter, + ] of nonContainedInTileBvVertexCounters.entries()) { + if (nonContainedCounter > 0) { + const message = + `The bounding volume of tile '${tilePath}' does not contain ${nonContainedCounter} ` + + `of the ${vertexCounter} vertices of content '${contentUri}'`; + const issue = + ContentDataValidationIssues.CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME( + contentUri, + message + ); + context.addIssue(issue); + allValid = false; + } + } + + // Generate a validation issue if there was a content bounding + // volume that did not contain all vertices + if ( + transformedContentBoundingVolume && + nonContainedInContentBvVertexCounter > 0 + ) { + const message = + `The content bounding volume for content '${contentUri}' does not contain ` + + `${nonContainedInContentBvVertexCounter} of the ${vertexCounter} vertices`; + const issue = + ContentDataValidationIssues.CONTENT_NOT_ENCLOSED_BY_BOUNDING_VOLUME( + contentUri, + message + ); + context.addIssue(issue); + allValid = false; + } + + return allValid; + } + + /** + * Transform the given point with the given matrix. + * + * If the given result is undefined, then a new array will be + * created and returned. + * + * @param point3D - The point as a 3-element array + * @param matrix - The 4x4 matrix as a 16-element array, column-major + * @param result - The optional result. + * @returns The result + */ + private static transformPoint3D( + matrix: number[], + point3D: number[], + result: undefined | number[] + ): number[] { + const px = point3D[0]; + const py = point3D[1]; + const pz = point3D[2]; + const x = matrix[0] * px + matrix[4] * py + matrix[8] * pz + matrix[12]; + const y = matrix[1] * px + matrix[5] * py + matrix[9] * pz + matrix[13]; + const z = matrix[2] * px + matrix[6] * py + matrix[10] * pz + matrix[14]; + if (!result) { + return [x, y, z]; + } + result[0] = x; + result[1] = y; + result[2] = z; + return result; + } + + /** + * Transform the given bounding volume with the given matrix, and + * return the result. + * + * The transform is given as a 16-element array representing the + * 4x4 matrix in column-major order. + * + * If the given bounding volume is a "region" bounding volume, then + * it will be returned directly. + * + * If the bounding volume is a "box" or "sphere" bounding volume, + * then it will be transformed with the given matrix, and the + * result will be returned. + * + * @param boundingVolume - The bounding volume + * @param transform - The transform + * @returns The resulting bounding volume + */ + private static computeTransformedBoundingVolume( + boundingVolume: BoundingVolume, + transform: number[] + ) { + // Regions are not transformed + const region = boundingVolume.region; + if (region) { + return { + region: region, + }; + } + + // Boxes are transformed by converting them into a CesiumJS + // OrientedBoundingBox and transforming that + const box = boundingVolume.box; + if (box) { + const obb = OrientedBoundingBox.unpack(box, 0, new OrientedBoundingBox()); + const matrix = Matrix4.fromArray(transform, 0, new Matrix4()); + ContentDataBoundingVolumeValidator.transformOrientedBoundingBox( + obb, + matrix, + obb + ); + const resultBox = Array(12); + OrientedBoundingBox.pack(obb, resultBox, 0); + return { + box: resultBox, + }; + } + + // Spheres are transformed by converting them into a CesiumJS + // BoundingSphere and transforming that + const sphere = boundingVolume.sphere; + if (sphere) { + const bs = BoundingSphere.unpack(sphere, 0, new BoundingSphere()); + const matrix = Matrix4.fromArray(transform, 0, new Matrix4()); + BoundingSphere.transform(bs, matrix, bs); + const resultSphere = Array(4); + BoundingSphere.pack(bs, resultSphere, 0); + return { + sphere: resultSphere, + }; + } + + console.warn("Unknown bounding volume type", boundingVolume); + return boundingVolume; + } + + /** + * Transforms the given oriented bounding box with the given matrix, + * stores the result in the given result parameter, and returns it. + * + * If the given result is `undefined`, then a new oriented bounding box + * will be created, filled, and returned. + * + * @param orientedBoundingBox - The oriented bounding box + * @param transform - The transform matrix + * @param result - The result + * @returns The result + */ + private static transformOrientedBoundingBox( + orientedBoundingBox: OrientedBoundingBox, + transform: Matrix4, + result: undefined | OrientedBoundingBox + ) { + if (!result) { + result = new OrientedBoundingBox(); + } + Matrix4.multiplyByPoint( + transform, + orientedBoundingBox.center, + result.center + ); + const rotationScaleTransform = new Matrix3(); + Matrix4.getMatrix3(transform, rotationScaleTransform); + Matrix3.multiply( + rotationScaleTransform, + orientedBoundingBox.halfAxes, + result.halfAxes + ); + return result; + } + + /** + * Computes the bounding volumes of the given tile and all its ancestors. + * + * The result will be a mapping from the `traversedTile.path` string to + * the bounding volume that results from transforming the bounding + * volumes of the respective tiles with their global transforms. + * + * @param traversedTile - The traversed tile + * @returns The transformed bounding volumes + */ + private static computeTransformedTileBoundingVolumes( + traversedTile: TraversedTile + ): Map { + const transformedTileBoundingVolumes = new Map(); + let currentTile: TraversedTile | undefined = traversedTile; + while (currentTile) { + const tilePath = currentTile.path; + const finalCurrentTile = currentTile.asFinalTile(); + const boundingVolume = finalCurrentTile.boundingVolume; + const transform = + ContentDataBoundingVolumeValidator.computeGlobalTransform(currentTile); + const transformedTileBoundingVolume = + ContentDataBoundingVolumeValidator.computeTransformedBoundingVolume( + boundingVolume, + transform + ); + transformedTileBoundingVolumes.set( + tilePath, + transformedTileBoundingVolume + ); + currentTile = currentTile.getParent(); + } + return transformedTileBoundingVolumes; + } + + /** + * Returns the global transform of the given tile. + * + * The result will be the transform of the tile, multiplied with all + * transforms of its ancestors, up to the root, returned as a 16-element + * array representing the 4x4 matrix in column-major order. + * + * @param traversedTile - The traversed tile + * @returns The global transform + */ + private static computeGlobalTransform( + traversedTile: TraversedTile + ): number[] { + const globalTransform = Matrix4.clone(Matrix4.IDENTITY); + const currentTransform = Matrix4.clone(Matrix4.IDENTITY); + let currentTile: TraversedTile | undefined = traversedTile; + while (currentTile) { + const finalTile = currentTile.asFinalTile(); + const transform = finalTile.transform; + if (transform) { + Matrix4.fromArray(transform, 0, currentTransform); + Matrix4.multiply(currentTransform, globalTransform, globalTransform); + } + currentTile = currentTile.getParent(); + } + const result = Matrix4.toArray(globalTransform); + return result; + } +} diff --git a/src/validation/TilesetValidator.ts b/src/validation/TilesetValidator.ts index 2171c32b..18710383 100644 --- a/src/validation/TilesetValidator.ts +++ b/src/validation/TilesetValidator.ts @@ -23,6 +23,7 @@ import { Group } from "3d-tiles-tools"; import { IoValidationIssues } from "../issues/IoValidationIssue"; import { StructureValidationIssues } from "../issues/StructureValidationIssues"; import { SemanticValidationIssues } from "../issues/SemanticValidationIssues"; +import { ContentDataBoundingVolumeValidator } from "./ContentDataBoundingVolumeValidator"; /** * A class that can validate a 3D Tiles tileset. @@ -277,6 +278,24 @@ export class TilesetValidator implements Validator { result = false; } + // Perform the bounding volume containment validation only when the traversal + // itself was valid, and it is explicitly requested via the options + if (traversalValid) { + const options = context.getOptions(); + if (options.validateBoundingVolumeContainment) { + const boundingVolumeContainmentValid = + await ContentDataBoundingVolumeValidator.validateBoundingVolumeContainment( + path, + tileset, + validationState, + context + ); + if (!boundingVolumeContainmentValid) { + result = false; + } + } + } + return result; } diff --git a/src/validation/ValidationOptions.ts b/src/validation/ValidationOptions.ts index 9cc3737c..9a92b6c4 100644 --- a/src/validation/ValidationOptions.ts +++ b/src/validation/ValidationOptions.ts @@ -38,6 +38,13 @@ export class ValidationOptions { */ private _semanticSchemaFileNames: string[] | undefined; + /** + * Whether the validator should check that the content data + * is fully contained in the content bounding volume, the + * bounding volume of a tile, and the bounding volumes of + * all ancestors of the tile + */ + private _validateBoundingVolumeContainment: boolean; /** * Default constructor. * @@ -55,6 +62,7 @@ export class ValidationOptions { this._includeContentTypes = undefined; this._excludeContentTypes = undefined; this._semanticSchemaFileNames = undefined; + this._validateBoundingVolumeContainment = false; } /** @@ -157,6 +165,20 @@ export class ValidationOptions { this._semanticSchemaFileNames = value; } + /** + * The flag that indicates whether the validator should check + * that the content data is fully contained in the content + * bounding volume, the bounding volume of a tile, and the + * bounding volumes of all ancestors of the tile + */ + get validateBoundingVolumeContainment(): boolean { + return this._validateBoundingVolumeContainment; + } + + set validateBoundingVolumeContainment(value: boolean) { + this._validateBoundingVolumeContainment = value; + } + /** * Creates a new `ValidationOptions` object where each property is * initialized from the given JSON object.