itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
174 lines (166 loc) • 8.2 kB
JavaScript
import * as THREE from 'three';
import C3DTBatchTable from "../Core/3DTiles/C3DTBatchTable.js";
import Capabilities from "../Core/System/Capabilities.js";
import { MeshBasicMaterial } from 'three';
import disposeThreeMaterial from "../Utils/ThreeUtils.js";
import shaderUtils from "../Renderer/Shader/ShaderUtils.js";
import ReferLayerProperties from "../Layer/ReferencingLayerProperties.js";
// A bit weird but temporary until we remove this deprecated parser. Mainly to benefit from the enableDracoLoader and enableKtx2Loader
// methods.
import { itownsGLTFLoader } from "../Layer/OGC3DTilesLayer.js";
const matrixChangeUpVectorYtoZ = new THREE.Matrix4().makeRotationX(Math.PI / 2);
const matrixChangeUpVectorXtoZ = new THREE.Matrix4().makeRotationZ(-Math.PI / 2);
const utf8Decoder = new TextDecoder();
/**
* 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 = ['MODELVIEW', 'MODELVIEWINVERSETRANSPOSE', 'PROJECTION', 'JOINTMATRIX'];
if (obj.gltfShader) {
const names = [];
// eslint-disable-next-line guard-for-in
for (const name in obj.gltfShader.boundUniforms) {
names.push(name);
}
for (const name of names) {
const semantic = obj.gltfShader.boundUniforms[name].semantic;
if (!supported.includes(semantic)) {
delete obj.gltfShader.boundUniforms[name];
}
}
}
}
/**
* Transforms loaded gltf model to z-up (either from y-up or from the up axis defined in gltfUpAxis). Note that
* gltfUpAxis was an attribut of pre-1.0 3D Tiles and is now deprecated.
* @param {THREE.Object3D} gltfScene - the parsed glTF scene
* @param {String} gltfUpAxis - the gltfUpAxis parameter
*/
function transformToZUp(gltfScene, gltfUpAxis) {
if (!gltfUpAxis || gltfUpAxis === 'Y') {
gltfScene.applyMatrix4(matrixChangeUpVectorYtoZ);
} else if (gltfUpAxis === 'X') {
gltfScene.applyMatrix4(matrixChangeUpVectorXtoZ);
}
}
/**
* @module B3dmParser
*/
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 {float} [options.opacity=1.0] - the b3dm opacity.
* @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 frustumCulled = options.frustumCulled === true;
if (!buffer) {
throw new Error('No array buffer provided.');
}
const view = new DataView(buffer, 4); // starts after magic
let byteOffset = 0;
const b3dmHeader = {};
// Magic type is unsigned char [4]
const magicNumberByteLength = 4;
b3dmHeader.magic = utf8Decoder.decode(new Uint8Array(buffer, 0, magicNumberByteLength));
if (b3dmHeader.magic) {
// Version, byteLength, batchTableJSONByteLength, batchTableBinaryByteLength and batchTable types are uint32
b3dmHeader.version = view.getUint32(byteOffset, true);
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
b3dmHeader.byteLength = view.getUint32(byteOffset, true);
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
b3dmHeader.FTJSONLength = view.getUint32(byteOffset, true);
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
b3dmHeader.FTBinaryLength = view.getUint32(byteOffset, true);
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
b3dmHeader.BTJSONLength = view.getUint32(byteOffset, true);
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
b3dmHeader.BTBinaryLength = view.getUint32(byteOffset, true);
byteOffset += Uint32Array.BYTES_PER_ELEMENT;
const headerByteLength = byteOffset + magicNumberByteLength;
const promises = [];
let FTJSON = {};
const FT_RTC = new THREE.Vector3();
if (b3dmHeader.FTJSONLength > 0) {
const sizeBegin = headerByteLength;
const jsonBuffer = buffer.slice(sizeBegin, b3dmHeader.FTJSONLength + sizeBegin);
const content = utf8Decoder.decode(new Uint8Array(jsonBuffer));
FTJSON = JSON.parse(content);
if (FTJSON.RTC_CENTER) {
FT_RTC.fromArray(FTJSON.RTC_CENTER);
} else {
FT_RTC.set(0, 0, 0);
}
}
if (b3dmHeader.FTBinaryLength > 0) {
console.warn('3D Tiles feature table binary not supported yet.');
}
// Parse batch table
if (b3dmHeader.BTJSONLength > 0) {
// sizeBegin is an index to the beginning of the batch table
const sizeBegin = headerByteLength + b3dmHeader.FTJSONLength + b3dmHeader.FTBinaryLength;
const BTBuffer = buffer.slice(sizeBegin, sizeBegin + b3dmHeader.BTJSONLength + b3dmHeader.BTBinaryLength);
promises.push(Promise.resolve(new C3DTBatchTable(BTBuffer, b3dmHeader.BTJSONLength, b3dmHeader.BTBinaryLength, FTJSON.BATCH_LENGTH, options.registeredExtensions)));
} else {
promises.push(Promise.resolve(new C3DTBatchTable()));
}
const posGltf = headerByteLength + b3dmHeader.FTJSONLength + b3dmHeader.FTBinaryLength + b3dmHeader.BTJSONLength + b3dmHeader.BTBinaryLength;
const gltfBuffer = buffer.slice(posGltf);
const init_mesh = function (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);
}
};
promises.push(itownsGLTFLoader.parseAsync(gltfBuffer, options).then(gltf => {
for (const scene of gltf.scenes) {
scene.traverse(filterUnsupportedSemantics);
}
transformToZUp(gltf.scene, options.gltfUpAxis);
const shouldBePatchedForLogDepthSupport = Capabilities.isLogDepthBufferSupported() && !options.doNotPatchMaterial;
if (options.frustumCulling === false || options.overrideMaterials || shouldBePatchedForLogDepthSupport || options.layer) {
gltf.scene.traverse(init_mesh);
}
// Apply relative center from Feature table.
gltf.scene.position.copy(FT_RTC);
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.');
}
}
};