diff --git a/TileFormats/Instanced3DModel/README.md b/TileFormats/Instanced3DModel/README.md index 178b6902f..a3350569e 100644 --- a/TileFormats/Instanced3DModel/README.md +++ b/TileFormats/Instanced3DModel/README.md @@ -22,65 +22,205 @@ A tile is composed of a header section immediately followed by a body section. **Figure 1**: Instanced 3D Model layout (dashes indicate optional fields). -![](figures/layout.png) +![header layout](figures/header-layout.png) ## Header -The 28-byte header contains the following fields: +The 36-byte header contains the following fields: -|Field name|Data type|Description| -|----------|---------|-----------| +| Field name | Data type | Description | +| --- | --- | --- | | `magic` | 4-byte ANSI string | `"i3dm"`. This can be used to identify the arraybuffer as an Instanced 3D Model tile. | | `version` | `uint32` | The version of the Instanced 3D Model format. It is currently `1`. | | `byteLength` | `uint32` | The length of the entire tile, including the header, in bytes. | -| `batchTableByteLength` | `uint32` | The length of the batch table in bytes. Zero indicates there is not a batch table. | +| `featureTableJSONByteLength` | `uint32` | The length of the feature table JSON section in bytes. | +| `featureTableBinaryByteLength` | `uint32` | The length of the feature table binary section in bytes. If `featureTableJSONByteLength` is zero, this will also be zero. | +| `batchTableJSONByteLength` | `uint32` | The length of the batch table JSON section in bytes. Zero indicates that there is no batch table. | +| `batchTableBinaryByteLength` | `uint32` | The length of the batch table binary section in bytes. If `batchTableJSONByteLength` is zero, this will also be zero. | | `gltfByteLength` | `uint32` | The length of the glTF field in bytes. | | `gltfFormat` | `uint32` | Indicates the format of the glTF field of the body. `0` indicates it is a url, `1` indicates it is embedded binary glTF. See the glTF section below. | -| `instancesLength` | `uint32` | The number of instances, also called features, in the tile. | -_TODO: Link to Cesium code for reading header_ +If either `featureTableJSONByteLength` or `gltfByteLength` equal zero, the tile does not need to be rendered. -If either `gltfByteLength` or `instancesLength` equal zero, the tile does not need to be rendered. +The body section immediately follows the header section, and is composed of three fields: `Feature Table`, `Batch Table`, and `glTF`. -The body section immediately follows the header section, and is composed of three fields: `Batch Table`, `glTF`, and `instances`. +Code for reading the header can be found in +[Instanced3DModelTileContent](https://github.com/AnalyticalGraphicsInc/cesium/blob/3d-tiles/Source/Scene/Instanced3DModel3DTileContent.js) +in the Cesium implementation of 3D Tiles. -## Batch Table +## Feature Table -_TODO: create a separate Batch Table spec that b3dm, i3dm, etc. can reference, [#32](https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/32)?_ +Contains values for `i3dm` semantics used to create instanced models. +[//]: # "TODO: Change this link to the feature table specification URL" -The batch table is a `UTF-8` string containing JSON. It immediately follows the header. It can be extracted from the arraybuffer using the `TextDecoder` JavaScript API and transformed to a JavaScript object with `JSON.parse`. +### Semantics -Each property in the object is an array with its length equal to `header.batchLength`. Array elements can be any valid JSON data type, including objects and arrays. Elements may be `null`. +#### Instance Semantics -An instance's `batchId` is used to access elements in each array and extract the corresponding properties. For example, the following batch table has properties for two instances: -```json -{ - "id" : ["unique id", "another unique id"], - "displayName" : ["Tree species", "Another tree species"], - "yearPlanted" : [1999, 2003], - "location" : [{"x" : 1, "y" : 2}, {"x" : 3, "y" : 4}] -} -``` +These semantics map to an array of feature values that are used to create instances. The length of these arrays must be the same for all semantics and is equal to the number of instances. + +If a semantic has a dependency on another semantic, that semantic must be defined. +If both `SCALE` and `SCALE_NON_UNIFORM` are defined for an instance, both scaling operations will be applied. +If both `POSITION` and `POSITION_QUANTIZED` are defined for an instance, the higher precision `POSITION` will be used. +If `NORMAL_UP`, `NORMAL_RIGHT`, `NORMAL_UP_OCT32P`, and `NORMAL_RIGHT_OCT32P` are defined for an instance, the higher precision `NORMAL_UP`, and `NORMAL_RIGHT` will be used. + +| Semantic | Data Type | Description | Required | +| --- | --- | --- | --- | --- | +| `POSITION` | `float32[3]` | A 3-component array of numbers containing `x`, `y`, and `z` Cartesian coordinates for the position of the instance. | :white_check_mark: Yes, unless `POSITION_QUANTIZED` is defined. | +| `POSITION_QUANTIZED` | `uint16[3]` | A 3-component array of numbers containing `x`, `y`, and `z` in quantized Cartesian coordinates for the position of the instance. | :white_check_mark: Yes, unless `POSITION` is defined. | +| `NORMAL_UP` | `float32[3]`| A unit vector defining the `up` direction for the orientation of the instance. | :red_circle: No, unless `NORMAL_RIGHT` is defined. | +| `NORMAL_RIGHT` | `float32[3]` | A unit vector defining the `right` direction for the orientation of the instance. Must be orthogonal to `up`. | :red_circle: No, unless `NORMAL_UP` is defined. | +| `NORMAL_UP_OCT32P` | `uint16[2]` | An oct-encoded unit vector with 32-bits of precision defining the `up` direction for the orientation of the instance. | :red_circle: No, unless `NORMAL_RIGHT_OCT32P` is defined. | +| `NORMAL_RIGHT_OCT32P` | `uint16[2]` | An oct-encoded unit vector with 32-bits of precision defining the `right` direction for the orientation of the instance. Must be orthogonal to `up`. | :red_circle: No, unless `NORMAL_UP_OCT32P` is defined. | +| `SCALE` | `float32` | A number defining a scale to apply to all axes of the instance. | :red_circle: No. | +| `SCALE_NON_UNIFORM` | `float32[3]` | A 3-component array of numbers defining the scale to apply to the `x`, `y`, and `z` axes of the instance. | :red_circle: No. | +| `BATCH_ID` | `unit16` | The `batchId` of the instance that can be used to retrieve metadata from the `Batch Table`. | :red_circle: No. | + +#### Global Semantics + +These semantics define global properties for all instances. + +| Semantic | Data Type | Description | Required | +| --- | --- | --- | --- | +| `INSTANCES_LENGTH`| `uint32` | The number of instances to generate. The length of each array value for an instance semantic should be equal to this. | :white_check_mark: Yes. | +| `QUANTIZED_VOLUME_OFFSET` | `float32[3]` | A 3-component array of numbers defining the offset for the quantized volume. | :red_circle: No, unless `POSITION_QUANTIZED` is defined. | +| `QUANTIZED_VOLUME_SCALE` | `float32[3]` | A 3-component array of numbers defining the scale for the quantized volume. |:red_circle: No, unless `POSITION_QUANTIZED` is defined. | + +Examples using these semantics can be found in the [examples section](#examples). + +### Instance Orientation + +An instance's orientation is defined by an orthonormal basis created by an `up` and `right` vector. If `NORMAL_UP` and `NORMAL_RIGHT` or `NORMAL_UP_OCT32P` and `NORMAL_RIGHT_OCT32P` are not present, +the instance will default to the `east/north/up` reference frame's orientation for the instance's Cartographic position (`x`, `y`, `z`) with the tileset transform applied, converted to `longitude` and `latitude` on the `WGS84` ellipsoid). +The normals will be transformed using the inverse transpose of the tileset transform. +[//]: # "TODO: Link to tileset transform spec" + +The `x` vector in the standard basis maps onto the `right` vector in the transformed basis, and the `y` vector maps on to the `up` vector. +The `z` vector would map onto a `forward` vector, but it is omitted because it will always be the cross product of `right` and `up`. + +**Figure 2**: A box in the standard basis. + +![box standard basis](figures/box-standard-basis.png) + +**Figure 3**: A box transformed into a rotated basis. + +![box rotated basis](figures/box-rotated-basis.png) + +#### Oct-encoded Normal Vectors + +If `NORMAL_UP` and `NORMAL_RIGHT` are not defined for an instance, its orientation may be stored as oct-encoded normals in `NORMAL_UP_OCT32P` and `NORMAL_RIGHT_OCT32P`. +These define `up` and `right` using the oct-encoding described in +[*A Survey of Efficient Representations of Independent Unit Vectors* by Cigolle et al.](http://jcgt.org/published/0003/02/01/). +An implementation for encoding and decoding these unit vectors can be found in Cesium's +[AttributeCompression](https://github.com/AnalyticalGraphicsInc/cesium/blob/master/Source/Core/AttributeCompression.js) +module. + +### Instance Position + +`POSITION` defines the location for an instance before any tileset transforms are applied. + +#### Quantized Positions + +If `POSITION` is not defined for an instance, its position may be stored in `POSITION_QUANTIZED` which defines the instance position relative to the quantized volume. +If neither `POSITION` or `POSITION_QUANTIZED` are defined, the instance will not be created. + +A quantized volume is defined by `offset` and `scale` to map quantized positions into model space. + +**Figure 4**: A quantized volume based on `offset` and `scale`. + +![quantized volume](figures/quantized-volume.png) + +`offset` is stored in the global semantic `QUANTIZED_VOLUME_OFFSET`, and `scale` is stored in the global semantic `QUANTIZED_VOLUME_SCALE`. +If those global semantics are not defined, `POSITION_QUANTIZED` cannot be used. + +Quantized positions can be mapped to model space using the formula: + +`POSITION = POSITION_QUANTIZED * QUANTIZED_VOLUME_SCALE + QUANTIZED_VOLUME_OFFSET` + +### Instance Scaling + +Scaling can be applied to instances using the `SCALE` and `SCALE_NON_UNIFORM` semantics. +`SCALE` applies a uniform scale along all axes, and `SCALE_NON_UNIFORM` applies scaling to the `x`, `y`, and `z` axes independently. + +### Examples + +These examples show how to generate JSON and binary buffers for the feature table. + +#### Positions Only + +In this minimal example, we place 4 instances on the corners of a unit length square with the default orientation. -The properties for the instance with `batchId = 0` are ```javascript -id[0] = 'unique id'; -displayName[0] = 'Tree species'; -yearBuilt[0] = 1999; -location[0] = {x : 1, y : 2}; +var featureTableJSON = { + INSTANCES_LENGTH : 4, + POSITION : { + byteOffset : 0 + } +}; + +var featureTableBinary = new Buffer(new Float32Array([ + 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, + 1.0, 0.0, 1.0 +]).buffer); ``` -The properties for `batchId = 1` are +#### Quantized Positions and Oct-Encoded Normals + +In this example, the 4 instances will be placed with an orientation `up` of `[0.0, 1.0, 0.0]` and `right` of `[1.0, 0.0, 0.0]` in oct-encoded format +and they will be placed on the corners of a quantized volume that spans from `-250.0` to `250.0` units in the `x` and `z` directions. + ```javascript -id[1] = 'another unique id'; -displayName[1] = 'Another tree species'; -yearBuilt[1] = 2003; -location[1] = {x : 3, y : 4}; +var featureTableJSON = { + INSTANCES_LENGTH : 4, + QUANTIZED_VOLUME_OFFSET : [-250.0, 0.0, -250.0], + QUANTIZED_VOLUME_SCALE : [500.0, 0.0, 500.0], + POSITION_QUANTIZED : { + byteOffset : 0 + }, + NORMAL_UP_OCT32P : { + byteOffset : 24 + }, + NORMAL_RIGHT_OCT32P : { + byteOffset : 40 + } +}; + +var positionQuantizedBinary = new Buffer(new UInt16Array([ + 0, 0, 0, + 65535, 0, 0, + 0, 0, 65535, + 65535, 0, 65535 +]).buffer); + +var normalUpOct32PBinary = new Buffer(new UInt16Array([ + 32768, 65535, + 32768, 65535, + 32768, 65535, + 32768, 65535 +]).buffer); + +var normalRightOct32PBinary = new Buffer(new UInt16Array([ + 65535, 32768, + 65535, 32768, + 65535, 32768, + 65535, 32768 +]).buffer); + +var featureTableBinary = Buffer.concat([positionQuantizedBinary, normalUpOct32PBinary, normalRightOct32PBinary]); ``` +## Batch Table + +Contains metadata organized by `batchId` that can be used for declarative styling. + +See the [Batch Table](..//Batched3DModel#batch-table) reference for more information. +[//]: # "TODO: Change this link to the batch table specification URL" + ## glTF -The glTF field immediately follows the batch table (or immediately follows the header, if `header.batchTableByteLength` is zero). +The glTF asset to be instanced is stored after the feature table and batch table. [glTF](https://www.khronos.org/gltf) is the runtime asset format for WebGL. [Binary glTF](https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_binary_glTF) is an extension defining a binary container for glTF. Instanced 3D Model uses glTF 1.0 with the [KHR_binary_glTF](https://github.com/KhronosGroup/glTF/tree/master/extensions/Khronos/KHR_binary_glTF) extension. @@ -94,30 +234,80 @@ When the value of `header.gltfFormat` is `1`, the glTF field is In either case, `header.gltfByteLength` contains the length of the glTF field in bytes. -## Instances +## File Extension + +`.i3dm` -The `instances` field immediately follows the `glTF` field (which may be omitted when `header.gltfByteLength` is `0`). +## MIME Type -The `instances` field contains `header.instancesLength` of tightly packed instances. Each instance has three fields: +_TODO, [#60](https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/60)_ -|Field name|Data type|Description| -|----------|---------|-----------| -| `longitude` | `double` | The longitude, in radians, in the range `[-PI, PI]`. | -| `latitude` | `double` | The latitude, in radians, in the range `[-PI / 2, PI / 2]`. | -| `batchId` | `uint16` | ID in the range `[0, length of arrays in the Batch Table)`, which indicates the corresponding properties. | +`application/octet-stream` -_TODO: make this much more memory efficient and more general, [#33](https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/33)._ +## Resources -When `header.batchTableByteLength` is zero, which indicates there is not a batch table, `batchId` is omitted, so each instance contains only `longitude` and `latitude` fields. +1. [*A Survey of Efficient Representations of Independent Unit Vectors* by Cigolle et al.](http://jcgt.org/published/0003/02/01/) +2. [*Mesh Geometry Compression for Mobile Graphics* by Jongseok Lee et al.](http://cg.postech.ac.kr/research/mesh_comp_mobile/mesh_comp_mobile_conference.pdf) +3. Cesium [AttributeCompression](https://github.com/AnalyticalGraphicsInc/cesium/blob/master/Source/Core/AttributeCompression.js) module for oct-encoding -Each instance is in the east-north-up reference frame (`x` points east, `y` points north, and `z` points along the geodetic surface normal). +## Implementation Examples -## File Extension +### Cesium -`.i3dm` +#### Generating up and right from longitude, latitude, and height -## MIME Type +```javascript +var position = Cartesian3.fromRadians(longitude, latitude, height); + +// Compute the up vector +var up = new Cartesian3(); +var transform = Transforms.eastNorthUpToFixedFrame(position); +var rotation = new Matrix3(); +Matrix4.getRotation(transform, rotation); + +// In east-north-up, the up vector is stored in the z component +Matrix3.multiplyByVector(rotation, Cartesian3.UNIT_Z, up); + +// Compute the right and forward vectors +var right = new Cartesian3(); +var forward = Cartesian3.clone(Cartesian3.UNIT_Z); + +// You can change the orientation of a model by rotating about the y-axis first +var orient = new Matrix3(); +var orientAngle = CesiumMath.fromDegrees(90.0); +Matrix3.fromRotationY(orientAngle, orient); +Matrix3.multiplyByVector(orient, forward, forward); + +// Cross up and forward to get right +Cartesian3.cross(up, forward, right); +Cartesian3.normalize(right, right); +``` -_TODO, [#60](https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/60)_ +#### Construct a model-matrix for an instance +```javascript +// Cross right and up to get forward +var forward = new Cartesian3(); +Cartesian3.cross(right, up, forward); + +// Place the basis into a rotation matrix +var rotation = new Matrix3(); +Matrix3.setColumn(rotation, 0, right, rotation); +Matrix3.setColumn(rotation, 1, up, rotation); +Matrix3.setColumn(rotation, 2, forward, rotation); + +// Get the scale +var scale = Matrix3.IDENTITY.clone(); +if (defined(uniformScale)) { + Matrix3.multiplyByScalar(scale, uniformScale, scale); +} +if (defined(nonUniformScale)) { + Matrix3.multiplyByVector(scale, nonUniformScale, scale); +} -`application/octet-stream` +// Generate the model matrix +var modelMatrix = new Matrix4(); +Matrix4.fromTranslationRotationScale( + new TranslationRotationScale(position, rotation, scale), + modelMatrix +); +``` \ No newline at end of file diff --git a/TileFormats/Instanced3DModel/figures/box-rotated-basis.png b/TileFormats/Instanced3DModel/figures/box-rotated-basis.png new file mode 100644 index 000000000..fc791ba65 Binary files /dev/null and b/TileFormats/Instanced3DModel/figures/box-rotated-basis.png differ diff --git a/TileFormats/Instanced3DModel/figures/box-rotated-basis.svg b/TileFormats/Instanced3DModel/figures/box-rotated-basis.svg new file mode 100644 index 000000000..08acdf4fc --- /dev/null +++ b/TileFormats/Instanced3DModel/figures/box-rotated-basis.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + y + + x + + z + + up + + right + + diff --git a/TileFormats/Instanced3DModel/figures/box-standard-basis.png b/TileFormats/Instanced3DModel/figures/box-standard-basis.png new file mode 100644 index 000000000..a4d3902f1 Binary files /dev/null and b/TileFormats/Instanced3DModel/figures/box-standard-basis.png differ diff --git a/TileFormats/Instanced3DModel/figures/box-standard-basis.svg b/TileFormats/Instanced3DModel/figures/box-standard-basis.svg new file mode 100644 index 000000000..8a5ec37ae --- /dev/null +++ b/TileFormats/Instanced3DModel/figures/box-standard-basis.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + y + + x + + z + + diff --git a/TileFormats/Instanced3DModel/figures/header-layout.png b/TileFormats/Instanced3DModel/figures/header-layout.png new file mode 100644 index 000000000..db8a08755 Binary files /dev/null and b/TileFormats/Instanced3DModel/figures/header-layout.png differ diff --git a/TileFormats/Instanced3DModel/figures/Figures.pptx b/TileFormats/Instanced3DModel/figures/header-layout.pptx similarity index 63% rename from TileFormats/Instanced3DModel/figures/Figures.pptx rename to TileFormats/Instanced3DModel/figures/header-layout.pptx index bb683a5ed..ecbf5d1e3 100644 Binary files a/TileFormats/Instanced3DModel/figures/Figures.pptx and b/TileFormats/Instanced3DModel/figures/header-layout.pptx differ diff --git a/TileFormats/Instanced3DModel/figures/layout.png b/TileFormats/Instanced3DModel/figures/layout.png deleted file mode 100644 index 7af777413..000000000 Binary files a/TileFormats/Instanced3DModel/figures/layout.png and /dev/null differ diff --git a/TileFormats/Instanced3DModel/figures/quantized-volume.png b/TileFormats/Instanced3DModel/figures/quantized-volume.png new file mode 100644 index 000000000..a3989c3c0 Binary files /dev/null and b/TileFormats/Instanced3DModel/figures/quantized-volume.png differ diff --git a/TileFormats/Instanced3DModel/figures/quantized-volume.svg b/TileFormats/Instanced3DModel/figures/quantized-volume.svg new file mode 100644 index 000000000..b1eebb35a --- /dev/null +++ b/TileFormats/Instanced3DModel/figures/quantized-volume.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 216-1 + 216-2 + 1 + 216-2 + 216-1 + 216-2 + + + + + + offset(x,y,z) + s + scale.y + scale.x + scale.z + + + + + 1 + 2 + 3 + 0 + 4 + 0 + 2 + 3 + 4 + 0 + 1 + 2 + 3 + 4 + + +