Skip to content

Commit

Permalink
feat(gltf): add a GLTFParser to parse gltf 1.0 and 2.0 files
Browse files Browse the repository at this point in the history
feat(3D tiles): add C3DTilesGoogleSource to start supporting Google 3D
Tiles (experimental)

BREAKING CHANGE: glTFLoader and legacyGLTFLoader are no longer exported.
Use GLTFParser instead (supports both 1.0 and 2.0 gltf files)
  • Loading branch information
jogarnier authored and jailln committed Feb 12, 2024
1 parent 47d0c7c commit c91731d
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 125 deletions.
1 change: 1 addition & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@

"Parser": [
"GeoJsonParser",
"GLTFParser",
"GpxParser",
"VectorTileParser",
"CameraCalibrationParser",
Expand Down
4 changes: 2 additions & 2 deletions docs/tutorials/Fundamentals.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ with `{@link WFSSource}` and [Tile Map Service](https://wiki.osgeo.org/wiki/Tile

iTowns also has sources for many data formats: [vector tile](https://docs.mapbox.com/help/glossary/vector-tiles/) resources from [MapBox](https://www.mapbox.com/) with `{@link VectorTilesSource}`, [Potree](https://github.com/potree/potree) (`{@link PotreeSource}`) and
[Entwine](https://entwine.io/) (`{@link EntwinePointTileSource}`) 3D point clouds, [3DTiles](https://www.ogc.org/standards/3DTiles)
mesh (b3dm) and point clouds (pnts) from web servers (`{@link C3DTilesSource}`) and from Cesium ion `{@link C3DTilesIonSource}`,
mesh (b3dm) and point clouds (pnts) from web servers (`{@link C3DTilesSource}`), Cesium ion `{@link C3DTilesIonSource}` and from Google api `{@link C3DTilesGoogleSource}`,
[GeoJSON](https://geojson.org/) with `{@link FileSource}` and `{@link GeoJsonParser}`,
[KML](https://www.ogc.org/standards/kml) with `{@link FileSource}` and `{@link KMLParser}`, [GPX](https://www.topografix.com/gpx.asp)
with `{@link FileSource}` and `{@link GpxParser}` and oriented images with `{@link OrientedImageSource}`.
Expand All @@ -68,7 +68,7 @@ Several specific types of `Layers` exist, the use of which depends on the data t
- `{@link PointCloudLayer}` can be used to display 3D point clouds. Any point cloud formats are supported as long as the corresponding `Source` is provided.
Some point clouds formats such as Potree, Las and Entwine already have parsers defined in itowns that you can use. For 3D Tiles point clouds (pnts), use
`C3DTilesLayer`.
- `{@link C3DTilesLayer}` can be used to display 3D Tiles layer (only b3dm and pnts).
- `{@link C3DTilesLayer}` can be used to display 3D Tiles datasets in version 1.0 (b3dm, pnts and gltf tiles are supported).
- `{@link OrientedImageLayer}` can be used to display oriented images.


Expand Down
3 changes: 2 additions & 1 deletion src/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export { default as OrientedImageSource } from 'Source/OrientedImageSource';
export { default as PotreeSource } from 'Source/PotreeSource';
export { default as C3DTilesSource } from 'Source/C3DTilesSource';
export { default as C3DTilesIonSource } from 'Source/C3DTilesIonSource';
export { default as C3DTilesGoogleSource } from 'Source/C3DTilesGoogleSource';
export { default as EntwinePointTileSource } from 'Source/EntwinePointTileSource';

// Parsers provided by default in iTowns
Expand All @@ -91,7 +92,7 @@ export { default as LASParser } from 'Parser/LASParser';
export { default as ISGParser } from 'Parser/ISGParser';
export { default as GDFParser } from 'Parser/GDFParser';
export { default as GTXParser } from 'Parser/GTXParser';
export { enableDracoLoader, enableKtx2Loader, glTFLoader, legacyGLTFLoader } from 'Parser/B3dmParser';
export { default as GLTFParser, enableDracoLoader, enableKtx2Loader } from 'Parser/GLTFParser';

// 3D Tiles classes and extensions
// Exported to allow one to implement its own 3D Tiles extension which needs to
Expand Down
187 changes: 71 additions & 116 deletions src/Parser/B3dmParser.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import * as THREE from 'three';
import Capabilities from 'Core/System/Capabilities';
import { GLTFLoader } from 'ThreeExtended/loaders/GLTFLoader';
import { DRACOLoader } from 'ThreeExtended/loaders/DRACOLoader';
import { KTX2Loader } from 'ThreeExtended/loaders/KTX2Loader';
import LegacyGLTFLoader from 'Parser/deprecated/LegacyGLTFLoader';
import shaderUtils from 'Renderer/Shader/ShaderUtils';
import utf8Decoder from 'Utils/Utf8Decoder';
import C3DTBatchTable from 'Core/3DTiles/C3DTBatchTable';
import ReferLayerProperties from 'Layer/ReferencingLayerProperties';
import Capabilities from 'Core/System/Capabilities';
import { MeshBasicMaterial } from 'three';
import disposeThreeMaterial from 'Utils/ThreeUtils';
import shaderUtils from 'Renderer/Shader/ShaderUtils';
import ReferLayerProperties from 'Layer/ReferencingLayerProperties';
import GLTFParser from './GLTFParser';

const matrixChangeUpVectorZtoY = (new THREE.Matrix4()).makeRotationX(Math.PI / 2);
// For gltf rotation
const matrixChangeUpVectorZtoX = (new THREE.Matrix4()).makeRotationZ(-Math.PI / 2);

export const glTFLoader = new GLTFLoader();

export const legacyGLTFLoader = new LegacyGLTFLoader();
const matrixChangeUpVectorYtoZInv = (new THREE.Matrix4()).makeRotationX(-Math.PI / 2);
const matrixChangeUpVectorXtoZ = (new THREE.Matrix4()).makeRotationZ(-Math.PI / 2);

/**
* 3D Tiles pre-1.0 contain not standardized and specific uniforms that we filter out to avoid shader compilation errors
* This method is passed to scene.traverse and applied to all 3D objects of the loaded gltf.
* @param {THREE.Object3D} obj - 3D object of the gltf hierarchy
*/
function filterUnsupportedSemantics(obj) {
// see GLTFLoader GLTFShader.prototype.update function
const supported = [
Expand All @@ -43,77 +40,45 @@ function filterUnsupportedSemantics(obj) {
}

/**
* @module B3dmParser
* 3D Tiles pre-1.0 had a gltfUpAxis parameter that defined the up vector of the gltf file that might be different from
* the standard y-up for gltf. Manage the case when this gltfUpAxis is defined (i.e. apply the correct rotation to the
* gltf file to have it z-up in the end).
* @param {THREE.Object3D} gltfScene - the parsed glTF scene
* @param {String} gltfUpAxis - the gltfUpAxis parameter
*/
/**
* Enable Draco decoding for gltf.
*
* The Draco library files are in folder itowns/examples/libs/draco/.
* You must indicate this path when you want to enable Draco Decoding.
* For more information on Draco, read /itowns/examples/libs/draco/README.md.
*
* @example <caption>Enable draco decoder</caption>
* // if you copied /itowns/examples/libs/draco/ to the root folder of your project,you can set the path to './'.
* itowns.enableDracoLoader('./');
*
* @param {string} path path to draco library folder.
* This library is mandatory to load b3dm and gltf with Draco compression.
* @param {object} config optional configuration for Draco compression.
*/
export function enableDracoLoader(path, config) {
if (!path) {
throw new Error('Path to draco folder is mandatory');
}
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(path);
if (config) {
dracoLoader.setDecoderConfig(config);
function applyDeprecatedGltfUpAxis(gltfScene, gltfUpAxis) {
if (gltfUpAxis === 'Z') {
// If gltf up was already z-up, apply the inverse transform matrix that was applied in the glTFParser
gltfScene.applyMatrix4(matrixChangeUpVectorYtoZInv);
} else if (gltfUpAxis === 'X') {
gltfScene.applyMatrix4(matrixChangeUpVectorYtoZInv);
gltfScene.applyMatrix4(matrixChangeUpVectorXtoZ);
}
glTFLoader.setDRACOLoader(dracoLoader);
}

/**
* Enable KTX2 decoding for gltf. This library is mandatory to load b3dm and gltf with KTX2 compression.
*
* The KTX2 library files are in folder itowns/examples/libs/basis/.
* You must indicate this path when you want to enable KTX2 decoding.
* For more information about KTX2, read /itowns/examples/libs/basis/README.md.
*
* @example <caption>Enable ktx2 decoder</caption>
* // if you copied /itowns/examples/libs/draco/ to the root folder of your project,you can set the path to './'.
* itowns.enableKtx2Loader('./', view.mainLoop.gfxEngine.renderer);
*
* @param {string} path path to KTX2 library folder.
* @param {THREE.WebGLRenderer} renderer the threejs renderer
* @module B3dmParser
*/
export function enableKtx2Loader(path, renderer) {
if (!path || !renderer) {
throw new Error('Path to ktx2 folder and renderer are mandatory');
}
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath(path);
ktx2Loader.detectSupport(renderer);
glTFLoader.setKTX2Loader(ktx2Loader);
}

export default {
/** Parse b3dm buffer and extract THREE.Scene and batch table
* @param {ArrayBuffer} buffer - the b3dm buffer.
* @param {Object} options - additional properties.
* @param {string=} [options.gltfUpAxis='Y'] - embedded glTF model up axis.
* @param {string} options.urlBase - the base url of the b3dm file (used to fetch textures for the embedded glTF model).
* @param {boolean=} [options.doNotPatchMaterial='false'] - disable patching material with logarithmic depth buffer support.
* @param {boolean=} [options.doNotPatchMaterial=false] - disable patching material with logarithmic depth buffer support.
* @param {float} [options.opacity=1.0] - the b3dm opacity.
* @param {boolean|Material=} [options.overrideMaterials='false'] - override b3dm's embedded glTF materials. If
* @param {boolean=} [options.frustumCulled=false] - enable frustum culling.
* @param {boolean|Material=} [options.overrideMaterials=false] - override b3dm's embedded glTF materials. If
* true, a threejs [MeshBasicMaterial](https://threejs.org/docs/index.html?q=meshbasic#api/en/materials/MeshBasicMaterial)
* is set up. config.overrideMaterials can also be a threejs [Material](https://threejs.org/docs/index.html?q=material#api/en/materials/Material)
* in which case it will be the material used to override.
* @return {Promise} - a promise that resolves with an object containig a THREE.Scene (gltf) and a batch table (batchTable).
*
*/
parse(buffer, options) {
const gltfUpAxis = options.gltfUpAxis;
const urlBase = options.urlBase;
const frustumCulled = options.frustumCulled === true ?? false;

if (!buffer) {
throw new Error('No array buffer provided.');
}
Expand Down Expand Up @@ -185,66 +150,56 @@ export default {
const gltfBuffer = buffer.slice(posGltf);
const headerView = new DataView(gltfBuffer, 0, 20);

promises.push(new Promise((resolve/* , reject */) => {
const onload = (gltf) => {
for (const scene of gltf.scenes) {
scene.traverse(filterUnsupportedSemantics);
}
// Rotation managed
if (gltfUpAxis === undefined || gltfUpAxis === 'Y') {
gltf.scene.applyMatrix4(matrixChangeUpVectorZtoY);
} else if (gltfUpAxis === 'X') {
gltf.scene.applyMatrix4(matrixChangeUpVectorZtoX);
const init_mesh = function f_init(mesh) {
mesh.frustumCulled = frustumCulled;
if (mesh.material) {
if (options.overrideMaterials) {
const oldMat = mesh.material;
// Set up new material
if (typeof (options.overrideMaterials) === 'object' &&
options.overrideMaterials.isMaterial) {
mesh.material = options.overrideMaterials;
} else {
mesh.material = new MeshBasicMaterial();
}
disposeThreeMaterial(oldMat);
} else if (Capabilities.isLogDepthBufferSupported()
&& mesh.material.isRawShaderMaterial
&& !options.doNotPatchMaterial) {
shaderUtils.patchMaterialForLogDepthSupport(mesh.material);
console.warn('glTF shader has been patched to add log depth buffer support');
}
ReferLayerProperties(mesh.material, options.layer);
}
};

// Apply relative center from Feature table.
gltf.scene.position.copy(FT_RTC);
promises.push(GLTFParser.parse(gltfBuffer, options).then((gltf) => {
for (const scene of gltf.scenes) {
scene.traverse(filterUnsupportedSemantics);
}

// Apply relative center from gltf json.
const contentArray = new Uint8Array(gltfBuffer, 20, headerView.getUint32(12, true));
const content = utf8Decoder.decode(new Uint8Array(contentArray));
const json = JSON.parse(content);
if (json.extensions && json.extensions.CESIUM_RTC) {
gltf.scene.position.fromArray(json.extensions.CESIUM_RTC.center);
gltf.scene.updateMatrixWorld(true);
}
applyDeprecatedGltfUpAxis(gltf.scene, options.gltfUpAxis);

const init_mesh = function f_init(mesh) {
mesh.frustumCulled = false;
if (mesh.material) {
if (options.overrideMaterials) {
const oldMat = mesh.material;
// Set up new material
if (typeof (options.overrideMaterials) === 'object' &&
options.overrideMaterials.isMaterial) {
mesh.material = options.overrideMaterials;
} else {
mesh.material = new MeshBasicMaterial();
}
disposeThreeMaterial(oldMat);
} else if (Capabilities.isLogDepthBufferSupported()
&& mesh.material.isRawShaderMaterial
&& !options.doNotPatchMaterial) {
shaderUtils.patchMaterialForLogDepthSupport(mesh.material);
console.warn('b3dm shader has been patched to add log depth buffer support');
}
ReferLayerProperties(mesh.material, options.layer);
}
};
const shouldBePatchedForLogDepthSupport = Capabilities.isLogDepthBufferSupported() && !options.doNotPatchMaterial;
if (options.frustumCulling === false || options.overrideMaterials || shouldBePatchedForLogDepthSupport || options.layer) {
gltf.scene.traverse(init_mesh);
}

resolve(gltf);
};

const version = headerView.getUint32(4, true);
// Apply relative center from Feature table.
gltf.scene.position.copy(FT_RTC);

if (version === 1) {
legacyGLTFLoader.parse(gltfBuffer, urlBase, onload);
} else {
glTFLoader.parse(gltfBuffer, urlBase, onload);
// Apply relative center from gltf json.
const contentArray = new Uint8Array(gltfBuffer, 20, headerView.getUint32(12, true));
const content = utf8Decoder.decode(new Uint8Array(contentArray));
const json = JSON.parse(content);
if (json.extensions && json.extensions.CESIUM_RTC) {
gltf.scene.position.fromArray(json.extensions.CESIUM_RTC.center);
gltf.scene.updateMatrixWorld(true);
}
}));
return Promise.all(promises).then(values => ({ gltf: values[1], batchTable: values[0] }));

return gltf;
}).catch((e) => { throw new Error(e); }));
return Promise.all(promises).then(values => ({ gltf: values[1], batchTable: values[0] })).catch((e) => { throw new Error(e); });
} else {
throw new Error('Invalid b3dm file.');
}
Expand Down
92 changes: 92 additions & 0 deletions src/Parser/GLTFParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as THREE from 'three';
import { GLTFLoader } from 'ThreeExtended/loaders/GLTFLoader';
import { DRACOLoader } from 'ThreeExtended/loaders/DRACOLoader';
import { KTX2Loader } from 'ThreeExtended/loaders/KTX2Loader';
import LegacyGLTFLoader from 'Parser/deprecated/LegacyGLTFLoader';

const matrixChangeUpVectorYtoZ = (new THREE.Matrix4()).makeRotationX(Math.PI / 2);

export const glTFLoader = new GLTFLoader();
export const legacyGLTFLoader = new LegacyGLTFLoader();

/**
* @module GLTFParser
* @description Parses [glTF](https://www.khronos.org/gltf/) 1.0 and 2.0 files.
*
* Under the hood, glTF 2.0 files are parsed with THREE.GltfLoader() and GLTF 1.0 are parsed with the previous THREE
* GltfLoader (for 1.0 glTF) that has been kept and maintained in iTowns.
*/

/**
* Enable loading gltf files with [Draco](https://google.github.io/draco/) geometry extension.
*
* @param {String} path path to draco library folder containing the JS and WASM decoder libraries. They can be found in
* [itowns examples](https://github.com/iTowns/itowns/tree/master/examples/libs/draco).
* @param {Object} [config] optional configuration for Draco decoder (see threejs'
* [setDecoderConfig](https://threejs.org/docs/index.html?q=draco#examples/en/loaders/DRACOLoader.setDecoderConfig) that
* is called under the hood with this configuration for details.
*/
export function enableDracoLoader(path, config) {
if (!path) {
throw new Error('Path to draco folder is mandatory');
}
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath(path);
if (config) {
dracoLoader.setDecoderConfig(config);
}
glTFLoader.setDRACOLoader(dracoLoader);
}

/**
* Enable loading gltf files with [KTX2](https://www.khronos.org/ktx/) texture extension.
*
* @param {String} path path to ktx2 library folder containing the JS and WASM decoder libraries. They can be found in
* [itowns examples](https://github.com/iTowns/itowns/tree/master/examples/libs/basis).
* @param {THREE.WebGLRenderer} renderer the threejs renderer
*/
export function enableKtx2Loader(path, renderer) {
if (!path || !renderer) {
throw new Error('Path to ktx2 folder and renderer are mandatory');
}
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath(path);
ktx2Loader.detectSupport(renderer);
glTFLoader.setKTX2Loader(ktx2Loader);
}

export default {
/** Parses a gltf buffer to an object with threejs structures and applies a y-up to z-up conversion to align with
* itowns convention. Essentially calls THREE.GltfLoader.parse() for glTF 2.0 files and the legacy threejs parser
* for gtTF 1.0 files.
* @param {ArrayBuffer} buffer - the glTF asset to parse, as an ArrayBuffer, JSON string or object.
* @param {String} path - the base path from which to find subsequent glTF resources such as textures and .bin data files.
* @return {Promise} - a promise that resolves with an object containing an Object that contains loaded parts:
* .scene, .scenes, .cameras, .animations, and .asset.
*/
parse(buffer, path) {
return new Promise((resolve, reject) => {
if (!buffer || !path) {
reject(new Error('[GLTFParser]: Buffer and path are mandatory to parse a glTF.'));
return;
}

// Apply y-up (gltf convention) to z-up (itowns convention) conversion
const onload = (gltf) => {
gltf.scene.applyMatrix4(matrixChangeUpVectorYtoZ);
resolve(gltf);
};
const onError = (e) => {
reject(new Error(`[GLTFParser]: Failed to parse gltf with error: ${e}`));
};
const headerView = new DataView(buffer, 0, 20);
const version = headerView.getUint32(4, true);

if (version === 1) {
legacyGLTFLoader.parse(buffer, path, onload, onError);
} else {
glTFLoader.parse(buffer, path, onload, onError);
}
});
},
};
Loading

0 comments on commit c91731d

Please sign in to comment.