playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,404 lines (1,403 loc) • 45.6 kB
JavaScript
import { path } from "../../core/path.js";
import { Mat4 } from "../../core/math/mat4.js";
import { math } from "../../core/math/math.js";
import { Quat } from "../../core/math/quat.js";
import { Vec3 } from "../../core/math/vec3.js";
import {
typedArrayTypes,
typedArrayTypesByteSize,
ADDRESS_CLAMP_TO_EDGE,
ADDRESS_MIRRORED_REPEAT,
ADDRESS_REPEAT,
BUFFER_STATIC,
CULLFACE_NONE,
CULLFACE_BACK,
FILTER_NEAREST,
FILTER_LINEAR,
FILTER_NEAREST_MIPMAP_NEAREST,
FILTER_LINEAR_MIPMAP_NEAREST,
FILTER_NEAREST_MIPMAP_LINEAR,
FILTER_LINEAR_MIPMAP_LINEAR,
INDEXFORMAT_UINT8,
INDEXFORMAT_UINT16,
INDEXFORMAT_UINT32,
SEMANTIC_POSITION,
SEMANTIC_NORMAL,
SEMANTIC_TANGENT,
SEMANTIC_COLOR,
SEMANTIC_BLENDINDICES,
SEMANTIC_BLENDWEIGHT,
SEMANTIC_TEXCOORD0,
SEMANTIC_TEXCOORD1,
SEMANTIC_TEXCOORD2,
SEMANTIC_TEXCOORD3,
SEMANTIC_TEXCOORD4,
SEMANTIC_TEXCOORD5,
SEMANTIC_TEXCOORD6,
SEMANTIC_TEXCOORD7,
TYPE_FLOAT32
} from "../../platform/graphics/constants.js";
import { IndexBuffer } from "../../platform/graphics/index-buffer.js";
import { Texture } from "../../platform/graphics/texture.js";
import { VertexBuffer } from "../../platform/graphics/vertex-buffer.js";
import { VertexFormat } from "../../platform/graphics/vertex-format.js";
import { http } from "../../platform/net/http.js";
import {
BLEND_NONE,
BLEND_NORMAL,
PROJECTION_ORTHOGRAPHIC,
PROJECTION_PERSPECTIVE,
ASPECT_MANUAL,
ASPECT_AUTO,
SPECOCC_AO
} from "../../scene/constants.js";
import { GraphNode } from "../../scene/graph-node.js";
import { Mesh } from "../../scene/mesh.js";
import { Morph } from "../../scene/morph.js";
import { MorphTarget } from "../../scene/morph-target.js";
import { calculateNormals } from "../../scene/geometry/geometry-utils.js";
import { Render } from "../../scene/render.js";
import { Skin } from "../../scene/skin.js";
import { StandardMaterial } from "../../scene/materials/standard-material.js";
import { Entity } from "../entity.js";
import { INTERPOLATION_CUBIC, INTERPOLATION_LINEAR, INTERPOLATION_STEP } from "../anim/constants.js";
import { AnimCurve } from "../anim/evaluator/anim-curve.js";
import { AnimData } from "../anim/evaluator/anim-data.js";
import { AnimTrack } from "../anim/evaluator/anim-track.js";
import { Asset } from "../asset/asset.js";
import { ABSOLUTE_URL } from "../asset/constants.js";
import { createInstancing } from "./glb/extensions/ext-mesh-gpu-instancing.js";
import { createDracoMesh } from "./glb/extensions/khr-draco-mesh-compression.js";
import { createGSplats, hasGSplatExtension } from "./glb/extensions/khr-gaussian-splatting.js";
import { createLights } from "./glb/extensions/khr-lights-punctual.js";
import { createVariants, registerMeshVariants } from "./glb/extensions/khr-materials-variants.js";
import { getTextureSource } from "./glb/extensions/texture-source.js";
import { glbMaterialExtensions } from "./glb/extensions/index.js";
import { extractTextureTransform } from "./glb/extensions/khr-texture-transform.js";
import { GltfAccessor, getPrimitiveType, gltfToEngineSemanticMap } from "./glb/gltf-accessor.js";
class GlbResources {
gltf;
nodes;
scenes;
animations;
textures;
materials;
variants;
meshVariants;
meshDefaultMaterials;
renders;
gsplats;
skins;
lights;
cameras;
nodeInstancingMap;
destroy() {
if (this.renders) {
this.renders.forEach((render) => {
render.meshes = null;
});
}
}
}
const isDataURI = (uri) => {
return /^data:[^\n\r,\u2028\u2029]*,.*$/i.test(uri);
};
const getDataURIMimeType = (uri) => {
return uri.substring(uri.indexOf(":") + 1, uri.indexOf(";"));
};
const attributeOrder = {
[SEMANTIC_POSITION]: 0,
[SEMANTIC_NORMAL]: 1,
[SEMANTIC_TANGENT]: 2,
[SEMANTIC_COLOR]: 3,
[SEMANTIC_BLENDINDICES]: 4,
[SEMANTIC_BLENDWEIGHT]: 5,
[SEMANTIC_TEXCOORD0]: 6,
[SEMANTIC_TEXCOORD1]: 7,
[SEMANTIC_TEXCOORD2]: 8,
[SEMANTIC_TEXCOORD3]: 9,
[SEMANTIC_TEXCOORD4]: 10,
[SEMANTIC_TEXCOORD5]: 11,
[SEMANTIC_TEXCOORD6]: 12,
[SEMANTIC_TEXCOORD7]: 13
};
const generateIndices = (numVertices) => {
const dummyIndices = new Uint16Array(numVertices);
for (let i = 0; i < numVertices; i++) {
dummyIndices[i] = i;
}
return dummyIndices;
};
const generateNormals = (sourceDesc, indices) => {
const p = sourceDesc[SEMANTIC_POSITION];
if (!p || p.components !== 3) {
return;
}
let positions;
if (p.size !== p.stride) {
const srcStride = p.stride / typedArrayTypesByteSize[p.type];
const src = new typedArrayTypes[p.type](p.buffer, p.offset, p.count * srcStride);
positions = new typedArrayTypes[p.type](p.count * 3);
for (let i = 0; i < p.count; ++i) {
positions[i * 3 + 0] = src[i * srcStride + 0];
positions[i * 3 + 1] = src[i * srcStride + 1];
positions[i * 3 + 2] = src[i * srcStride + 2];
}
} else {
positions = new typedArrayTypes[p.type](p.buffer, p.offset, p.count * 3);
}
const numVertices = p.count;
if (!indices) {
indices = generateIndices(numVertices);
}
const normalsTemp = calculateNormals(positions, indices);
const normals = new Float32Array(normalsTemp.length);
normals.set(normalsTemp);
sourceDesc[SEMANTIC_NORMAL] = {
buffer: normals.buffer,
size: 12,
offset: 0,
stride: 12,
count: numVertices,
components: 3,
type: TYPE_FLOAT32
};
};
const cloneTexture = (texture) => {
const shallowCopyLevels = (texture2) => {
const result2 = [];
for (let mip = 0; mip < texture2._levels.length; ++mip) {
let level = [];
if (texture2.cubemap) {
for (let face = 0; face < 6; ++face) {
level.push(texture2._levels[mip][face]);
}
} else {
level = texture2._levels[mip];
}
result2.push(level);
}
return result2;
};
const result = new Texture(texture.device, texture);
result._levels = shallowCopyLevels(texture);
return result;
};
const cloneTextureAsset = (src) => {
const result = new Asset(
`${src.name}_clone`,
src.type,
src.file,
src.data,
src.options
);
result.loaded = true;
result.resource = cloneTexture(src.resource);
src.registry.add(result);
return result;
};
const createVertexBufferInternal = (device, sourceDesc) => {
const positionDesc = sourceDesc[SEMANTIC_POSITION];
if (!positionDesc) {
return null;
}
const numVertices = positionDesc.count;
const vertexDesc = [];
for (const semantic in sourceDesc) {
if (sourceDesc.hasOwnProperty(semantic)) {
const element = {
semantic,
components: sourceDesc[semantic].components,
type: sourceDesc[semantic].type,
normalize: !!sourceDesc[semantic].normalize
};
if (!VertexFormat.isElementValid(device, element)) {
element.components++;
}
vertexDesc.push(element);
}
}
vertexDesc.sort((lhs, rhs) => {
return attributeOrder[lhs.semantic] - attributeOrder[rhs.semantic];
});
let i, j, k;
let source, target, sourceOffset;
const vertexFormat = new VertexFormat(device, vertexDesc);
let isCorrectlyInterleaved = true;
for (i = 0; i < vertexFormat.elements.length; ++i) {
target = vertexFormat.elements[i];
source = sourceDesc[target.name];
sourceOffset = source.offset - positionDesc.offset;
if (source.buffer !== positionDesc.buffer || source.stride !== target.stride || source.size !== target.size || sourceOffset !== target.offset) {
isCorrectlyInterleaved = false;
break;
}
}
const vertexBuffer = new VertexBuffer(device, vertexFormat, numVertices);
const vertexData = vertexBuffer.lock();
const targetArray = new Uint32Array(vertexData);
let sourceArray;
if (isCorrectlyInterleaved) {
sourceArray = new Uint32Array(
positionDesc.buffer,
positionDesc.offset,
numVertices * vertexBuffer.format.size / 4
);
targetArray.set(sourceArray);
} else {
let targetStride, sourceStride;
for (i = 0; i < vertexBuffer.format.elements.length; ++i) {
target = vertexBuffer.format.elements[i];
targetStride = target.stride / 4;
source = sourceDesc[target.name];
sourceStride = source.stride / 4;
sourceArray = new Uint32Array(source.buffer, source.offset, (source.count - 1) * sourceStride + (source.size + 3) / 4);
let src = 0;
let dst = target.offset / 4;
const kend = Math.floor((source.size + 3) / 4);
for (j = 0; j < numVertices; ++j) {
for (k = 0; k < kend; ++k) {
targetArray[dst + k] = sourceArray[src + k];
}
src += sourceStride;
dst += targetStride;
}
}
}
vertexBuffer.unlock();
return vertexBuffer;
};
const createVertexBuffer = (device, attributes, indices, accessors, bufferViews, vertexBufferDict) => {
const useAttributes = {};
const attribIds = [];
for (const attrib in attributes) {
if (attributes.hasOwnProperty(attrib) && gltfToEngineSemanticMap.hasOwnProperty(attrib)) {
useAttributes[attrib] = attributes[attrib];
attribIds.push(`${attrib}:${attributes[attrib]}`);
}
}
attribIds.sort();
const vbKey = attribIds.join();
let vb = vertexBufferDict[vbKey];
if (!vb) {
const sourceDesc = {};
for (const attrib in useAttributes) {
const accessor = accessors[attributes[attrib]];
const accessorData = GltfAccessor.getData(accessor, bufferViews);
const bufferView = bufferViews[accessor.bufferView];
const semantic = gltfToEngineSemanticMap[attrib];
const size = GltfAccessor.getNumComponents(accessor.type) * GltfAccessor.getComponentSizeInBytes(accessor.componentType);
const stride = bufferView && bufferView.hasOwnProperty("byteStride") ? bufferView.byteStride : size;
sourceDesc[semantic] = {
buffer: accessorData.buffer,
size,
offset: accessorData.byteOffset,
stride,
count: accessor.count,
components: GltfAccessor.getNumComponents(accessor.type),
type: GltfAccessor.getComponentType(accessor.componentType),
normalize: accessor.normalized
};
}
if (!sourceDesc.hasOwnProperty(SEMANTIC_NORMAL)) {
generateNormals(sourceDesc, indices);
}
vb = createVertexBufferInternal(device, sourceDesc);
vertexBufferDict[vbKey] = vb;
}
return vb;
};
const createSkin = (device, gltfSkin, accessors, bufferViews, nodes, glbSkins) => {
let i, j, bindMatrix;
const joints = gltfSkin.joints;
const numJoints = joints.length;
const ibp = [];
if (gltfSkin.hasOwnProperty("inverseBindMatrices")) {
const inverseBindMatrices = gltfSkin.inverseBindMatrices;
const ibmData = GltfAccessor.getData(accessors[inverseBindMatrices], bufferViews, true);
const ibmValues = [];
for (i = 0; i < numJoints; i++) {
for (j = 0; j < 16; j++) {
ibmValues[j] = ibmData[i * 16 + j];
}
bindMatrix = new Mat4();
bindMatrix.set(ibmValues);
ibp.push(bindMatrix);
}
} else {
for (i = 0; i < numJoints; i++) {
bindMatrix = new Mat4();
ibp.push(bindMatrix);
}
}
const boneNames = [];
for (i = 0; i < numJoints; i++) {
boneNames[i] = nodes[joints[i]].name;
}
const key = boneNames.join("#");
let skin = glbSkins.get(key);
if (!skin) {
skin = new Skin(device, ibp, boneNames);
glbSkins.set(key, skin);
}
return skin;
};
const createMesh = (device, gltfMesh, accessors, bufferViews, vertexBufferDict, meshVariants, meshDefaultMaterials, assetOptions, promises) => {
const meshes = [];
gltfMesh.primitives.forEach((primitive) => {
if (hasGSplatExtension(primitive)) {
return;
}
if (primitive.extensions?.KHR_draco_mesh_compression) {
meshes.push(createDracoMesh(device, primitive, accessors, bufferViews, meshVariants, meshDefaultMaterials, promises));
} else {
let indices = primitive.hasOwnProperty("indices") ? GltfAccessor.getData(accessors[primitive.indices], bufferViews, true) : null;
const vertexBuffer = createVertexBuffer(device, primitive.attributes, indices, accessors, bufferViews, vertexBufferDict);
const primitiveType = getPrimitiveType(primitive);
const mesh = new Mesh(device);
mesh.vertexBuffer = vertexBuffer;
mesh.primitive[0].type = primitiveType;
mesh.primitive[0].base = 0;
mesh.primitive[0].indexed = indices !== null;
if (indices !== null) {
let indexFormat;
if (indices instanceof Uint8Array) {
indexFormat = INDEXFORMAT_UINT8;
} else if (indices instanceof Uint16Array) {
indexFormat = INDEXFORMAT_UINT16;
} else {
indexFormat = INDEXFORMAT_UINT32;
}
if (indexFormat === INDEXFORMAT_UINT8 && device.isWebGPU) {
indexFormat = INDEXFORMAT_UINT16;
indices = new Uint16Array(indices);
}
const indexBuffer = new IndexBuffer(device, indexFormat, indices.length, BUFFER_STATIC, indices);
mesh.indexBuffer[0] = indexBuffer;
mesh.primitive[0].count = indices.length;
} else {
mesh.primitive[0].count = vertexBuffer.numVertices;
}
registerMeshVariants(primitive, mesh.id, meshVariants);
meshDefaultMaterials[mesh.id] = primitive.material;
let accessor = accessors[primitive.attributes.POSITION];
mesh.aabb = GltfAccessor.getBoundingBox(accessor);
if (primitive.hasOwnProperty("targets")) {
const targets = [];
primitive.targets.forEach((target, index) => {
const options = {};
if (target.hasOwnProperty("POSITION")) {
accessor = accessors[target.POSITION];
options.deltaPositions = GltfAccessor.getDataFloat32(accessor, bufferViews);
options.aabb = GltfAccessor.getBoundingBox(accessor);
}
if (target.hasOwnProperty("NORMAL")) {
accessor = accessors[target.NORMAL];
options.deltaNormals = GltfAccessor.getDataFloat32(accessor, bufferViews);
}
if (gltfMesh.hasOwnProperty("extras") && gltfMesh.extras.hasOwnProperty("targetNames")) {
options.name = gltfMesh.extras.targetNames[index];
} else {
options.name = index.toString(10);
}
if (gltfMesh.hasOwnProperty("weights")) {
options.defaultWeight = gltfMesh.weights[index];
}
options.preserveData = assetOptions.morphPreserveData;
targets.push(new MorphTarget(options));
});
mesh.morph = new Morph(targets, device, {
preferHighPrecision: assetOptions.morphPreferHighPrecision
});
}
meshes.push(mesh);
}
});
return meshes;
};
const createMaterial = (gltfMaterial, textures) => {
const material = new StandardMaterial();
if (gltfMaterial.hasOwnProperty("name")) {
material.name = gltfMaterial.name;
}
material.occludeSpecular = SPECOCC_AO;
material.diffuseVertexColor = true;
material.specularTint = true;
material.specularVertexColor = true;
material.specular.set(1, 1, 1);
material.gloss = 1;
material.glossInvert = true;
material.useMetalness = true;
let texture;
if (gltfMaterial.hasOwnProperty("pbrMetallicRoughness")) {
const pbrData = gltfMaterial.pbrMetallicRoughness;
if (pbrData.hasOwnProperty("baseColorFactor")) {
const [r, g, b, a] = pbrData.baseColorFactor;
material.diffuse.set(r, g, b).gamma();
material.opacity = a;
}
if (pbrData.hasOwnProperty("baseColorTexture")) {
const baseColorTexture = pbrData.baseColorTexture;
texture = textures[baseColorTexture.index];
material.diffuseMap = texture;
material.diffuseMapChannel = "rgb";
material.opacityMap = texture;
material.opacityMapChannel = "a";
extractTextureTransform(baseColorTexture, material, ["diffuse", "opacity"]);
}
if (pbrData.hasOwnProperty("metallicFactor")) {
material.metalness = pbrData.metallicFactor;
}
if (pbrData.hasOwnProperty("roughnessFactor")) {
material.gloss = pbrData.roughnessFactor;
}
if (pbrData.hasOwnProperty("metallicRoughnessTexture")) {
const metallicRoughnessTexture = pbrData.metallicRoughnessTexture;
material.metalnessMap = material.glossMap = textures[metallicRoughnessTexture.index];
material.metalnessMapChannel = "b";
material.glossMapChannel = "g";
extractTextureTransform(metallicRoughnessTexture, material, ["gloss", "metalness"]);
}
}
if (gltfMaterial.hasOwnProperty("normalTexture")) {
const normalTexture = gltfMaterial.normalTexture;
material.normalMap = textures[normalTexture.index];
extractTextureTransform(normalTexture, material, ["normal"]);
if (normalTexture.hasOwnProperty("scale")) {
material.bumpiness = normalTexture.scale;
}
}
if (gltfMaterial.hasOwnProperty("occlusionTexture")) {
const occlusionTexture = gltfMaterial.occlusionTexture;
material.aoMap = textures[occlusionTexture.index];
material.aoMapChannel = "r";
extractTextureTransform(occlusionTexture, material, ["ao"]);
if (occlusionTexture.hasOwnProperty("strength")) {
material.aoIntensity = occlusionTexture.strength;
}
}
if (gltfMaterial.hasOwnProperty("emissiveFactor")) {
const [r, g, b] = gltfMaterial.emissiveFactor;
material.emissive.set(r, g, b).gamma();
}
if (gltfMaterial.hasOwnProperty("emissiveTexture")) {
const emissiveTexture = gltfMaterial.emissiveTexture;
material.emissiveMap = textures[emissiveTexture.index];
extractTextureTransform(emissiveTexture, material, ["emissive"]);
}
if (gltfMaterial.hasOwnProperty("alphaMode")) {
switch (gltfMaterial.alphaMode) {
case "MASK":
material.blendType = BLEND_NONE;
if (gltfMaterial.hasOwnProperty("alphaCutoff")) {
material.alphaTest = gltfMaterial.alphaCutoff;
} else {
material.alphaTest = 0.5;
}
break;
case "BLEND":
material.blendType = BLEND_NORMAL;
material.depthWrite = false;
break;
default:
case "OPAQUE":
material.blendType = BLEND_NONE;
break;
}
} else {
material.blendType = BLEND_NONE;
}
if (gltfMaterial.hasOwnProperty("doubleSided")) {
material.twoSidedLighting = gltfMaterial.doubleSided;
material.cull = gltfMaterial.doubleSided ? CULLFACE_NONE : CULLFACE_BACK;
} else {
material.twoSidedLighting = false;
material.cull = CULLFACE_BACK;
}
if (gltfMaterial.hasOwnProperty("extensions")) {
for (const key in gltfMaterial.extensions) {
const extension = glbMaterialExtensions[key];
if (extension !== void 0) {
extension.apply(gltfMaterial.extensions[key], material, textures);
}
}
}
material.update();
return material;
};
const createAnimation = (gltfAnimation, animationIndex, gltfAccessors, bufferViews, nodes, meshes, gltfNodes) => {
const createAnimData = (gltfAccessor) => {
return new AnimData(GltfAccessor.getNumComponents(gltfAccessor.type), GltfAccessor.getDataFloat32(gltfAccessor, bufferViews));
};
const interpMap = {
"STEP": INTERPOLATION_STEP,
"LINEAR": INTERPOLATION_LINEAR,
"CUBICSPLINE": INTERPOLATION_CUBIC
};
const inputMap = {};
const outputMap = {};
const curveMap = {};
let outputCounter = 1;
let i;
for (i = 0; i < gltfAnimation.samplers.length; ++i) {
const sampler = gltfAnimation.samplers[i];
if (!inputMap.hasOwnProperty(sampler.input)) {
inputMap[sampler.input] = createAnimData(gltfAccessors[sampler.input]);
}
if (!outputMap.hasOwnProperty(sampler.output)) {
outputMap[sampler.output] = createAnimData(gltfAccessors[sampler.output]);
}
const interpolation = sampler.hasOwnProperty("interpolation") && interpMap.hasOwnProperty(sampler.interpolation) ? interpMap[sampler.interpolation] : INTERPOLATION_LINEAR;
const curve = {
paths: [],
input: sampler.input,
output: sampler.output,
interpolation
};
curveMap[i] = curve;
}
const quatArrays = [];
const transformSchema = {
"translation": "localPosition",
"rotation": "localRotation",
"scale": "localScale"
};
const constructNodePath = (node) => {
const path2 = [];
while (node) {
path2.unshift(node.name);
node = node.parent;
}
return path2;
};
const createMorphTargetCurves = (curve, gltfNode, entityPath) => {
const out = outputMap[curve.output];
if (!out) {
return;
}
let targetNames;
if (meshes && meshes[gltfNode.mesh]) {
const mesh = meshes[gltfNode.mesh];
if (mesh.hasOwnProperty("extras") && mesh.extras.hasOwnProperty("targetNames")) {
targetNames = mesh.extras.targetNames;
}
}
const outData = out.data;
const morphTargetCount = outData.length / inputMap[curve.input].data.length;
const keyframeCount = outData.length / morphTargetCount;
const singleBufferSize = keyframeCount * 4;
const buffer = new ArrayBuffer(singleBufferSize * morphTargetCount);
for (let j = 0; j < morphTargetCount; j++) {
const morphTargetOutput = new Float32Array(buffer, singleBufferSize * j, keyframeCount);
for (let k = 0; k < keyframeCount; k++) {
morphTargetOutput[k] = outData[k * morphTargetCount + j];
}
const output = new AnimData(1, morphTargetOutput);
const weightName = targetNames?.[j] ? `name.${targetNames[j]}` : j;
outputMap[-outputCounter] = output;
const morphCurve = {
paths: [{
entityPath,
component: "graph",
propertyPath: [`weight.${weightName}`]
}],
// each morph target curve input can use the same sampler.input from the channel they were all in
input: curve.input,
// but each morph target curve should reference its individual output that was just created
output: -outputCounter,
interpolation: curve.interpolation
};
outputCounter++;
curveMap[`morphCurve-${i}-${j}`] = morphCurve;
}
};
for (i = 0; i < gltfAnimation.channels.length; ++i) {
const channel = gltfAnimation.channels[i];
const target = channel.target;
const curve = curveMap[channel.sampler];
const node = nodes[target.node];
const gltfNode = gltfNodes[target.node];
const entityPath = constructNodePath(node);
if (target.path.startsWith("weights")) {
createMorphTargetCurves(curve, gltfNode, entityPath);
curveMap[channel.sampler].morphCurve = true;
} else {
curve.paths.push({
entityPath,
component: "graph",
propertyPath: [transformSchema[target.path]]
});
}
}
const inputs = [];
const outputs = [];
const curves = [];
for (const inputKey in inputMap) {
inputs.push(inputMap[inputKey]);
inputMap[inputKey] = inputs.length - 1;
}
for (const outputKey in outputMap) {
outputs.push(outputMap[outputKey]);
outputMap[outputKey] = outputs.length - 1;
}
for (const curveKey in curveMap) {
const curveData = curveMap[curveKey];
if (curveData.morphCurve) {
continue;
}
curves.push(new AnimCurve(
curveData.paths,
inputMap[curveData.input],
outputMap[curveData.output],
curveData.interpolation
));
if (curveData.paths.length > 0 && curveData.paths[0].propertyPath[0] === "localRotation" && curveData.interpolation !== INTERPOLATION_CUBIC) {
quatArrays.push(curves[curves.length - 1].output);
}
}
quatArrays.sort();
let prevIndex = null;
let data;
for (i = 0; i < quatArrays.length; ++i) {
const index = quatArrays[i];
if (i === 0 || index !== prevIndex) {
data = outputs[index];
if (data.components === 4) {
const d = data.data;
const len = d.length - 4;
for (let j = 0; j < len; j += 4) {
const dp = d[j + 0] * d[j + 4] + d[j + 1] * d[j + 5] + d[j + 2] * d[j + 6] + d[j + 3] * d[j + 7];
if (dp < 0) {
d[j + 4] *= -1;
d[j + 5] *= -1;
d[j + 6] *= -1;
d[j + 7] *= -1;
}
}
}
prevIndex = index;
}
}
let duration = 0;
for (i = 0; i < inputs.length; i++) {
data = inputs[i]._data;
duration = Math.max(duration, data.length === 0 ? 0 : data[data.length - 1]);
}
return new AnimTrack(
gltfAnimation.hasOwnProperty("name") ? gltfAnimation.name : `animation_${animationIndex}`,
duration,
inputs,
outputs,
curves
);
};
const tempMat = new Mat4();
const tempVec = new Vec3();
const tempQuat = new Quat();
const createNode = (gltfNode, nodeIndex, nodeInstancingMap) => {
const entity = new GraphNode();
if (gltfNode.hasOwnProperty("name") && gltfNode.name.length > 0) {
entity.name = gltfNode.name;
} else {
entity.name = `node_${nodeIndex}`;
}
if (gltfNode.hasOwnProperty("matrix")) {
tempMat.data.set(gltfNode.matrix);
tempMat.getTranslation(tempVec);
entity.setLocalPosition(tempVec);
tempQuat.setFromMat4(tempMat);
entity.setLocalRotation(tempQuat);
tempMat.getScale(tempVec);
tempVec.x *= tempMat.scaleSign;
entity.setLocalScale(tempVec);
}
if (gltfNode.hasOwnProperty("rotation")) {
const r = gltfNode.rotation;
entity.setLocalRotation(r[0], r[1], r[2], r[3]);
}
if (gltfNode.hasOwnProperty("translation")) {
const t = gltfNode.translation;
entity.setLocalPosition(t[0], t[1], t[2]);
}
if (gltfNode.hasOwnProperty("scale")) {
const s = gltfNode.scale;
entity.setLocalScale(s[0], s[1], s[2]);
}
if (gltfNode.hasOwnProperty("extensions") && gltfNode.extensions.EXT_mesh_gpu_instancing) {
nodeInstancingMap.set(gltfNode, {
ext: gltfNode.extensions.EXT_mesh_gpu_instancing
});
}
return entity;
};
const createCamera = (gltfCamera, node) => {
const isOrthographic = gltfCamera.type === "orthographic";
const gltfProperties = isOrthographic ? gltfCamera.orthographic : gltfCamera.perspective;
const componentData = {
enabled: false,
projection: isOrthographic ? PROJECTION_ORTHOGRAPHIC : PROJECTION_PERSPECTIVE,
nearClip: gltfProperties.znear,
aspectRatioMode: ASPECT_AUTO
};
if (gltfProperties.zfar) {
componentData.farClip = gltfProperties.zfar;
}
if (isOrthographic) {
componentData.orthoHeight = gltfProperties.ymag;
if (gltfProperties.xmag && gltfProperties.ymag) {
componentData.aspectRatioMode = ASPECT_MANUAL;
componentData.aspectRatio = gltfProperties.xmag / gltfProperties.ymag;
}
} else {
componentData.fov = gltfProperties.yfov * math.RAD_TO_DEG;
if (gltfProperties.aspectRatio) {
componentData.aspectRatioMode = ASPECT_MANUAL;
componentData.aspectRatio = gltfProperties.aspectRatio;
}
}
const cameraEntity = new Entity(gltfCamera.name);
cameraEntity.addComponent("camera", componentData);
return cameraEntity;
};
const createSkins = (device, gltf, nodes, bufferViews) => {
if (!gltf.hasOwnProperty("skins") || gltf.skins.length === 0) {
return [];
}
const glbSkins = /* @__PURE__ */ new Map();
return gltf.skins.map((gltfSkin) => {
return createSkin(device, gltfSkin, gltf.accessors, bufferViews, nodes, glbSkins);
});
};
const createMeshes = (device, gltf, bufferViews, options) => {
const vertexBufferDict = {};
const meshVariants = {};
const meshDefaultMaterials = {};
const promises = [];
const valid = !options.skipMeshes && gltf?.meshes?.length && gltf?.accessors?.length && gltf?.bufferViews?.length;
const meshes = valid ? gltf.meshes.map((gltfMesh) => {
return createMesh(device, gltfMesh, gltf.accessors, bufferViews, vertexBufferDict, meshVariants, meshDefaultMaterials, options, promises);
}) : [];
return {
meshes,
meshVariants,
meshDefaultMaterials,
promises
};
};
const createMaterials = (gltf, textures, options) => {
if (!gltf.hasOwnProperty("materials") || gltf.materials.length === 0) {
return [];
}
const preprocess = options?.material?.preprocess;
const process = options?.material?.process ?? createMaterial;
const postprocess = options?.material?.postprocess;
return gltf.materials.map((gltfMaterial) => {
if (preprocess) {
preprocess(gltfMaterial);
}
const material = process(gltfMaterial, textures);
if (postprocess) {
postprocess(gltfMaterial, material);
}
return material;
});
};
const createAnimations = (gltf, nodes, bufferViews, options) => {
if (!gltf.hasOwnProperty("animations") || gltf.animations.length === 0) {
return [];
}
const preprocess = options?.animation?.preprocess;
const postprocess = options?.animation?.postprocess;
return gltf.animations.map((gltfAnimation, index) => {
if (preprocess) {
preprocess(gltfAnimation);
}
const animation = createAnimation(gltfAnimation, index, gltf.accessors, bufferViews, nodes, gltf.meshes, gltf.nodes);
if (postprocess) {
postprocess(gltfAnimation, animation);
}
return animation;
});
};
const createNodes = (gltf, options, nodeInstancingMap) => {
if (!gltf.hasOwnProperty("nodes") || gltf.nodes.length === 0) {
return [];
}
const preprocess = options?.node?.preprocess;
const process = options?.node?.process ?? createNode;
const postprocess = options?.node?.postprocess;
const nodes = gltf.nodes.map((gltfNode, index) => {
if (preprocess) {
preprocess(gltfNode);
}
const node = process(gltfNode, index, nodeInstancingMap);
if (postprocess) {
postprocess(gltfNode, node);
}
return node;
});
for (let i = 0; i < gltf.nodes.length; ++i) {
const gltfNode = gltf.nodes[i];
if (gltfNode.hasOwnProperty("children")) {
const parent = nodes[i];
const uniqueNames = {};
for (let j = 0; j < gltfNode.children.length; ++j) {
const child = nodes[gltfNode.children[j]];
if (!child.parent) {
if (uniqueNames.hasOwnProperty(child.name)) {
child.name += uniqueNames[child.name]++;
} else {
uniqueNames[child.name] = 1;
}
parent.addChild(child);
}
}
}
}
return nodes;
};
const createScenes = (gltf, nodes) => {
const scenes = [];
const count = gltf.scenes.length;
if (count === 1 && gltf.scenes[0].nodes?.length === 1) {
const nodeIndex = gltf.scenes[0].nodes[0];
scenes.push(nodes[nodeIndex]);
} else {
for (let i = 0; i < count; i++) {
const scene = gltf.scenes[i];
if (scene.nodes) {
const sceneRoot = new GraphNode(scene.name);
for (let n = 0; n < scene.nodes.length; n++) {
const childNode = nodes[scene.nodes[n]];
sceneRoot.addChild(childNode);
}
scenes.push(sceneRoot);
}
}
}
return scenes;
};
const createCameras = (gltf, nodes, options) => {
let cameras = null;
if (gltf.hasOwnProperty("nodes") && gltf.hasOwnProperty("cameras") && gltf.cameras.length > 0) {
const preprocess = options?.camera?.preprocess;
const process = options?.camera?.process ?? createCamera;
const postprocess = options?.camera?.postprocess;
gltf.nodes.forEach((gltfNode, nodeIndex) => {
if (gltfNode.hasOwnProperty("camera")) {
const gltfCamera = gltf.cameras[gltfNode.camera];
if (gltfCamera) {
if (preprocess) {
preprocess(gltfCamera);
}
const camera = process(gltfCamera, nodes[nodeIndex]);
if (postprocess) {
postprocess(gltfCamera, camera);
}
if (camera) {
if (!cameras) cameras = /* @__PURE__ */ new Map();
cameras.set(gltfNode, camera);
}
}
}
});
}
return cameras;
};
const linkSkins = (gltf, renders, skins) => {
gltf.nodes.forEach((gltfNode) => {
if (gltfNode.hasOwnProperty("mesh") && gltfNode.hasOwnProperty("skin")) {
const meshGroup = renders[gltfNode.mesh].meshes;
meshGroup.forEach((mesh) => {
mesh.skin = skins[gltfNode.skin];
});
}
});
};
const createResources = async (device, gltf, bufferViews, textures, options) => {
const preprocess = options?.global?.preprocess;
const postprocess = options?.global?.postprocess;
if (preprocess) {
preprocess(gltf);
}
if (gltf.asset && gltf.asset.generator === "PlayCanvas") {
}
const nodeInstancingMap = /* @__PURE__ */ new Map();
const nodes = createNodes(gltf, options, nodeInstancingMap);
const scenes = createScenes(gltf, nodes);
const lights = createLights(gltf, nodes, options);
const cameras = createCameras(gltf, nodes, options);
const variants = createVariants(gltf);
const bufferViewData = await Promise.all(bufferViews);
const { meshes, meshVariants, meshDefaultMaterials, promises } = createMeshes(device, gltf, bufferViewData, options);
const gsplats = options.skipMeshes ? [] : createGSplats(device, gltf, bufferViewData);
const animations = createAnimations(gltf, nodes, bufferViewData, options);
createInstancing(device, gltf, nodeInstancingMap, bufferViewData);
const textureAssets = await Promise.all(textures);
const textureInstances = textureAssets.map((t) => t.resource);
const materials = createMaterials(gltf, textureInstances, options);
const skins = createSkins(device, gltf, nodes, bufferViewData);
const renders = [];
for (let i = 0; i < meshes.length; i++) {
renders[i] = new Render();
renders[i].meshes = meshes[i];
}
linkSkins(gltf, renders, skins);
const result = new GlbResources();
result.gltf = gltf;
result.nodes = nodes;
result.scenes = scenes;
result.animations = animations;
result.textures = textureAssets;
result.materials = materials;
result.variants = variants;
result.meshVariants = meshVariants;
result.meshDefaultMaterials = meshDefaultMaterials;
result.renders = renders;
result.gsplats = gsplats;
result.skins = skins;
result.lights = lights;
result.cameras = cameras;
result.nodeInstancingMap = nodeInstancingMap;
if (postprocess) {
postprocess(gltf, result);
}
await Promise.all(promises);
return result;
};
const applySampler = (texture, gltfSampler) => {
const getFilter = (filter, defaultValue) => {
switch (filter) {
case 9728:
return FILTER_NEAREST;
case 9729:
return FILTER_LINEAR;
case 9984:
return FILTER_NEAREST_MIPMAP_NEAREST;
case 9985:
return FILTER_LINEAR_MIPMAP_NEAREST;
case 9986:
return FILTER_NEAREST_MIPMAP_LINEAR;
case 9987:
return FILTER_LINEAR_MIPMAP_LINEAR;
default:
return defaultValue;
}
};
const getWrap = (wrap, defaultValue) => {
switch (wrap) {
case 33071:
return ADDRESS_CLAMP_TO_EDGE;
case 33648:
return ADDRESS_MIRRORED_REPEAT;
case 10497:
return ADDRESS_REPEAT;
default:
return defaultValue;
}
};
if (texture) {
gltfSampler = gltfSampler ?? {};
texture.minFilter = getFilter(gltfSampler.minFilter, FILTER_LINEAR_MIPMAP_LINEAR);
texture.magFilter = getFilter(gltfSampler.magFilter, FILTER_LINEAR);
texture.addressU = getWrap(gltfSampler.wrapS, ADDRESS_REPEAT);
texture.addressV = getWrap(gltfSampler.wrapT, ADDRESS_REPEAT);
}
};
let gltfTextureUniqueId = 0;
const createImages = (gltf, bufferViews, urlBase, registry, options) => {
if (!gltf.images || gltf.images.length === 0) {
return [];
}
const preprocess = options?.image?.preprocess;
const processAsync = options?.image?.processAsync;
const postprocess = options?.image?.postprocess;
const mimeTypeFileExtensions = {
"image/png": "png",
"image/jpeg": "jpg",
"image/basis": "basis",
"image/ktx": "ktx",
"image/ktx2": "ktx2",
"image/vnd-ms.dds": "dds"
};
const getGammaTextures = (gltf2) => {
const set = /* @__PURE__ */ new Set();
if (gltf2.hasOwnProperty("materials")) {
gltf2.materials.forEach((gltfMaterial) => {
if (gltfMaterial.hasOwnProperty("pbrMetallicRoughness")) {
const pbrData = gltfMaterial.pbrMetallicRoughness;
if (pbrData.hasOwnProperty("baseColorTexture")) {
const gltfTexture = gltf2.textures[pbrData.baseColorTexture.index];
set.add(getTextureSource(gltfTexture));
}
}
if (gltfMaterial.hasOwnProperty("emissiveTexture")) {
const gltfTexture = gltf2.textures[gltfMaterial.emissiveTexture.index];
set.add(getTextureSource(gltfTexture));
}
if (gltfMaterial.hasOwnProperty("extensions")) {
for (const key in gltfMaterial.extensions) {
const extension = glbMaterialExtensions[key];
if (extension?.getColorTextures) {
const textureInfos = extension.getColorTextures(gltfMaterial.extensions[key]);
for (const textureInfo of textureInfos) {
const gltfTexture = gltf2.textures[textureInfo.index];
set.add(getTextureSource(gltfTexture));
}
}
}
}
});
}
return set;
};
const loadTexture = (gltfImage, url, bufferView, mimeType, options2, srgb) => {
return new Promise((resolve, reject) => {
const continuation = (bufferViewData) => {
const name = `${gltfImage.name || "gltf-texture"}-${gltfTextureUniqueId++}`;
const file = {
url: url || name
};
if (bufferViewData) {
file.contents = bufferViewData.slice(0).buffer;
}
if (mimeType) {
const extension = mimeTypeFileExtensions[mimeType];
if (extension) {
file.filename = `${file.url}.${extension}`;
}
}
const data = { srgb };
const asset = new Asset(name, "texture", file, data, options2);
asset.on("load", (asset2) => resolve(asset2));
asset.on("error", (err) => reject(err));
registry.add(asset);
registry.load(asset);
};
if (bufferView) {
bufferView.then((bufferViewData) => continuation(bufferViewData));
} else {
continuation(null);
}
});
};
const gammaTextures = getGammaTextures(gltf);
return gltf.images.map((gltfImage, i) => {
if (preprocess) {
preprocess(gltfImage);
}
let promise;
if (processAsync) {
promise = new Promise((resolve, reject) => {
processAsync(gltfImage, (err, textureAsset) => {
if (err) {
reject(err);
} else {
resolve(textureAsset);
}
});
});
} else {
promise = new Promise((resolve) => {
resolve(null);
});
}
promise = promise.then((textureAsset) => {
const srgb = gammaTextures.has(i);
if (textureAsset) {
return textureAsset;
} else if (gltfImage.hasOwnProperty("uri")) {
if (isDataURI(gltfImage.uri)) {
return loadTexture(gltfImage, gltfImage.uri, null, getDataURIMimeType(gltfImage.uri), null, srgb);
}
return loadTexture(gltfImage, ABSOLUTE_URL.test(gltfImage.uri) ? gltfImage.uri : path.join(urlBase, gltfImage.uri), null, null, { crossOrigin: "anonymous" }, srgb);
} else if (gltfImage.hasOwnProperty("bufferView") && gltfImage.hasOwnProperty("mimeType")) {
return loadTexture(gltfImage, null, bufferViews[gltfImage.bufferView], gltfImage.mimeType, null, srgb);
}
return Promise.reject(new Error(`Invalid image found in gltf (neither uri or bufferView found). index=${i}`));
});
if (postprocess) {
promise = promise.then((textureAsset) => {
postprocess(gltfImage, textureAsset);
return textureAsset;
});
}
return promise;
});
};
const createTextures = (gltf, images, options) => {
if (!gltf?.images?.length || !gltf?.textures?.length) {
return [];
}
const preprocess = options?.texture?.preprocess;
const processAsync = options?.texture?.processAsync;
const postprocess = options?.texture?.postprocess;
const seenImages = /* @__PURE__ */ new Set();
return gltf.textures.map((gltfTexture) => {
if (preprocess) {
preprocess(gltfTexture);
}
let promise;
if (processAsync) {
promise = new Promise((resolve, reject) => {
processAsync(gltfTexture, gltf.images, (err, gltfImageIndex) => {
if (err) {
reject(err);
} else {
resolve(gltfImageIndex);
}
});
});
} else {
promise = new Promise((resolve) => {
resolve(null);
});
}
promise = promise.then((gltfImageIndex) => {
gltfImageIndex = gltfImageIndex ?? getTextureSource(gltfTexture);
const cloneAsset = seenImages.has(gltfImageIndex);
seenImages.add(gltfImageIndex);
return images[gltfImageIndex].then((imageAsset) => {
const asset = cloneAsset ? cloneTextureAsset(imageAsset) : imageAsset;
applySampler(asset.resource, (gltf.samplers ?? [])[gltfTexture.sampler]);
return asset;
});
});
if (postprocess) {
promise = promise.then((textureAsset) => {
postprocess(gltfTexture, textureAsset);
return textureAsset;
});
}
return promise;
});
};
const loadBuffers = (gltf, binaryChunk, urlBase, options) => {
if (!gltf.buffers || gltf.buffers.length === 0) {
return [];
}
const preprocess = options?.buffer?.preprocess;
const processAsync = options?.buffer?.processAsync;
const postprocess = options?.buffer?.postprocess;
return gltf.buffers.map((gltfBuffer, i) => {
if (preprocess) {
preprocess(gltfBuffer);
}
let promise;
if (processAsync) {
promise = new Promise((resolve, reject) => {
processAsync(gltfBuffer, (err, arrayBuffer) => {
if (err) {
reject(err);
} else {
resolve(arrayBuffer);
}
});
});
} else {
promise = new Promise((resolve) => {
resolve(null);
});
}
promise = promise.then((arrayBuffer) => {
if (arrayBuffer) {
return arrayBuffer;
} else if (gltfBuffer.hasOwnProperty("uri")) {
if (isDataURI(gltfBuffer.uri)) {
const byteString = atob(gltfBuffer.uri.split(",")[1]);
const binaryArray = new Uint8Array(byteString.length);
for (let j = 0; j < byteString.length; j++) {
binaryArray[j] = byteString.charCodeAt(j);
}
return binaryArray;
}
return new Promise((resolve, reject) => {
http.get(
ABSOLUTE_URL.test(gltfBuffer.uri) ? gltfBuffer.uri : path.join(urlBase, gltfBuffer.uri),
{ cache: true, responseType: "arraybuffer", retry: false },
(err, result) => {
if (err) {
reject(err);
} else {
resolve(new Uint8Array(result));
}
}
);
});
}
return binaryChunk;
});
if (postprocess) {
promise = promise.then((buffer) => {
postprocess(gltf.buffers[i], buffer);
return buffer;
});
}
return promise;
});
};
const parseGltf = (gltfChunk, callback) => {
const decodeBinaryUtf8 = (array) => {
if (typeof TextDecoder !== "undefined") {
return new TextDecoder().decode(array);
}
let str = "";
for (let i = 0; i < array.length; i++) {
str += String.fromCharCode(array[i]);
}
return decodeURIComponent(escape(str));
};
const gltf = JSON.parse(decodeBinaryUtf8(gltfChunk));
if (gltf.asset && gltf.asset.version && parseFloat(gltf.asset.version) < 2) {
callback(`Invalid gltf version. Expected version 2.0 or above but found version '${gltf.asset.version}'.`);
return;
}
callback(null, gltf);
};
const parseGlb = (glbData, callback) => {
const data = glbData instanceof ArrayBuffer ? new DataView(glbData) : new DataView(glbData.buffer, glbData.byteOffset, glbData.byteLength);
const magic = data.getUint32(0, true);
const version = data.getUint32(4, true);
const length = data.getUint32(8, true);
if (magic !== 1179937895) {
callback(`Invalid magic number found in glb header. Expected 0x46546C67, found 0x${magic.toString(16)}`);
return;
}
if (version !== 2) {
callback(`Invalid version number found in glb header. Expected 2, found ${version}`);
return;
}
if (length <= 0 || length > data.byteLength) {
callback(`Invalid length found in glb header. Found ${length}`);
return;
}
const chunks = [];
let offset = 12;
while (offset < length) {
const chunkLength = data.getUint32(offset, true);
if (offset + chunkLength + 8 > data.byteLength) {
callback(`Invalid chunk length found in glb. Found ${chunkLength}`);
}
const chunkType = data.getUint32(offset + 4, true);
const chunkData = new Uint8Array(data.buffer, data.byteOffset + offset + 8, chunkLength);
chunks.push({ length: chunkLength, type: chunkType, data: chunkData });
offset += chunkLength + 8;
}
if (chunks.length !== 1 && chunks.length !== 2) {
callback("Invalid number of chunks found in glb file.");
return;
}
if (chunks[0].type !== 1313821514) {
callback(`Invalid chunk type found in glb file. Expected 0x4E4F534A, found 0x${chunks[0].type.toString(16)}`);
return;
}
if (chunks.length > 1 && chunks[1].type !== 5130562) {
callback(`Invalid chunk type found in glb file. Expected 0x004E4942, found 0x${chunks[1].type.toString(16)}`);
return;
}
callback(null, {
gltfChunk: chunks[0].data,
binaryChunk: chunks.length === 2 ? chunks[1].data : null
});
};
const parseChunk = (filename, data, callback) => {
const hasGlbHeader = () => {
const u8 = new Uint8Array(data);
return u8[0] === 103 && u8[1] === 108 && u8[2] === 84 && u8[3] === 70;
};
if (filename && filename.toLowerCase().endsWith(".glb") || hasGlbHeader()) {
parseGlb(data, callback);
} else {
callback(null, {
gltfChunk: data,
binaryChunk: null
});
}
};
const createBufferViews = (gltf, buffers, options) => {
const result = [];
const preprocess = options?.bufferView?.preprocess;
const processAsync = options?.bufferView?.processAsync;
const postprocess = options?.bufferView?.postprocess;
if (!gltf.bufferViews?.length) {
return result;
}
for (let i = 0; i < gltf.bufferViews.length; ++i) {
const gltfBufferView = gltf.bufferViews[i];
if (preprocess) {
preprocess(gltfBufferView);
}
let promise;
if (processAsync) {
promise = new Promise((resolve, reject) => {
processAsync(gltfBufferView, buffers, (err, result2) => {
if (err) {
reject(err);
} else {
resolve(result2);
}
});
});
} else {
promise = new Promise((resolve) => {
resolve(null);
});
}
promise = promise.then((buffer) => {
if (buffer) {
return buffer;
}
return buffers[gltfBufferView.buffer].then((buffer2) => {
return new Uint8Array(
buffer2.buffer,
buffer2.byteOffset + (gltfBufferView.byteOffset || 0),
gltfBufferView.byteLength
);
});
});
if (gltfBufferView.hasOwnProperty("byteStride")) {
promise = promise.then((typedArray) => {
typedArray.byteStride = gltfBufferView.byteStride;
return typedArray;
});
}
if (postprocess) {
promise = promise.then((typedArray) => {
postprocess(gltfBufferView, typedArray);
return typedArray;
});
}
result.push(promise);
}
return result;
};
class GlbParser {
// parse the gltf or glb data asynchronously, loading external resources
static parse(filename, urlBase, data, device, registry, options, callback) {
parseChunk(filename, data, (err, chunks) => {
if (err) {
callback(err);
return;
}
parseGltf(chunks.gltfChunk, (err2, gltf) => {
if (err2) {
callback(err2);
return;
}
const buffers = loadBuffers(gltf, chunks.binaryChunk, urlBase, options);
const bufferViews = createBufferViews(gltf, buffers, options);
const images = createImages(gltf, bufferViews, urlBase, registry, options);
const textures = createTextures(gltf, images, options);
createResources(device, gltf, bufferViews, textures, options).then((result) => callback(null, result)).catch((err3) => callback(err3));
});
});
}
static createDefaultMaterial() {
return createMaterial({
name: "defaultGlbMaterial"
}, []);
}
}
export {
GlbParser
};