viewer/gltfloader.js
import * as vec3 from "./glmatrix/vec3.js";
import * as vec4 from "./glmatrix/vec3.js";
import * as mat4 from "./glmatrix/mat4.js";
import * as mat3 from "./glmatrix/mat3.js";
import { Utils } from "./utils.js";
import {DefaultRenderLayer} from "./defaultrenderlayer.js"
var WEBGL_TYPE_SIZES = {
'SCALAR': 1,
'VEC2': 2,
'VEC3': 3,
'VEC4': 4,
'MAT2': 4,
'MAT3': 9,
'MAT4': 16
};
var WEBGL_COMPONENT_TYPES = {
5120: Int8Array,
5121: Uint8Array,
5122: Int16Array,
5123: Uint16Array,
5125: Uint32Array,
5126: Float32Array
};
var BINARY_EXTENSION_HEADER_MAGIC = 0x46546C67;
var BINARY_EXTENSION_HEADER_LENGTH = 12;
var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };
const IDENTITY = mat4.identity(mat4.create());
export class GLTFLoader {
constructor(viewer, gltfBuffer, params) {
this.viewer = viewer;
const layer = new DefaultRenderLayer(this.viewer);
if (params.name) {
layer.name = params.name;
}
layer.registerLoader(1);
layer.settings = JSON.parse(JSON.stringify(layer.settings));
layer.settings.loaderSettings.quantizeVertices = false;
layer.settings.loaderSettings.quantizeNormals = false;
layer.settings.loaderSettings.quantizeColors = false;
this.viewer.renderLayers.add(layer);
this.gltfBuffer = gltfBuffer;
this.renderLayer = layer;
this.params = params || {};
this.features = params.features;
this.primarilyProcess();
this.debug = params.debug;
}
primarilyProcess() {
var decoder = new TextDecoder("utf-8");
// Parse header
var bufferFourBits = new Uint32Array(this.gltfBuffer.slice(0, 20));
if (bufferFourBits[0] != BINARY_EXTENSION_HEADER_MAGIC) {
throw Error("Expected glTF");
}
// Get JSON Chunk
var firstChunkLength = bufferFourBits[3];
var firstChunkType = bufferFourBits[4];
if (firstChunkType !== BINARY_EXTENSION_CHUNK_TYPES.JSON) {
throw Error("Expected JSON");
}
var content = this.gltfBuffer.slice(20, 20 + firstChunkLength);
var contentString = decoder.decode(content);
this.json = JSON.parse(contentString);
// Get Binary Chunk
var secondChunkLength = new Uint32Array(this.gltfBuffer.slice(firstChunkLength + 20, firstChunkLength + 24))[0];
var secondChunkType = new Uint32Array(this.gltfBuffer.slice(firstChunkLength + 24, firstChunkLength + 28))[0];
if (secondChunkType !== BINARY_EXTENSION_CHUNK_TYPES.BIN) {
throw Error("Expected BIN");
}
this.secondChunkBits = this.gltfBuffer.slice(28 + firstChunkLength, 28 + firstChunkLength + secondChunkLength);
}
join(a, b) {
let c = new a.constructor(a.length + b.length);
c.set(a);
c.set(b, a.length);
return c;
}
computeAabb(positions, matrix) {
let aabb = Utils.emptyAabb();
let v = vec4.create();
v[3] = 1.;
for (let i = 0; i < positions.length; i += 3) {
v.set(positions.subarray(i, i + 3));
vec4.transformMat4(v, v, matrix);
Utils.growAabb(aabb, v);
}
return aabb;
}
transformAabb(aabb_local, matrix) {
// @nb this is a transformation of the aabb bounds and not the
// individual vertices, so we won't get an optimal aabb.
let aabb = new Float32Array(6);
let v = vec4.create();
v[3] = 1.;
for (var i = 0; i < 2; i ++) {
v.set(aabb_local.subarray(3 * i, 3 * i + 3));
vec4.transformMat4(v, v, matrix);
aabb.subarray(3 * i, 3 * i + 3).set(v.subarray(0, 3));
}
return aabb;
}
* segmentBuffer(positions, normals, colors, indices, ids) {
// Actually, in theory, we could have forwarded the _BATCHID attribute
// to the viewer as it more or less corresponds to the way object
// identification is handled in the viewer.
// We do however (a) need to compute AABB's for the elements and
// when multiple glTFs are loaded we need to make sure the batch ids
// do not overlap. In that sense it's cleaner to reuse the existing
// createGeometry() createObject() calls and segment the buffers in JS.
let last = -1;
let vStart = null;
let iStart = 0;
for (var i = 0; i <= ids.length; ++i) {
if (i < ids.length && ids[i] < last) {
throw Error("Non monotonic ids");
} else if (i == ids.length || ids[i] !== last) {
if (vStart !== null) {
let j;
for (j = iStart;; ++j) {
if ((j == indices.length) || (indices[j + 1] > i)) {
if ((j % 3) !== 0) {
throw Error("Indices shared between batches");
}
break;
}
}
let idxs = indices.subarray(iStart, j);
for (let k = idxs.length - 1; k >= 0; --k) {
idxs[k] -= idxs[0];
}
yield [
last,
positions.subarray(vStart * 3, i * 3),
normals.subarray(vStart * 3, i * 3),
colors.subarray(vStart * 4, i * 4),
idxs
];
iStart = j;
}
vStart = i;
}
last = ids[i];
}
}
processGLTFBuffer() {
let aabbs = {};
let idRanges = {};
let meshes = {};
this.json.meshes.forEach((mesh, i) => {
let positions, normals, indices, colors;
let aabb = Utils.emptyAabb();
let batchedPrimitive = mesh.primitives.length == 1;
mesh.primitives.forEach((primitive, j) => {
let [psAccessor, ps] = this.getBufferData(primitive, 'POSITION');
let [_, ns] = this.getBufferData(primitive, 'NORMAL');
let [__, idxs] = this.getBufferData(primitive, 'indices');
let [___, ids] = this.getBufferData(primitive, '_BATCHID');
let material = this.getMaterial(primitive);
batchedPrimitive = batchedPrimitive && ids !== null;
// @nb we assume glTF with _BATCHID has a single primitive
idRanges[i] = ids;
// Apparently indices are optional in glTF. In case they are absent
// we just create a monotonically increasing sequence that stretches
// all vertices.
if (idxs === null) {
idxs = new Uint32Array(ps.length / 3);
for (let k = 0; k < idxs.length; ++k) {
idxs[k] = k;
}
}
let aabb_ = Utils.emptyAabb();
aabb_.set(psAccessor.min);
aabb_.set(psAccessor.max, 3);
aabb = Utils.unionAabb(aabb, aabb_);
let color = material.pbrMetallicRoughness && material.pbrMetallicRoughness.baseColorFactor
? material.pbrMetallicRoughness.baseColorFactor
: [0.6, 0.6, 0.6, 1.0];
let cs = new Float32Array(ps.length / 3 * 4);
for (var k = 0; k < cs.length; k += 4) {
cs.set(color, k);
}
if (j === 0) {
[positions, normals, indices, colors] = [ps, ns, idxs, cs];
} else {
normals = this.join(normals, ns);
for (let k = 0; k < idxs.length; ++k) {
idxs[k] += positions.length / 3;
}
positions = this.join(positions, ps);
indices = this.join(indices, idxs);
colors = this.join(colors, cs);
}
});
if (this.params.elevation) {
for (var k = 1; k < positions.length; k += 3) {
positions[k] -= this.params.elevation;
}
}
for (var k = 0; k < aabb.length; ++k) {
aabb[k] *= 1000.;
}
if (batchedPrimitive) {
meshes[i] = {
positions: positions,
normals: normals,
colors: colors,
indices: indices
}
} else {
this.renderLayer.createGeometry(
1,
null,
null,
i,
positions,
normals,
colors,
null,
indices,
null,
false,
false,
0
);
}
aabbs[i] = aabb;
});
let childToParent = {};
this.json.nodes.forEach((n, i) => {
(n.children || []).forEach(c => {
childToParent[c] = i;
});
});
this.json.nodes.forEach((n, i) => {
if (typeof(n.mesh) !== 'undefined') {
const aabb = aabbs[n.mesh];
const idRange = idRanges[n.mesh];
let m4, m3;
if (!this.params.ignoreMatrix && n.matrix) {
let m = n.matrix;
m4 = new Float32Array([
m[ 0], -m[ 2], m[ 1], m[ 3],
m[ 4], -m[ 6], m[ 5], m[ 7],
m[ 8], -m[10], m[ 9], m[11],
m[12], -m[14], m[13], m[15]
]);
} else if (this.params.refMatrix && this.json.extensions && this.json.extensions.CESIUM_RTC) {
m4 = mat4.identity(mat4.create());
function dump(m) {
let t = mat4.transpose(mat4.create(), m);
console.log(...t.subarray(0, 4));
console.log(...t.subarray(4, 8));
console.log(...t.subarray(8, 12));
console.log(...t.subarray(12, 16));
}
let m = new Float64Array(this.params.refMatrix);
let uff = new Float64Array([
m[ 0], +m[ 2], -m[ 1], m[ 3],
m[ 4], +m[ 6], -m[ 5], m[ 7],
m[ 8], +m[10], -m[ 9], m[11],
m[12], +m[14], -m[13], m[15]
]);
let ref = new Float64Array(uff.subarray(12, 15));
uff.subarray(12, 15).set([0,0,0]);
mat4.transpose(uff, uff);
if (this.debug) {
console.log("uff")
dump(uff);
}
let uffi = new Float64Array(16);
mat4.invert(uffi, uff);
mat4.transpose(uffi, uffi);
if (this.debug) {
console.log("uffi")
dump(uffi);
}
m = n.matrix;
// @todo multiply the complete stack of matrices,
// it's likely not needed for a city model thouhgh
if ((!m || mat4.equals(m, IDENTITY)) && i in childToParent) {
m = this.json.nodes[childToParent[i]].matrix;
}
if (!m) {
m = new Float64Array(16);
mat4.identity(m);
}
let c = this.json.extensions.CESIUM_RTC.center;
c = new Float64Array([c[0], c[2], -c[1]]);
vec3.subtract(c, c, ref);
let nodeMatrixZup = new Float64Array([
m[ 0], m[ 1], m[ 2], m[ 3],
m[ 4], m[ 5], m[ 6], m[ 7],
m[ 8], m[ 9], m[10], m[11],
c[ 0], c[ 1], c[ 2], m[15]
]);
if (this.debug) {
console.log("nodeMatrixZup")
dump(nodeMatrixZup);
}
mat4.multiply(m4, uffi, nodeMatrixZup);
if (this.debug) {
console.log("m4");
dump(m4);
}
} else {
m4 = mat4.identity(mat4.create());
}
m3 = mat3.create();
mat3.normalFromMat4(m3, m4);
if (idRange) {
let m = meshes[n.mesh];
let geomId = 1;
for (let subarrays of this.segmentBuffer(m.positions, m.normals, m.colors, m.indices, idRange)) {
let [batchId, positions, normals, colors, indices] = subarrays;
let uniqueId = this.renderLayer.viewer.oidCounter ++;
let aabb_global = this.computeAabb(positions, m4);
this.renderLayer.createGeometry(
1,
null,
null,
geomId,
positions,
normals,
colors,
null,
indices,
null,
false,
false,
0
);
this.renderLayer.createObject(1, null, uniqueId, [geomId++], m4, m3, m3, false, null, aabb_global, this.params.geospatial);
let globalizedAabb;
if (this.renderLayer.viewer.globalTranslationVector) {
globalizedAabb = Utils.transformBounds(aabb_global, this.renderLayer.viewer.globalTranslationVector);
} else {
globalizedAabb = aabb_global;
}
let featureData = this.features
? Object.fromEntries(
Object.keys(this.features)
.map(k => [k, this.features[k][batchId]]))
: null;
let viewObject = {
renderLayer: this.renderLayer,
type: "glTF Object",
data: featureData,
aabb: aabb_global,
globalizedAabb: globalizedAabb,
uniqueId: uniqueId
};
this.viewer.addViewObject(uniqueId, viewObject);
}
} else {
let uniqueId = this.renderLayer.viewer.oidCounter ++;
let aabb_global = this.transformAabb(aabb, m4);
this.renderLayer.createObject(1, null, uniqueId, [n.mesh], m4, m3, m3, false, null, aabb, this.params.geospatial);
let globalizedAabb;
if (this.renderLayer.viewer.globalTranslationVector) {
globalizedAabb = Utils.transformBounds(aabb_global, this.renderLayer.viewer.globalTranslationVector);
} else {
globalizedAabb = aabb_global;
}
let viewObject = {
renderLayer: this.renderLayer,
type: "glTF Object",
aabb: aabb_global,
globalizedAabb: globalizedAabb,
uniqueId: uniqueId
};
this.viewer.addViewObject(uniqueId, viewObject);
}
}
});
this.renderLayer.flushAllBuffers();
}
getMaterial(primitive) {
var materialIndex = primitive['material'];
return this.json['materials'][materialIndex];
}
getBufferData(primitive, primitiveAttributeType) {
var accessorIndex = primitive.attributes[primitiveAttributeType];
if (typeof(accessorIndex) === 'undefined') {
var accessorIndex = primitive[primitiveAttributeType]
}
if (typeof(accessorIndex) === 'undefined') {
return [null, null];
}
var accessor = this.json['accessors'][accessorIndex];
var accessorOffset = accessor['byteOffset'] || 0;
var accesorType = accessor['type'];
var componentType = accessor['componentType'];
// Accessor count property defines the number of elements in the bufferView
var accessorCount = accessor['count'];
// BufferView
var concernedBufferViewIndex = accessor['bufferView'];
var concernedBufferView = this.json['bufferViews'][concernedBufferViewIndex];
var byteOffset = concernedBufferView['byteOffset'];
var byteStride = concernedBufferView['byteStride'];
var byteLength = concernedBufferView['byteLength'];
// Buffer
var concernedBufferIndex = concernedBufferView['buffer'];
var concernedBuffer = this.json['buffers'][concernedBufferIndex];
var concernedBufferLength = concernedBuffer['byteLength'];
// Segment Buffer according to 1.BufferView offset, 2. Accessor offset, 3.BufferView stride
var dataSize = WEBGL_TYPE_SIZES[accesorType];
// Borrowed from ThreeJS GLTFLoader.js
var TypedArray = WEBGL_COMPONENT_TYPES[componentType];
var elementBytes = TypedArray.BYTES_PER_ELEMENT;
// One acessor will have an accessor count number of, for example, VEC3.
// A VEC3 is represented by 3 floats, 1 float being written on 4 bytes,
// so the upperbound will be the the multiplication of these 3 variables.
var upperBound = accessorCount * elementBytes * dataSize;
if (byteStride && byteStride != (WEBGL_TYPE_SIZES[accesorType] * elementBytes)) {
let arrayBufferSubset = this.secondChunkBits.slice(byteOffset, byteOffset + byteLength);
let all = new WEBGL_COMPONENT_TYPES[componentType](arrayBufferSubset);
let strideSubset = new WEBGL_COMPONENT_TYPES[componentType](accessorCount * dataSize);
let j = 0;
for (let i = 0; i < all.length; ++i) {
let i_within_stride = (i % (byteStride / elementBytes)) - (accessorOffset / elementBytes);
if (i_within_stride >= 0 && i_within_stride < WEBGL_TYPE_SIZES[accesorType]) {
strideSubset[j++] = all[i];
}
}
return [accessor, strideSubset];
} else {
if (byteOffset) {
var segmentedBuffer = this.secondChunkBits.slice(byteOffset, byteOffset + byteLength);
}
else {
var segmentedBuffer = this.secondChunkBits
}
var segmentedBufferFromAccessor = segmentedBuffer.slice(accessorOffset, accessorOffset + upperBound);
return [accessor, new WEBGL_COMPONENT_TYPES[componentType](segmentedBufferFromAccessor)];
}
}
}