├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGES.md ├── LICENSE.md ├── README.md ├── ThirdParty.json ├── bin └── 3d-tiles-samples-generator.ts ├── conf.json ├── data ├── animated_box.glb ├── box.glb ├── building.glb ├── dragon_high.glb ├── dragon_low.glb ├── dragon_medium.glb ├── house │ ├── door0.gltf │ ├── door1.gltf │ ├── door2.gltf │ ├── door3.gltf │ ├── doorknob0.gltf │ ├── doorknob1.gltf │ ├── doorknob2.gltf │ ├── doorknob3.gltf │ ├── roof.gltf │ └── wall.gltf ├── red_box.glb ├── textured_box.glb ├── textured_box_separate │ ├── bricks.jpg │ └── textured_box.glb ├── tree.glb ├── tree_billboard.glb └── wood_red.jpg ├── gulpfile.js ├── index.js ├── jest.config.js ├── lib ├── Extensions.ts ├── Material.ts ├── Mesh.ts ├── addMeshToGltf.ts ├── arguments.ts ├── atLeastN.ts ├── attribute.ts ├── bufferUtil.ts ├── calculateBufferPadding.ts ├── calculateFilenameExt.ts ├── calculateMinMax.ts ├── colorTypes.ts ├── compositeSamplesNext.ts ├── constants.ts ├── createB3dm.js ├── createBatchTableHierarchy.ts ├── createBuilding.ts ├── createBuildingsTile.ts ├── createByteBuffer.ts ├── createCmpt.js ├── createConstantAttribute.ts ├── createEXTMeshInstancing.ts ├── createFeatureHierarchySubExtension.ts ├── createFeatureMetadataExtension.ts ├── createGlb.js ├── createGltf.js ├── createGltfFromPnts.js ├── createI3dm.js ├── createInstancesTile.ts ├── createPnts.js ├── createPointCloudTile.ts ├── createTilesetJsonSingle.ts ├── featureHierarchyClass.ts ├── featureMetadata.ts ├── featureMetadataExtension.ts ├── featureMetadataType.ts ├── featureMetatableUtilsNext.ts ├── getBufferPadded.js ├── getJsonBufferPadded.js ├── getMinMax.js ├── getProperties.js ├── gltfFromUri.ts ├── gltfType.ts ├── gltfUtil.ts ├── initializeGltf.ts ├── instanceSamplesNext.ts ├── instanceUtilsNext.ts ├── ioUtil.ts ├── meshView.ts ├── modifyImageUri.ts ├── sampleOutput.ts ├── samplesNext.ts ├── saveBinary.js ├── saveJson.js ├── tilesNextExtension.ts ├── tilesetJson.ts ├── tilesetSamplesNext.ts ├── tilesetUtilsNext.ts ├── typeConversion.js ├── typeSize.ts ├── utility.ts └── xor.ts ├── package.json ├── test ├── .eslintrc.json ├── bin │ ├── createConstantAttribute.test.ts │ ├── createFeatureMetadataExtensionSpec.test.ts │ ├── createFeatureMetadataExtensionSpec.ts │ ├── createGltfFromPntsSpec.test.ts │ └── getQuaternionNormalsSpec.test.ts └── data │ └── triangle.gltf └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "cesium/node" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | package.json text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /output 3 | /.idea 4 | .eslintrc.json 5 | .gitattributes 6 | .npmignore 7 | gulpfile.js 8 | *.tgz 9 | *.zip 10 | .eslintcache 11 | .nyc_output 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | ### 0.1.0 - 2017-02-07 5 | 6 | * Initial release. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 3D Tiles Samples Generator 2 | 3 | > **A note about the repository structure** 4 | > 5 | > This repository was originally part of the `3d-tiles-validator` repository, which contained multiple projects. Now, these project are maintained in separate repositories: 6 | > 7 | > - The `3d-tiles-validator` can be found in [the `3d-tiles-validator` repository](https://github.com/CesiumGS/3d-tiles-validator) 8 | > - The `3d-tiles-tools` can be found in [the `3d-tiles-tools` repository](https://github.com/CesiumGS/3d-tiles-tools) 9 | > 10 | 11 | 12 | The tilesets generated here are included in [3d-tiles-samples](https://github.com/CesiumGS/3d-tiles-samples) and [Cesium](https://github.com/CesiumGS/cesium). 13 | 14 | ## Instructions 15 | 16 | Clone this repo and install [Node.js](http://nodejs.org/). From this directory, run: 17 | 18 | ``` 19 | npm install 20 | 21 | npm run build 22 | 23 | cd dist/ 24 | 25 | node bin/3d-tiles-samples-generator.js 26 | ``` 27 | 28 | This commands generates a set of tilesets and saves them in a folder called `output`. The `Batched`, `Composite`, `Instanced`, `PointCloud`, and `Tilesets` folders may be copied directly to CesiumJS's `Specs/Data/Cesium3DTiles/` folder for testing with CesiumJS. The tilesets in the `Samples` folder may be copied to the `tilesets` folder in `3d-tiles-samples`. 29 | 30 | Run the tests: 31 | ``` 32 | npm run test 33 | ``` 34 | To run ESLint on the entire codebase, run: 35 | ``` 36 | npm run eslint 37 | ``` 38 | To run ESLint automatically when a file is saved, run the following and leave it open in a console window: 39 | ``` 40 | npm run eslint-watch 41 | ``` 42 | 43 | ## Auto Recompilation 44 | You can use 45 | ``` 46 | npm run watch 47 | ``` 48 | 49 | to automatically recompile your changes while editing. 50 | 51 | ## License 52 | 53 | Tilesets generated by this tool are licensed under [CC0](https://creativecommons.org/share-your-work/public-domain/cc0/) with the following exceptions: 54 | 55 | * `TilesetWithRequestVolume` is licensed under a [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/). The building model was created by Richard Edwards: http://www.blendswap.com/blends/view/45211. 56 | 57 | ## Contributions 58 | 59 | Pull requests are appreciated! Please use the same [Contributor License Agreement (CLA)](https://github.com/CesiumGS/cesium/blob/main/CONTRIBUTING.md) and [Coding Guide](https://github.com/CesiumGS/cesium/blob/main/Documentation/Contributors/CodingGuide/README.md) used for [CesiumJS](https://cesium.com/cesiumjs/). 60 | 61 | --- 62 | 63 |

64 | 65 |

66 | -------------------------------------------------------------------------------- /ThirdParty.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "bluebird", 4 | "license": [ 5 | "MIT" 6 | ], 7 | "version": "3.7.2", 8 | "url": "https://www.npmjs.com/package/bluebird" 9 | }, 10 | { 11 | "name": "cesium", 12 | "license": [ 13 | "Apache-2.0" 14 | ], 15 | "version": "1.93.0", 16 | "url": "https://www.npmjs.com/package/cesium" 17 | }, 18 | { 19 | "name": "draco3d", 20 | "license": [ 21 | "Apache-2.0" 22 | ], 23 | "version": "1.3.6", 24 | "url": "https://www.npmjs.com/package/draco3d" 25 | }, 26 | { 27 | "name": "fs-extra", 28 | "license": [ 29 | "MIT" 30 | ], 31 | "version": "9.1.0", 32 | "url": "https://www.npmjs.com/package/fs-extra" 33 | }, 34 | { 35 | "name": "gltf-pipeline", 36 | "license": [ 37 | "Apache-2.0" 38 | ], 39 | "version": "3.0.4", 40 | "url": "https://www.npmjs.com/package/gltf-pipeline" 41 | }, 42 | { 43 | "name": "mime", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "version": "2.6.0", 48 | "url": "https://www.npmjs.com/package/mime" 49 | }, 50 | { 51 | "name": "simplex-noise", 52 | "license": [ 53 | "MIT" 54 | ], 55 | "version": "2.4.0", 56 | "url": "https://www.npmjs.com/package/simplex-noise" 57 | } 58 | ] -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": false 4 | }, 5 | "source": { 6 | "include": ["lib"], 7 | "includePattern": ".+\\.js(doc)?$", 8 | "excludePattern": "(^|\\/|\\\\)_" 9 | }, 10 | "templates": { 11 | "cleverLinks": true, 12 | "default": { 13 | "outputSourceFiles": false 14 | }, 15 | "sourceUrl": "https://github.com/CesiumGS/3d-tiles-validator/blob/{version}/lib/{filename}" 16 | }, 17 | "opts": { 18 | "destination": "doc", 19 | "recurse": true 20 | } 21 | } -------------------------------------------------------------------------------- /data/animated_box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/animated_box.glb -------------------------------------------------------------------------------- /data/box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/box.glb -------------------------------------------------------------------------------- /data/building.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/building.glb -------------------------------------------------------------------------------- /data/dragon_high.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/dragon_high.glb -------------------------------------------------------------------------------- /data/dragon_low.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/dragon_low.glb -------------------------------------------------------------------------------- /data/dragon_medium.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/dragon_medium.glb -------------------------------------------------------------------------------- /data/house/door0.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 72, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 45 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 46, 21 | "min": [ 22 | -0.3696739971637726, 23 | 0, 24 | -1.0869100093841553 25 | ], 26 | "max": [ 27 | 0.3696739971637726, 28 | 1.3130379915237427, 29 | -0.9400410056114197 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 552, 36 | "componentType": 5126, 37 | "count": 46, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 1248, 58 | "uri": "data:application/octet-stream;base64,6UW9vqERqD+HpnC/6UW9vqERqD/eH4u/6UW9vgAAAADeH4u/6UW9vgAAAACHpnC/6UW9vqERqD/eH4u/6UW9PqERqD/eH4u/7fVePhXjND/eH4u/7fVePpmCkz/eH4u/6UW9vgAAAADeH4u/6UW9PgAAAADeH4u/6UW9PqERqD/eH4u/6UW9PqERqD+HpnC/6UW9PgAAAACHpnC/6UW9PgAAAADeH4u/6UW9PqERqD+HpnC/6UW9vqERqD+HpnC/6UW9vgAAAACHpnC/6UW9PgAAAACHpnC/6UW9vgAAAACHpnC/6UW9vgAAAADeH4u/6UW9PgAAAADeH4u/6UW9PgAAAACHpnC/6UW9PqERqD+HpnC/6UW9PqERqD/eH4u/6UW9vqERqD/eH4u/6UW9vqERqD+HpnC/hpJpvpmCkz987YG/hpJpvhXjND987YG/hpJpvhXjND/eH4u/hpJpvpmCkz/eH4u/7fVePpmCkz987YG/7fVePhXjND987YG/hpJpvhXjND987YG/hpJpvpmCkz987YG/hpJpvhXjND987YG/7fVePhXjND987YG/7fVePhXjND/eH4u/hpJpvhXjND/eH4u/7fVePpmCkz987YG/hpJpvpmCkz987YG/hpJpvpmCkz/eH4u/7fVePpmCkz/eH4u/7fVePhXjND/eH4u/7fVePhXjND987YG/7fVePpmCkz987YG/7fVePpmCkz/eH4u/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AAAAgAAAAAAAAIC/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcABQAIAAQABQAJAAgACgALAAwACgAMAA0ADgAPABAADgAQABEAEgATABQAEgAUABUAFgAXABgAFgAYABkAGgAbABwAGgAcAB0AHgAfACAAHgAgACEAIgAjACQAIgAkACUAJgAnACgAJgAoACkAKgArACwAKgAsAC0A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 1104, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 144, 72 | "byteOffset": 1104, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.45435, 81 | 0.245963, 82 | 0.171126, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/door1.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 72, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 45 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 46, 21 | "min": [ 22 | 0.9400410056114197, 23 | 0, 24 | -0.3696739971637726 25 | ], 26 | "max": [ 27 | 1.0869100093841553, 28 | 1.3130379915237427, 29 | 0.3696739971637726 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 552, 36 | "componentType": 5126, 37 | "count": 46, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 1248, 58 | "uri": "data:application/octet-stream;base64,h6ZwP6ERqD/pRb2+3h+LP6ERqD/pRb2+3h+LPwAAAADpRb2+h6ZwPwAAAADpRb2+3h+LP6ERqD/pRb2+3h+LP6ERqD/pRb0+3h+LPxXjND/t9V4+3h+LP5mCkz/t9V4+3h+LPwAAAADpRb2+3h+LPwAAAADpRb0+3h+LP6ERqD/pRb0+h6ZwP6ERqD/pRb0+h6ZwPwAAAADpRb0+3h+LPwAAAADpRb0+h6ZwP6ERqD/pRb0+h6ZwP6ERqD/pRb2+h6ZwPwAAAADpRb2+h6ZwPwAAAADpRb0+h6ZwPwAAAADpRb2+3h+LPwAAAADpRb2+3h+LPwAAAADpRb0+h6ZwPwAAAADpRb0+h6ZwP6ERqD/pRb0+3h+LP6ERqD/pRb0+3h+LP6ERqD/pRb2+h6ZwP6ERqD/pRb2+fO2BP5mCkz+Gkmm+fO2BPxXjND+Gkmm+3h+LPxXjND+Gkmm+3h+LP5mCkz+Gkmm+fO2BP5mCkz/t9V4+fO2BPxXjND/t9V4+fO2BPxXjND+Gkmm+fO2BP5mCkz+Gkmm+fO2BPxXjND+Gkmm+fO2BPxXjND/t9V4+3h+LPxXjND/t9V4+3h+LPxXjND+Gkmm+fO2BP5mCkz/t9V4+fO2BP5mCkz+Gkmm+3h+LP5mCkz+Gkmm+3h+LP5mCkz/t9V4+3h+LPxXjND/t9V4+fO2BPxXjND/t9V4+fO2BP5mCkz/t9V4+3h+LP5mCkz/t9V4+AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAABAAIAAAACAAMABAAFAAYABAAGAAcABQAIAAQABQAJAAgACgALAAwACgAMAA0ADgAPABAADgAQABEAEgATABQAEgAUABUAFgAXABgAFgAYABkAGgAbABwAGgAcAB0AHgAfACAAHgAgACEAIgAjACQAIgAkACUAJgAnACgAJgAoACkAKgArACwAKgAsAC0A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 1104, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 144, 72 | "byteOffset": 1104, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.45435, 81 | 0.245963, 82 | 0.171126, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/door2.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 72, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 45 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 46, 21 | "min": [ 22 | -0.3696739971637726, 23 | 0, 24 | 0.9400410056114197 25 | ], 26 | "max": [ 27 | 0.3696739971637726, 28 | 1.3130379915237427, 29 | 1.0869100093841553 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 552, 36 | "componentType": 5126, 37 | "count": 46, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 1248, 58 | "uri": "data:application/octet-stream;base64,6UW9PqERqD+HpnA/6UW9PqERqD/eH4s/6UW9PgAAAADeH4s/6UW9PgAAAACHpnA/6UW9PqERqD/eH4s/6UW9vqERqD/eH4s/7fVevhXjND/eH4s/7fVevpmCkz/eH4s/6UW9PgAAAADeH4s/6UW9vgAAAADeH4s/6UW9vqERqD/eH4s/6UW9vqERqD+HpnA/6UW9vgAAAACHpnA/6UW9vgAAAADeH4s/6UW9vqERqD+HpnA/6UW9PqERqD+HpnA/6UW9PgAAAACHpnA/6UW9vgAAAACHpnA/6UW9PgAAAACHpnA/6UW9PgAAAADeH4s/6UW9vgAAAADeH4s/6UW9vgAAAACHpnA/6UW9vqERqD+HpnA/6UW9vqERqD/eH4s/6UW9PqERqD/eH4s/6UW9PqERqD+HpnA/hpJpPpmCkz987YE/hpJpPhXjND987YE/hpJpPhXjND/eH4s/hpJpPpmCkz/eH4s/7fVevpmCkz987YE/7fVevhXjND987YE/hpJpPhXjND987YE/hpJpPpmCkz987YE/hpJpPhXjND987YE/7fVevhXjND987YE/7fVevhXjND/eH4s/hpJpPhXjND/eH4s/7fVevpmCkz987YE/hpJpPpmCkz987YE/hpJpPpmCkz/eH4s/7fVevpmCkz/eH4s/7fVevhXjND/eH4s/7fVevhXjND987YE/7fVevpmCkz987YE/7fVevpmCkz/eH4s/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcABQAIAAQABQAJAAgACgALAAwACgAMAA0ADgAPABAADgAQABEAEgATABQAEgAUABUAFgAXABgAFgAYABkAGgAbABwAGgAcAB0AHgAfACAAHgAgACEAIgAjACQAIgAkACUAJgAnACgAJgAoACkAKgArACwAKgAsAC0A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 1104, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 144, 72 | "byteOffset": 1104, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.45435, 81 | 0.245963, 82 | 0.171126, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/door3.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 72, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 45 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 46, 21 | "min": [ 22 | -1.0869100093841553, 23 | 0, 24 | -0.3696739971637726 25 | ], 26 | "max": [ 27 | -0.9400410056114197, 28 | 1.3130379915237427, 29 | 0.3696739971637726 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 552, 36 | "componentType": 5126, 37 | "count": 46, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 1248, 58 | "uri": "data:application/octet-stream;base64,h6Zwv6ERqD/pRb0+3h+Lv6ERqD/pRb0+3h+LvwAAAADpRb0+h6ZwvwAAAADpRb0+3h+Lv6ERqD/pRb0+3h+Lv6ERqD/pRb2+3h+LvxXjND/t9V6+3h+Lv5mCkz/t9V6+3h+LvwAAAADpRb0+3h+LvwAAAADpRb2+3h+Lv6ERqD/pRb2+h6Zwv6ERqD/pRb2+h6ZwvwAAAADpRb2+3h+LvwAAAADpRb2+h6Zwv6ERqD/pRb2+h6Zwv6ERqD/pRb0+h6ZwvwAAAADpRb0+h6ZwvwAAAADpRb2+h6ZwvwAAAADpRb0+3h+LvwAAAADpRb0+3h+LvwAAAADpRb2+h6ZwvwAAAADpRb2+h6Zwv6ERqD/pRb2+3h+Lv6ERqD/pRb2+3h+Lv6ERqD/pRb0+h6Zwv6ERqD/pRb0+fO2Bv5mCkz+Gkmk+fO2BvxXjND+Gkmk+3h+LvxXjND+Gkmk+3h+Lv5mCkz+Gkmk+fO2Bv5mCkz/t9V6+fO2BvxXjND/t9V6+fO2BvxXjND+Gkmk+fO2Bv5mCkz+Gkmk+fO2BvxXjND+Gkmk+fO2BvxXjND/t9V6+3h+LvxXjND/t9V6+3h+LvxXjND+Gkmk+fO2Bv5mCkz/t9V6+fO2Bv5mCkz+Gkmk+3h+Lv5mCkz+Gkmk+3h+Lv5mCkz/t9V6+3h+LvxXjND/t9V6+fO2BvxXjND/t9V6+fO2Bv5mCkz/t9V6+3h+Lv5mCkz/t9V6+AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAAAgAAAAAAAAIA/AAABAAIAAAACAAMABAAFAAYABAAGAAcABQAIAAQABQAJAAgACgALAAwACgAMAA0ADgAPABAADgAQABEAEgATABQAEgAUABUAFgAXABgAFgAYABkAGgAbABwAGgAcAB0AHgAfACAAHgAgACEAIgAjACQAIgAkACUAJgAnACgAJgAoACkAKgArACwAKgAsAC0A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 1104, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 144, 72 | "byteOffset": 1104, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.45435, 81 | 0.245963, 82 | 0.171126, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/doorknob0.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 672, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 113 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 114, 21 | "min": [ 22 | -0.2817629873752594, 23 | 0.5537300109863281, 24 | -1.1363259553909302 25 | ], 26 | "max": [ 27 | -0.1847810000181198, 28 | 0.6507120132446289, 29 | -1.0393439531326294 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 1368, 36 | "componentType": 5126, 37 | "count": 114, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 4080, 58 | "uri": "data:application/octet-stream;base64,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/mDOviFIar8AAAAA+6m+vglJar+W5R2+MwIqv1v4Mb+y2Yy+AAABAAIAAAACAAMABAAAAAMABQAGAAAABQAAAAQABgABAAAAAQAHAAgAAQAIAAIACQAHAAEABgAJAAEAAgAIAAoAAgAKAAsAAwACAAsABAADAAwADAADAA0AAwALAA0ADgAEAAwADwAFAAQADwAEAA4AEAARAAUAEAAFAA8AEQAGAAUAEgAJAAYAEQASAAYAEgATAAkAEwAUAAkACQAUAAcABwAVAAgAFAAVAAcACAAVAAoACgAVABYACwAKABYADQALABcACwAWABcADAANABgADQAXABkAGAANABkADgAMABoAGgAMABgAGwAOABoAHAAPAA4AHAAOABsAHQAQAA8AHQAPABwAHgARABAAHwAeABAAHwAQAB0AHgAgABEAIAASABEAIQATABIAIAAhABIAIQAiABMAIgAjABMAEwAjABQAIwAVABQAJAAVACMAIgAkACMAJQAkACIAJgAlACIAJgAiACEAJwAmACEAJwAhACAAKAAnACAAKAAgAB4AKQAoAB4AKQAeAB8AKgApAB8AKgAfACsAKwAfAB0AKwAdACwALAAdABwALAAcAC0ALQAcABsALgArACwALgAsAC0ALgAqACsALgAvACoALwApACoAMAAoACkALwAwACkAMAAxACgAMQAnACgAMgAmACcAMQAyACcAMgAzACYAMwAlACYANAA1ACUAMwA0ACUAJQA1ACQANQAVACQANgAVADUANAA2ADUANwA4ADQANwA0ADMAOAA2ADQAOQA3ADMAOQAzADIAOgA5ADIAOgAyADEAOwA6ADEAOwAxADAAPAA7ADAAPAAwAC8ALgA8AC8APQA7ADwALgA9ADwAPQA+ADsAPgA6ADsAPwA5ADoAPgA/ADoAPwBAADkAQAA3ADkAQQA4ADcAQABBADcAQgBDADgAQQBCADgAOABDADYAQwAVADYAQgBEAEMARAAVAEMARQBEAEIARgBFAEIARgBCAEEARwBGAEEARwBBAEAASABHAEAASABAAD8ASQBIAD8ASQA/AD4ASgBJAD4ASgA+AD0ALgBKAD0ALgBLAEoASwBJAEoATABIAEkASwBMAEkATABNAEgATQBHAEgATgBGAEcATQBOAEcATwBFAEYATgBPAEYATwBQAEUAUABRAEUARQBRAEQAUQAVAEQAUgAVAFEAUABSAFEAUwBUAFAAUwBQAE8AVABSAFAAVQBTAE8AVQBPAE4AVgBVAE4AVgBOAE0AVwBWAE0AVwBNAEwAWABXAEwAWABMAEsALgBYAEsAWQBXAFgALgBZAFgAWQBaAFcAWgBWAFcAWwBVAFYAWgBbAFYAXABTAFUAWwBcAFUAXABdAFMAXQBUAFMAXgBfAFQAXQBeAFQAVABfAFIAXwAVAFIAFgAVAGAAYAAVAGEAYQAVAF8AXgBhAF8AYgBhAF4AYwBiAF4AYwBeAF0AZABjAF0AZABdAFwAZQBkAFwAZQBcAFsAZgBlAFsAZgBbAFoAZwBmAFoAZwBaAFkALgBnAFkALgBoAGcAaABmAGcAaQBlAGYAaABpAGYAagBkAGUAaQBqAGUAagBrAGQAawBjAGQAGQBiAGMAawAZAGMAGQAXAGIAFwBgAGIAYgBgAGEAFwAWAGAAGAAZAGsAbAAYAGsAbABrAGoAbQBsAGoAbQBqAGkAbgBtAGkAbgBpAGgALgBuAGgALgBvAHAALgBwAG4ALgAtAG8AcABtAG4AcQBsAG0AcABxAG0AcQAaAGwAGgAYAGwAGwAaAHEAbwAbAHEAbwBxAHAALQAbAG8A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 2736, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 1344, 72 | "byteOffset": 2736, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.8, 81 | 0.24657, 82 | 0.026813, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/doorknob1.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 672, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 113 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 114, 21 | "min": [ 22 | 1.0393439531326294, 23 | 0.5537300109863281, 24 | -0.2817629873752594 25 | ], 26 | "max": [ 27 | 1.1363259553909302, 28 | 0.6507120132446289, 29 | -0.1847810000181198 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 1368, 36 | "componentType": 5126, 37 | "count": 114, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 4080, 58 | "uri": "data:application/octet-stream;base64,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AAAAABBHar/UZc6+luUdPglJar/7qb6+stmMPlv4Mb8zAiq/AAABAAIAAAACAAMABAAAAAMABQAGAAAABQAAAAQABgABAAAAAQAHAAgAAQAIAAIACQAHAAEABgAJAAEAAgAIAAoAAgAKAAsAAwACAAsABAADAAwADAADAA0AAwALAA0ADgAEAAwADwAFAAQADwAEAA4AEAARAAUAEAAFAA8AEQAGAAUAEgAJAAYAEQASAAYAEgATAAkAEwAUAAkACQAUAAcABwAVAAgAFAAVAAcACAAVAAoACgAVABYACwAKABYADQALABcACwAWABcADAANABgADQAXABkAGAANABkADgAMABoAGgAMABgAGwAOABoAHAAPAA4AHAAOABsAHQAQAA8AHQAPABwAHgARABAAHwAeABAAHwAQAB0AHgAgABEAIAASABEAIQATABIAIAAhABIAIQAiABMAIgAjABMAEwAjABQAIwAVABQAJAAVACMAIgAkACMAJQAkACIAJgAlACIAJgAiACEAJwAmACEAJwAhACAAKAAnACAAKAAgAB4AKQAoAB4AKQAeAB8AKgApAB8AKgAfACsAKwAfAB0AKwAdACwALAAdABwALAAcAC0ALQAcABsALgArACwALgAsAC0ALgAqACsALgAvACoALwApACoAMAAoACkALwAwACkAMAAxACgAMQAnACgAMgAmACcAMQAyACcAMgAzACYAMwAlACYANAA1ACUAMwA0ACUAJQA1ACQANQAVACQANgAVADUANAA2ADUANwA4ADQANwA0ADMAOAA2ADQAOQA3ADMAOQAzADIAOgA5ADIAOgAyADEAOwA6ADEAOwAxADAAPAA7ADAAPAAwAC8ALgA8AC8APQA7ADwALgA9ADwAPQA+ADsAPgA6ADsAPwA5ADoAPgA/ADoAPwBAADkAQAA3ADkAQQA4ADcAQABBADcAQgBDADgAQQBCADgAOABDADYAQwAVADYAQgBEAEMARAAVAEMARQBEAEIARgBFAEIARgBCAEEARwBGAEEARwBBAEAASABHAEAASABAAD8ASQBIAD8ASQA/AD4ASgBJAD4ASgA+AD0ALgBKAD0ALgBLAEoASwBJAEoATABIAEkASwBMAEkATABNAEgATQBHAEgATgBGAEcATQBOAEcATwBFAEYATgBPAEYATwBQAEUAUABRAEUARQBRAEQAUQAVAEQAUgAVAFEAUABSAFEAUwBUAFAAUwBQAE8AVABSAFAAVQBTAE8AVQBPAE4AVgBVAE4AVgBOAE0AVwBWAE0AVwBNAEwAWABXAEwAWABMAEsALgBYAEsAWQBXAFgALgBZAFgAWQBaAFcAWgBWAFcAWwBVAFYAWgBbAFYAXABTAFUAWwBcAFUAXABdAFMAXQBUAFMAXgBfAFQAXQBeAFQAVABfAFIAXwAVAFIAFgAVAGAAYAAVAGEAYQAVAF8AXgBhAF8AYgBhAF4AYwBiAF4AYwBeAF0AZABjAF0AZABdAFwAZQBkAFwAZQBcAFsAZgBlAFsAZgBbAFoAZwBmAFoAZwBaAFkALgBnAFkALgBoAGcAaABmAGcAaQBlAGYAaABpAGYAagBkAGUAaQBqAGUAagBrAGQAawBjAGQAGQBiAGMAawAZAGMAGQAXAGIAFwBgAGIAYgBgAGEAFwAWAGAAGAAZAGsAbAAYAGsAbABrAGoAbQBsAGoAbQBqAGkAbgBtAGkAbgBpAGgALgBuAGgALgBvAHAALgBwAG4ALgAtAG8AcABtAG4AcQBsAG0AcABxAG0AcQAaAGwAGgAYAGwAGwAaAHEAbwAbAHEAbwBxAHAALQAbAG8A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 2736, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 1344, 72 | "byteOffset": 2736, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.8, 81 | 0.24657, 82 | 0.026813, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/doorknob2.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 672, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 113 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 114, 21 | "min": [ 22 | 0.1847810000181198, 23 | 0.5537300109863281, 24 | 1.0393439531326294 25 | ], 26 | "max": [ 27 | 0.2817629873752594, 28 | 0.6507120132446289, 29 | 1.1363259553909302 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 1368, 36 | "componentType": 5126, 37 | "count": 114, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 4080, 58 | "uri": "data:application/octet-stream;base64,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/mDOPiFIar8AAAAA+6m+PglJar+W5R0+MwIqP1v4Mb+y2Yw+AAABAAIAAAACAAMABAAAAAMABQAGAAAABQAAAAQABgABAAAAAQAHAAgAAQAIAAIACQAHAAEABgAJAAEAAgAIAAoAAgAKAAsAAwACAAsABAADAAwADAADAA0AAwALAA0ADgAEAAwADwAFAAQADwAEAA4AEAARAAUAEAAFAA8AEQAGAAUAEgAJAAYAEQASAAYAEgATAAkAEwAUAAkACQAUAAcABwAVAAgAFAAVAAcACAAVAAoACgAVABYACwAKABYADQALABcACwAWABcADAANABgADQAXABkAGAANABkADgAMABoAGgAMABgAGwAOABoAHAAPAA4AHAAOABsAHQAQAA8AHQAPABwAHgARABAAHwAeABAAHwAQAB0AHgAgABEAIAASABEAIQATABIAIAAhABIAIQAiABMAIgAjABMAEwAjABQAIwAVABQAJAAVACMAIgAkACMAJQAkACIAJgAlACIAJgAiACEAJwAmACEAJwAhACAAKAAnACAAKAAgAB4AKQAoAB4AKQAeAB8AKgApAB8AKgAfACsAKwAfAB0AKwAdACwALAAdABwALAAcAC0ALQAcABsALgArACwALgAsAC0ALgAqACsALgAvACoALwApACoAMAAoACkALwAwACkAMAAxACgAMQAnACgAMgAmACcAMQAyACcAMgAzACYAMwAlACYANAA1ACUAMwA0ACUAJQA1ACQANQAVACQANgAVADUANAA2ADUANwA4ADQANwA0ADMAOAA2ADQAOQA3ADMAOQAzADIAOgA5ADIAOgAyADEAOwA6ADEAOwAxADAAPAA7ADAAPAAwAC8ALgA8AC8APQA7ADwALgA9ADwAPQA+ADsAPgA6ADsAPwA5ADoAPgA/ADoAPwBAADkAQAA3ADkAQQA4ADcAQABBADcAQgBDADgAQQBCADgAOABDADYAQwAVADYAQgBEAEMARAAVAEMARQBEAEIARgBFAEIARgBCAEEARwBGAEEARwBBAEAASABHAEAASABAAD8ASQBIAD8ASQA/AD4ASgBJAD4ASgA+AD0ALgBKAD0ALgBLAEoASwBJAEoATABIAEkASwBMAEkATABNAEgATQBHAEgATgBGAEcATQBOAEcATwBFAEYATgBPAEYATwBQAEUAUABRAEUARQBRAEQAUQAVAEQAUgAVAFEAUABSAFEAUwBUAFAAUwBQAE8AVABSAFAAVQBTAE8AVQBPAE4AVgBVAE4AVgBOAE0AVwBWAE0AVwBNAEwAWABXAEwAWABMAEsALgBYAEsAWQBXAFgALgBZAFgAWQBaAFcAWgBWAFcAWwBVAFYAWgBbAFYAXABTAFUAWwBcAFUAXABdAFMAXQBUAFMAXgBfAFQAXQBeAFQAVABfAFIAXwAVAFIAFgAVAGAAYAAVAGEAYQAVAF8AXgBhAF8AYgBhAF4AYwBiAF4AYwBeAF0AZABjAF0AZABdAFwAZQBkAFwAZQBcAFsAZgBlAFsAZgBbAFoAZwBmAFoAZwBaAFkALgBnAFkALgBoAGcAaABmAGcAaQBlAGYAaABpAGYAagBkAGUAaQBqAGUAagBrAGQAawBjAGQAGQBiAGMAawAZAGMAGQAXAGIAFwBgAGIAYgBgAGEAFwAWAGAAGAAZAGsAbAAYAGsAbABrAGoAbQBsAGoAbQBqAGkAbgBtAGkAbgBpAGgALgBuAGgALgBvAHAALgBwAG4ALgAtAG8AcABtAG4AcQBsAG0AcABxAG0AcQAaAGwAGgAYAGwAGwAaAHEAbwAbAHEAbwBxAHAALQAbAG8A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 2736, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 1344, 72 | "byteOffset": 2736, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.8, 81 | 0.24657, 82 | 0.026813, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/doorknob3.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 672, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 113 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 114, 21 | "min": [ 22 | -1.1363259553909302, 23 | 0.5537300109863281, 24 | 0.1847810000181198 25 | ], 26 | "max": [ 27 | -1.0393439531326294, 28 | 0.6507120132446289, 29 | 0.2817629873752594 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 1368, 36 | "componentType": 5126, 37 | "count": 114, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 4080, 58 | "uri": "data:application/octet-stream;base64,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AAAAABBHar/UZc4+luUdvglJar/7qb4+stmMvlv4Mb8zAio/AAABAAIAAAACAAMABAAAAAMABQAGAAAABQAAAAQABgABAAAAAQAHAAgAAQAIAAIACQAHAAEABgAJAAEAAgAIAAoAAgAKAAsAAwACAAsABAADAAwADAADAA0AAwALAA0ADgAEAAwADwAFAAQADwAEAA4AEAARAAUAEAAFAA8AEQAGAAUAEgAJAAYAEQASAAYAEgATAAkAEwAUAAkACQAUAAcABwAVAAgAFAAVAAcACAAVAAoACgAVABYACwAKABYADQALABcACwAWABcADAANABgADQAXABkAGAANABkADgAMABoAGgAMABgAGwAOABoAHAAPAA4AHAAOABsAHQAQAA8AHQAPABwAHgARABAAHwAeABAAHwAQAB0AHgAgABEAIAASABEAIQATABIAIAAhABIAIQAiABMAIgAjABMAEwAjABQAIwAVABQAJAAVACMAIgAkACMAJQAkACIAJgAlACIAJgAiACEAJwAmACEAJwAhACAAKAAnACAAKAAgAB4AKQAoAB4AKQAeAB8AKgApAB8AKgAfACsAKwAfAB0AKwAdACwALAAdABwALAAcAC0ALQAcABsALgArACwALgAsAC0ALgAqACsALgAvACoALwApACoAMAAoACkALwAwACkAMAAxACgAMQAnACgAMgAmACcAMQAyACcAMgAzACYAMwAlACYANAA1ACUAMwA0ACUAJQA1ACQANQAVACQANgAVADUANAA2ADUANwA4ADQANwA0ADMAOAA2ADQAOQA3ADMAOQAzADIAOgA5ADIAOgAyADEAOwA6ADEAOwAxADAAPAA7ADAAPAAwAC8ALgA8AC8APQA7ADwALgA9ADwAPQA+ADsAPgA6ADsAPwA5ADoAPgA/ADoAPwBAADkAQAA3ADkAQQA4ADcAQABBADcAQgBDADgAQQBCADgAOABDADYAQwAVADYAQgBEAEMARAAVAEMARQBEAEIARgBFAEIARgBCAEEARwBGAEEARwBBAEAASABHAEAASABAAD8ASQBIAD8ASQA/AD4ASgBJAD4ASgA+AD0ALgBKAD0ALgBLAEoASwBJAEoATABIAEkASwBMAEkATABNAEgATQBHAEgATgBGAEcATQBOAEcATwBFAEYATgBPAEYATwBQAEUAUABRAEUARQBRAEQAUQAVAEQAUgAVAFEAUABSAFEAUwBUAFAAUwBQAE8AVABSAFAAVQBTAE8AVQBPAE4AVgBVAE4AVgBOAE0AVwBWAE0AVwBNAEwAWABXAEwAWABMAEsALgBYAEsAWQBXAFgALgBZAFgAWQBaAFcAWgBWAFcAWwBVAFYAWgBbAFYAXABTAFUAWwBcAFUAXABdAFMAXQBUAFMAXgBfAFQAXQBeAFQAVABfAFIAXwAVAFIAFgAVAGAAYAAVAGEAYQAVAF8AXgBhAF8AYgBhAF4AYwBiAF4AYwBeAF0AZABjAF0AZABdAFwAZQBkAFwAZQBcAFsAZgBlAFsAZgBbAFoAZwBmAFoAZwBaAFkALgBnAFkALgBoAGcAaABmAGcAaQBlAGYAaABpAGYAagBkAGUAaQBqAGUAagBrAGQAawBjAGQAGQBiAGMAawAZAGMAGQAXAGIAFwBgAGIAYgBgAGEAFwAWAGAAGAAZAGsAbAAYAGsAbABrAGoAbQBsAGoAbQBqAGkAbgBtAGkAbgBpAGgALgBuAGgALgBvAHAALgBwAG4ALgAtAG8AcABtAG4AcQBsAG0AcABxAG0AcQAaAGwAGgAYAGwAGwAaAHEAbwAbAHEAbwBxAHAALQAbAG8A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 2736, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 1344, 72 | "byteOffset": 2736, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.8, 81 | 0.24657, 82 | 0.026813, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/roof.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 18, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 15 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 16, 21 | "min": [ 22 | -1, 23 | 2, 24 | -1 25 | ], 26 | "max": [ 27 | 1, 28 | 2.979520082473755, 29 | 1 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 192, 36 | "componentType": 5126, 37 | "count": 16, 38 | "type": "VEC3", 39 | "min": [ 40 | -0.6997694373130798, 41 | -1, 42 | -0.6997694373130798 43 | ], 44 | "max": [ 45 | 0.6997694373130798, 46 | 0.7143687605857849, 47 | 0.6997694373130798 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 420, 58 | "uri": "data:application/octet-stream;base64,AAAAAHWwPkAAAAAAAACAvwAAAEAAAIC/AACAvwAAAEAAAIA/AAAAAHWwPkAAAAAAAACAPwAAAEAAAIC/AACAvwAAAEAAAIC/AAAAAHWwPkAAAAAAAACAPwAAAEAAAIA/AACAPwAAAEAAAIC/AAAAAHWwPkAAAAAAAACAvwAAAEAAAIA/AACAPwAAAEAAAIA/AACAvwAAAEAAAIA/AACAvwAAAEAAAIC/AACAPwAAAEAAAIC/AACAPwAAAEAAAIA/FyQzv9/gNj8AAAAAFyQzv9/gNj8AAAAAFyQzv9/gNj8AAAAAAAAAAN/gNj8XJDO/AAAAAN/gNj8XJDO/AAAAAN/gNj8XJDO/FyQzP9/gNj8AAAAAFyQzP9/gNj8AAAAAFyQzP9/gNj8AAAAAAAAAAN/gNj8XJDM/AAAAAN/gNj8XJDM/AAAAAN/gNj8XJDM/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADAAOAA8A" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 384, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 36, 72 | "byteOffset": 384, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.64, 81 | 0.204491, 82 | 0.219181, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/house/wall.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "accessors": [ 3 | { 4 | "bufferView": 1, 5 | "byteOffset": 0, 6 | "componentType": 5123, 7 | "count": 36, 8 | "type": "SCALAR", 9 | "min": [ 10 | 0 11 | ], 12 | "max": [ 13 | 23 14 | ] 15 | }, 16 | { 17 | "bufferView": 0, 18 | "byteOffset": 0, 19 | "componentType": 5126, 20 | "count": 24, 21 | "min": [ 22 | -1, 23 | 0, 24 | -1 25 | ], 26 | "max": [ 27 | 1, 28 | 2, 29 | 1 30 | ], 31 | "type": "VEC3" 32 | }, 33 | { 34 | "bufferView": 0, 35 | "byteOffset": 288, 36 | "componentType": 5126, 37 | "count": 24, 38 | "type": "VEC3", 39 | "min": [ 40 | -1, 41 | -1, 42 | -1 43 | ], 44 | "max": [ 45 | 1, 46 | 1, 47 | 1 48 | ] 49 | } 50 | ], 51 | "asset": { 52 | "generator": "OBJ2GLTF", 53 | "version": "2.0" 54 | }, 55 | "buffers": [ 56 | { 57 | "byteLength": 648, 58 | "uri": "data:application/octet-stream;base64,AACAvwAAAEAAAIA/AACAvwAAAEAAAIC/AACAvwAAAAAAAIC/AACAvwAAAAAAAIA/AACAvwAAAEAAAIC/AACAPwAAAEAAAIC/AACAPwAAAAAAAIC/AACAvwAAAAAAAIC/AACAPwAAAEAAAIC/AACAPwAAAEAAAIA/AACAPwAAAAAAAIA/AACAPwAAAAAAAIC/AACAPwAAAEAAAIA/AACAvwAAAEAAAIA/AACAvwAAAAAAAIA/AACAPwAAAAAAAIA/AACAvwAAAAAAAIA/AACAvwAAAAAAAIC/AACAPwAAAAAAAIC/AACAPwAAAAAAAIA/AACAPwAAAEAAAIA/AACAPwAAAEAAAIC/AACAvwAAAEAAAIC/AACAvwAAAEAAAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA" 59 | } 60 | ], 61 | "bufferViews": [ 62 | { 63 | "buffer": 0, 64 | "byteLength": 576, 65 | "byteOffset": 0, 66 | "target": 34962, 67 | "byteStride": 12 68 | }, 69 | { 70 | "buffer": 0, 71 | "byteLength": 72, 72 | "byteOffset": 576, 73 | "target": 34963 74 | } 75 | ], 76 | "materials": [ 77 | { 78 | "pbrMetallicRoughness": { 79 | "baseColorFactor": [ 80 | 0.8, 81 | 0.431283, 82 | 0.253046, 83 | 1 84 | ], 85 | "metallicFactor": 0, 86 | "roughnessFactor": 1 87 | }, 88 | "emissiveFactor": [ 89 | 0, 90 | 0, 91 | 0 92 | ], 93 | "alphaMode": "OPAQUE", 94 | "doubleSided": false 95 | } 96 | ], 97 | "meshes": [ 98 | { 99 | "primitives": [ 100 | { 101 | "attributes": { 102 | "POSITION": 1, 103 | "NORMAL": 2 104 | }, 105 | "indices": 0, 106 | "material": 0, 107 | "mode": 4 108 | } 109 | ] 110 | } 111 | ], 112 | "nodes": [ 113 | { 114 | "mesh": 0 115 | } 116 | ], 117 | "scene": 0, 118 | "scenes": [ 119 | { 120 | "nodes": [ 121 | 0 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /data/red_box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/red_box.glb -------------------------------------------------------------------------------- /data/textured_box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/textured_box.glb -------------------------------------------------------------------------------- /data/textured_box_separate/bricks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/textured_box_separate/bricks.jpg -------------------------------------------------------------------------------- /data/textured_box_separate/textured_box.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/textured_box_separate/textured_box.glb -------------------------------------------------------------------------------- /data/tree.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/tree.glb -------------------------------------------------------------------------------- /data/tree_billboard.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/tree_billboard.glb -------------------------------------------------------------------------------- /data/wood_red.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/3d-tiles-samples-generator/2f79f88e572caf80759a1262c67d039c7c0ac530/data/wood_red.jpg -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Promise = require('bluebird'); 4 | const Cesium = require('cesium'); 5 | const child_process = require('child_process'); 6 | const gulp = require('gulp'); 7 | const Jasmine = require('jasmine'); 8 | const SpecReporter = require('jasmine-spec-reporter').SpecReporter; 9 | const path = require('path'); 10 | const yargs = require('yargs'); 11 | const fsExtra = require('fs-extra'); 12 | const open = require('open'); 13 | 14 | const defaultValue = Cesium.defaultValue; 15 | const defined = Cesium.defined; 16 | const argv = yargs.argv; 17 | 18 | // Add third-party node module binaries to the system path 19 | // since some tasks need to call them directly. 20 | const environmentSeparator = (process.platform === 'win32') ? ';' : ':'; 21 | const nodeBinaries = path.join(__dirname, 'node_modules', '.bin'); 22 | process.env.PATH += environmentSeparator + nodeBinaries; 23 | 24 | const specFiles = ['**/*.js', '!node_modules/**']; 25 | 26 | gulp.task('test', function (done) { 27 | const jasmine = new Jasmine(); 28 | jasmine.loadConfigFile('specs/jasmine.json'); 29 | jasmine.addReporter(new SpecReporter({ 30 | displaySuccessfulSpec: !defined(argv.suppressPassed) || !argv.suppressPassed 31 | })); 32 | jasmine.execute(); 33 | jasmine.onComplete(function (passed) { 34 | done(argv.failTaskOnError && !passed ? 1 : 0); 35 | }); 36 | }); 37 | 38 | gulp.task('test-watch', function () { 39 | gulp.watch(specFiles).on('change', function () { 40 | // We can't simply depend on the test task because Jasmine 41 | // does not like being run multiple times in the same process. 42 | try { 43 | child_process.execSync('jasmine JASMINE_CONFIG_PATH=specs/jasmine.json', { 44 | stdio: [process.stdin, process.stdout, process.stderr] 45 | }); 46 | } catch (exception) { 47 | console.log('Tests failed to execute.'); 48 | } 49 | }); 50 | }); 51 | 52 | gulp.task('coverage', function () { 53 | fsExtra.removeSync('coverage'); 54 | child_process.execSync('nyc' + 55 | ' --all' + 56 | ' --reporter=lcov' + 57 | ' --dir coverage' + 58 | ' -x "specs/**"' + 59 | ' -x "coverage/**"' + 60 | ' -x "gulpfile.js"' + 61 | ' -x "index.js"' + 62 | ' node_modules/jasmine/bin/jasmine.js' + 63 | ' JASMINE_CONFIG_PATH=specs/jasmine.json', { 64 | stdio: [process.stdin, process.stdout, process.stderr] 65 | }); 66 | open('coverage/lcov-report/index.html'); 67 | }); 68 | 69 | gulp.task('cloc', function() { 70 | let cmdLine; 71 | const clocPath = path.join('node_modules', 'cloc', 'lib', 'cloc'); 72 | 73 | //Run cloc on primary Source files only 74 | const source = new Promise(function(resolve, reject) { 75 | cmdLine = `perl ${ clocPath } --quiet --progress-rate=0` + 76 | ` lib/ bin/`; 77 | 78 | child_process.exec(cmdLine, function(error, stdout, stderr) { 79 | if (error) { 80 | console.log(stderr); 81 | return reject(error); 82 | } 83 | console.log('Source:'); 84 | console.log(stdout); 85 | resolve(); 86 | }); 87 | }); 88 | 89 | //If running cloc on source succeeded, also run it on the tests. 90 | return source.then(function() { 91 | return new Promise(function(resolve, reject) { 92 | cmdLine = `perl ${ clocPath } --quiet --progress-rate=0` + 93 | ` test/lib/ test/bin/`; 94 | child_process.exec(cmdLine, function(error, stdout, stderr) { 95 | if (error) { 96 | console.log(stderr); 97 | return reject(error); 98 | } 99 | console.log('Specs:'); 100 | console.log(stdout); 101 | resolve(); 102 | }); 103 | }); 104 | }); 105 | }); 106 | 107 | function getLicenseDataFromPackage(packageName, override) { 108 | override = defaultValue(override, defaultValue.EMPTY_OBJECT); 109 | const packagePath = path.join('node_modules', packageName, 'package.json'); 110 | 111 | if (!fsExtra.existsSync(packagePath)) { 112 | throw new Error(`Unable to find ${packageName} license information`); 113 | } 114 | 115 | const contents = fsExtra.readFileSync(packagePath); 116 | const packageJson = JSON.parse(contents); 117 | 118 | let licenseField = override.license; 119 | 120 | if (!licenseField) { 121 | licenseField = [packageJson.license]; 122 | } 123 | 124 | if (!licenseField && packageJson.licenses) { 125 | licenseField = packageJson.licenses; 126 | } 127 | 128 | if (!licenseField) { 129 | console.log(`No license found for ${packageName}`); 130 | licenseField = ['NONE']; 131 | } 132 | 133 | let version = packageJson.version; 134 | if (!packageJson.version) { 135 | console.log(`No version information found for ${packageName}`); 136 | version = 'NONE'; 137 | } 138 | 139 | return { 140 | name: packageName, 141 | license: licenseField, 142 | version: version, 143 | url: `https://www.npmjs.com/package/${packageName}`, 144 | notes: override.notes 145 | }; 146 | } 147 | 148 | function readThirdPartyExtraJson() { 149 | const path = 'ThirdParty.extra.json'; 150 | if (fsExtra.existsSync(path)) { 151 | const contents = fsExtra.readFileSync(path); 152 | return JSON.parse(contents); 153 | } 154 | return []; 155 | } 156 | 157 | gulp.task('generate-third-party', async function() { 158 | const packageJson = JSON.parse(fsExtra.readFileSync('package.json')); 159 | const thirdPartyExtraJson = readThirdPartyExtraJson(); 160 | 161 | const thirdPartyJson = []; 162 | 163 | const dependencies = packageJson.dependencies; 164 | for (const packageName in dependencies) { 165 | if (dependencies.hasOwnProperty(packageName)) { 166 | const override = thirdPartyExtraJson.find( 167 | (entry) => entry.name === packageName 168 | ); 169 | thirdPartyJson.push( 170 | getLicenseDataFromPackage(packageName, override) 171 | ); 172 | } 173 | } 174 | 175 | thirdPartyJson.sort(function (a, b) { 176 | const nameA = a.name.toLowerCase(); 177 | const nameB = b.name.toLowerCase(); 178 | if (nameA < nameB) { 179 | return -1; 180 | } 181 | if (nameA > nameB) { 182 | return 1; 183 | } 184 | return 0; 185 | }); 186 | 187 | fsExtra.writeFileSync( 188 | 'ThirdParty.json', 189 | JSON.stringify(thirdPartyJson, null, 2) 190 | ); 191 | }); 192 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | }; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | preset: 'ts-jest', 4 | clearMocks: true, 5 | coverageDirectory: 'coverage', 6 | testEnvironment: 'node', 7 | collectCoverageFrom: ['bin/**/*.ts', 'lib/**/*.ts', 'lib/**/*.js'], 8 | rootDir: '.', 9 | testMatch: [ 10 | '/specs/**/__tests__/**/*.[jt]s?(x)', 11 | '/test/**/*(*.)@(spec|test).[tj]s?(x)' 12 | ], 13 | globals: { 14 | 'ts-jest': { 15 | diagnostics: false, 16 | isolatedModules: true 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/Extensions.ts: -------------------------------------------------------------------------------- 1 | import { TilesetJson } from './tilesetJson'; 2 | 3 | export namespace Extensions { 4 | 5 | export type ExtensionObject = Pick 6 | /** 7 | * Add an extension to the list of extensions used for a tileset JSON. 8 | * @param {Object} tilesetJson The root tileset JSON object to which to add 9 | * the extension. 10 | * @param {String} extensionName The name of the extension to add. 11 | */ 12 | export function addExtensionsUsed( 13 | tilesetJson: TilesetJson, 14 | extensionName: string 15 | ) { 16 | if (tilesetJson.extensionsUsed == null) { 17 | tilesetJson.extensionsUsed = []; 18 | } 19 | 20 | tilesetJson.extensionsUsed.push(extensionName); 21 | } 22 | 23 | /** 24 | * Add an extension to the list of extensions required for a tileset JSON. 25 | * @param {Object} tilesetJson The root tileset JSON object to which to 26 | * add the extension. 27 | * @param {String} extensionName The name of the extension to add. 28 | */ 29 | export function addExtensionsRequired( 30 | tilesetJson: TilesetJson, 31 | extensionName: string 32 | ) { 33 | if (tilesetJson.extensionsRequired == null) { 34 | tilesetJson.extensionsRequired = []; 35 | } 36 | 37 | tilesetJson.extensionsRequired.push(extensionName); 38 | } 39 | 40 | /** 41 | * Add an extension to the extensions dictionary object for a JSON object. 42 | * @param {Object} tilesetJson The JSON object to which to add the extension. 43 | * @param {String} extensionName The name of the extension to add. 44 | * @param {*} extension The contents of the extension. 45 | */ 46 | export function addExtension( 47 | tilesetJson: TilesetJson, 48 | extensionName: string, 49 | extension: object 50 | ) { 51 | if (tilesetJson.extensions == null) { 52 | tilesetJson.extensions = {}; 53 | } 54 | 55 | tilesetJson.extensions[extensionName] = extension; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/Material.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A material that is applied to a mesh. 3 | * 4 | * @param {Object} [options] An object with the following properties: 5 | * @param {Array|String} [options.baseColor] The base color or base color texture path. 6 | * 7 | * @constructor 8 | */ 9 | 10 | export class Material { 11 | baseColor: number[]; 12 | 13 | // TODO: Original code combined rgbas with jpg uris, should refactor 14 | // this too. 15 | constructor(baseColor: number[] = [0.5, 0.5, 0.5, 1.0]) { 16 | this.baseColor = baseColor; 17 | } 18 | 19 | /** 20 | * Creates a Material from a glTF material. This utility is designed only for simple glTFs like those in the data folder. 21 | * 22 | * @param {Object} material The glTF material. 23 | * @returns {Material} The material. 24 | */ 25 | 26 | static fromGltf(material: any): Material { 27 | return new Material(material.pbrMetallicRoughness.baseColorFactor); 28 | } 29 | } 30 | 31 | export class TexturedMaterial { 32 | // TODO: This MUST be named baseColor for now. Original version of this 33 | // code didn't discriminate between RGBA / TexturePath coordinates 34 | // createGltf.js will inspect the type of `baseColor` to determine 35 | // what to do with this object. Needs to be refactored later. 36 | baseColor: string; 37 | 38 | constructor(baseColor: string) { 39 | this.baseColor = baseColor; 40 | } 41 | } -------------------------------------------------------------------------------- /lib/arguments.ts: -------------------------------------------------------------------------------- 1 | export interface GeneratorArgs { 2 | use3dTilesNext: boolean; 3 | useGlb: boolean; 4 | gltfConversionOptions: { resourceDirectory: string }; 5 | prettyJson: boolean; 6 | gzip: boolean; 7 | geometricError: number; 8 | versionNumber: string; 9 | } 10 | -------------------------------------------------------------------------------- /lib/atLeastN.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/59003541 2 | export type AtLeastOne = { [K in keyof T]: { [K2 in K]: T[K] } }[keyof T] & 3 | Partial; 4 | -------------------------------------------------------------------------------- /lib/attribute.ts: -------------------------------------------------------------------------------- 1 | import { GltfType } from './gltfType'; 2 | 3 | export interface Attribute { 4 | buffer: Buffer; 5 | propertyName: string; 6 | byteOffset?: number; 7 | byteAlignment?: number; 8 | componentType: GLenum; 9 | count: number; 10 | max: number[]; 11 | min: number[]; 12 | type: GltfType; 13 | } 14 | -------------------------------------------------------------------------------- /lib/bufferUtil.ts: -------------------------------------------------------------------------------- 1 | import { FLOAT32_SIZE_BYTES, UINT16_SIZE_BYTES } from './typeSize'; 2 | 3 | export function bufferToUint16Array( 4 | buffer: Buffer, 5 | byteOffset: number, 6 | length: number 7 | ) { 8 | const uint16Array = new Uint16Array(length); 9 | for (let i = 0; i < length; ++i) { 10 | uint16Array[i] = buffer.readUInt16LE( 11 | byteOffset + i * UINT16_SIZE_BYTES 12 | ); 13 | } 14 | return uint16Array; 15 | } 16 | 17 | export function bufferToFloat32Array( 18 | buffer: Buffer, 19 | byteOffset: number, 20 | length: number 21 | ) { 22 | const float32Array = new Float32Array(length); 23 | for (let i = 0; i < length; ++i) { 24 | float32Array[i] = buffer.readFloatLE( 25 | byteOffset + i * FLOAT32_SIZE_BYTES 26 | ); 27 | } 28 | return float32Array; 29 | } 30 | -------------------------------------------------------------------------------- /lib/calculateBufferPadding.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Typed version of `getBufferPadded.js 3 | * @param {Buffer} buffer The buffer. 4 | * @param {Number} [byteOffset=0] The byte offset on which the buffer starts. 5 | * @returns {Buffer} The padded buffer. 6 | * @todo Delete `getBufferPadded.js` once all its callers are converted to 7 | * .ts 8 | */ 9 | 10 | export function calculateBufferPadding(buffer: Buffer, byteOffset = 0): Buffer { 11 | var boundary = 8; 12 | var byteLength = buffer.length; 13 | var remainder = (byteOffset + byteLength) % boundary; 14 | var padding = (remainder === 0) ? 0 : boundary - remainder; 15 | var emptyBuffer = Buffer.alloc(padding); 16 | return Buffer.concat([buffer, emptyBuffer]); 17 | } 18 | -------------------------------------------------------------------------------- /lib/calculateFilenameExt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculates the filename extension suffix based off flags 3 | * @param {Boolean} use3dTilesNext If the extension is for use3dTilesNext 4 | * @param {Boolean} useGlb If the extension is for 3d-tiles-next, and we want 5 | * to use glb. 6 | * @param {String} defaultExt If use3dTilesNext and useGlb are both false, 7 | * then use defaultExt 8 | */ 9 | export function calculateFilenameExt( 10 | use3dTilesNext: boolean, 11 | useGlb: boolean, 12 | defaultExt: string 13 | ) { 14 | if (use3dTilesNext && !useGlb) { 15 | return '.gltf'; 16 | } else if (useGlb) { 17 | return '.glb'; 18 | } 19 | return defaultExt; 20 | } 21 | -------------------------------------------------------------------------------- /lib/calculateMinMax.ts: -------------------------------------------------------------------------------- 1 | export type MinMax = { min: number[]; max: number[] }; 2 | 3 | /** 4 | * Type safe version of `getMinMax.js` 5 | * @param array Elements to check 6 | * @param components Size of each element in the array (1,2,3,4) 7 | * @param start where to begin iteration in the array (inclusive) 8 | * @param length Total number of elements to check 9 | * @todo Delete getMinMax.js once its callers are converted to .ts 10 | */ 11 | 12 | export function calculateMinMax( 13 | array: number[], 14 | components: number, 15 | start: number = 0, 16 | length = array.length 17 | ): MinMax { 18 | const min = new Array(components).fill(+Infinity); 19 | const max = new Array(components).fill(-Infinity); 20 | const count = length / components; 21 | for (let i = 0; i < count; ++i) { 22 | for (let j = 0; j < components; ++j) { 23 | const index = start + i * components + j; 24 | const value = array[index]; 25 | min[j] = Math.min(min[j], value); 26 | max[j] = Math.max(max[j], value); 27 | } 28 | } 29 | 30 | return { min: min, max: max }; 31 | } 32 | -------------------------------------------------------------------------------- /lib/colorTypes.ts: -------------------------------------------------------------------------------- 1 | export enum BaseColorType { 2 | White, 3 | Color, 4 | Texture 5 | } 6 | 7 | export enum TranslucencyType { 8 | Opaque, 9 | Translucent, 10 | Mix 11 | } -------------------------------------------------------------------------------- /lib/compositeSamplesNext.ts: -------------------------------------------------------------------------------- 1 | import { GeneratorArgs } from './arguments'; 2 | import { toCamelCase } from './utility'; 3 | import { getTilesetOpts } from './tilesetJson'; 4 | import { InstanceTileUtils } from './instanceUtilsNext'; 5 | import { addBinaryBuffers } from './gltfUtil'; 6 | import { getGltfFromGlbUri } from './gltfFromUri'; 7 | import { instancesRegion } from './constants'; 8 | import { FeatureTableUtils } from './featureMetatableUtilsNext'; 9 | import { createBuildings } from './createBuilding'; 10 | import { Mesh } from './Mesh'; 11 | import { addBatchedMeshToGltf } from './addMeshToGltf'; 12 | import { generateBuildingBatchTable } from './createBuildingsTile'; 13 | import { createEXTMeshInstancingExtension } from './createEXTMeshInstancing'; 14 | import { FeatureMetadata } from './featureMetadata'; 15 | import { createTilesetJsonSingle } from './createTilesetJsonSingle'; 16 | import { writeTilesetAndTile } from './ioUtil'; 17 | import path = require('path'); 18 | import getProperties = require('./getProperties'); 19 | 20 | export namespace CompositeSamplesNext { 21 | export async function createComposite(args: GeneratorArgs) { 22 | // i3dm 23 | const opts = InstanceTileUtils.getDefaultTileOptions( 24 | 'output/Composite' 25 | ); 26 | 27 | const i3dmHeights = new Array(opts.instancesLength).fill( 28 | opts.modelSize 29 | ); 30 | 31 | const gltf = await getGltfFromGlbUri( 32 | opts.instancesUri, 33 | args.gltfConversionOptions 34 | ); 35 | 36 | const positions = InstanceTileUtils.getPositions( 37 | opts.instancesLength, 38 | opts.tileWidth, 39 | opts.modelSize, 40 | opts.transform 41 | ); 42 | 43 | // add EXT_mesh_gpu_instancing extension to i3dm mesh 44 | const positionAccessorIndex = gltf.accessors.length; 45 | addBinaryBuffers(gltf, positions); 46 | createEXTMeshInstancingExtension(gltf, gltf.nodes[0], { 47 | attributes: { 48 | TRANSLATION: positionAccessorIndex 49 | } 50 | }); 51 | 52 | // add CESIUM_3dtiles_metadata_feature extension to i3dm mesh 53 | const prim = gltf.meshes[0].primitives[0]; 54 | FeatureMetadata.updateExtensionUsed(gltf); 55 | FeatureMetadata.addFeatureLayer(prim, { 56 | featureTable: 0, 57 | instanceStride: 1, 58 | vertexAttribute: { 59 | implicit: { 60 | increment: 0, 61 | start: 0 62 | } 63 | } 64 | }); 65 | 66 | FeatureMetadata.addFeatureTable(gltf, { 67 | featureCount: opts.instancesLength, 68 | properties: { 69 | Height: { 70 | values: i3dmHeights 71 | } 72 | } 73 | } 74 | ); 75 | 76 | // b3dm 77 | const transform = FeatureTableUtils.getDefaultTransform(); 78 | const buildingOptions = FeatureTableUtils.getDefaultBuildingGenerationOptions(); 79 | const buildings = createBuildings(buildingOptions); 80 | const batchedMesh = Mesh.batch( 81 | FeatureTableUtils.createMeshes(transform, buildings, false) 82 | ); 83 | 84 | addBatchedMeshToGltf(gltf, batchedMesh); 85 | 86 | const rtc = batchedMesh.center; 87 | const buildingTable = generateBuildingBatchTable(buildings); 88 | // explicit ids are unnecessary for the feature_metadata extension 89 | // but required for legacy b3dm 90 | delete buildingTable.id; 91 | 92 | // add CESIUM_3dtiles_feature_metadata extension 93 | gltf.extensions.CESIUM_3dtiles_feature_metadata.featureTables.push({ 94 | featureCount: buildingTable.Height.length, 95 | properties: { 96 | Height: { values: buildingTable.Height }, 97 | Longitude: { values: buildingTable.Longitude }, 98 | Latitude: { values: buildingTable.Latitude } 99 | } 100 | }); 101 | 102 | // setup the primitives.extension 103 | gltf.meshes[1].primitives[0].extensions = { 104 | CESIUM_3dtiles_feature_metadata: { 105 | featureLayers: [ 106 | { 107 | featureTable: 1, 108 | instanceStride: 1, 109 | vertexAttribute: { 110 | implicit: { 111 | increment: 1, 112 | start: 0 113 | } 114 | } 115 | } 116 | ] 117 | } 118 | }; 119 | 120 | // override nodes to use rtcCenter for the b3dm mesh 121 | gltf.nodes[1] = { 122 | name: 'RTC_CENTER', 123 | mesh: gltf.nodes[1].mesh!, 124 | translation: [rtc.x, rtc.y, rtc.z] 125 | }; 126 | 127 | // 128 | // common 129 | // 130 | 131 | gltf.scenes[0].nodes.push(1); 132 | 133 | const outputFolder = 'Composite'; 134 | const ext = args.useGlb ? '.glb' : '.gltf'; 135 | const tileFilename = toCamelCase(outputFolder) + ext; 136 | const rootDir = 'output/Composite'; 137 | const fullPath = path.join(rootDir, outputFolder); 138 | const tilesetOpts = getTilesetOpts( 139 | tileFilename, 140 | args.geometricError, 141 | args.versionNumber, 142 | instancesRegion 143 | ); 144 | 145 | const compositeFeatureTable = { 146 | Longitude: buildingTable.Longitude, 147 | Latitude: buildingTable.Latitude, 148 | Height: [...i3dmHeights, ...buildingTable.Height] 149 | }; 150 | 151 | tilesetOpts.properties = getProperties(compositeFeatureTable); 152 | let tilesetJson = createTilesetJsonSingle(tilesetOpts); 153 | 154 | await writeTilesetAndTile( 155 | fullPath, 156 | tileFilename, 157 | tilesetJson, 158 | gltf, 159 | args 160 | ); 161 | } 162 | 163 | export async function createCompositeOfInstanced(args: GeneratorArgs) { 164 | const opts = InstanceTileUtils.getDefaultTileOptions( 165 | 'output/Composite' 166 | ); 167 | 168 | const i3dmHeights = new Array(opts.instancesLength).fill( 169 | opts.modelSize 170 | ); 171 | 172 | const gltf = await getGltfFromGlbUri( 173 | opts.instancesUri, 174 | args.gltfConversionOptions 175 | ); 176 | 177 | const positions1 = InstanceTileUtils.getPositions( 178 | opts.instancesLength, 179 | opts.tileWidth, 180 | opts.modelSize, 181 | opts.transform 182 | ); 183 | 184 | const positions2 = InstanceTileUtils.getPositions( 185 | opts.instancesLength, 186 | opts.tileWidth, 187 | opts.modelSize, 188 | opts.transform 189 | ); 190 | 191 | const positionAccessorIndex = gltf.accessors.length; 192 | addBinaryBuffers(gltf, positions1, positions2); 193 | createEXTMeshInstancingExtension(gltf, gltf.nodes[0], { 194 | attributes: { 195 | TRANSLATION: positionAccessorIndex 196 | } 197 | }); 198 | 199 | // add CESIUM_3dtiles_metadata_feature extension to i3dm mesh 200 | const prim = gltf.meshes[0].primitives[0]; 201 | FeatureMetadata.updateExtensionUsed(gltf); 202 | FeatureMetadata.addFeatureLayer(prim, { 203 | featureTable: 0, 204 | instanceStride: 1, 205 | vertexAttribute: { 206 | implicit: { 207 | increment: 0, 208 | start: 0 209 | } 210 | } 211 | }); 212 | 213 | FeatureMetadata.addFeatureTable(gltf, { 214 | featureCount: opts.instancesLength, 215 | properties: { 216 | Height: { 217 | values: i3dmHeights 218 | } 219 | } 220 | }); 221 | 222 | const ext = args.useGlb ? '.glb' : '.gltf'; 223 | const outputFolder = 'CompositeOfInstanced'; 224 | const tileFilename = toCamelCase(outputFolder) + ext; 225 | const fullPath = path.join(opts.rootDir, outputFolder); 226 | 227 | const tilesetOpts = getTilesetOpts( 228 | tileFilename, 229 | args.geometricError, 230 | args.versionNumber, 231 | instancesRegion 232 | ); 233 | 234 | tilesetOpts.properties = getProperties({ 235 | Height: i3dmHeights 236 | }); 237 | let tilesetJson = createTilesetJsonSingle(tilesetOpts); 238 | 239 | await writeTilesetAndTile( 240 | fullPath, 241 | tileFilename, 242 | tilesetJson, 243 | gltf, 244 | args 245 | ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | const Cesium = require('cesium'); 2 | const clone = Cesium.clone; 3 | const path = require('path'); 4 | import { BaseColorType, TranslucencyType } from './colorTypes'; 5 | import { Building } from './createBuilding'; 6 | 7 | export const gltfConversionOptions = { resourceDirectory: path.join(__dirname, '../') }; 8 | export const util = require('../lib/utility'); 9 | export const wgs84Transform = util.wgs84Transform; 10 | export const metersToLongitude = util.metersToLongitude; 11 | export const metersToLatitude = util.metersToLatitude; 12 | 13 | export const prettyJson = true; 14 | export const gzip = false; 15 | 16 | export const outputDirectory = 'output'; 17 | 18 | export const longitude = -1.31968; 19 | export const latitude = 0.698874; 20 | export const tileWidth = 200.0; 21 | 22 | export const longitudeExtent = metersToLongitude(tileWidth, latitude); 23 | export const latitudeExtent = metersToLatitude(tileWidth); 24 | 25 | export const west = longitude - longitudeExtent / 2.0; 26 | export const south = latitude - latitudeExtent / 2.0; 27 | export const east = longitude + longitudeExtent / 2.0; 28 | export const north = latitude + latitudeExtent / 2.0; 29 | 30 | export type BuildingTemplate = { 31 | uniform: boolean; 32 | numberOfBuildings: number; 33 | tileWidth: number; 34 | averageWidth: number; 35 | averageHeight: number; 36 | baseColorType: BaseColorType; 37 | translucencyType: TranslucencyType; 38 | longitude: number; 39 | latitude: number; 40 | }; 41 | 42 | export const buildingTemplate = { 43 | numberOfBuildings: 10, 44 | tileWidth: tileWidth, 45 | averageWidth: 8.0, 46 | averageHeight: 10.0, 47 | baseColorType: BaseColorType.White, 48 | translucencyType: TranslucencyType.Opaque, 49 | longitude: longitude, 50 | latitude: latitude 51 | }; 52 | 53 | // height is 0.0 because the base of building models is at the origin 54 | export const buildingsTransform = wgs84Transform(longitude, latitude, 0.0); 55 | export const buildingsCenter = [ 56 | buildingsTransform[12], 57 | buildingsTransform[13], 58 | buildingsTransform[14] 59 | ]; 60 | 61 | // Small buildings 62 | export const smallGeometricError = 70.0; // Estimated 63 | export const smallHeight = 20.0; // Estimated 64 | export const smallRegion = [west, south, east, north, 0.0, smallHeight]; 65 | export const smallRadius = tileWidth * 0.707107; 66 | export const smallSphere = [ 67 | buildingsCenter[0], 68 | buildingsCenter[1], 69 | buildingsCenter[2] + smallHeight / 2.0, 70 | smallRadius 71 | ]; 72 | export const smallSphereLocal = [0.0, 0.0, smallHeight / 2.0, smallRadius]; 73 | export const smallBoxLocal = [ 74 | 0.0, 75 | 0.0, 76 | smallHeight / 2.0, // center 77 | tileWidth / 2.0, 78 | 0.0, 79 | 0.0, // width 80 | 0.0, 81 | tileWidth / 2.0, 82 | 0.0, // depth 83 | 0.0, 84 | 0.0, 85 | smallHeight / 2.0 // height 86 | ]; 87 | 88 | // Large buildings 89 | export const largeGeometricError = 240.0; // Estimated 90 | export const largeHeight = 88.0; // Estimated 91 | 92 | // Point cloud 93 | export const pointsLength = 1000; 94 | export const pointCloudTileWidth = 10.0; 95 | export const pointCloudRadius = pointCloudTileWidth / 2.0; 96 | export const pointCloudTransform = wgs84Transform( 97 | longitude, 98 | latitude, 99 | pointCloudRadius 100 | ); 101 | export const pointCloudGeometricError = 1.732 * pointCloudTileWidth; // Diagonal of the point cloud box 102 | export const pointCloudCenter = [ 103 | pointCloudTransform[12], 104 | pointCloudTransform[13], 105 | pointCloudTransform[14] 106 | ]; 107 | export const pointCloudSphere = [ 108 | pointCloudCenter[0], 109 | pointCloudCenter[1], 110 | pointCloudCenter[2], 111 | pointCloudRadius 112 | ]; 113 | export const pointCloudSphereLocal = [0.0, 0.0, 0.0, pointCloudRadius]; 114 | 115 | // Instances 116 | export const instancesLength = 25; 117 | export const instancesGeometricError = 70.0; // Estimated 118 | export const instancesTileWidth = tileWidth; 119 | export const instancesUri = 'data/box.glb'; // Model's center is at the origin (and for below) 120 | export const instancesRedUri = 'data/red_box.glb'; 121 | export const instancesTexturedUri = 'data/textured_box.glb'; 122 | export const instancesAnimatedUri = 'data/animated_box.glb'; 123 | export const instancesModelSize = 20.0; 124 | export const instancesHeight = instancesModelSize + 10.0; // Just a little extra padding at the top for aiding Cesium tests 125 | export const instancesTransform = wgs84Transform( 126 | longitude, 127 | latitude, 128 | instancesModelSize / 2.0 129 | ); 130 | export const instancesRegion = [west, south, east, north, 0.0, instancesHeight]; 131 | export const instancesBoxLocal = [ 132 | 0.0, 133 | 0.0, 134 | 0.0, // center 135 | instancesTileWidth / 2.0, 136 | 0.0, 137 | 0.0, // width 138 | 0.0, 139 | instancesTileWidth / 2.0, 140 | 0.0, // depth 141 | 0.0, 142 | 0.0, 143 | instancesHeight / 2.0 // height 144 | ]; 145 | 146 | // Composite 147 | export const compositeRegion = instancesRegion; 148 | export const compositeGeometricError = instancesGeometricError; 149 | 150 | // City Tileset 151 | export const parentRegion = [ 152 | longitude - longitudeExtent, 153 | latitude - latitudeExtent, 154 | longitude + longitudeExtent, 155 | latitude + latitudeExtent, 156 | 0.0, 157 | largeHeight 158 | ]; 159 | export const parentContentRegion = [ 160 | longitude - longitudeExtent / 2.0, 161 | latitude - latitudeExtent / 2.0, 162 | longitude + longitudeExtent / 2.0, 163 | latitude + latitudeExtent / 2.0, 164 | 0.0, 165 | largeHeight 166 | ]; 167 | export const parentOptions = clone(buildingTemplate); 168 | parentOptions.averageWidth = 20.0; 169 | parentOptions.averageHeight = 82.0; 170 | parentOptions.longitude = longitude; 171 | parentOptions.latitude = latitude; 172 | 173 | export type TileOptions = { 174 | buildingOptions: BuildingTemplate, 175 | createBatchTable: boolean, 176 | transform: object; 177 | relativeToCenter: boolean; 178 | } 179 | 180 | export const parentTileOptions: TileOptions = { 181 | buildingOptions: parentOptions, 182 | createBatchTable: true, 183 | transform: buildingsTransform, 184 | relativeToCenter: true 185 | }; 186 | 187 | export const childrenRegion = [ 188 | longitude - longitudeExtent, 189 | latitude - latitudeExtent, 190 | longitude + longitudeExtent, 191 | latitude + latitudeExtent, 192 | 0.0, 193 | smallHeight 194 | ]; 195 | 196 | export const llRegion = [ 197 | longitude - longitudeExtent, 198 | latitude - latitudeExtent, 199 | longitude, 200 | latitude, 201 | 0.0, 202 | smallHeight 203 | ]; 204 | export const llLongitude = longitude - longitudeExtent / 2.0; 205 | export const llLatitude = latitude - latitudeExtent / 2.0; 206 | export const llTransform = wgs84Transform(llLongitude, llLatitude); 207 | export const llOptions = clone(buildingTemplate); 208 | llOptions.longitude = llLongitude; 209 | llOptions.latitude = llLatitude; 210 | llOptions.seed = 0; 211 | export const llTileOptions = { 212 | buildingOptions: llOptions, 213 | createBatchTable: true, 214 | transform: llTransform, 215 | relativeToCenter: true 216 | }; 217 | 218 | export const lrRegion = [ 219 | longitude, 220 | latitude - latitudeExtent, 221 | longitude + longitudeExtent, 222 | latitude, 223 | 0.0, 224 | smallHeight 225 | ]; 226 | export const lrLongitude = longitude + longitudeExtent / 2.0; 227 | export const lrLatitude = latitude - latitudeExtent / 2.0; 228 | export const lrTransform = wgs84Transform(lrLongitude, lrLatitude); 229 | export const lrOptions = clone(buildingTemplate); 230 | lrOptions.longitude = lrLongitude; 231 | lrOptions.latitude = lrLatitude; 232 | lrOptions.seed = 1; 233 | export const lrTileOptions = { 234 | buildingOptions: lrOptions, 235 | createBatchTable: true, 236 | transform: lrTransform, 237 | relativeToCenter: true 238 | }; 239 | 240 | export const urRegion = [ 241 | longitude, 242 | latitude, 243 | longitude + longitudeExtent, 244 | latitude + latitudeExtent, 245 | 0.0, 246 | smallHeight 247 | ]; 248 | export const urLongitude = longitude + longitudeExtent / 2.0; 249 | export const urLatitude = latitude + latitudeExtent / 2.0; 250 | export const urTransform = wgs84Transform(urLongitude, urLatitude); 251 | export const urOptions = clone(buildingTemplate); 252 | urOptions.longitude = urLongitude; 253 | urOptions.latitude = urLatitude; 254 | urOptions.seed = 2; 255 | export const urTileOptions = { 256 | buildingOptions: urOptions, 257 | createBatchTable: true, 258 | transform: urTransform, 259 | relativeToCenter: true 260 | }; 261 | 262 | export const ulRegion = [ 263 | longitude - longitudeExtent, 264 | latitude, 265 | longitude, 266 | latitude + latitudeExtent, 267 | 0.0, 268 | smallHeight 269 | ]; 270 | export const ulLongitude = longitude - longitudeExtent / 2.0; 271 | export const ulLatitude = latitude + latitudeExtent / 2.0; 272 | export const ulTransform = wgs84Transform(ulLongitude, ulLatitude); 273 | export const ulOptions = clone(buildingTemplate); 274 | ulOptions.longitude = ulLongitude; 275 | ulOptions.latitude = ulLatitude; 276 | ulOptions.seed = 3; 277 | export const ulTileOptions = { 278 | buildingOptions: ulOptions, 279 | createBatchTable: true, 280 | transform: ulTransform, 281 | relativeToCenter: true 282 | }; 283 | 284 | 285 | 286 | // Models are z-up, so add a z-up to y-up transform. 287 | // The glTF spec defines the y-axis as up, so this is the default behavior. 288 | // In CesiumJS a y-up to z-up transform is applied later so that the glTF and 289 | // 3D Tiles coordinate systems are consistent 290 | export const rootMatrix = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1]; 291 | 292 | 293 | // 3d-tiles-next 294 | export const tilesNextTilesetJsonVersion = '1.1'; -------------------------------------------------------------------------------- /lib/createB3dm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | const getBufferPadded = require('./getBufferPadded'); 4 | const getJsonBufferPadded = require('./getJsonBufferPadded'); 5 | 6 | const defaultValue = Cesium.defaultValue; 7 | 8 | module.exports = createB3dm; 9 | 10 | /** 11 | * Create a Batched 3D Model (b3dm) tile from a binary glTF and per-feature metadata. 12 | * 13 | * @param {Object} options An object with the following properties: 14 | * @param {Buffer} options.glb The binary glTF buffer. 15 | * @param {Object} [options.featureTableJson] Feature table JSON. 16 | * @param {Buffer} [options.featureTableBinary] Feature table binary. 17 | * @param {Object} [options.batchTableJson] Batch table describing the per-feature metadata. 18 | * @param {Buffer} [options.batchTableBinary] The batch table binary. 19 | * @param {Boolean} [options.deprecated1=false] Save the b3dm with the deprecated 20-byte header. 20 | * @param {Boolean} [options.deprecated2=false] Save the b3dm with the deprecated 24-byte header. 21 | * @returns {Buffer} The generated b3dm tile buffer. 22 | */ 23 | function createB3dm(options) { 24 | const glb = options.glb; 25 | const defaultFeatureTable = { 26 | BATCH_LENGTH : 0 27 | }; 28 | const featureTableJson = defaultValue(options.featureTableJson, defaultFeatureTable); 29 | const batchLength = featureTableJson.BATCH_LENGTH; 30 | 31 | const headerByteLength = 28; 32 | const featureTableJsonBuffer = getJsonBufferPadded(featureTableJson, headerByteLength); 33 | const featureTableBinary = getBufferPadded(options.featureTableBinary); 34 | const batchTableJsonBuffer = getJsonBufferPadded(options.batchTableJson); 35 | const batchTableBinary = getBufferPadded(options.batchTableBinary); 36 | 37 | const deprecated1 = defaultValue(options.deprecated1, false); 38 | const deprecated2 = defaultValue(options.deprecated2, false); 39 | 40 | if (deprecated1) { 41 | return createB3dmDeprecated1(glb, batchLength, batchTableJsonBuffer); 42 | } else if (deprecated2) { 43 | return createB3dmDeprecated2(glb, batchLength, batchTableJsonBuffer, batchTableBinary); 44 | } 45 | 46 | return createB3dmCurrent(glb, featureTableJsonBuffer, featureTableBinary, batchTableJsonBuffer, batchTableBinary); 47 | } 48 | 49 | function createB3dmCurrent(glb, featureTableJson, featureTableBinary, batchTableJson, batchTableBinary) { 50 | 51 | const glbPadded = getBufferPadded(glb); 52 | 53 | const version = 1; 54 | const headerByteLength = 28; 55 | const featureTableJsonByteLength = featureTableJson.length; 56 | const featureTableBinaryByteLength = featureTableBinary.length; 57 | const batchTableJsonByteLength = batchTableJson.length; 58 | const batchTableBinaryByteLength = batchTableBinary.length; 59 | const gltfByteLength = glbPadded.length; 60 | const byteLength = headerByteLength + featureTableJsonByteLength + featureTableBinaryByteLength + batchTableJsonByteLength + batchTableBinaryByteLength + gltfByteLength; 61 | 62 | const header = Buffer.alloc(headerByteLength); 63 | header.write('b3dm', 0); 64 | header.writeUInt32LE(version, 4); 65 | header.writeUInt32LE(byteLength, 8); 66 | header.writeUInt32LE(featureTableJsonByteLength, 12); 67 | header.writeUInt32LE(featureTableBinaryByteLength, 16); 68 | header.writeUInt32LE(batchTableJsonByteLength, 20); 69 | header.writeUInt32LE(batchTableBinaryByteLength, 24); 70 | 71 | return Buffer.concat([header, featureTableJson, featureTableBinary, batchTableJson, batchTableBinary, glbPadded]); 72 | } 73 | 74 | function createB3dmDeprecated1(glb, batchLength, batchTableJson) { 75 | const version = 1; 76 | const headerByteLength = 20; 77 | const batchTableJsonByteLength = batchTableJson.length; 78 | const gltfByteLength = glb.length; 79 | const byteLength = headerByteLength + batchTableJsonByteLength + gltfByteLength; 80 | 81 | const header = Buffer.alloc(headerByteLength); 82 | header.write('b3dm', 0); 83 | header.writeUInt32LE(version, 4); 84 | header.writeUInt32LE(byteLength, 8); 85 | header.writeUInt32LE(batchLength, 12); 86 | header.writeUInt32LE(batchTableJsonByteLength, 16); 87 | 88 | return Buffer.concat([header, batchTableJson, glb]); 89 | } 90 | 91 | function createB3dmDeprecated2(glb, batchLength, batchTableJson, batchTableBinary) { 92 | const version = 1; 93 | const headerByteLength = 24; 94 | const batchTableJsonByteLength = batchTableJson.length; 95 | const batchTableBinaryByteLength = batchTableBinary.length; 96 | const gltfByteLength = glb.length; 97 | const byteLength = headerByteLength + batchTableJsonByteLength + batchTableBinaryByteLength + gltfByteLength; 98 | 99 | const header = Buffer.alloc(headerByteLength); 100 | header.write('b3dm', 0); 101 | header.writeUInt32LE(version, 4); 102 | header.writeUInt32LE(byteLength, 8); 103 | header.writeUInt32LE(batchTableJsonByteLength, 12); 104 | header.writeUInt32LE(batchTableBinaryByteLength, 16); 105 | header.writeUInt32LE(batchLength, 20); 106 | 107 | return Buffer.concat([header, batchTableJson, batchTableBinary, glb]); 108 | } 109 | -------------------------------------------------------------------------------- /lib/createByteBuffer.ts: -------------------------------------------------------------------------------- 1 | import { FLOAT32_SIZE_BYTES, UINT8_SIZE_BYTES, UINT16_SIZE_BYTES } from './typeSize'; 2 | 3 | export function createUInt16Buffer(items: number[]) { 4 | const out = Buffer.alloc(items.length * UINT16_SIZE_BYTES); 5 | for (let i = 0; i < items.length; ++i) { 6 | out.writeUInt16LE(items[i], i * UINT16_SIZE_BYTES); 7 | } 8 | return out; 9 | } 10 | 11 | export function createUInt8Buffer(items: number[]) { 12 | const out = Buffer.alloc(items.length * UINT8_SIZE_BYTES); 13 | for (let i = 0; i < items.length; ++i) { 14 | out.writeUInt8(items[i], i * UINT8_SIZE_BYTES); 15 | } 16 | return out; 17 | } 18 | 19 | export function createFloat32Buffer(items: number[]) { 20 | const out = Buffer.alloc(items.length * FLOAT32_SIZE_BYTES); 21 | for (let i = 0; i < items.length; ++i) { 22 | out.writeFloatLE(items[i], i * FLOAT32_SIZE_BYTES); 23 | } 24 | return out; 25 | } 26 | -------------------------------------------------------------------------------- /lib/createCmpt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const getBufferPadded = require('./getBufferPadded'); 3 | 4 | module.exports = createCmpt; 5 | 6 | /** 7 | * Create a Composite (cmpt) tile from a set of tiles. 8 | * 9 | * @param {Buffer[]} tiles An array of buffers holding tile data. 10 | * @returns {Buffer} The generated cmpt tile buffer. 11 | */ 12 | function createCmpt(tiles) { 13 | let byteLength = 0; 14 | const buffers = []; 15 | const tilesLength = tiles.length; 16 | for (let i = 0; i < tilesLength; i++) { 17 | const tile = getBufferPadded(tiles[i]); 18 | const tileByteLength = tile.length; 19 | tile.writeUInt32LE(tileByteLength, 8); // Edit the tile's byte length 20 | buffers.push(tile); 21 | byteLength += tileByteLength; 22 | } 23 | 24 | const version = 1; 25 | const headerByteLength = 16; 26 | byteLength += headerByteLength; 27 | 28 | const header = Buffer.alloc(headerByteLength); 29 | header.write('cmpt', 0); 30 | header.writeUInt32LE(version, 4); 31 | header.writeUInt32LE(byteLength, 8); 32 | header.writeUInt32LE(tilesLength, 12); 33 | 34 | buffers.unshift(header); // Add header first 35 | 36 | return Buffer.concat(buffers); 37 | } 38 | -------------------------------------------------------------------------------- /lib/createConstantAttribute.ts: -------------------------------------------------------------------------------- 1 | import { Attribute } from './attribute'; 2 | import { UINT32_SIZE_BYTES } from './typeSize'; 3 | import { GltfComponentType, GltfType } from './gltfType'; 4 | 5 | export function createConstantAttributeLEU32( 6 | name: string, 7 | constant: number, 8 | len: number 9 | ): Attribute { 10 | const buffer = Buffer.alloc(len * UINT32_SIZE_BYTES); 11 | for (let i = 0; i < len; ++i) { 12 | buffer.writeUInt32LE(constant, i * UINT32_SIZE_BYTES); 13 | } 14 | 15 | return { 16 | buffer: buffer, 17 | byteOffset: 0, 18 | componentType: GltfComponentType.UNSIGNED_INT, 19 | type: GltfType.SCALAR, 20 | count: len, 21 | min: [constant], 22 | max: [constant], 23 | propertyName: name, 24 | byteAlignment: 1 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /lib/createEXTMeshInstancing.ts: -------------------------------------------------------------------------------- 1 | import { Gltf, GltfNode } from './gltfType'; 2 | import { AtLeastOne } from './atLeastN'; 3 | 4 | const extensionName = 'EXT_mesh_gpu_instancing'; 5 | 6 | export interface ExtMeshGpuInstancing { 7 | attributes: AtLeastOne<{ 8 | TRANSLATION?: number; 9 | ROTATION?: number; 10 | SCALE?: number; 11 | }>; 12 | } 13 | 14 | export function createEXTMeshInstancingExtension( 15 | gltf: Gltf, 16 | node: GltfNode, 17 | extMeshGpuInstancing: ExtMeshGpuInstancing 18 | ) { 19 | if (gltf.extensionsUsed == null) { 20 | gltf.extensionsUsed = []; 21 | } 22 | gltf.extensionsUsed.push(extensionName); 23 | 24 | if (node.extensions == null) { 25 | node.extensions = {}; 26 | } 27 | 28 | node.extensions.EXT_mesh_gpu_instancing = extMeshGpuInstancing; 29 | } 30 | -------------------------------------------------------------------------------- /lib/createFeatureHierarchySubExtension.ts: -------------------------------------------------------------------------------- 1 | import { Gltf, GltfAccessor, GLenumName } from './gltfType'; 2 | import { 3 | FeatureHierarchyClass, 4 | CompositeAccessorBufferView 5 | } from './featureHierarchyClass'; 6 | 7 | export type FeatureHierarchyExtIds = { values: number[] }; 8 | export type FeatureHierarchyExtValues = { 9 | values: (number | string | boolean)[]; 10 | }; 11 | export type FeatureHierarchyExtAccessor = { accessor: number }; 12 | export type FeatureHierarchyExtValuesOrAccessor = 13 | | FeatureHierarchyExtValues 14 | | FeatureHierarchyExtAccessor; 15 | export type FeatureHierarchyExtIdsOrAccessor = 16 | | FeatureHierarchyExtIds 17 | | FeatureHierarchyExtAccessor; 18 | 19 | export type FeatureHiearchyExtProperties = { 20 | [propertyName: string]: FeatureHierarchyExtValuesOrAccessor; 21 | }; 22 | 23 | /** 24 | * Differs from [featureHierarchyClass] in that the properties can only 25 | * be of the form {values: …} or {accessor: …}. 26 | */ 27 | export interface FeatureHierarchyClassExt { 28 | name: string; 29 | instanceCount: number; 30 | properties: FeatureHiearchyExtProperties; 31 | } 32 | 33 | export interface FeatureHierarchyExtension { 34 | classes: FeatureHierarchyClassExt[]; 35 | instanceCount: number; 36 | classIds: FeatureHierarchyExtIdsOrAccessor; 37 | parentIds?: FeatureHierarchyExtIdsOrAccessor; 38 | parentCounts?: FeatureHierarchyExtIdsOrAccessor; 39 | } 40 | 41 | const extensionName = 'CESIUM_3dtiles_feature_hierarchy'; 42 | /** 43 | * Add CESIUM_3dtiles_feature_hierarchy as a sub extension to an existing 44 | * glTF object. 45 | * 46 | * @param gltf glTF asset to edit. This glTF asset should already have a 47 | * `CESIUM_3dtiles_feature_metadata` extension in the extensions member of 48 | * the glTF asset. 49 | * @param classes An array of FeatureHierarchyClasses to insert into the glTF 50 | * asset 51 | * @param classIds classIds for the feature hierarchy 52 | * @param instanceCount Total number of instances. Should be equal to the sum 53 | * of each classes[i].instancesCount 54 | * @param [parentIds] Optional list of parentIds associated with each classIds 55 | * @param [parentCounts] Optional numerical array containing the number of 56 | * parents each instance has. 57 | * @param [binaryData] Optional binaryData parameter. Should be provided if 58 | * using binary accessors. This function makes the assumption that all of the 59 | * binary class accessors / classIds / parentIds / parentCounts are all refering 60 | * to this buffer. 61 | * @throws TypeError If the provided glTF asset does not have a 62 | * `CESIUM_3dtiles_feature_metadata` extension already. 63 | * @throws RangeError If classes, parentIds, or parentCounts is empty. Or if 64 | * instancesLength is <= 0. 65 | */ 66 | 67 | export function createFeatureHierarchySubExtension( 68 | gltf: Gltf, 69 | classes: FeatureHierarchyClass[], 70 | classIds: number[] | CompositeAccessorBufferView, 71 | instanceCount: number, 72 | parentIds?: number[] | CompositeAccessorBufferView, 73 | parentCounts?: number[] | CompositeAccessorBufferView, 74 | binaryData?: Buffer 75 | ) { 76 | assertParametersAreValid( 77 | classes, 78 | classIds, 79 | instanceCount, 80 | parentIds, 81 | parentCounts 82 | ); 83 | 84 | if (gltf.extensions?.CESIUM_3dtiles_feature_metadata == null) { 85 | throw new TypeError( 86 | 'glTF asset is missing CESIUM_3dtiles_feature_metadata!' 87 | ); 88 | } 89 | 90 | if (binaryData) { 91 | gltf.buffers.push({ 92 | uri: 93 | 'data:application/octet-stream;base64,' + 94 | binaryData.toString('base64'), 95 | byteLength: binaryData.length 96 | }); 97 | } 98 | 99 | var extension: FeatureHierarchyExtension = { 100 | classes: featureHierarchyClassToExt(gltf, classes), 101 | instanceCount: instanceCount, 102 | classIds: normalizeIdsOrGltfAccessor(gltf, classIds) 103 | }; 104 | 105 | if (parentIds != null) { 106 | extension.parentIds = normalizeIdsOrGltfAccessor(gltf, parentIds); 107 | } 108 | 109 | if (parentCounts != null) { 110 | extension.parentCounts = normalizeIdsOrGltfAccessor(gltf, parentCounts); 111 | } 112 | 113 | gltf.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0]; 114 | 115 | // TODO: Right now we assume that the first featureTable is where the 116 | // featureHierarchy extension should go. This should be refactored. 117 | // eventually when we support exporting multiple feature tables 118 | // in the samples. 119 | 120 | const firstFeatureTable = 121 | gltf.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0]; 122 | if (firstFeatureTable.extensions == null) { 123 | firstFeatureTable.extensions = {}; 124 | } 125 | 126 | firstFeatureTable.extensions.CESIUM_3dtiles_feature_hierarchy = extension; 127 | 128 | if (gltf.extensionsUsed == null) { 129 | gltf.extensionsUsed = [extensionName]; 130 | } else { 131 | gltf.extensionsUsed.push(extensionName); 132 | } 133 | } 134 | 135 | /** 136 | * For each instance in each class, if the instance is a primitive array, 137 | * leave it as-is, otherwise add a new BufferView / Accessor to the glTF asset 138 | * and wrap it with the binary data with {accessor: index} 139 | */ 140 | 141 | function featureHierarchyClassToExt( 142 | gltf: Gltf, 143 | classes: FeatureHierarchyClass[] 144 | ): FeatureHierarchyClassExt[] { 145 | const result: FeatureHierarchyClassExt[] = []; 146 | 147 | for (const cls of classes) { 148 | let normalizedProperties: FeatureHiearchyExtProperties = {}; 149 | for (const propName in cls.properties) { 150 | if (cls.properties.hasOwnProperty(propName)) { 151 | const property = cls.properties[propName]; 152 | if (Array.isArray(property)) { 153 | normalizedProperties[propName] = { values: property }; 154 | } else { 155 | createAccessorAndBufferView(gltf, property); 156 | normalizedProperties[propName] = { 157 | accessor: gltf.accessors.length - 1 158 | }; 159 | } 160 | } 161 | } 162 | 163 | result.push({ 164 | name: cls.name, 165 | properties: normalizedProperties, 166 | instanceCount: cls.instanceCount 167 | }); 168 | } 169 | 170 | return result; 171 | } 172 | 173 | function normalizeIdsOrGltfAccessor( 174 | gltf: Gltf, 175 | data: number[] | CompositeAccessorBufferView 176 | ): FeatureHierarchyExtIdsOrAccessor { 177 | if (Array.isArray(data)) { 178 | return { values: data }; 179 | } 180 | 181 | createAccessorAndBufferView(gltf, data); 182 | return { accessor: gltf.accessors.length - 1 }; 183 | } 184 | 185 | function createAccessorAndBufferView( 186 | gltf: Gltf, 187 | composite: CompositeAccessorBufferView 188 | ) { 189 | gltf.bufferViews.push({ 190 | buffer: gltf.buffers.length - 1, 191 | byteLength: composite.byteLength, 192 | byteOffset: composite.byteOffset, 193 | target: GLenumName.ARRAY_BUFFER 194 | }); 195 | 196 | const bufferViewIndex = gltf.bufferViews.length - 1; 197 | gltf.accessors.push({ 198 | bufferView: bufferViewIndex, 199 | byteOffset: 0, 200 | componentType: composite.componentType, 201 | count: composite.count, 202 | min: composite.min, 203 | max: composite.max, 204 | type: composite.type 205 | }); 206 | } 207 | 208 | function assertParametersAreValid( 209 | classes: FeatureHierarchyClass[], 210 | classIds: number[] | GltfAccessor, 211 | instanceCount: number, 212 | parentIds?: number[] | GltfAccessor, 213 | parentCounts?: number[] | GltfAccessor 214 | ) { 215 | if (classes.length === 0) { 216 | throw new RangeError( 217 | `Classes array is empty, should contain at least one element` 218 | ); 219 | } 220 | 221 | if (Array.isArray(classIds) && classIds.length === 0) { 222 | throw new RangeError( 223 | `Classes array is empty, should contain at least one element` 224 | ); 225 | } 226 | 227 | if (instanceCount <= 0) { 228 | throw new RangeError('instanceCount must be positive'); 229 | } 230 | 231 | if (Array.isArray(parentIds) && parentIds?.length === 0) { 232 | throw new RangeError( 233 | 'Empty parentIds array provided, should contain at least one' + 234 | 'parentId' 235 | ); 236 | } 237 | 238 | if (Array.isArray(parentCounts) && parentCounts?.length === 0) { 239 | throw new RangeError( 240 | 'Empty parentCounts provided, should contain at least one parentId' 241 | ); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /lib/createFeatureMetadataExtension.ts: -------------------------------------------------------------------------------- 1 | import { GltfAccessor, GLenumName } from './gltfType'; 2 | import { Extensions } from './Extensions'; 3 | 4 | const Cesium = require('cesium'); 5 | const defined = Cesium.defined; 6 | const typeConversion = require('./typeConversion'); 7 | const featureTableExtensionName = 'CESIUM_3dtiles_feature_metadata'; 8 | 9 | function sortByByteOffset(a, b) { 10 | if (a.byteOffset < b.byteOffset) { 11 | return -1; 12 | } 13 | 14 | if (a.byteOffset > b.byteOffset) { 15 | return 1; 16 | } 17 | 18 | return 0; 19 | } 20 | 21 | function assertHumanReadableFeatureTableValueArraysHaveIdenticalLength( 22 | humanfeatureTables 23 | ) { 24 | if (humanfeatureTables.length === 0) { 25 | return 0; 26 | } 27 | 28 | const firstLength = humanfeatureTables[0].value.length; 29 | for (let i = 1; i < humanfeatureTables.length; ++i) { 30 | if (humanfeatureTables[i].value.length !== firstLength) { 31 | const badFeatureTable = humanfeatureTables[i].name; 32 | const badLength = humanfeatureTables[i].value.length; 33 | throw new Error( 34 | `${badFeatureTable} has incorrect length of ${badLength}, should match first length: ${firstLength}` 35 | ); 36 | } 37 | } 38 | 39 | return firstLength; 40 | } 41 | 42 | function assertBinaryHasIdenticalCount(binaryfeatureTables) { 43 | if (binaryfeatureTables.length === 0) { 44 | return 0; 45 | } 46 | 47 | const firstLength = binaryfeatureTables[0].count; 48 | for (let i = 1; i < binaryfeatureTables.length; ++i) { 49 | if (binaryfeatureTables[i].count !== firstLength) { 50 | const badFeatureTable = binaryfeatureTables[i].name; 51 | const badLength = binaryfeatureTables[i].count; 52 | throw new Error( 53 | badFeatureTable + 54 | ' has incorrect length of: ' + 55 | badLength + 56 | ', should match the first length: ' + 57 | firstLength 58 | ); 59 | } 60 | } 61 | 62 | return firstLength; 63 | } 64 | 65 | function isCandidateForImplicitBufferView(batchAttribute) { 66 | const min = batchAttribute.min; 67 | const max = batchAttribute.max; 68 | const count = batchAttribute.count; 69 | const minExists = defined(batchAttribute.min); 70 | const maxExists = defined(batchAttribute.max); 71 | const minPasses = minExists && min.length === 1 && min[0] === 0; 72 | const maxPasses = maxExists && max.length === 1 && max[0] === count - 1; 73 | return minPasses && maxPasses; 74 | } 75 | 76 | /** 77 | * @typedef humanReadableFeatureTableValue 78 | * @type {Object} 79 | * @property {Array} values An array of arbitrary values in human readable form 80 | * (e.g ["A", "B", "C"] or [1.3, 4.3, 1.5]) 81 | */ 82 | 83 | /** 84 | * @typedef binaryReadableFeatureTableValue 85 | * @type {Object} 86 | * @property {String} name Name of the featureTableAttribute (e.g to be placed 87 | * into the accessor) 88 | * @property {Number} [byteoffset] ByteOffset of the featureTableAttribute 89 | * @property {Number} [byteLength] Length of the attribute in bytes 90 | * @property {Number} count Count of logical number of elements in the 91 | * attribute (eg 9 floating point numbers with a vec3 92 | * property means count = 3) 93 | * @property {Number} componentType WebGL enum of the component property 94 | * (e.g GL_UNSIGNED_BYTE / 0x1401) 95 | */ 96 | 97 | /** 98 | * @typedef featureTableAttribute 99 | * @type {Object.} 100 | */ 101 | 102 | /** 103 | * @typedef attributeBufferType 104 | * @type {Object} 105 | * @property {Buffer} buffer BufferAttribute data 106 | * @property {String} componentType BufferAttribute componentType (FLOAT, UNSIGNED_BYTE, DOUBLE) 107 | * @property {String} propertyName BufferAttribute property name (POSITION, NORMAL, COLOR, WEIGHT) 108 | * @property {String} type (SCALAR, VEC2, VEC3, VEC4) 109 | * @property {String} target WebGL rendering target, like ARRAY_BUFFER, or ELEMENT_ARRAY_BUFFE (e.g 0x8892, 0x8893) 110 | * @property {Array.} min Minimum value for each component in the bufferAttribute 111 | * @property {Array.} max Maximum value for each component in the bufferAttribute 112 | */ 113 | 114 | /** 115 | * Iterates over featureTableAttributes and adorns the provided gltf object with them via 116 | * the CESIUM_3dtiles_feature_metadata extension 117 | * 118 | * @param {Object} gltf The gltf object to edit 119 | * @param {featureTableAttribute} featureTableAttributes List of batch table attributes. The function will intelligently try to detect 120 | * if the attribute is a human readable value (a key value where the value is 121 | * just an array of non-binary data) or if the values are more keys describing the 122 | * layout of binary data in a binary buffer. 123 | * @param {Buffer} sharedBinaryBuffer Binary buffer. Must be defined if using at 124 | * least 1 binary batch attribute. This function currently assumes that all of 125 | * the binary batch attributes in featureTableAttributes are directly referring 126 | * to this buffer. 127 | */ 128 | export function createFeatureMetadataExtension( 129 | gltf, 130 | featureTableAttributes, 131 | sharedBinaryBuffer 132 | ) { 133 | Extensions.addExtensionsUsed(gltf, featureTableExtensionName); 134 | Extensions.addExtension(gltf, featureTableExtensionName, { 135 | featureTables: [ 136 | { 137 | properties: {} 138 | } 139 | ] 140 | }); 141 | 142 | const humanReadable = []; 143 | const binaryReadable = []; 144 | 145 | const featureTableIndex = 0; // TODO: This will change when we add multiple batch table support 146 | const activeFeatureTable = 147 | gltf.extensions[featureTableExtensionName].featureTables[ 148 | featureTableIndex 149 | ]; 150 | 151 | const attributeNames = Object.keys(featureTableAttributes); 152 | for (let i = 0; i < attributeNames.length; ++i) { 153 | if (featureTableAttributes[attributeNames[i]] instanceof Array) { 154 | humanReadable.push({ 155 | name: attributeNames[i], 156 | value: featureTableAttributes[attributeNames[i]] 157 | }); 158 | } else { 159 | binaryReadable.push(featureTableAttributes[attributeNames[i]]); 160 | } 161 | } 162 | 163 | const humanFeatureCount = assertHumanReadableFeatureTableValueArraysHaveIdenticalLength( 164 | humanReadable 165 | ); 166 | const binaryFeatureCount = assertBinaryHasIdenticalCount(binaryReadable); 167 | 168 | if (humanFeatureCount === 0 && binaryFeatureCount === 0) { 169 | throw new Error( 170 | 'Must have at least one binary or human readable batch table attribute' 171 | ); 172 | } 173 | 174 | if ( 175 | humanFeatureCount > 0 && 176 | binaryFeatureCount > 0 && 177 | binaryFeatureCount !== humanFeatureCount 178 | ) { 179 | throw new Error( 180 | 'Count must be identical across all batch table properties (binary or human readable)' 181 | ); 182 | } 183 | 184 | activeFeatureTable.featureCount = Math.max( 185 | humanFeatureCount, 186 | binaryFeatureCount 187 | ); 188 | 189 | for (let i = 0; i < humanReadable.length; ++i) { 190 | const attribute = humanReadable[i]; 191 | activeFeatureTable.properties[attribute.name] = { 192 | values: attribute.value 193 | }; 194 | } 195 | 196 | if (binaryReadable.length > 0 && !defined(sharedBinaryBuffer)) { 197 | throw new Error( 198 | 'Detected a binary attribute, but function called without binary buffer' 199 | ); 200 | } 201 | 202 | binaryReadable.sort(sortByByteOffset); 203 | if (binaryReadable.length > 0) { 204 | gltf.buffers.push({ 205 | byteLength: sharedBinaryBuffer.length, 206 | uri: 207 | 'data:application/octet-stream;base64,' + 208 | sharedBinaryBuffer.toString('base64') 209 | }); 210 | 211 | const newBufferIndex = gltf.buffers.length - 1; 212 | let bufferViewIndex = gltf.bufferViews.length; 213 | let accessorIndex = gltf.accessors.length; 214 | 215 | for (let i = 0; i < binaryReadable.length; ++i, ++accessorIndex) { 216 | const batchAttribute = binaryReadable[i]; 217 | 218 | const componentType = batchAttribute.componentType; 219 | const validComponentType = typeConversion.isValidWebGLDataTypeEnum( 220 | componentType 221 | ); 222 | const normalizedComponentType = validComponentType 223 | ? componentType 224 | : typeConversion.componentTypeStringToInteger(componentType); 225 | 226 | const implicitBufferView = isCandidateForImplicitBufferView( 227 | batchAttribute 228 | ); 229 | if (!implicitBufferView) { 230 | gltf.bufferViews.push({ 231 | buffer: newBufferIndex, 232 | byteLength: batchAttribute.byteLength, 233 | byteOffset: batchAttribute.byteOffset, 234 | target: GLenumName.ARRAY_BUFFER 235 | }); 236 | } 237 | 238 | const accessor: GltfAccessor = { 239 | componentType: normalizedComponentType, 240 | type: batchAttribute.type, 241 | count: batchAttribute.count, 242 | min: batchAttribute.min, 243 | max: batchAttribute.max 244 | }; 245 | 246 | if (!implicitBufferView) { 247 | accessor.bufferView = bufferViewIndex++; 248 | accessor.byteOffset = 0; 249 | } 250 | 251 | gltf.accessors.push(accessor); 252 | activeFeatureTable.properties[batchAttribute.name] = { 253 | accessor: accessorIndex 254 | }; 255 | } 256 | } 257 | 258 | // also add the extension to attributes 259 | const primitives = gltf.meshes[0].primitives; 260 | for (let i = 0; i < primitives.length; ++i) { 261 | primitives[i].extensions = { 262 | CESIUM_3dtiles_feature_metadata: { 263 | attributes: { 264 | _FEATURE_ID_0: 0 265 | } 266 | } 267 | }; 268 | } 269 | 270 | return gltf; 271 | } 272 | -------------------------------------------------------------------------------- /lib/createGlb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const createGltf = require('./createGltf'); 4 | const gltfPipeline = require('gltf-pipeline'); 5 | const gltfToGlb = gltfPipeline.gltfToGlb; 6 | const gltfConversionOptions = { resourceDirectory: path.join(__dirname, '../') }; 7 | 8 | module.exports = createGlb; 9 | 10 | /** 11 | * Create a glb from a Mesh. 12 | * 13 | * @param {Object} options An object with the following properties: 14 | * @param {Mesh} options.mesh The mesh. 15 | * @param {Boolean} [options.useBatchIds=true] Modify the glTF to include the batchId vertex attribute. 16 | * @param {Boolean} [options.relativeToCenter=false] Set mesh positions relative to center. 17 | * @param {Boolean} [options.deprecated=false] Save the glTF with the old BATCHID semantic. 18 | * 19 | * @returns {Promise} A promise that resolves with the binary glb buffer. 20 | */ 21 | 22 | function createGlb(options) { 23 | const gltf = createGltf(options); 24 | return gltfToGlb(gltf, gltfConversionOptions).then(function (results) { 25 | return results.glb; 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/createGltfFromPnts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | const defined = Cesium.defined; 4 | const typeConversion = require('./typeConversion'); 5 | module.exports = createGltfFromPnts; 6 | 7 | function isImplicitBufferView(attributeBuffer) { 8 | return ( 9 | !defined(attributeBuffer.buffer) || attributeBuffer.buffer.length === 0 10 | ); 11 | } 12 | 13 | /** 14 | @typedef attributeBufferType 15 | @type {Object} 16 | @property {Buffer} buffer BufferAttribute data 17 | @property {String} componentType BufferAttribute componentType 18 | (FLOAT, UNSIGNED_BYTE, DOUBLE) 19 | @property {String} propertyName BufferAttribute property name 20 | (POSITION, NORMAL, COLOR, WEIGHT) 21 | @property {String} type (SCALAR, VEC2, VEC3, VEC4) 22 | @property {String} target WebGL rendering target, like ARRAY_BUFFER, or 23 | ELEMENT_ARRAY_BUFFER (e.g 0x8892, 0x8893) 24 | @property {Array.} min Minimum value for each component in the 25 | bufferAttribute 26 | @property {Array.} max Maximum value for each component in the 27 | bufferAttribute 28 | */ 29 | 30 | function createAmalgamatedGltfBuffer(attributeBuffers, indexBuffer) { 31 | let megaBuffer = Buffer.concat( 32 | attributeBuffers.map(function (ab) { 33 | return ab.buffer; 34 | }) 35 | ); 36 | if (defined(indexBuffer)) { 37 | megaBuffer = Buffer.concat([megaBuffer, indexBuffer.buffer]); 38 | } 39 | 40 | return [ 41 | { 42 | uri: 43 | `data:application/octet-stream;base64,${ 44 | Buffer.from(megaBuffer).toString('base64')}`, 45 | byteLength: megaBuffer.length 46 | } 47 | ]; 48 | } 49 | 50 | /** 51 | * Generates a list of bufferViews. Buffer is currently hardcoded to 0, as 52 | * this module will only ever generate 1 buffer when converting pointcloud data 53 | * into a GLTF 54 | * 55 | * @param {Array} attributeBuffers A list of buffer 56 | * attributes to convert to GLTF 57 | * @param {attributeBufferType} [indexBuffer] An optional indexBuffer. 58 | * @returns {Object} a buffer views array 59 | */ 60 | function createBufferViewsFromAttributeBuffers(attributeBuffers, indexBuffer) { 61 | const result = []; 62 | let byteOffset = 0; 63 | 64 | for (let i = 0; i < attributeBuffers.length; ++i) { 65 | if (isImplicitBufferView(attributeBuffers[i])) { 66 | continue; 67 | } 68 | 69 | const bufferView = { 70 | buffer: 0, 71 | byteLength: attributeBuffers[i].buffer.byteLength, 72 | byteOffset: byteOffset, 73 | target: attributeBuffers[i].target 74 | }; 75 | 76 | if (defined(attributeBuffers[i].byteStride)) { 77 | bufferView.byteStride = attributeBuffers[i].byteStride; 78 | } 79 | 80 | result.push(bufferView); 81 | 82 | // All attribute data is tightly packed 83 | byteOffset += attributeBuffers[i].buffer.byteLength; 84 | } 85 | 86 | if (defined(indexBuffer)) { 87 | result.push({ 88 | buffer: 0, 89 | byteLength: indexBuffer.buffer.byteLength, 90 | byteOffset: byteOffset, 91 | target: indexBuffer.target 92 | }); 93 | } 94 | 95 | return result; 96 | } 97 | 98 | /** 99 | * Create a meshes singleton using bufferAttributes 100 | * @param {Array.} attributeBuffers A list of buffer 101 | * attributes to convert to GLTF 102 | * @param {attributeBufferType} [indexBuffer] An optional indexBuffer. 103 | * @returns {Array.} A GLTF meshes array 104 | */ 105 | function createMeshFromAttributeBuffers(attributeBuffers, indexBuffer) { 106 | // the index of the attribute in the inputted bufferAttributes array directly 107 | // corresponds to the accessor ID 108 | const primitives = { 109 | attributes: {}, 110 | mode: 0 111 | }; 112 | 113 | let i; 114 | for (i = 0; i < attributeBuffers.length; ++i) { 115 | primitives.attributes[attributeBuffers[i].propertyName] = i; 116 | } 117 | 118 | if (defined(indexBuffer)) { 119 | primitives.indices = i; 120 | } 121 | 122 | return [ 123 | { 124 | primitives: [primitives] 125 | } 126 | ]; 127 | } 128 | 129 | /** 130 | * Creates accessors from attributeBuffers 131 | * @param {Array.} bufferAttributes A list of buffer 132 | * attributes to convert to GLTF 133 | * @param {attributeBufferType} [indexBuffer] An optional indexBuffer. 134 | * @returns {Object} a buffer views array 135 | */ 136 | 137 | function createAccessorsFromAttributeBuffers(attributeBuffers, indexBuffer) { 138 | let componentType; 139 | let validComponentType; 140 | let normalizedComponentType; 141 | const accessors = []; 142 | let i; 143 | 144 | for (i = 0; i < attributeBuffers.length; ++i) { 145 | componentType = attributeBuffers[i].componentType; 146 | validComponentType = typeConversion.isValidWebGLDataTypeEnum( 147 | componentType 148 | ); 149 | normalizedComponentType = validComponentType 150 | ? componentType 151 | : typeConversion.componentTypeStringToInteger(componentType); 152 | 153 | const accessor = { 154 | componentType: normalizedComponentType, 155 | type: attributeBuffers[i].type, 156 | count: attributeBuffers[i].count, 157 | min: attributeBuffers[i].min, 158 | max: attributeBuffers[i].max 159 | }; 160 | 161 | if (defined(attributeBuffers[i].normalized)) { 162 | accessor.normalized = attributeBuffers[i].normalized; 163 | } 164 | 165 | // detect accessors with implicit buffer views 166 | if (!isImplicitBufferView(attributeBuffers[i])) { 167 | accessor.bufferView = i; 168 | accessor.byteOffset = 0; 169 | } 170 | 171 | accessors.push(accessor); 172 | } 173 | 174 | if (defined(indexBuffer)) { 175 | componentType = indexBuffer.componentType; 176 | validComponentType = typeConversion.isValidWebGLDataTypeEnum( 177 | componentType 178 | ); 179 | normalizedComponentType = validComponentType 180 | ? componentType 181 | : typeConversion.componentTypeStringToInteger(componentType); 182 | accessors.push({ 183 | bufferView: i, 184 | byteOffset: 0, 185 | componentType: normalizedComponentType, 186 | count: indexBuffer.count, 187 | type: indexBuffer.type, 188 | min: indexBuffer.min, 189 | max: indexBuffer.max 190 | }); 191 | } 192 | 193 | return accessors; 194 | } 195 | 196 | /** 197 | * Create a GLTF from PNTS data 198 | * 199 | * @param {Array.} attributeBuffers An object where each 200 | * key is the name of a bufferAttribute, 201 | * and each value is another js object with the following keys: 202 | * @param {attributeBufferType} [indexBuffer] An optional indexBuffer. 203 | * @param {Object} [rtc] Optional RTC vec3. Will be inserted into the node hierarchy. 204 | * 205 | * @returns {Object} a GLTF object 206 | */ 207 | 208 | function createGltfFromPnts(attributeBuffers, indexBuffer, rtc) { 209 | const gltf = { 210 | asset: { 211 | generator: '3d-tiles-samples-generator', 212 | version: '2.0' 213 | } 214 | }; 215 | 216 | // z-up to y-up transform. 217 | const rootMatrix = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1]; 218 | 219 | gltf.buffers = createAmalgamatedGltfBuffer(attributeBuffers, indexBuffer); 220 | gltf.bufferViews = createBufferViewsFromAttributeBuffers( 221 | attributeBuffers, 222 | indexBuffer 223 | ); 224 | gltf.meshes = createMeshFromAttributeBuffers(attributeBuffers, indexBuffer); 225 | gltf.accessors = createAccessorsFromAttributeBuffers( 226 | attributeBuffers, 227 | indexBuffer 228 | ); 229 | gltf.nodes = [{ matrix: rootMatrix, mesh: 0, name: 'rootNode' }]; 230 | if (defined(rtc)) { 231 | delete gltf.nodes[0].mesh; 232 | gltf.nodes[0].children = [1]; 233 | gltf.nodes.push({ name: 'RTC_CENTER', mesh: 0, translation: rtc }); 234 | } 235 | gltf.scenes = [{ nodes: [0] }]; 236 | 237 | return gltf; 238 | } 239 | -------------------------------------------------------------------------------- /lib/createI3dm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | const getJsonBufferPadded = require('./getJsonBufferPadded'); 4 | const getBufferPadded = require('./getBufferPadded'); 5 | 6 | const defined = Cesium.defined; 7 | 8 | module.exports = createI3dm; 9 | 10 | /** 11 | * Create an Instanced 3D Model (i3dm) tile from a feature table, batch table, and gltf buffer or uri. 12 | * 13 | * @param {Object} options An object with the following properties: 14 | * @param {Object} options.featureTableJson The feature table JSON. 15 | * @param {Buffer} options.featureTableBinary The feature table binary. 16 | * @param {Object} [options.batchTableJson] Batch table describing the per-feature metadata. 17 | * @param {Buffer} [options.batchTableBinary] The batch table binary. 18 | * @param {Buffer} [options.glb] The binary glTF buffer. 19 | * @param {String} [options.uri] Uri to an external glTF model when options.glb is not specified. 20 | * @returns {Buffer} The generated i3dm tile buffer. 21 | */ 22 | function createI3dm(options) { 23 | const version = 1; 24 | const headerByteLength = 32; 25 | 26 | const featureTableJson = getJsonBufferPadded(options.featureTableJson, headerByteLength); 27 | const featureTableBinary = getBufferPadded(options.featureTableBinary); 28 | const batchTableJson = getJsonBufferPadded(options.batchTableJson); 29 | const batchTableBinary = getBufferPadded(options.batchTableBinary); 30 | 31 | const gltfFormat = defined(options.glb) ? 1 : 0; 32 | const gltfBuffer = defined(options.glb) ? options.glb : getGltfUriBuffer(options.uri); 33 | const gltfBufferPadded = getBufferPadded(gltfBuffer); 34 | 35 | const featureTableJsonByteLength = featureTableJson.length; 36 | const featureTableBinaryByteLength = featureTableBinary.length; 37 | const batchTableJsonByteLength = batchTableJson.length; 38 | const batchTableBinaryByteLength = batchTableBinary.length; 39 | const gltfByteLength = gltfBufferPadded.length; 40 | const byteLength = headerByteLength + featureTableJsonByteLength + featureTableBinaryByteLength + batchTableJsonByteLength + batchTableBinaryByteLength + gltfByteLength; 41 | 42 | const header = Buffer.alloc(headerByteLength); 43 | header.write('i3dm', 0); 44 | header.writeUInt32LE(version, 4); 45 | header.writeUInt32LE(byteLength, 8); 46 | header.writeUInt32LE(featureTableJsonByteLength, 12); 47 | header.writeUInt32LE(featureTableBinaryByteLength, 16); 48 | header.writeUInt32LE(batchTableJsonByteLength, 20); 49 | header.writeUInt32LE(batchTableBinaryByteLength, 24); 50 | header.writeUInt32LE(gltfFormat, 28); 51 | 52 | return Buffer.concat([header, featureTableJson, featureTableBinary, batchTableJson, batchTableBinary, gltfBufferPadded]); 53 | } 54 | 55 | function getGltfUriBuffer(uri) { 56 | uri = uri.replace(/\\/g, '/'); 57 | return Buffer.from(uri); 58 | } 59 | -------------------------------------------------------------------------------- /lib/createPnts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const getJsonBufferPadded = require('./getJsonBufferPadded'); 3 | const getBufferPadded = require('./getBufferPadded'); 4 | 5 | module.exports = createPnts; 6 | 7 | /** 8 | * Create a Point Cloud (pnts) tile from a feature table and batch table. 9 | * 10 | * @param {Object} options An object with the following properties: 11 | * @param {Object} options.featureTableJson The feature table JSON. 12 | * @param {Buffer} options.featureTableBinary The feature table binary. 13 | * @param {Object} [options.batchTableJson] Batch table describing the per-point metadata. 14 | * @param {Buffer} [options.batchTableBinary] The batch table binary. 15 | * @returns {Buffer} The generated pnts tile buffer. 16 | */ 17 | function createPnts(options) { 18 | const version = 1; 19 | const headerByteLength = 28; 20 | 21 | const featureTableJson = getJsonBufferPadded(options.featureTableJson, headerByteLength); 22 | const featureTableBinary = getBufferPadded(options.featureTableBinary); 23 | const batchTableJson = getJsonBufferPadded(options.batchTableJson); 24 | const batchTableBinary = getBufferPadded(options.batchTableBinary); 25 | 26 | const featureTableJsonByteLength = featureTableJson.length; 27 | const featureTableBinaryByteLength = featureTableBinary.length; 28 | const batchTableJsonByteLength = batchTableJson.length; 29 | const batchTableBinaryByteLength = batchTableBinary.length; 30 | const byteLength = headerByteLength + featureTableJsonByteLength + featureTableBinaryByteLength + batchTableJsonByteLength + batchTableBinaryByteLength; 31 | 32 | const header = Buffer.alloc(headerByteLength); 33 | header.write('pnts', 0); 34 | header.writeUInt32LE(version, 4); 35 | header.writeUInt32LE(byteLength, 8); 36 | header.writeUInt32LE(featureTableJsonByteLength, 12); 37 | header.writeUInt32LE(featureTableBinaryByteLength, 16); 38 | header.writeUInt32LE(batchTableJsonByteLength, 20); 39 | header.writeUInt32LE(batchTableBinaryByteLength, 24); 40 | 41 | return Buffer.concat([header, featureTableJson, featureTableBinary, batchTableJson, batchTableBinary]); 42 | } 43 | -------------------------------------------------------------------------------- /lib/createTilesetJsonSingle.ts: -------------------------------------------------------------------------------- 1 | import { AtLeastOne } from './atLeastN'; 2 | import { Matrix4 } from 'cesium'; 3 | import { TilesetJson } from './tilesetJson'; 4 | 5 | const defaultTilesetVersion = '1.0'; 6 | 7 | /** 8 | * Create a tileset JSON for a single tile. 9 | * 10 | * @param contentUri The content URI of the root tile. This may be a relative filepath or a data URI. 11 | * @param geometricError Geometric error of the tile. 12 | * @param versionNumber The 3D Tiles version number string. 13 | * @param [region] Bounding region of the tile. 14 | * @param {Object} [options.box] Bounding box of the tile. 15 | * @param {Object} [options.sphere] Bounding sphere of the tile. 16 | * @param {Matrix4} [options.transform=Matrix4.IDENTITY] The tile transform. 17 | * @param {Object} [options.properties] An object containing the min and max values for each property in the batch table. 18 | * @param {Object} [options.extensions] An object containing extensionsUsed and extensionsRequired properties. 19 | * @param {Object} [options.expire] Tile expiration options. 20 | */ 21 | 22 | type TilesetBoundingVolumeKeys = { 23 | region: number[]; 24 | box: number[]; 25 | sphere: number[]; 26 | }; 27 | 28 | export type TilesetBoundingVolume = AtLeastOne; 29 | 30 | export type TilesetOption = { 31 | contentUri: string; 32 | geometricError: number; 33 | versionNumber: string; 34 | transform?: Matrix4; 35 | properties?: { 36 | [propertyName: string]: { minimum: number; maximum: number }; 37 | }; 38 | extensions?: { 39 | extensionsUsed?: string[]; 40 | extensionsRequired?: string[]; 41 | }; 42 | expire?: any; 43 | } & TilesetBoundingVolume; 44 | 45 | export function createTilesetJsonSingle(options: TilesetOption): TilesetJson { 46 | const transform = 47 | options.transform != null ? options.transform : Matrix4.IDENTITY; 48 | const transformArray = !Matrix4.equals(transform, Matrix4.IDENTITY) 49 | ? Matrix4.pack(transform, new Array(16)) 50 | : undefined; 51 | const boundingVolume = getBoundingVolume( 52 | options.region, 53 | options.box, 54 | options.sphere 55 | ); 56 | const extensionsUsed = options?.extensions?.extensionsUsed; 57 | const extensionsRequired = options?.extensions?.extensionsRequired; 58 | const version = 59 | options.versionNumber != null 60 | ? options.versionNumber 61 | : defaultTilesetVersion; 62 | 63 | return { 64 | asset: { 65 | version: version 66 | }, 67 | properties: options.properties, 68 | ...(extensionsUsed != null 69 | ? { extensionsUsed: extensionsUsed } 70 | : {}), 71 | ...(extensionsRequired != null 72 | ? { extensionsRequired: extensionsRequired } 73 | : {}), 74 | geometricError: options.geometricError, 75 | root: { 76 | transform: transformArray, 77 | expire: options.expire, 78 | refine: 'ADD', 79 | boundingVolume: boundingVolume, 80 | geometricError: 0.0, 81 | content: { 82 | uri: options.contentUri 83 | } 84 | } 85 | }; 86 | } 87 | 88 | function getBoundingVolume( 89 | region?: number[], 90 | box?: number[], 91 | sphere?: number[] 92 | ): TilesetBoundingVolume { 93 | if (region != null) { 94 | return { region: region }; 95 | } 96 | 97 | if (box != null) { 98 | return { box: box }; 99 | } 100 | 101 | return { sphere: sphere }; 102 | } 103 | -------------------------------------------------------------------------------- /lib/featureHierarchyClass.ts: -------------------------------------------------------------------------------- 1 | import { GltfType } from './gltfType'; 2 | 3 | export interface CompositeAccessorBufferView { 4 | bufferView: number; 5 | byteLength: number; 6 | byteOffset: number; 7 | componentType: GLenum; 8 | count: number; 9 | max: number[]; 10 | min: number[]; 11 | type: GltfType; 12 | target: GLenum; 13 | } 14 | 15 | export type Primitive = string | boolean | number; 16 | export type FeatureHierarchyProperties = 17 | | { [name: string]: Primitive[] } 18 | | CompositeAccessorBufferView; 19 | 20 | export class FeatureHierarchyClass { 21 | readonly name: string; 22 | readonly instanceCount: number; 23 | readonly properties: FeatureHierarchyProperties; 24 | 25 | constructor( 26 | name: string, 27 | instanceCount: number, 28 | properties: FeatureHierarchyProperties 29 | ) { 30 | this.name = name; 31 | this.instanceCount = instanceCount; 32 | this.properties = properties; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/featureMetadata.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Similar to createFutureMetadataExtension.js, but typed 3 | * and contains smaller utility functions for editing an existing 4 | * glTF asset. 5 | */ 6 | 7 | import { Gltf, GltfPrimitive } from './gltfType'; 8 | import { FeatureTable, FeatureLayer } from './featureMetadataType'; 9 | 10 | export namespace FeatureMetadata { 11 | /** 12 | * Updates the extensionsUsed section of a provided glTF asset. Creates 13 | * this array if it does not exist already. 14 | * @param gltf The glTF asset to modify 15 | */ 16 | export function updateExtensionUsed(gltf: Gltf) { 17 | if (gltf.extensionsUsed == null) { 18 | gltf.extensionsUsed = []; 19 | } 20 | 21 | gltf.extensionsUsed.push('CESIUM_3dtiles_feature_metadata'); 22 | } 23 | 24 | /** 25 | * Adds a featureTable to the root section of a glTF asset. Automatically 26 | * creates missing intermediate objects. 27 | * @param gltf The glTF asset to modify 28 | * @param featureTable A featureTable to add to the root extensions section 29 | * of the provided glTF asset. 30 | */ 31 | 32 | export function addFeatureTable(gltf: Gltf, featureTable: FeatureTable) { 33 | if (gltf.extensions == null) { 34 | gltf.extensions = {}; 35 | } 36 | 37 | if (gltf.extensions.CESIUM_3dtiles_feature_metadata == null) { 38 | gltf.extensions.CESIUM_3dtiles_feature_metadata = { 39 | featureTables: [] 40 | }; 41 | } 42 | 43 | gltf.extensions.CESIUM_3dtiles_feature_metadata.featureTables.push( 44 | featureTable 45 | ); 46 | } 47 | 48 | export function addFeatureLayer( 49 | primitive: GltfPrimitive, 50 | featureLayer: FeatureLayer 51 | ) { 52 | if (primitive.extensions == null) { 53 | primitive.extensions = {}; 54 | } 55 | 56 | if (primitive.extensions.CESIUM_3dtiles_feature_metadata == null) { 57 | primitive.extensions.CESIUM_3dtiles_feature_metadata = { 58 | featureLayers: [] 59 | }; 60 | } 61 | 62 | primitive.extensions.CESIUM_3dtiles_feature_metadata.featureLayers.push( 63 | featureLayer 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/featureMetadataExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Also referred to as 'batchTable' throughout the code, 3 | * byteOffset and byteLength are mutually inclusive, if these two keys 4 | * are used values will be ignored. 5 | */ 6 | 7 | import { Gltf, GltfAccessor } from './gltfType'; 8 | import { 9 | FeatureTableBinary, 10 | FeatureTablePlainText, 11 | FeatureTableProperties 12 | } from './featureMetadataType'; 13 | 14 | const extensionName = 'CESIUM_3dtiles_feature_metadata'; 15 | function addExtension(gltf: Gltf) { 16 | if ( 17 | gltf.extensionsUsed != null && 18 | gltf.extensionsUsed.indexOf(extensionName) != -1 19 | ) { 20 | return; 21 | } 22 | 23 | if (gltf.extensionsUsed == null) { 24 | gltf.extensionsUsed = [extensionName]; 25 | } else { 26 | gltf.extensionsUsed.push(extensionName); 27 | } 28 | 29 | if (gltf.extensions == null) { 30 | gltf.extensions = {}; 31 | } 32 | 33 | gltf.extensions[extensionName] = { 34 | featureTables: [] 35 | }; 36 | } 37 | 38 | function calcAndAssertBinaryHaveSameCount( 39 | binary: FeatureTableBinary[] 40 | ): number { 41 | let first = 0; 42 | if (binary.length === 0) { 43 | return first; 44 | } 45 | 46 | first = binary[0].count; 47 | for (let i = 1; i < binary.length; ++i) { 48 | if (first != binary[i].count) { 49 | throw new RangeError(`binary[${i}].count should be ${first}`); 50 | } 51 | } 52 | 53 | return first; 54 | } 55 | 56 | function calcAndAssertPlainTextHaveSameCount( 57 | plainText: FeatureTablePlainText[] 58 | ): number { 59 | let first = 0; 60 | 61 | for (const values of plainText) { 62 | const keys = Object.keys(values); 63 | if (keys.length === 0) { 64 | continue; 65 | } 66 | 67 | first = values[keys[0]].length; 68 | for (let i = 1; i < keys.length; ++i) { 69 | if (values[keys[i]].length !== first) { 70 | throw new Error(`values[${keys[i]}].length should be ${first}`); 71 | } 72 | } 73 | } 74 | 75 | return first; 76 | } 77 | 78 | function addPlainTextAccessorsToFeatureTable( 79 | currentFeatureTableProperties: FeatureTableProperties, 80 | plainText: FeatureTablePlainText[] 81 | ) { 82 | plainText.forEach((plainTextProperty) => { 83 | const keys = Object.keys(plainTextProperty); 84 | for (let k of keys) { 85 | const array = plainTextProperty[k]; 86 | currentFeatureTableProperties[k] = { values: array }; 87 | } 88 | }); 89 | } 90 | 91 | function addBinaryAccessorsToFeatureTable( 92 | gltf: Gltf, 93 | currentFeatureTableProperties: FeatureTableProperties, 94 | binary: FeatureTableBinary[], 95 | buffer: Buffer 96 | ) { 97 | const uri = 98 | 'data:application/octet-stream;base64,' + buffer.toString('base64'); 99 | 100 | gltf.buffers.push({ 101 | byteLength: buffer.length, 102 | uri: uri 103 | }); 104 | 105 | const newBufferIndex = gltf.buffers.length - 1; 106 | let bufferViewIndex = gltf.bufferViews.length; 107 | let accessorIndex = gltf.accessors.length; 108 | 109 | for (let i = 0; i < binary.length; ++i, ++accessorIndex) { 110 | const binaryAttribute = binary[i]; 111 | const componentType = binaryAttribute.componentType; 112 | 113 | const implicitBufferView = isCandidateForImplicitBufferView( 114 | binaryAttribute 115 | ); 116 | 117 | if (!implicitBufferView) { 118 | gltf.bufferViews.push({ 119 | buffer: newBufferIndex, 120 | byteLength: binaryAttribute.byteLength, 121 | byteOffset: binaryAttribute.byteOffset, 122 | target: 0x8892 // ARRAY_BUFFER 123 | }); 124 | } 125 | 126 | const accessor: GltfAccessor = { 127 | componentType: componentType, 128 | type: binaryAttribute.type, 129 | count: binaryAttribute.count, 130 | min: binaryAttribute.min, 131 | max: binaryAttribute.max 132 | }; 133 | 134 | if (!implicitBufferView) { 135 | accessor.bufferView = bufferViewIndex++; 136 | accessor.byteOffset = 0; 137 | } 138 | 139 | gltf.accessors.push(accessor); 140 | currentFeatureTableProperties[binaryAttribute.name] = { 141 | accessor: accessorIndex 142 | }; 143 | } 144 | } 145 | 146 | function isCandidateForImplicitBufferView(binaryAttribute: FeatureTableBinary) { 147 | const min = binaryAttribute.min; 148 | const max = binaryAttribute.max; 149 | const count = binaryAttribute.count; 150 | const minPasses = min.length === 1 && min[0] === 0; 151 | const maxPasses = max.length === 1 && max[0] === count - 1; 152 | return minPasses && maxPasses; 153 | } 154 | -------------------------------------------------------------------------------- /lib/featureMetadataType.ts: -------------------------------------------------------------------------------- 1 | import { FeatureHierarchyExtension } from './createFeatureHierarchySubExtension'; 2 | import { GltfType } from './gltfType'; 3 | import { XOR } from './xor'; 4 | 5 | export interface FeatureMetatableExtensions { 6 | CESIUM_3dtiles_feature_hierarchy?: FeatureHierarchyExtension; 7 | } 8 | 9 | export enum FeatureTableType { 10 | Binary, 11 | PlainText 12 | } 13 | 14 | export type Primitive = string | boolean | number; 15 | 16 | export type FeatureTablePlainText = { [name: string]: Primitive[] }; 17 | 18 | export interface FeatureTableBinary { 19 | name: string; 20 | byteOffset: number; 21 | byteLength: number; 22 | count: number; 23 | componentType: GLenum; 24 | min: number[]; 25 | max: number[]; 26 | type: GltfType; 27 | } 28 | 29 | export interface FeatureTablePair { 30 | type: FeatureTableType; 31 | data: FeatureTablePlainText | FeatureTableBinary; 32 | } 33 | 34 | export type FeatureTableProperties = 35 | | { [name: string]: { accessor: number } } 36 | | { [name: string]: { values: Primitive[] } }; 37 | 38 | export interface FeatureTable { 39 | properties?: FeatureTableProperties; 40 | featureCount: number; 41 | extensions?: FeatureMetatableExtensions; 42 | } 43 | 44 | // extensions.CESIUM_3dtiles_feature_metadata 45 | export interface FeatureMetadata { 46 | featureTables: FeatureTable[]; 47 | } 48 | 49 | // meshes[0].primitives.extension.CESIUM_3dtiles_feature_metadata 50 | export type ImplicitFeatureID = { 51 | implicit: { 52 | start: number; 53 | increment: number; 54 | }; 55 | }; 56 | 57 | export type AttributeIndex = { attributeindex: number }; 58 | 59 | export type VertexAttribute = { 60 | vertexAttribute: XOR; 61 | }; 62 | 63 | export type TextureAccessor = { 64 | texture: { 65 | texCoord: number; 66 | normalized: boolean; 67 | channels: string; // /^[rgba]{1,4}$/ 68 | texture: { 69 | index: number; 70 | }; 71 | }; 72 | }; 73 | 74 | export type FeatureLayer = { 75 | featureTable: number; 76 | instanceStride?: number; 77 | } & XOR; 78 | 79 | export type FeatureMetadataPrimitiveExtension = { 80 | featureLayers: FeatureLayer[]; 81 | }; 82 | -------------------------------------------------------------------------------- /lib/featureMetatableUtilsNext.ts: -------------------------------------------------------------------------------- 1 | import { Building } from './createBuilding'; 2 | import { Mesh } from './Mesh'; 3 | import { BaseColorType, TranslucencyType } from './colorTypes'; 4 | 5 | const Cesium = require('cesium'); 6 | const Matrix4 = Cesium.Matrix4; 7 | 8 | export namespace FeatureTableUtils { 9 | const tileWidth = 200.0; 10 | const longitude = -1.31968; 11 | const latitude = 0.698874; 12 | 13 | export interface BuildingGenerationOptions { 14 | uniform?: boolean; 15 | seed?: number; 16 | numberOfBuildings?: number; 17 | tileWidth?: number; 18 | averageWidth?: number; 19 | averageHeight?: number; 20 | baseColorType?: BaseColorType; 21 | translucencyType?: TranslucencyType; 22 | longitude?: number; 23 | latitude?: number; 24 | } 25 | 26 | export function getDefaultBuildingGenerationOptions(): BuildingGenerationOptions { 27 | return { 28 | uniform: false, 29 | numberOfBuildings: 10, 30 | tileWidth: tileWidth, 31 | averageWidth: 8.0, 32 | averageHeight: 10.0, 33 | baseColorType: BaseColorType.White, 34 | translucencyType: TranslucencyType.Opaque, 35 | longitude: longitude, 36 | latitude: latitude, 37 | seed: 11 38 | }; 39 | } 40 | 41 | export function getDefaultTransform() { 42 | return Matrix4.IDENTITY; 43 | } 44 | 45 | export function createFeatureTableBinary() {} 46 | 47 | export function createFeatureTableJson() {} 48 | 49 | const scratchMatrix = new Matrix4(); 50 | 51 | /** 52 | * 53 | * @param tileTransform Cesium.Matrix4 54 | * @param buildings 55 | */ 56 | 57 | export function createMeshes( 58 | tileTransform: object, 59 | buildings: Building[], 60 | useVertexColors: boolean 61 | ): Mesh[] { 62 | var meshes = new Array(buildings.length); 63 | for (var i = 0; i < buildings.length; ++i) { 64 | var building = buildings[i]; 65 | var transform = Matrix4.multiply( 66 | tileTransform, 67 | building.matrix, 68 | scratchMatrix 69 | ); 70 | var mesh = Mesh.createCube(); 71 | mesh.transform(transform); 72 | mesh.material = building.material; 73 | if (useVertexColors) { 74 | mesh.transferMaterialToVertexColors(); 75 | } 76 | meshes[i] = mesh; 77 | } 78 | 79 | return meshes; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/getBufferPadded.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | 4 | const defaultValue = Cesium.defaultValue; 5 | const defined = Cesium.defined; 6 | 7 | module.exports = getBufferPadded; 8 | 9 | /** 10 | * Pad the buffer to the next 8-byte boundary to ensure proper alignment for the section that follows. 11 | * 12 | * @param {Buffer} buffer The buffer. 13 | * @param {Number} [byteOffset=0] The byte offset on which the buffer starts. 14 | * @returns {Buffer} The padded buffer. 15 | */ 16 | function getBufferPadded(buffer, byteOffset) { 17 | if (!defined(buffer)) { 18 | return Buffer.alloc(0); 19 | } 20 | 21 | byteOffset = defaultValue(byteOffset, 0); 22 | 23 | const boundary = 8; 24 | const byteLength = buffer.length; 25 | const remainder = (byteOffset + byteLength) % boundary; 26 | const padding = (remainder === 0) ? 0 : boundary - remainder; 27 | const emptyBuffer = Buffer.alloc(padding); 28 | return Buffer.concat([buffer, emptyBuffer]); 29 | } 30 | -------------------------------------------------------------------------------- /lib/getJsonBufferPadded.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | 4 | const defaultValue = Cesium.defaultValue; 5 | const defined = Cesium.defined; 6 | 7 | module.exports = getJsonBufferPadded; 8 | 9 | /** 10 | * Convert the JSON object to a padded buffer. 11 | * 12 | * Pad the JSON with extra whitespace to fit the next 8-byte boundary. This ensures proper alignment 13 | * for the section that follows (for example, batch table binary or feature table binary). 14 | * 15 | * @param {Object} [json] The JSON object. 16 | * @param {Number} [byteOffset=0] The byte offset on which the buffer starts. 17 | * @returns {Buffer} The padded JSON buffer. 18 | */ 19 | function getJsonBufferPadded(json, byteOffset) { 20 | if (!defined(json)) { 21 | return Buffer.alloc(0); 22 | } 23 | 24 | byteOffset = defaultValue(byteOffset, 0); 25 | let string = JSON.stringify(json); 26 | 27 | const boundary = 8; 28 | const byteLength = Buffer.byteLength(string); 29 | const remainder = (byteOffset + byteLength) % boundary; 30 | const padding = (remainder === 0) ? 0 : boundary - remainder; 31 | let whitespace = ''; 32 | for (let i = 0; i < padding; ++i) { 33 | whitespace += ' '; 34 | } 35 | string += whitespace; 36 | 37 | return Buffer.from(string); 38 | } 39 | -------------------------------------------------------------------------------- /lib/getMinMax.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | const defaultValue = Cesium.defaultValue; 4 | module.exports = getMinMax; 5 | 6 | function getMinMax(array, components, start, length) { 7 | start = defaultValue(start, 0); 8 | length = defaultValue(length, array.length); 9 | const min = new Array(components).fill(Number.POSITIVE_INFINITY); 10 | const max = new Array(components).fill(Number.NEGATIVE_INFINITY); 11 | const count = length / components; 12 | for (let i = 0; i < count; ++i) { 13 | for (let j = 0; j < components; ++j) { 14 | const index = start + i * components + j; 15 | const value = array[index]; 16 | min[j] = Math.min(min[j], value); 17 | max[j] = Math.max(max[j], value); 18 | } 19 | } 20 | return { 21 | min: min, 22 | max: max 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /lib/getProperties.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | 4 | const defined = Cesium.defined; 5 | 6 | module.exports = getProperties; 7 | 8 | /** 9 | * Get the minimum and maximum values for each property in the batch table. 10 | * Ignore properties in the batch table binary for now. Also ignore non-number values. 11 | * 12 | * @param {Object|Object[]} batchTable The batch table(s). 13 | * @returns {Object} An object with the minimum and maximum values for each property in the batch table. 14 | */ 15 | function getProperties(batchTable) { 16 | if (!defined(batchTable)) { 17 | return undefined; 18 | } 19 | const properties = {}; 20 | const batchTables = Array.isArray(batchTable) ? batchTable : [batchTable]; 21 | const batchTablesLength = batchTables.length; 22 | for (let i = 0; i < batchTablesLength; ++i) { 23 | batchTable = batchTables[i]; 24 | for (const name in batchTable) { 25 | if (batchTable.hasOwnProperty(name)) { 26 | const values = batchTable[name]; 27 | if (Array.isArray(values)) { 28 | if (typeof values[0] === 'number') { 29 | if (!defined(properties[name])) { 30 | properties[name] = { 31 | minimum : Number.POSITIVE_INFINITY, 32 | maximum : Number.NEGATIVE_INFINITY 33 | }; 34 | } 35 | let min = properties[name].minimum; 36 | let max = properties[name].maximum; 37 | const length = values.length; 38 | for (let j = 0; j < length; ++j) { 39 | const value = values[j]; 40 | min = Math.min(value, min); 41 | max = Math.max(value, max); 42 | } 43 | properties[name].minimum = min; 44 | properties[name].maximum = max; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | if (Object.keys(properties).length === 0) { 51 | return undefined; 52 | } 53 | return properties; 54 | } 55 | -------------------------------------------------------------------------------- /lib/gltfFromUri.ts: -------------------------------------------------------------------------------- 1 | const gltfPipeline = require('gltf-pipeline'); 2 | const glbToGltf = gltfPipeline.glbToGltf; 3 | import fsExtra = require('fs-extra'); 4 | import { Gltf } from './gltfType'; 5 | 6 | export async function getGltfFromGlbUri( 7 | uri: string, 8 | gltfConversionOptions: { resourceDirectory: string } 9 | ): Promise { 10 | const glb = (await fsExtra.readFile(uri)) as Buffer; 11 | return (await glbToGltf(glb, gltfConversionOptions)).gltf as Gltf; 12 | } 13 | -------------------------------------------------------------------------------- /lib/gltfType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FeatureMetadata, 3 | FeatureMetadataPrimitiveExtension 4 | } from './featureMetadataType'; 5 | import { AtLeastOne } from './atLeastN'; 6 | import { ExtMeshGpuInstancing } from './createEXTMeshInstancing'; 7 | 8 | export enum GLenumName { 9 | ARRAY_BUFFER = 0x8892, 10 | ELEMENT_ARRAY_BUFFER = 0x8993 11 | } 12 | 13 | export enum GltfComponentType { 14 | BYTE = 5120, 15 | UNSIGNED_BYTE = 5121, 16 | SHORT = 5122, 17 | UNSIGNED_SHORT = 5123, 18 | UNSIGNED_INT = 5125, 19 | FLOAT = 5126 20 | } 21 | 22 | export enum GltfType { 23 | SCALAR = 'SCALAR', 24 | VEC2 = 'VEC2', 25 | VEC3 = 'VEC3', 26 | VEC4 = 'VEC4', 27 | MAT2 = 'MAT2', 28 | MAT3 = 'MAT3', 29 | MAT4 = 'MAT4' 30 | } 31 | 32 | export interface GltfScene { 33 | nodes: number[]; 34 | } 35 | 36 | export interface GltfNodeExtensions { 37 | EXT_mesh_gpu_instancing?: ExtMeshGpuInstancing; 38 | } 39 | 40 | export interface GltfNode { 41 | name?: string; 42 | mesh?: number; 43 | children?: number[]; 44 | rotation?: number[]; 45 | scale?: number[]; 46 | translation?: number[]; 47 | matrix?: number[]; 48 | extensions?: GltfNodeExtensions; 49 | } 50 | 51 | export interface GltfBuffer { 52 | uri?: string; 53 | byteLength: number; 54 | } 55 | 56 | export interface GltfBufferView { 57 | buffer: number; 58 | byteLength: number; 59 | byteOffset: number; 60 | target: GLenum; 61 | } 62 | 63 | export interface GltfAccessor { 64 | bufferView?: number; 65 | byteOffset?: number; 66 | componentType: GLenum; 67 | count: number; 68 | max?: number[]; 69 | min?: number[]; 70 | type: GltfType; 71 | } 72 | 73 | export interface GltfMesh { 74 | primitives: GltfPrimitive[]; 75 | } 76 | 77 | export interface GltfMaterial {} 78 | 79 | export type ValidPrimitiveAttributes = { 80 | POSITION: number; 81 | NORMAL: number; 82 | TANGENT: number; 83 | [other: string]: number; 84 | }; 85 | 86 | export type GltfPrimitive = { 87 | attributes: AtLeastOne; 88 | indices?: number; 89 | material?: number; 90 | mode?: number; 91 | extensions?: GltfPrimitiveExtensions; 92 | }; 93 | 94 | export type GltfTexture = { 95 | sampler?: number; 96 | source?: number; 97 | name?: string; 98 | extensions?: object; 99 | extras?: object; 100 | }; 101 | 102 | export type GltfSampler = { 103 | magFilter?: number; 104 | minFilter?: number; 105 | wrapS?: number; 106 | wrapT?: number; 107 | name?: string; 108 | extensions?: object; 109 | extras?: object; 110 | }; 111 | 112 | export type GltfImage = { 113 | uri?: string; 114 | mimeType?: string; 115 | bufferView?: number; 116 | name?: string; 117 | extensions?: object; 118 | extras?: object; 119 | }; 120 | export interface Gltf { 121 | scenes?: GltfScene[]; 122 | nodes: GltfNode[]; 123 | meshes: GltfMesh[]; 124 | buffers?: GltfBuffer[]; 125 | bufferViews?: GltfBufferView[]; 126 | accessors?: GltfAccessor[]; 127 | asset: any[]; 128 | extensions?: GltfExtensions; 129 | extensionsUsed?: string[]; 130 | materials: GltfMaterial[]; 131 | images?: GltfImage[]; 132 | textures?: GltfTexture[]; 133 | samplers?: GltfSampler[]; 134 | } 135 | 136 | // Extensions 137 | export interface GltfExtensions { 138 | CESIUM_3dtiles_feature_metadata?: FeatureMetadata; 139 | } 140 | 141 | export interface GltfPrimitiveExtensions { 142 | CESIUM_3dtiles_feature_metadata?: FeatureMetadataPrimitiveExtension; 143 | } 144 | -------------------------------------------------------------------------------- /lib/gltfUtil.ts: -------------------------------------------------------------------------------- 1 | import { Gltf, GltfBuffer, GltfType, GLenumName } from './gltfType'; 2 | import { Attribute } from './attribute'; 3 | 4 | export function buffersToGltfBuffer(buffers: Buffer[]): GltfBuffer { 5 | const amalgamated = Buffer.concat(buffers); 6 | const uri = 'data:application/octet-stream;base64,' + 7 | amalgamated.toString('base64') 8 | return { 9 | uri: uri, 10 | byteLength: amalgamated.length 11 | }; 12 | } 13 | 14 | /** 15 | * Iterates over the provided attributes, calculating the necessary 16 | * byteOffset for each attribute (accounting for padding) 17 | * @param attributes An array of InstancedAttributes to process 18 | * @returns The sum of each byteOffset for all of the attributes (including) 19 | * padding. 20 | */ 21 | 22 | export function calcByteOffset(attributes: Omit[]): number { 23 | let byteOffset = 0; 24 | for (const attrib of attributes) { 25 | const byteAlignment = attrib.byteAlignment == null ? 1 : attrib.byteAlignment; 26 | attrib.byteOffset = Math.ceil(byteOffset / byteAlignment) * byteAlignment; 27 | byteOffset += attrib.buffer.length; 28 | } 29 | return byteOffset; 30 | } 31 | 32 | /** 33 | * Combines multiple attributes into a singular buffer, then creates the 34 | * necessary bufferViews / accessors. 35 | * @param gltf The glTF asset to modify 36 | * @param attributes An array of attributes to inject into the provided glTF 37 | * asset 38 | */ 39 | export function addBinaryBuffers(gltf: Gltf, ...attributes: Omit[]) { 40 | calcByteOffset(attributes); 41 | 42 | // gltf.buffer 43 | const amalgamatedBuffer = buffersToGltfBuffer(attributes.map((a) => a.buffer)); 44 | gltf.buffers.push(amalgamatedBuffer); 45 | const bufferIndex = gltf.buffers.length - 1; 46 | 47 | // gltf.bufferviews / gltf.accessors 48 | for (const attrib of attributes) { 49 | gltf.bufferViews.push({ 50 | buffer: bufferIndex, 51 | byteLength: attrib.buffer.byteLength, 52 | byteOffset: attrib.byteOffset, 53 | target: GLenumName.ARRAY_BUFFER 54 | }); 55 | 56 | const bufferViewIndex = gltf.bufferViews.length - 1; 57 | 58 | gltf.accessors.push({ 59 | bufferView: bufferViewIndex, 60 | byteOffset: 0, 61 | componentType: attrib.componentType, 62 | count: attrib.count, 63 | min: attrib.min, 64 | max: attrib.max, 65 | type: attrib.type 66 | }); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /lib/initializeGltf.ts: -------------------------------------------------------------------------------- 1 | import { Gltf } from "./gltfType"; 2 | 3 | /** 4 | * Initializes root level properties in a glTF asset to an empty array 5 | * if they do not exist already. 6 | * @param gltf The glTF asset to modify. 7 | */ 8 | export function initalizeGltf(gltf: Gltf) { 9 | gltf.accessors = gltf.accessors == null ? [] : gltf.accessors; 10 | gltf.bufferViews = gltf.bufferViews == null ? [] : gltf.bufferViews; 11 | gltf.meshes = gltf.meshes == null ? [] : gltf.meshes; 12 | gltf.textures = gltf.textures == null ? [] : gltf.textures; 13 | gltf.images = gltf.images == null ? [] : gltf.images; 14 | gltf.textures = gltf.textures == null ? [] : gltf.textures; 15 | gltf.samplers = gltf.samplers == null ? [] : gltf.samplers; 16 | gltf.materials = gltf.materials == null ? [] : gltf.materials; 17 | } -------------------------------------------------------------------------------- /lib/ioUtil.ts: -------------------------------------------------------------------------------- 1 | import { TilesetJson } from './tilesetJson'; 2 | import { Gltf } from './gltfType'; 3 | import { GeneratorArgs } from './arguments'; 4 | import path = require('path'); 5 | import saveJson = require('./saveJson'); 6 | import saveBinary = require('./saveBinary'); 7 | 8 | const gltfPipeline = require('gltf-pipeline'); 9 | const gltfToGlb = gltfPipeline.gltfToGlb; 10 | 11 | export async function writeTile( 12 | destFolder: string, 13 | tileFileName: string, 14 | gltf: Gltf, 15 | args: GeneratorArgs 16 | ) { 17 | let tileDestination = path.join(destFolder, tileFileName); 18 | if (!args.useGlb) { 19 | await saveJson(tileDestination, gltf, args.prettyJson, args.gzip); 20 | } else { 21 | const glb = (await gltfToGlb(gltf, args.gltfConversionOptions)).glb; 22 | await saveBinary(tileDestination, glb, args.gzip); 23 | } 24 | } 25 | 26 | export async function writeTileset( 27 | destFolder: string, 28 | tileset: TilesetJson, 29 | args: GeneratorArgs 30 | ) { 31 | const tilesetDestination = path.join(destFolder, 'tileset.json'); 32 | await saveJson(tilesetDestination, tileset, args.prettyJson, args.gzip); 33 | } 34 | 35 | export async function writeTilesetAndTile( 36 | destFolder: string, 37 | tileFileName: string, 38 | tileset: TilesetJson, 39 | gltf: Gltf, 40 | args: GeneratorArgs 41 | ) { 42 | await writeTileset(destFolder, tileset, args); 43 | await writeTile(destFolder, tileFileName, gltf, args); 44 | } 45 | -------------------------------------------------------------------------------- /lib/meshView.ts: -------------------------------------------------------------------------------- 1 | import { Material } from './Material'; 2 | 3 | export class MeshView { 4 | material: Material; 5 | indexOffset: number; 6 | indexCount: number; 7 | 8 | /** 9 | * A subsection of the mesh with its own material. 10 | * 11 | * @param {Material} material The material. 12 | * @param {Number} indexOffset The start index into the mesh's indices 13 | * array. 14 | * @param {Number} indexCount The number of indices. 15 | * 16 | * @constructor 17 | * @private 18 | */ 19 | constructor(material: Material, indexOffset: number, indexCount: number) { 20 | this.material = material; 21 | this.indexOffset = indexOffset; 22 | this.indexCount = indexCount; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/modifyImageUri.ts: -------------------------------------------------------------------------------- 1 | import { Gltf } from './gltfType'; 2 | const gltfPipeline = require('gltf-pipeline'); 3 | const processGlb = gltfPipeline.processGlb; 4 | 5 | export async function modifyImageUri( 6 | glb: Buffer, 7 | resourceDirectory: string, 8 | newResourceDirectory: string 9 | ): Promise { 10 | const gltfOptions = { 11 | resourceDirectory: resourceDirectory, 12 | customStages: [ 13 | (gltf: Gltf) => { 14 | gltf.images[0].uri = newResourceDirectory + gltf.images[0].uri; 15 | return gltf; 16 | } 17 | ] 18 | }; 19 | const processed = await processGlb(glb, gltfOptions); 20 | return processed.glb as Buffer; 21 | } 22 | -------------------------------------------------------------------------------- /lib/sampleOutput.ts: -------------------------------------------------------------------------------- 1 | import { TilesetJson } from "./tilesetJson"; 2 | import { Gltf } from "./gltfType"; 3 | 4 | export interface SampleOutput { 5 | gltf: Gltf, 6 | tileset: TilesetJson 7 | } -------------------------------------------------------------------------------- /lib/saveBinary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | const fsExtra = require('fs-extra'); 4 | const zlib = require('zlib'); 5 | 6 | const defaultValue = Cesium.defaultValue; 7 | 8 | module.exports = saveBinary; 9 | 10 | /** 11 | * Save a binary file to disk. (Optionally using gzip) 12 | * 13 | * @param {String} path The object destination path. 14 | * @param {Buffer} contents A binary blob to write. 15 | * @param {Boolean} [gzip=false] Whether to gzip the tile. 16 | * 17 | * @returns {Promise} A promise that resolves when the tile is saved. 18 | */ 19 | 20 | function saveBinary(path, contents, gzip) { 21 | gzip = defaultValue(gzip, false); 22 | if (gzip) { 23 | contents = zlib.gzipSync(contents); 24 | } 25 | return fsExtra.outputFile(path, contents); 26 | } 27 | -------------------------------------------------------------------------------- /lib/saveJson.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const Cesium = require('cesium'); 3 | const fsExtra = require('fs-extra'); 4 | const saveBinary = require('./saveBinary'); 5 | 6 | const defaultValue = Cesium.defaultValue; 7 | 8 | module.exports = saveJson; 9 | 10 | /** 11 | * Save a json file to disk. 12 | * 13 | * @param {String} path The path to save the tileset.json. 14 | * @param {Object} json The JSON. 15 | * @param {Boolean} [prettyJson=true] Whether to prettify the JSON. 16 | * @param {Boolean} [gzip=false] Whether to use GZIP or not 17 | * 18 | * @returns {Promise} A promise that resolves when the tileset.json is saved. 19 | */ 20 | function saveJson(path, json, prettyJson, gzip) { 21 | prettyJson = defaultValue(prettyJson, true); 22 | const options = {}; 23 | if (prettyJson) { 24 | options.spaces = 2; 25 | } 26 | gzip = defaultValue(gzip, false); 27 | if (gzip) { 28 | return saveBinary(path, Buffer.from(JSON.stringify(json)), gzip); 29 | } 30 | 31 | return fsExtra.outputJson(path, json, options); 32 | } 33 | -------------------------------------------------------------------------------- /lib/tilesNextExtension.ts: -------------------------------------------------------------------------------- 1 | export enum TilesNextExtension { 2 | Gltf = '.gltf', 3 | Glb = '.glb' 4 | } 5 | -------------------------------------------------------------------------------- /lib/tilesetJson.ts: -------------------------------------------------------------------------------- 1 | import { AtLeastOne } from './atLeastN'; 2 | import { instancesRegion } from './constants'; 3 | import { TilesetOption } from './createTilesetJsonSingle'; 4 | 5 | type BoundingVolume = AtLeastOne<{ 6 | region: number[]; 7 | box: number[]; 8 | sphere: number[]; 9 | }>; 10 | 11 | export interface TilesetJsonRootChildren { 12 | children?: TilesetJsonRootChildren[]; 13 | boundingVolume: BoundingVolume; 14 | geometricError: number; 15 | transform?: number[]; 16 | content: { 17 | uri: string; 18 | }; 19 | extras?: { 20 | id: string; 21 | }; 22 | viewerRequestVolume?: BoundingVolume; 23 | } 24 | 25 | export interface TilesetJson { 26 | asset: { 27 | version: string; 28 | tilesetVersion?: string; 29 | }; 30 | properties?: { 31 | [propertyName: string]: { 32 | minimum: number; 33 | maximum: number; 34 | }; 35 | }; 36 | geometricError: number; 37 | root: { 38 | content?: { 39 | uri: string; 40 | boundingVolume?: { 41 | region: number[]; 42 | }; 43 | }; 44 | children?: TilesetJsonRootChildren[]; 45 | geometricError: number; 46 | versionNumber?: string; 47 | region?: number[]; 48 | box?: object; 49 | sphere?: object; 50 | transform?: number[]; 51 | eastNorthUp?: boolean; 52 | expire?: any; 53 | refine: string; 54 | boundingVolume: BoundingVolume; 55 | }; 56 | extensionsUsed?: string[]; 57 | extensionsRequired?: string[]; 58 | extensions?: object; 59 | extras?: { 60 | name?: string; 61 | }; 62 | } 63 | 64 | export function getTilesetOpts( 65 | contentUri: string, 66 | geometricError: number, 67 | versionNumber: string, 68 | region: number[] = instancesRegion 69 | ): TilesetOption { 70 | return { 71 | contentUri: contentUri, 72 | geometricError: geometricError, 73 | versionNumber: versionNumber, 74 | region: region 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /lib/tilesetUtilsNext.ts: -------------------------------------------------------------------------------- 1 | import { createBuildings } from './createBuilding'; 2 | import { BatchTable, generateBuildingBatchTable } from './createBuildingsTile'; 3 | import { Mesh } from './Mesh'; 4 | import { FeatureTableUtils } from './featureMetatableUtilsNext'; 5 | import { Gltf } from './gltfType'; 6 | import { FeatureMetadata } from './featureMetadata'; 7 | import { 8 | buildingsTransform, 9 | largeGeometricError, 10 | latitude, 11 | latitudeExtent, 12 | longitude, 13 | longitudeExtent, TileOptions, tilesNextTilesetJsonVersion, 14 | tileWidth 15 | } from './constants'; 16 | import { TilesNextExtension } from './tilesNextExtension'; 17 | import createGltf = require('./createGltf'); 18 | import { Cartesian3, defined, Matrix4 } from 'cesium'; 19 | 20 | export namespace TilesetUtilsNext { 21 | export function createBuildingGltfsWithFeatureMetadata( 22 | tileOptions: TileOptions[] 23 | ): { gltfs: Gltf[]; batchTables: BatchTable[] } { 24 | const buildings = tileOptions.map(opt => 25 | createBuildings(opt.buildingOptions) 26 | ); 27 | 28 | const batchTables = buildings.map(building => 29 | generateBuildingBatchTable(building) 30 | ); 31 | 32 | const batchedMeshes = buildings.map((building, i) => { 33 | const transform = tileOptions[i].transform; 34 | return Mesh.batch( 35 | FeatureTableUtils.createMeshes(transform, building, false) 36 | ); 37 | }); 38 | 39 | // remove explicit ID from batchTable, it's not used in the 40 | // feature metadata gltf extension 41 | batchTables.forEach(table => delete table.id); 42 | 43 | const gltfs = batchedMeshes.map(mesh => 44 | createGltf({ mesh: mesh, useBatchIds: false }) 45 | ) as Gltf[]; 46 | 47 | gltfs.forEach((gltf, i) => { 48 | const batchTable = batchTables[i]; 49 | 50 | FeatureMetadata.updateExtensionUsed(gltf); 51 | FeatureMetadata.addFeatureTable(gltf, { 52 | featureCount: batchTable.Longitude.length, 53 | properties: { 54 | Longitude: { values: batchTable.Longitude }, 55 | Latitude: { values: batchTable.Latitude }, 56 | Height: { values: batchTable.Height } 57 | } 58 | }); 59 | 60 | const primitive = gltf.meshes[0].primitives[0]; 61 | FeatureMetadata.addFeatureLayer(primitive, { 62 | featureTable: 0, 63 | vertexAttribute: { 64 | implicit: { 65 | increment: 1, 66 | start: 0 67 | } 68 | } 69 | }); 70 | 71 | const batchedMesh = batchedMeshes[i]; 72 | const rtcCenter = Cartesian3.pack(batchedMesh.center, new Array(3)); 73 | 74 | // apply RTC 75 | delete gltf.nodes[0].mesh; 76 | gltf.nodes[0].children = [1]; 77 | gltf.nodes.push({ 78 | name: 'RTC_CENTER', 79 | translation: rtcCenter, 80 | mesh: 0 81 | }); 82 | }); 83 | 84 | return { 85 | gltfs: gltfs, 86 | batchTables: batchTables 87 | }; 88 | } 89 | 90 | export type SubdivisionCallback = ( 91 | level: number, 92 | x: number, 93 | y: number 94 | ) => boolean; 95 | 96 | export function createUniformTileset( 97 | depth: number, 98 | divisions: number, 99 | extension: TilesNextExtension, 100 | subdivideCallback: SubdivisionCallback 101 | ) { 102 | depth = Math.max(depth, 1); 103 | divisions = Math.max(divisions, 1); 104 | 105 | const tileOptions: TileOptions[] = []; 106 | const tileNames: string[] = []; 107 | 108 | const tilesetJson = { 109 | asset : { 110 | version : tilesNextTilesetJsonVersion 111 | }, 112 | properties : undefined, 113 | geometricError : largeGeometricError 114 | }; 115 | 116 | divideTile( 117 | 0, 118 | 0, 119 | 0, 120 | divisions, 121 | depth, 122 | tilesetJson as any, // TODO: 123 | tileOptions, 124 | tileNames, 125 | extension, 126 | subdivideCallback 127 | ); 128 | 129 | return { 130 | tilesetJson : tilesetJson, 131 | tileOptions : tileOptions, 132 | tileNames : tileNames 133 | }; 134 | } 135 | 136 | // TODO: Reduce the number of arguments? The orginal version of this 137 | // function had 9 arguments already. 138 | // TODO: Remove references to `any`. The original version of this function 139 | // used recursion to dynamically build the object, which doens't 140 | // translate to Typescript well without casting. 141 | function divideTile( 142 | level: number, x: number, y: number, divisions: number, 143 | depth: number, parent: any, tileOptions: any, 144 | tileNames: string[], extension: TilesNextExtension, 145 | subdivideCallback: SubdivisionCallback) { 146 | const uri = level + '_' + x + '_' + y + extension; 147 | const tilesPerAxis = Math.pow(divisions, level); 148 | 149 | const buildingsLength = divisions * divisions; 150 | const buildingsPerAxis = Math.sqrt(buildingsLength); 151 | 152 | const tileWidthMeters = tileWidth / tilesPerAxis; 153 | const tileLongitudeExtent = longitudeExtent / tilesPerAxis; 154 | const tileLatitudeExtent = latitudeExtent / tilesPerAxis; 155 | const tileHeightMeters = tileWidthMeters / (buildingsPerAxis * 3); 156 | 157 | const xOffset = -tileWidth / 2.0 + (x + 0.5) * tileWidthMeters; 158 | const yOffset = -tileWidth / 2.0 + (y + 0.5) * tileWidthMeters; 159 | const transform = Matrix4.fromTranslation(new Cartesian3(xOffset, yOffset, 0)); 160 | 161 | const west = longitude - longitudeExtent / 2.0 + x * tileLongitudeExtent; 162 | const south = latitude - latitudeExtent / 2.0 + y * tileLatitudeExtent; 163 | const east = west + tileLongitudeExtent; 164 | const north = south + tileLatitudeExtent; 165 | const tileLongitude = west + (east - west) / 2.0; 166 | const tileLatitude = south + (north - south) / 2.0; 167 | const region = [west, south, east, north, 0, tileHeightMeters]; 168 | 169 | const isLeaf = (level === depth - 1); 170 | const isRoot = (level === 0); 171 | const subdivide = !isLeaf && (!defined(subdivideCallback) || subdivideCallback(level, x, y)); 172 | const geometricError = (isLeaf) ? 0.0 : largeGeometricError / Math.pow(2, level + 1); 173 | const children = (subdivide) ? [] : undefined; 174 | 175 | const tileJson: any = { 176 | boundingVolume : { 177 | region : region 178 | }, 179 | geometricError : geometricError, 180 | content : { 181 | uri : uri 182 | }, 183 | children : children 184 | }; 185 | 186 | if (isRoot) { 187 | parent.root = tileJson; 188 | tileJson.transform = Matrix4.pack(buildingsTransform, new Array(16)); 189 | tileJson.refine = 'REPLACE'; 190 | } else { 191 | parent.children.push(tileJson); 192 | } 193 | 194 | tileOptions.push({ 195 | buildingOptions: { 196 | uniform: true, 197 | numberOfBuildings: buildingsLength, 198 | tileWidth: tileWidthMeters, 199 | longitude: tileLongitude, 200 | latitude: tileLatitude 201 | }, 202 | createBatchTable: true, 203 | transform: transform 204 | }); 205 | 206 | tileNames.push(uri); 207 | 208 | const nextLevel = level + 1; 209 | const nextX = divisions * x; 210 | const nextY = divisions * y; 211 | 212 | 213 | if (subdivide) { 214 | for (let i = 0; i < divisions; ++i) { 215 | for (let j = 0; j < divisions; ++j) { 216 | divideTile( 217 | nextLevel, 218 | nextX + i, 219 | nextY + j, 220 | divisions, 221 | depth, 222 | tileJson, 223 | tileOptions, 224 | tileNames, 225 | extension, 226 | subdivideCallback 227 | ); 228 | } 229 | } 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /lib/typeConversion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | isValidWebGLDataTypeEnum: isValidWebGLDataTypeEnum, 5 | componentTypeStringToInteger: componentTypeToInteger, 6 | webglDataTypeToByteSize: webglDataTypeToByteSize, 7 | elementTypeToCount: elementTypeToCount 8 | }; 9 | 10 | function isValidWebGLDataTypeEnum(value) { 11 | const isNumber = typeof value === 'number'; 12 | return isNumber && value >= 0x1400 && value <= 0x1406; 13 | } 14 | 15 | function componentTypeToInteger(value) { 16 | if (value === 'BYTE') { 17 | return 0x1400; 18 | } 19 | if (value === 'UNSIGNED_BYTE') { 20 | return 0x1401; 21 | } 22 | if (value === 'SHORT') { 23 | return 0x1402; 24 | } 25 | if (value === 'UNSIGNED_SHORT') { 26 | return 0x1403; 27 | } 28 | if (value === 'INT') { 29 | return 0x1404; 30 | } 31 | if (value === 'UNSIGNED_INT') { 32 | return 0x1405; 33 | } 34 | if (value === 'FLOAT') { 35 | return 0x1406; 36 | } 37 | throw new Error(`Unknown component type string: ${ value}`); 38 | } 39 | 40 | function elementTypeToCount(value) { 41 | if (value === 'VEC4') { 42 | return 4; 43 | } 44 | if (value === 'VEC3') { 45 | return 3; 46 | } 47 | if (value === 'VEC2') { 48 | return 2; 49 | } 50 | if (value === 'SCALAR') { 51 | return 1; 52 | } 53 | throw Error(`Unknown value${ value}`); 54 | } 55 | 56 | function webglDataTypeToByteSize(value) { 57 | if (value === 0x1400) { 58 | return 1; 59 | } // Byte 60 | if (value === 0x1401) { 61 | return 1; 62 | } // Unsigned Byte 63 | if (value === 0x1402) { 64 | return 2; 65 | } // Short 66 | if (value === 0x1403) { 67 | return 2; 68 | } // Unsigned Short 69 | if (value === 0x1404) { 70 | return 4; 71 | } // Int 72 | if (value === 0x1405) { 73 | return 4; 74 | } // Unsigned Int 75 | if (value === 0x1406) { 76 | return 4; 77 | } // Float 78 | throw new Error(`Unknown WebGL data type: ${ value}`); 79 | } 80 | -------------------------------------------------------------------------------- /lib/typeSize.ts: -------------------------------------------------------------------------------- 1 | export const FLOAT32_SIZE_BYTES = 4; 2 | export const UINT32_SIZE_BYTES = 4; 3 | export const UINT16_SIZE_BYTES = 2; 4 | export const UINT8_SIZE_BYTES = 1; 5 | -------------------------------------------------------------------------------- /lib/utility.ts: -------------------------------------------------------------------------------- 1 | import { Cartesian3, HeadingPitchRoll, Matrix4, Transforms } from 'cesium'; 2 | 3 | export function metersToLongitude(meters: number, latitude: number): number { 4 | return (meters * 0.000000156785) / Math.cos(latitude); 5 | } 6 | 7 | export function metersToLatitude(meters: number): number { 8 | return meters * 0.000000157891; 9 | } 10 | 11 | export function wgs84Transform( 12 | longitude: number, 13 | latitude: number, 14 | height: number 15 | ): Matrix4 { 16 | return Transforms.headingPitchRollToFixedFrame( 17 | Cartesian3.fromRadians(longitude, latitude, height), 18 | new HeadingPitchRoll() 19 | ); 20 | } 21 | 22 | export function toCamelCase(s: string): string { 23 | return s[0].toLowerCase() + s.slice(1); 24 | } 25 | 26 | export function typeToNumberOfComponents(type: string): number { 27 | switch (type) { 28 | case 'SCALAR': 29 | return 1; 30 | case 'VEC2': 31 | return 2; 32 | case 'VEC3': 33 | return 3; 34 | case 'VEC4': 35 | return 4; 36 | case 'MAT2': 37 | return 4; 38 | case 'MAT3': 39 | return 9; 40 | case 'MAT4': 41 | return 16; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/xor.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/53229857 2 | type Without = { [P in Exclude]?: never }; 3 | export type XOR = T | U extends object 4 | ? (Without & U) | (Without & T) 5 | : T | U; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3d-tiles-samples-generator", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "description": "Tools for generating sample 3D Tiles tilesets.", 6 | "author": { 7 | "name": "Cesium GS, Inc. and Contributors", 8 | "url": "https://github.com/CesiumGS/3d-tiles-samples-generator/graphs/contributors" 9 | }, 10 | "keywords": [ 11 | "3D Tiles" 12 | ], 13 | "homepage": "https://github.com/CesiumGS/3d-tiles-samples-generator", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/CesiumGS/3d-tiles-samples-generator.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/CesiumGS/3d-tiles-samples-generator/issues" 20 | }, 21 | "main": "index.js", 22 | "engines": { 23 | "node": ">=16.0.0" 24 | }, 25 | "dependencies": { 26 | "bluebird": "^3.7.2", 27 | "cesium": "^1.71.0", 28 | "draco3d": "1.3.6", 29 | "fs-extra": "^9.0.1", 30 | "gltf-pipeline": "^3.0.1", 31 | "mime": "^2.4.6", 32 | "simplex-noise": "^2.4.0" 33 | }, 34 | "devDependencies": { 35 | "@types/bluebird": "3.5.20", 36 | "@types/fs-extra": "^9.0.1", 37 | "@types/jasmine": "^3.5.11", 38 | "@types/jest": "^26.0.5", 39 | "@types/node": "^14.0.24", 40 | "cloc": "^2.5.1", 41 | "copyfiles": "^2.3.0", 42 | "datauri": "^3.0.0", 43 | "del-cli": "^3.0.1", 44 | "eslint": "^8.0.0", 45 | "eslint-config-cesium": "^9.0.0", 46 | "eslint-config-prettier": "^8.5.0", 47 | "eslint-plugin-node": "^11.1.0", 48 | "gulp": "^4.0.2", 49 | "jasmine": "^3.5.0", 50 | "jasmine-spec-reporter": "^5.0.2", 51 | "jest": "^26.1.0", 52 | "nyc": "^15.1.0", 53 | "open": "^7.1.0", 54 | "ts-jest": "^26.1.3", 55 | "typescript": "^3.9.7", 56 | "yargs": "^15.4.1" 57 | }, 58 | "scripts": { 59 | "prebuild": "del-cli dist", 60 | "build": "tsc", 61 | "postbuild": "copyfiles -u \"lib/**/*\" \"specs/**/*\" \"data/**/*\" -e '*.js' -e '*.ts' dist", 62 | "watch": "tsc -w", 63 | "eslint": "eslint \"./**/*.js\" --cache --quiet", 64 | "test": "jest", 65 | "test-watch": "npx jest --watch", 66 | "coverage": "npx jest --coverage", 67 | "cloc": "gulp cloc", 68 | "generate-third-party": "gulp generate-third-party" 69 | }, 70 | "bin": { 71 | "3d-tiles-samples-generator": "./bin/3d-tiles-samples-generator" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc.json", 3 | "env": { 4 | "jest": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/bin/createConstantAttribute.test.ts: -------------------------------------------------------------------------------- 1 | import { createConstantAttributeLEU32 } from '../../lib/createConstantAttribute'; 2 | import { GltfComponentType } from '../../lib/gltfType'; 3 | 4 | describe('createConstantAttributeTest', () => { 5 | it('generated attribute is valid', () => { 6 | const propertyName = 'test;'; 7 | const constant = 1995; 8 | const len = 10; 9 | 10 | const result = createConstantAttributeLEU32( 11 | propertyName, 12 | constant, 13 | len 14 | ); 15 | expect(result.buffer.byteLength).toEqual(4 * 10); 16 | expect(result.byteOffset).toEqual(0); 17 | expect(result.componentType).toEqual(GltfComponentType.UNSIGNED_INT); 18 | expect(result.count).toEqual(10); 19 | expect(result.min.length).toEqual(1); 20 | expect(result.max.length).toEqual(1); 21 | expect(result.min[0]).toEqual(constant); 22 | expect(result.max[0]).toEqual(constant); 23 | expect(result.propertyName).toEqual(propertyName); 24 | expect(result.byteAlignment).toEqual(1); 25 | 26 | // inspect the actual contents of the buffer 27 | for (let i = 0; i < result.buffer.byteLength; i += 4) { 28 | const b = result.buffer; 29 | const v = 30 | (b[i + 3] << 24) | 31 | (b[i + 2] << 16) | 32 | (b[i + 1] << 8) | 33 | b[i + 0]; 34 | expect(v).toEqual(constant); 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/bin/createFeatureMetadataExtensionSpec.test.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureMetadataExtension } from '../../lib/createFeatureMetadataExtension'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const trianglePath = path.join(__dirname, '..', 'data', 'triangle.gltf'); 6 | const minimalTriangleGLTF = JSON.parse(fs.readFileSync(trianglePath, 'utf8')); 7 | 8 | describe('createFeatureMetadataExtension', function() { 9 | const triangleGLTF = minimalTriangleGLTF; 10 | const sharedBuffer = Buffer.from('abcdefghijklmnopqrstuvwxyz'); 11 | const featureTableAttributes = { 12 | aToMInclusive: { 13 | name: 'aToMInclusive', 14 | byteOffset: 0, 15 | byteLength: 13, 16 | count: 13, 17 | componentType: 0x1401, 18 | type: 'SCALAR' 19 | }, 20 | 21 | nToZInclusive: { 22 | name: 'nToZInclusive', 23 | byteOffset: 12, 24 | byteLength: 13, 25 | count: 13, 26 | componentType: 0x1401, 27 | type: 'SCALAR' 28 | }, 29 | 30 | cornerName: [ 31 | 'alfa', 'bravo', 'charlie', 32 | 'delta', 'echo', 'foxtrot', 33 | 'golf', 'hotel', 'india', 34 | 'juliett', 'kilo', 'lima', 35 | 'mike' 36 | ], 37 | 38 | cornerColor: [ 39 | 'red', 'blue', 'green', 40 | 'indgio', 'cyan', 'yellow', 41 | 'periwinkle', 'grey', 'brown', 42 | 'purple', 'magenta', 'lime', 43 | 'orange' 44 | ] 45 | }; 46 | 47 | const oldAccessorLength = Object.keys(triangleGLTF.accessors).length; 48 | const oldBufferViewLength = Object.keys(triangleGLTF.bufferViews).length; 49 | const oldBufferLength = Object.keys(triangleGLTF.buffers).length; 50 | const gltfWithExt = createFeatureMetadataExtension(triangleGLTF, featureTableAttributes, sharedBuffer); 51 | 52 | it('extensions used / extension keys are present', function() { 53 | expect('extensionsUsed' in gltfWithExt).toBe(true); 54 | expect('extensions' in gltfWithExt).toBe(true); 55 | expect('CESIUM_3dtiles_feature_metadata' in gltfWithExt.extensions).toBe(true); 56 | const featureTables = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables; 57 | expect(featureTables).toBeInstanceOf(Array); 58 | expect(featureTables.length).toBe(1); // Multiple Batch Table not Supported Yet 59 | }); 60 | 61 | it('has correct number of feature table attributes', function() { 62 | const table = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0]; 63 | expect(table.featureCount).toBe(featureTableAttributes.cornerName.length); 64 | }); 65 | 66 | it('human readable attributes are detected and left as-is in the ext section', function() { 67 | const properties = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0].properties; 68 | // human readable values are embedded directly 69 | expect(properties.cornerName.values).toEqual(featureTableAttributes.cornerName); 70 | expect(properties.cornerColor.values).toEqual(featureTableAttributes.cornerColor); 71 | 72 | // binary values should only have an accessor key 73 | // accessor ids are sorted by byteOffset, so aToL should be first 74 | expect(Object.keys(properties.aToMInclusive).length).toEqual(1); 75 | expect(Object.keys(properties.nToZInclusive).length).toEqual(1); 76 | expect(properties.aToMInclusive.accessor).toEqual(oldAccessorLength); 77 | expect(properties.nToZInclusive.accessor).toEqual(oldAccessorLength + 1); 78 | }); 79 | 80 | it('buffers / accessors / bufferviews are updated with binary data', function() { 81 | const accessors = gltfWithExt.accessors; 82 | expect(accessors.length).toEqual(4); 83 | expect(Object.keys(gltfWithExt.accessors).length).toBe(oldAccessorLength + 2); 84 | expect(Object.keys(gltfWithExt.bufferViews).length).toBe(oldBufferViewLength + 2); 85 | expect(Object.keys(gltfWithExt.buffers).length).toBe(oldBufferLength + 1); 86 | expect(gltfWithExt.buffers[1].byteLength).toEqual(sharedBuffer.length); 87 | 88 | // verify the buffer was written correctly for the binary data 89 | const newBuffer = triangleGLTF.buffers[1]; 90 | const decodedBase64 = Buffer.from(newBuffer.uri.replace('data:application/octet-stream;base64,', ''), 'base64'); 91 | expect(decodedBase64).toEqual(sharedBuffer); 92 | 93 | const secondToLastAccessor = gltfWithExt.accessors[oldAccessorLength]; 94 | const lastAccessor = gltfWithExt.accessors[oldAccessorLength + 1]; 95 | expect(secondToLastAccessor.bufferView).toBe(oldAccessorLength); 96 | expect(secondToLastAccessor.byteOffset).toBe(0); 97 | expect(secondToLastAccessor.componentType).toBe(featureTableAttributes.aToMInclusive.componentType); 98 | expect(secondToLastAccessor.type).toBe(featureTableAttributes.aToMInclusive.type); 99 | expect(secondToLastAccessor.count).toBe(featureTableAttributes.aToMInclusive.count); 100 | 101 | expect(lastAccessor.bufferView).toBe(oldAccessorLength + 1); 102 | expect(lastAccessor.byteOffset).toBe(0); 103 | expect(lastAccessor.componentType).toBe(featureTableAttributes.nToZInclusive.componentType); 104 | expect(lastAccessor.type).toBe(featureTableAttributes.nToZInclusive.type); 105 | expect(lastAccessor.count).toBe(featureTableAttributes.nToZInclusive.count); 106 | 107 | // verify those bufferviews are both referencing `buffer 1` 108 | const secondToLastBufferView = gltfWithExt.bufferViews[oldBufferViewLength]; 109 | const lastBufferView = gltfWithExt.bufferViews[oldBufferViewLength + 1]; 110 | expect(secondToLastBufferView.buffer).toBe(1); 111 | expect(secondToLastBufferView.byteLength).toBe(featureTableAttributes.aToMInclusive.byteLength); 112 | expect(secondToLastBufferView.byteOffset).toBe(featureTableAttributes.aToMInclusive.byteOffset); 113 | expect(lastBufferView.buffer).toBe(1); 114 | expect(lastBufferView.byteLength).toBe(featureTableAttributes.nToZInclusive.byteLength); 115 | expect(lastBufferView.byteOffset).toBe(featureTableAttributes.nToZInclusive.byteOffset); 116 | 117 | // verify that references to the binary accessors are in `extensions: {...}` 118 | const featureTableProperties = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0].properties; 119 | expect(featureTableAttributes.aToMInclusive.name in featureTableProperties).toEqual(true); 120 | expect(featureTableAttributes.nToZInclusive.name in featureTableProperties).toEqual(true); 121 | expect(featureTableProperties[featureTableAttributes.aToMInclusive.name].accessor).toBe(oldAccessorLength); 122 | expect(featureTableProperties[featureTableAttributes.nToZInclusive.name].accessor).toBe(oldAccessorLength + 1); 123 | }); 124 | }); -------------------------------------------------------------------------------- /test/bin/createFeatureMetadataExtensionSpec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { createFeatureMetadataExtension } from "../../lib/createFeatureMetadataExtension"; 3 | 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var trianglePath = path.join(__dirname, '..', 'data', 'triangle.gltf'); 7 | var minimalTriangleGLTF = JSON.parse(fs.readFileSync(trianglePath, 'utf8')); 8 | 9 | describe('createFeatureMetadataExtension', function() { 10 | var triangleGLTF = minimalTriangleGLTF; 11 | var sharedBuffer = Buffer.from('abcdefghijklmnopqrstuvwxyz'); 12 | var featureTableAttributes = { 13 | aToMInclusive: { 14 | name: 'aToMInclusive', 15 | byteOffset: 0, 16 | byteLength: 13, 17 | count: 13, 18 | componentType: 0x1401, 19 | type: 'SCALAR' 20 | }, 21 | 22 | nToZInclusive: { 23 | name: 'nToZInclusive', 24 | byteOffset: 12, 25 | byteLength: 13, 26 | count: 13, 27 | componentType: 0x1401, 28 | type: 'SCALAR' 29 | }, 30 | 31 | cornerName: [ 32 | 'alfa', 'bravo', 'charlie', 33 | 'delta', 'echo', 'foxtrot', 34 | 'golf', 'hotel', 'india', 35 | 'juliett', 'kilo', 'lima', 36 | 'mike' 37 | ], 38 | 39 | cornerColor: [ 40 | 'red', 'blue', 'green', 41 | 'indgio', 'cyan', 'yellow', 42 | 'periwinkle', 'grey', 'brown', 43 | 'purple', 'magenta', 'lime', 44 | 'orange' 45 | ] 46 | }; 47 | 48 | /* 49 | data = [ 50 | {type: binary, data: BINARY}, 51 | {type: plaintext, data: {cornerNames: […], cornerColors: […]}} 52 | {type: plaintext, data: {moreData: […]}} 53 | ] 54 | */ 55 | 56 | 57 | var oldAccessorLength = Object.keys(triangleGLTF.accessors).length; 58 | var oldBufferViewLength = Object.keys(triangleGLTF.bufferViews).length; 59 | var oldBufferLength = Object.keys(triangleGLTF.buffers).length; 60 | var gltfWithExt = createFeatureMetadataExtension(triangleGLTF, featureTableAttributes, sharedBuffer); 61 | 62 | it('extensions used / extension keys are present', function() { 63 | expect('extensionsUsed' in gltfWithExt).toBe(true); 64 | expect('extensions' in gltfWithExt).toBe(true); 65 | expect('CESIUM_3dtiles_feature_metadata' in gltfWithExt.extensions).toBe(true); 66 | var featureTables = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables; 67 | expect(featureTables).toBeInstanceOf(Array); 68 | expect(featureTables.length).toBe(1); // Multiple Batch Table not Supported Yet 69 | }); 70 | 71 | it('has correct number of feature table attributes', function() { 72 | var table = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0]; 73 | expect(table.featureCount).toBe(featureTableAttributes.cornerName.length); 74 | }); 75 | 76 | it('human readable attributes are detected and left as-is in the ext section', function() { 77 | var properties = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0].properties; 78 | // human readable values are embedded directly 79 | expect(properties.cornerName.values).toEqual(featureTableAttributes.cornerName); 80 | expect(properties.cornerColor.values).toEqual(featureTableAttributes.cornerColor); 81 | 82 | // binary values should only have an accessor key 83 | // accessor ids are sorted by byteOffset, so aToL should be first 84 | expect(Object.keys(properties.aToMInclusive).length).toEqual(1); 85 | expect(Object.keys(properties.nToZInclusive).length).toEqual(1); 86 | expect(properties.aToMInclusive.accessor).toEqual(oldAccessorLength); 87 | expect(properties.nToZInclusive.accessor).toEqual(oldAccessorLength + 1); 88 | }); 89 | 90 | it('buffers / accessors / bufferviews are updated with binary data', function() { 91 | var accessors = gltfWithExt.accessors; 92 | expect(accessors.length).toEqual(4); 93 | expect(Object.keys(gltfWithExt.accessors).length).toBe(oldAccessorLength + 2); 94 | expect(Object.keys(gltfWithExt.bufferViews).length).toBe(oldBufferViewLength + 2); 95 | expect(Object.keys(gltfWithExt.buffers).length).toBe(oldBufferLength + 1); 96 | expect(gltfWithExt.buffers[1].byteLength).toEqual(sharedBuffer.length); 97 | 98 | // verify the buffer was written correctly for the binary data 99 | var newBuffer = triangleGLTF.buffers[1]; 100 | var decodedBase64 = Buffer.from(newBuffer.uri.replace('data:application/octet-stream;base64,', ''), 'base64'); 101 | expect(decodedBase64).toEqual(sharedBuffer); 102 | 103 | var secondToLastAccessor = gltfWithExt.accessors[oldAccessorLength]; 104 | var lastAccessor = gltfWithExt.accessors[oldAccessorLength + 1]; 105 | expect(secondToLastAccessor.bufferView).toBe(oldAccessorLength); 106 | expect(secondToLastAccessor.byteOffset).toBe(0); 107 | expect(secondToLastAccessor.componentType).toBe(featureTableAttributes.aToMInclusive.componentType); 108 | expect(secondToLastAccessor.type).toBe(featureTableAttributes.aToMInclusive.type); 109 | expect(secondToLastAccessor.count).toBe(featureTableAttributes.aToMInclusive.count); 110 | 111 | expect(lastAccessor.bufferView).toBe(oldAccessorLength + 1); 112 | expect(lastAccessor.byteOffset).toBe(0); 113 | expect(lastAccessor.componentType).toBe(featureTableAttributes.nToZInclusive.componentType); 114 | expect(lastAccessor.type).toBe(featureTableAttributes.nToZInclusive.type); 115 | expect(lastAccessor.count).toBe(featureTableAttributes.nToZInclusive.count); 116 | 117 | // verify those bufferviews are both referencing `buffer 1` 118 | var secondToLastBufferView = gltfWithExt.bufferViews[oldBufferViewLength]; 119 | var lastBufferView = gltfWithExt.bufferViews[oldBufferViewLength + 1]; 120 | expect(secondToLastBufferView.buffer).toBe(1); 121 | expect(secondToLastBufferView.byteLength).toBe(featureTableAttributes.aToMInclusive.byteLength); 122 | expect(secondToLastBufferView.byteOffset).toBe(featureTableAttributes.aToMInclusive.byteOffset); 123 | expect(lastBufferView.buffer).toBe(1); 124 | expect(lastBufferView.byteLength).toBe(featureTableAttributes.nToZInclusive.byteLength); 125 | expect(lastBufferView.byteOffset).toBe(featureTableAttributes.nToZInclusive.byteOffset); 126 | 127 | // verify that references to the binary accessors are in `extensions: {...}` 128 | var featureTableProperties = gltfWithExt.extensions.CESIUM_3dtiles_feature_metadata.featureTables[0].properties; 129 | expect(featureTableAttributes.aToMInclusive.name in featureTableProperties).toEqual(true); 130 | expect(featureTableAttributes.nToZInclusive.name in featureTableProperties).toEqual(true); 131 | expect(featureTableProperties[featureTableAttributes.aToMInclusive.name].accessor).toBe(oldAccessorLength); 132 | expect(featureTableProperties[featureTableAttributes.nToZInclusive.name].accessor).toBe(oldAccessorLength + 1); 133 | }); 134 | }); -------------------------------------------------------------------------------- /test/bin/createGltfFromPntsSpec.test.ts: -------------------------------------------------------------------------------- 1 | import { GLenumName } from '../../lib/gltfType'; 2 | 3 | const createGltfFromPnts = require('../../lib/createGltfFromPnts'); 4 | const typeConversion = require('../../lib/typeConversion'); 5 | 6 | describe('createGltfFromPnts', function() { 7 | const attributeBuffers = [ 8 | { 9 | // position 10 | buffer: Buffer.from([-1, 0, 0, 0, 1, 0, 1, 0, 0]), 11 | componentType: 'FLOAT', 12 | propertyName: 'POSITION', 13 | type: 'VEC3', 14 | target: GLenumName.ARRAY_BUFFER, 15 | count: 3, 16 | min: [-1, 0, 0], 17 | max: [1, 1, 1] 18 | }, 19 | { 20 | // normals 21 | buffer: Buffer.from([0, 0, 1, 0, 0, 1, 0, 0, 1]), 22 | componentType: 'FLOAT', 23 | propertyName: 'NORMAL', 24 | type: 'VEC3', 25 | target: GLenumName.ARRAY_BUFFER, 26 | count: 3, 27 | min: [0, 0, 1], 28 | max: [0, 0, 1] 29 | }, 30 | { 31 | // colors 32 | buffer: Buffer.from([255, 0, 0, 0, 255, 0, 0, 0, 255]), 33 | componentType: 'UNSIGNED_BYTE', 34 | propertyName: 'RGB', 35 | type: 'VEC3', 36 | target: GLenumName.ARRAY_BUFFER, 37 | count: 3, 38 | min: [0, 0, 0], 39 | max: [255, 255, 255] 40 | } 41 | ]; 42 | 43 | const indexBuffer = { 44 | buffer: Uint8Array.of(0, 1, 2), 45 | componentType: 'UNSIGNED_SHORT', 46 | propertyName: 'INDICES' , 47 | type: 'SCALAR', 48 | target: GLenumName.ELEMENT_ARRAY_BUFFER, 49 | count: 3, 50 | min: [0], 51 | max: [2] 52 | }; 53 | 54 | const gltfWithIndex = createGltfFromPnts(attributeBuffers, indexBuffer); 55 | 56 | it('attributeBuffers / indexAttribute is encoded as base64 properly', function() { 57 | let megaBuffer = Buffer.concat(attributeBuffers.map(function (ab) { return ab.buffer; })); 58 | megaBuffer = Buffer.concat([megaBuffer, indexBuffer.buffer]); 59 | const megaBufferBase64 = 'data:application/octet-stream;base64,' + Buffer.from(megaBuffer).toString('base64'); 60 | const buffers = gltfWithIndex.buffers; 61 | expect(buffers).not.toBeUndefined(); 62 | expect(buffers[0].byteLength).toEqual(megaBuffer.length); 63 | expect(buffers[0].uri).toEqual(megaBufferBase64); 64 | }); 65 | 66 | it('generated gltf has four bufferviews', function() { 67 | const bufferViews = gltfWithIndex.bufferViews; 68 | expect(bufferViews).not.toBeUndefined(); 69 | expect(bufferViews.length).toEqual(attributeBuffers.length + 1); 70 | 71 | let byteOffset = 0; 72 | let i: number; 73 | for (i = 0; i < attributeBuffers.length; ++i) { 74 | expect(bufferViews[i].buffer).toEqual(0); 75 | expect(bufferViews[i].byteOffset).toEqual(byteOffset); 76 | expect(bufferViews[i].target).toEqual(attributeBuffers[i].target); 77 | expect(bufferViews[i].byteLength).toEqual(attributeBuffers[i].buffer.byteLength); 78 | byteOffset += attributeBuffers[i].buffer.byteLength; 79 | } 80 | 81 | expect(bufferViews[i].buffer).toEqual(0); 82 | expect(bufferViews[i].byteOffset).toEqual(byteOffset); 83 | expect(bufferViews[i].target).toEqual(indexBuffer.target); 84 | expect(bufferViews[i].byteLength).toEqual(indexBuffer.buffer.byteLength); 85 | }); 86 | 87 | it('generated gltf has four valid accessors', function() { 88 | expect(gltfWithIndex.accessors).toBeInstanceOf(Array); 89 | // +1 for the indexBuffer 90 | expect(gltfWithIndex.accessors.length).toBe(attributeBuffers.length + 1); 91 | 92 | const accessors = gltfWithIndex.accessors; 93 | let expectedItemCount; 94 | let i: number; 95 | 96 | for (i = 0; i < attributeBuffers.length - 1; ++i) { 97 | expect(accessors[i].bufferView).toEqual(i); 98 | expect(accessors[i].byteOffset).toEqual(0); 99 | expect(accessors[i].componentType).toEqual(typeConversion.componentTypeStringToInteger(attributeBuffers[i].componentType)); 100 | expectedItemCount = attributeBuffers[i].buffer.length / typeConversion.elementTypeToCount(attributeBuffers[i].type); 101 | expect(accessors[i].count).toEqual(expectedItemCount); 102 | } 103 | 104 | i += 1; // Iterate to to the final bufferAttribute (should be the indexBuffer always) 105 | expect(accessors[i].bufferView).toEqual(i); 106 | expect(accessors[i].byteOffset).toEqual(0); 107 | expect(accessors[i].componentType).toEqual(typeConversion.componentTypeStringToInteger(indexBuffer.componentType)); 108 | expectedItemCount = indexBuffer.buffer.length / typeConversion.elementTypeToCount(indexBuffer.type); 109 | expect(accessors[i].count).toEqual(expectedItemCount); 110 | 111 | }); 112 | 113 | it('generated gltf has a mesh, with valid attributes / indices', function() { 114 | expect(gltfWithIndex.meshes).toBeInstanceOf(Array); 115 | expect(gltfWithIndex.meshes[0].primitives).toBeInstanceOf(Array); 116 | const primitive = gltfWithIndex.meshes[0].primitives[0]; 117 | expect('attributes' in primitive).toEqual(true); 118 | expect('indices' in primitive).toEqual(true); 119 | 120 | let i: number; 121 | for (i = 0; i < attributeBuffers.length; ++i) { 122 | const propertyName = attributeBuffers[i].propertyName; 123 | expect(primitive.attributes[propertyName]).toEqual(i); 124 | } 125 | 126 | expect(primitive.indices).toEqual(i); 127 | }); 128 | 129 | it('has a single node / scene', function() { 130 | expect(gltfWithIndex.nodes).toBeInstanceOf(Array); 131 | expect('mesh' in gltfWithIndex.nodes[0]).toEqual(true); 132 | expect(gltfWithIndex.nodes[0].mesh).toEqual(0); 133 | 134 | expect(gltfWithIndex.scenes).toBeInstanceOf(Array); 135 | expect('nodes' in gltfWithIndex.scenes[0]).toEqual(true); 136 | expect(gltfWithIndex.scenes[0].nodes).toEqual([0]); 137 | }); 138 | 139 | it('byteOffset of each accessor is divisible by its componentType', function() { 140 | const accessors = gltfWithIndex.accessors; 141 | for (let i = 0; i < accessors.length; ++i) { 142 | const componentType = accessors[i].componentType; 143 | const byteOffset = accessors[i].byteOffset; 144 | const size = typeConversion.webglDataTypeToByteSize(componentType); 145 | expect(byteOffset / size).toEqual(0); 146 | } 147 | }); 148 | 149 | it('accessor.byteOffset + bufferView[accessor.bufferView].byteOffset is divisible by size of its componentType', function() { 150 | const accessors = gltfWithIndex.accessors; 151 | for (let i = 0; i < accessors.length; ++i) { 152 | const accessorByteOffset = accessors[i].byteOffset; 153 | const bufferViewByteOffset = gltfWithIndex.bufferViews[accessors[i].bufferView].byteOffset; 154 | const sum = accessorByteOffset + bufferViewByteOffset; 155 | const componentTypeSize = typeConversion.elementTypeToCount(accessors[i].type); 156 | expect(sum % componentTypeSize).toEqual(0); 157 | } 158 | }); 159 | }); -------------------------------------------------------------------------------- /test/bin/getQuaternionNormalsSpec.test.ts: -------------------------------------------------------------------------------- 1 | const Cesium = require('cesium'); 2 | const Quaternion = Cesium.Quaternion; 3 | 4 | import { InstanceTileUtils } from '../../lib/instanceUtilsNext'; 5 | import { FLOAT32_SIZE_BYTES } from '../../lib/typeSize'; 6 | import { GltfComponentType, GltfType } from '../../lib/gltfType'; 7 | 8 | describe('getQuaternionNormalsSpec', () => { 9 | it('smoke test for quaternion generation', () => { 10 | // prettier-ignore 11 | const numbers = [ 12 | 0, 1, 0, // Up 13 | 0, 0, 1, // Right 14 | 0, -1, 0, // Up 15 | 0, 0, 1, // Right 16 | ]; 17 | 18 | let i = 0; 19 | const rng = () => { 20 | if (i === numbers.length) { 21 | i = 0; 22 | } 23 | return numbers[i++]; 24 | }; 25 | 26 | const expectedMin = [0, 0]; 27 | 28 | const count = 2; 29 | const result = InstanceTileUtils.getQuaternionNormals(count, rng); 30 | expect(result.buffer.length).toEqual(count * 4 * FLOAT32_SIZE_BYTES); 31 | expect(result.propertyName).toEqual('QUATERNION_ROTATION'); 32 | expect(result.byteAlignment).toEqual(FLOAT32_SIZE_BYTES); 33 | expect(result.count).toBe(count); 34 | expect(result.componentType).toBe(GltfComponentType.FLOAT); 35 | expect(result.type).toBe(GltfType.VEC4); 36 | 37 | const q0 = new Quaternion( 38 | result.buffer.readFloatLE(0), 39 | result.buffer.readFloatLE(4), 40 | result.buffer.readFloatLE(8), 41 | result.buffer.readFloatLE(12) 42 | ); 43 | 44 | expect( 45 | q0.x * q0.x + q0.y * q0.y + q0.z * q0.z + q0.w * q0.w 46 | ).toBeCloseTo(1); 47 | 48 | const q1 = new Quaternion( 49 | result.buffer.readFloatLE(16), 50 | result.buffer.readFloatLE(20), 51 | result.buffer.readFloatLE(24), 52 | result.buffer.readFloatLE(28) 53 | ); 54 | 55 | expect( 56 | q1.x * q1.x + q1.y * q1.y + q1.z * q1.z + q1.w * q1.w 57 | ).toBeCloseTo(1); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/data/triangle.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "scenes": [ 3 | { 4 | "nodes": [ 5 | 0 6 | ] 7 | } 8 | ], 9 | "nodes": [ 10 | { 11 | "mesh": 0 12 | } 13 | ], 14 | "meshes": [ 15 | { 16 | "primitives": [ 17 | { 18 | "attributes": { 19 | "POSITION": 1 20 | }, 21 | "indices": 0 22 | } 23 | ] 24 | } 25 | ], 26 | "buffers": [ 27 | { 28 | "uri": "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=", 29 | "byteLength": 44 30 | } 31 | ], 32 | "bufferViews": [ 33 | { 34 | "buffer": 0, 35 | "byteOffset": 0, 36 | "byteLength": 6, 37 | "target": 34963 38 | }, 39 | { 40 | "buffer": 0, 41 | "byteOffset": 8, 42 | "byteLength": 36, 43 | "target": 34962 44 | } 45 | ], 46 | "accessors": [ 47 | { 48 | "bufferView": 0, 49 | "byteOffset": 0, 50 | "componentType": 5123, 51 | "count": 3, 52 | "type": "SCALAR", 53 | "max": [ 54 | 2 55 | ], 56 | "min": [ 57 | 0 58 | ] 59 | }, 60 | { 61 | "bufferView": 1, 62 | "byteOffset": 0, 63 | "componentType": 5126, 64 | "count": 3, 65 | "type": "VEC3", 66 | "max": [ 67 | 1.0, 68 | 1.0, 69 | 0.0 70 | ], 71 | "min": [ 72 | 0.0, 73 | 0.0, 74 | 0.0 75 | ] 76 | } 77 | ], 78 | "asset": { 79 | "version": "2.0" 80 | } 81 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "./dist", 5 | "target": "es5", 6 | "allowJs": true, 7 | "noImplicitAny": false, 8 | "sourceMap": true, 9 | "types": [ "node", "jest" ] 10 | }, 11 | "include": [ 12 | "./lib/**/*", 13 | "./bin/**/*", 14 | "test/**/*" 15 | ] 16 | } --------------------------------------------------------------------------------