playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
1,383 lines • 76.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Debug } from "../../core/debug.js";
import { path } from "../../core/path.js";
import { Color } from "../../core/math/color.js";
import { Mat4 } from "../../core/math/mat4.js";
import { math } from "../../core/math/math.js";
import { Vec2 } from "../../core/math/vec2.js";
import { Vec3 } from "../../core/math/vec3.js";
import { BoundingBox } from "../../core/shape/bounding-box.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,
PRIMITIVE_LINELOOP,
PRIMITIVE_LINESTRIP,
PRIMITIVE_LINES,
PRIMITIVE_POINTS,
PRIMITIVE_TRIANGLES,
PRIMITIVE_TRIFAN,
PRIMITIVE_TRISTRIP,
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_INT8,
TYPE_UINT8,
TYPE_INT16,
TYPE_UINT16,
TYPE_INT32,
TYPE_UINT32,
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,
LIGHTFALLOFF_INVERSESQUARED,
PROJECTION_ORTHOGRAPHIC,
PROJECTION_PERSPECTIVE,
ASPECT_MANUAL,
ASPECT_AUTO,
SPECOCC_AO
} from "../../scene/constants.js";
import { GraphNode } from "../../scene/graph-node.js";
import { Light, lightTypes } from "../../scene/light.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 { dracoDecode } from "./draco-decoder.js";
import { Quat } from "../../core/math/quat.js";
class GlbResources {
constructor() {
__publicField(this, "gltf");
__publicField(this, "nodes");
__publicField(this, "scenes");
__publicField(this, "animations");
__publicField(this, "textures");
__publicField(this, "materials");
__publicField(this, "variants");
__publicField(this, "meshVariants");
__publicField(this, "meshDefaultMaterials");
__publicField(this, "renders");
__publicField(this, "skins");
__publicField(this, "lights");
__publicField(this, "cameras");
__publicField(this, "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 getNumComponents = (accessorType) => {
switch (accessorType) {
case "SCALAR":
return 1;
case "VEC2":
return 2;
case "VEC3":
return 3;
case "VEC4":
return 4;
case "MAT2":
return 4;
case "MAT3":
return 9;
case "MAT4":
return 16;
default:
return 3;
}
};
const getComponentType = (componentType) => {
switch (componentType) {
case 5120:
return TYPE_INT8;
case 5121:
return TYPE_UINT8;
case 5122:
return TYPE_INT16;
case 5123:
return TYPE_UINT16;
case 5124:
return TYPE_INT32;
case 5125:
return TYPE_UINT32;
case 5126:
return TYPE_FLOAT32;
default:
return 0;
}
};
const getComponentSizeInBytes = (componentType) => {
switch (componentType) {
case 5120:
return 1;
// int8
case 5121:
return 1;
// uint8
case 5122:
return 2;
// int16
case 5123:
return 2;
// uint16
case 5124:
return 4;
// int32
case 5125:
return 4;
// uint32
case 5126:
return 4;
// float32
default:
return 0;
}
};
const getComponentDataType = (componentType) => {
switch (componentType) {
case 5120:
return Int8Array;
case 5121:
return Uint8Array;
case 5122:
return Int16Array;
case 5123:
return Uint16Array;
case 5124:
return Int32Array;
case 5125:
return Uint32Array;
case 5126:
return Float32Array;
default:
return null;
}
};
const gltfToEngineSemanticMap = {
"POSITION": SEMANTIC_POSITION,
"NORMAL": SEMANTIC_NORMAL,
"TANGENT": SEMANTIC_TANGENT,
"COLOR_0": SEMANTIC_COLOR,
"JOINTS_0": SEMANTIC_BLENDINDICES,
"WEIGHTS_0": SEMANTIC_BLENDWEIGHT,
"TEXCOORD_0": SEMANTIC_TEXCOORD0,
"TEXCOORD_1": SEMANTIC_TEXCOORD1,
"TEXCOORD_2": SEMANTIC_TEXCOORD2,
"TEXCOORD_3": SEMANTIC_TEXCOORD3,
"TEXCOORD_4": SEMANTIC_TEXCOORD4,
"TEXCOORD_5": SEMANTIC_TEXCOORD5,
"TEXCOORD_6": SEMANTIC_TEXCOORD6,
"TEXCOORD_7": SEMANTIC_TEXCOORD7
};
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 getDequantizeFunc = (srcType) => {
switch (srcType) {
case TYPE_INT8:
return (x) => Math.max(x / 127, -1);
case TYPE_UINT8:
return (x) => x / 255;
case TYPE_INT16:
return (x) => Math.max(x / 32767, -1);
case TYPE_UINT16:
return (x) => x / 65535;
default:
return (x) => x;
}
};
const dequantizeArray = (dstArray, srcArray, srcType) => {
const convFunc = getDequantizeFunc(srcType);
const len = srcArray.length;
for (let i = 0; i < len; ++i) {
dstArray[i] = convFunc(srcArray[i]);
}
return dstArray;
};
const getAccessorData = (gltfAccessor, bufferViews, flatten = false) => {
const numComponents = getNumComponents(gltfAccessor.type);
const dataType = getComponentDataType(gltfAccessor.componentType);
if (!dataType) {
return null;
}
let result;
if (gltfAccessor.sparse) {
const sparse = gltfAccessor.sparse;
const indicesAccessor = {
count: sparse.count,
type: "SCALAR"
};
const indices = getAccessorData(Object.assign(indicesAccessor, sparse.indices), bufferViews, true);
const valuesAccessor = {
count: sparse.count,
type: gltfAccessor.type,
componentType: gltfAccessor.componentType
};
const values = getAccessorData(Object.assign(valuesAccessor, sparse.values), bufferViews, true);
if (gltfAccessor.hasOwnProperty("bufferView")) {
const baseAccessor = {
bufferView: gltfAccessor.bufferView,
byteOffset: gltfAccessor.byteOffset,
componentType: gltfAccessor.componentType,
count: gltfAccessor.count,
type: gltfAccessor.type
};
result = getAccessorData(baseAccessor, bufferViews, true).slice();
} else {
result = new dataType(gltfAccessor.count * numComponents);
}
for (let i = 0; i < sparse.count; ++i) {
const targetIndex = indices[i];
for (let j = 0; j < numComponents; ++j) {
result[targetIndex * numComponents + j] = values[i * numComponents + j];
}
}
} else {
if (gltfAccessor.hasOwnProperty("bufferView")) {
const bufferView = bufferViews[gltfAccessor.bufferView];
if (flatten && bufferView.hasOwnProperty("byteStride")) {
const bytesPerElement = numComponents * dataType.BYTES_PER_ELEMENT;
const storage = new ArrayBuffer(gltfAccessor.count * bytesPerElement);
const tmpArray = new Uint8Array(storage);
let dstOffset = 0;
for (let i = 0; i < gltfAccessor.count; ++i) {
let srcOffset = (gltfAccessor.byteOffset || 0) + i * bufferView.byteStride;
for (let b = 0; b < bytesPerElement; ++b) {
tmpArray[dstOffset++] = bufferView[srcOffset++];
}
}
result = new dataType(storage);
} else {
result = new dataType(
bufferView.buffer,
bufferView.byteOffset + (gltfAccessor.byteOffset || 0),
gltfAccessor.count * numComponents
);
}
} else {
result = new dataType(gltfAccessor.count * numComponents);
}
}
return result;
};
const getAccessorDataFloat32 = (gltfAccessor, bufferViews) => {
const data = getAccessorData(gltfAccessor, bufferViews, true);
if (data instanceof Float32Array || !gltfAccessor.normalized) {
return data;
}
const float32Data = new Float32Array(data.length);
dequantizeArray(float32Data, data, getComponentType(gltfAccessor.componentType));
return float32Data;
};
const getAccessorBoundingBox = (gltfAccessor) => {
let min = gltfAccessor.min;
let max = gltfAccessor.max;
if (!min || !max) {
return null;
}
if (gltfAccessor.normalized) {
const ctype = getComponentType(gltfAccessor.componentType);
min = dequantizeArray([], min, ctype);
max = dequantizeArray([], max, ctype);
}
return new BoundingBox(
new Vec3((max[0] + min[0]) * 0.5, (max[1] + min[1]) * 0.5, (max[2] + min[2]) * 0.5),
new Vec3((max[0] - min[0]) * 0.5, (max[1] - min[1]) * 0.5, (max[2] - min[2]) * 0.5)
);
};
const getPrimitiveType = (primitive) => {
if (!primitive.hasOwnProperty("mode")) {
return PRIMITIVE_TRIANGLES;
}
switch (primitive.mode) {
case 0:
return PRIMITIVE_POINTS;
case 1:
return PRIMITIVE_LINES;
case 2:
return PRIMITIVE_LINELOOP;
case 3:
return PRIMITIVE_LINESTRIP;
case 4:
return PRIMITIVE_TRIANGLES;
case 5:
return PRIMITIVE_TRISTRIP;
case 6:
return PRIMITIVE_TRIFAN;
default:
return PRIMITIVE_TRIANGLES;
}
};
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 = getAccessorData(accessor, bufferViews);
const bufferView = bufferViews[accessor.bufferView];
const semantic = gltfToEngineSemanticMap[attrib];
const size = getNumComponents(accessor.type) * 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: getNumComponents(accessor.type),
type: 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 = getAccessorData(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 createDracoMesh = (device, primitive, accessors, bufferViews, meshVariants, meshDefaultMaterials, promises) => {
const result = new Mesh(device);
result.aabb = getAccessorBoundingBox(accessors[primitive.attributes.POSITION]);
promises.push(new Promise((resolve, reject) => {
const dracoExt = primitive.extensions.KHR_draco_mesh_compression;
dracoDecode(bufferViews[dracoExt.bufferView].slice().buffer, (err, decompressedData) => {
if (err) {
console.log(err);
reject(err);
} else {
const idToSemantic = {};
for (const [name, id] of Object.entries(dracoExt.attributes)) {
idToSemantic[id] = gltfToEngineSemanticMap[name];
}
idToSemantic[-1] = SEMANTIC_NORMAL;
const vertexDesc = [];
for (const attr of decompressedData.attributes) {
const semantic = idToSemantic[attr.id];
if (semantic !== void 0) {
let normalize = false;
if (attr.id !== -1) {
for (const [name, id] of Object.entries(dracoExt.attributes)) {
if (id === attr.id && primitive.attributes[name] !== void 0) {
const accessor = accessors[primitive.attributes[name]];
normalize = accessor.normalized ?? (semantic === SEMANTIC_COLOR && (attr.dataType === TYPE_UINT8 || attr.dataType === TYPE_UINT16));
break;
}
}
}
vertexDesc.push({
semantic,
components: attr.numComponents,
type: attr.dataType,
normalize,
// use offset and stride from worker to handle cases where Draco mesh
// has additional attributes not listed in glTF
offset: attr.offset,
stride: decompressedData.stride
});
}
}
const vertexFormat = new VertexFormat(device, vertexDesc);
const numVertices = decompressedData.vertices.byteLength / decompressedData.stride;
const indexFormat = numVertices <= 65535 ? INDEXFORMAT_UINT16 : INDEXFORMAT_UINT32;
const numIndices = decompressedData.indices.byteLength / (numVertices <= 65535 ? 2 : 4);
Debug.call(() => {
if (numVertices !== accessors[primitive.attributes.POSITION].count) {
Debug.warn("mesh has invalid vertex count");
}
if (primitive.indices !== void 0 && numIndices !== accessors[primitive.indices].count) {
Debug.warn("mesh has invalid index count");
}
});
const vertexBuffer = new VertexBuffer(device, vertexFormat, numVertices, {
data: decompressedData.vertices
});
const indexBuffer = new IndexBuffer(device, indexFormat, numIndices, BUFFER_STATIC, decompressedData.indices);
result.vertexBuffer = vertexBuffer;
result.indexBuffer[0] = indexBuffer;
result.primitive[0].type = getPrimitiveType(primitive);
result.primitive[0].base = 0;
result.primitive[0].count = indexBuffer ? numIndices : numVertices;
result.primitive[0].indexed = !!indexBuffer;
resolve();
}
});
}));
if (primitive?.extensions?.KHR_materials_variants) {
const variants = primitive.extensions.KHR_materials_variants;
const tempMapping = {};
variants.mappings.forEach((mapping) => {
mapping.variants.forEach((variant) => {
tempMapping[variant] = mapping.material;
});
});
meshVariants[result.id] = tempMapping;
}
meshDefaultMaterials[result.id] = primitive.material;
return result;
};
const createMesh = (device, gltfMesh, accessors, bufferViews, vertexBufferDict, meshVariants, meshDefaultMaterials, assetOptions, promises) => {
const meshes = [];
gltfMesh.primitives.forEach((primitive) => {
if (primitive.extensions?.KHR_draco_mesh_compression) {
meshes.push(createDracoMesh(device, primitive, accessors, bufferViews, meshVariants, meshDefaultMaterials, promises));
} else {
let indices = primitive.hasOwnProperty("indices") ? getAccessorData(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;
}
if (primitive.hasOwnProperty("extensions") && primitive.extensions.hasOwnProperty("KHR_materials_variants")) {
const variants = primitive.extensions.KHR_materials_variants;
const tempMapping = {};
variants.mappings.forEach((mapping) => {
mapping.variants.forEach((variant) => {
tempMapping[variant] = mapping.material;
});
});
meshVariants[mesh.id] = tempMapping;
}
meshDefaultMaterials[mesh.id] = primitive.material;
let accessor = accessors[primitive.attributes.POSITION];
mesh.aabb = getAccessorBoundingBox(accessor);
if (primitive.hasOwnProperty("targets")) {
const targets = [];
primitive.targets.forEach((target, index) => {
const options = {};
if (target.hasOwnProperty("POSITION")) {
accessor = accessors[target.POSITION];
options.deltaPositions = getAccessorDataFloat32(accessor, bufferViews);
options.aabb = getAccessorBoundingBox(accessor);
}
if (target.hasOwnProperty("NORMAL")) {
accessor = accessors[target.NORMAL];
options.deltaNormals = getAccessorDataFloat32(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 extractTextureTransform = (source, material, maps) => {
let map;
const texCoord = source.texCoord;
if (texCoord) {
for (map = 0; map < maps.length; ++map) {
material[`${maps[map]}MapUv`] = texCoord;
}
}
const zeros = [0, 0];
const ones = [1, 1];
const textureTransform = source.extensions?.KHR_texture_transform;
if (textureTransform) {
const offset = textureTransform.offset || zeros;
const scale = textureTransform.scale || ones;
const rotation = textureTransform.rotation ? -textureTransform.rotation * math.RAD_TO_DEG : 0;
const tilingVec = new Vec2(scale[0], scale[1]);
const offsetVec = new Vec2(offset[0], 1 - scale[1] - offset[1]);
for (map = 0; map < maps.length; ++map) {
material[`${maps[map]}MapTiling`] = tilingVec;
material[`${maps[map]}MapOffset`] = offsetVec;
material[`${maps[map]}MapRotation`] = rotation;
}
}
};
const extensionPbrSpecGlossiness = (data, material, textures) => {
let texture;
if (data.hasOwnProperty("diffuseFactor")) {
const [r, g, b, a] = data.diffuseFactor;
material.diffuse.set(r, g, b).gamma();
material.opacity = a;
} else {
material.diffuse.set(1, 1, 1);
material.opacity = 1;
}
if (data.hasOwnProperty("diffuseTexture")) {
const diffuseTexture = data.diffuseTexture;
texture = textures[diffuseTexture.index];
material.diffuseMap = texture;
material.diffuseMapChannel = "rgb";
material.opacityMap = texture;
material.opacityMapChannel = "a";
extractTextureTransform(diffuseTexture, material, ["diffuse", "opacity"]);
}
material.useMetalness = false;
if (data.hasOwnProperty("specularFactor")) {
const [r, g, b] = data.specularFactor;
material.specular.set(r, g, b).gamma();
} else {
material.specular.set(1, 1, 1);
}
if (data.hasOwnProperty("glossinessFactor")) {
material.gloss = data.glossinessFactor;
} else {
material.gloss = 1;
}
if (data.hasOwnProperty("specularGlossinessTexture")) {
const specularGlossinessTexture = data.specularGlossinessTexture;
material.specularMap = material.glossMap = textures[specularGlossinessTexture.index];
material.specularMapChannel = "rgb";
material.glossMapChannel = "a";
extractTextureTransform(specularGlossinessTexture, material, ["gloss", "metalness"]);
}
};
const extensionClearCoat = (data, material, textures) => {
if (data.hasOwnProperty("clearcoatFactor")) {
material.clearCoat = data.clearcoatFactor * 0.25;
} else {
material.clearCoat = 0;
}
if (data.hasOwnProperty("clearcoatTexture")) {
const clearcoatTexture = data.clearcoatTexture;
material.clearCoatMap = textures[clearcoatTexture.index];
material.clearCoatMapChannel = "r";
extractTextureTransform(clearcoatTexture, material, ["clearCoat"]);
}
if (data.hasOwnProperty("clearcoatRoughnessFactor")) {
material.clearCoatGloss = data.clearcoatRoughnessFactor;
} else {
material.clearCoatGloss = 0;
}
if (data.hasOwnProperty("clearcoatRoughnessTexture")) {
const clearcoatRoughnessTexture = data.clearcoatRoughnessTexture;
material.clearCoatGlossMap = textures[clearcoatRoughnessTexture.index];
material.clearCoatGlossMapChannel = "g";
extractTextureTransform(clearcoatRoughnessTexture, material, ["clearCoatGloss"]);
}
if (data.hasOwnProperty("clearcoatNormalTexture")) {
const clearcoatNormalTexture = data.clearcoatNormalTexture;
material.clearCoatNormalMap = textures[clearcoatNormalTexture.index];
extractTextureTransform(clearcoatNormalTexture, material, ["clearCoatNormal"]);
if (clearcoatNormalTexture.hasOwnProperty("scale")) {
material.clearCoatBumpiness = clearcoatNormalTexture.scale;
} else {
material.clearCoatBumpiness = 1;
}
}
material.clearCoatGlossInvert = true;
};
const extensionUnlit = (data, material, textures) => {
material.useLighting = false;
material.emissive.copy(material.diffuse);
material.emissiveMap = material.diffuseMap;
material.emissiveMapUv = material.diffuseMapUv;
material.emissiveMapTiling.copy(material.diffuseMapTiling);
material.emissiveMapOffset.copy(material.diffuseMapOffset);
material.emissiveMapRotation = material.diffuseMapRotation;
material.emissiveMapChannel = material.diffuseMapChannel;
material.emissiveVertexColor = material.diffuseVertexColor;
material.emissiveVertexColorChannel = material.diffuseVertexColorChannel;
material.useLighting = false;
material.useSkybox = false;
material.diffuse.set(1, 1, 1);
material.diffuseMap = null;
material.diffuseVertexColor = false;
};
const extensionSpecular = (data, material, textures) => {
material.useMetalnessSpecularColor = true;
if (data.hasOwnProperty("specularColorTexture")) {
material.specularMap = textures[data.specularColorTexture.index];
material.specularMapChannel = "rgb";
extractTextureTransform(data.specularColorTexture, material, ["specular"]);
}
if (data.hasOwnProperty("specularColorFactor")) {
const [r, g, b] = data.specularColorFactor;
material.specular.set(r, g, b).gamma();
} else {
material.specular.set(1, 1, 1);
}
if (data.hasOwnProperty("specularFactor")) {
material.specularityFactor = data.specularFactor;
} else {
material.specularityFactor = 1;
}
if (data.hasOwnProperty("specularTexture")) {
material.specularityFactorMapChannel = "a";
material.specularityFactorMap = textures[data.specularTexture.index];
extractTextureTransform(data.specularTexture, material, ["specularityFactor"]);
}
};
const extensionIor = (data, material, textures) => {
if (data.hasOwnProperty("ior")) {
material.refractionIndex = 1 / data.ior;
}
};
const extensionDispersion = (data, material, textures) => {
if (data.hasOwnProperty("dispersion")) {
material.dispersion = data.dispersion;
}
};
const extensionTransmission = (data, material, textures) => {
material.blendType = BLEND_NORMAL;
material.useDynamicRefraction = true;
if (data.hasOwnProperty("transmissionFactor")) {
material.refraction = data.transmissionFactor;
}
if (data.hasOwnProperty("transmissionTexture")) {
material.refractionMapChannel = "r";
material.refractionMap = textures[data.transmissionTexture.index];
extractTextureTransform(data.transmissionTexture, material, ["refraction"]);
}
};
const extensionSheen = (data, material, textures) => {
material.useSheen = true;
if (data.hasOwnProperty("sheenColorFactor")) {
const [r, g, b] = data.sheenColorFactor;
material.sheen.set(r, g, b).gamma();
} else {
material.sheen.set(1, 1, 1);
}
if (data.hasOwnProperty("sheenColorTexture")) {
material.sheenMap = textures[data.sheenColorTexture.index];
extractTextureTransform(data.sheenColorTexture, material, ["sheen"]);
}
material.sheenGloss = data.hasOwnProperty("sheenRoughnessFactor") ? data.sheenRoughnessFactor : 0;
if (data.hasOwnProperty("sheenRoughnessTexture")) {
material.sheenGlossMap = textures[data.sheenRoughnessTexture.index];
material.sheenGlossMapChannel = "a";
extractTextureTransform(data.sheenRoughnessTexture, material, ["sheenGloss"]);
}
material.sheenGlossInvert = true;
};
const extensionVolume = (data, material, textures) => {
material.blendType = BLEND_NORMAL;
material.useDynamicRefraction = true;
if (data.hasOwnProperty("thicknessFactor")) {
material.thickness = data.thicknessFactor;
}
if (data.hasOwnProperty("thicknessTexture")) {
material.thicknessMap = textures[data.thicknessTexture.index];
material.thicknessMapChannel = "g";
extractTextureTransform(data.thicknessTexture, material, ["thickness"]);
}
if (data.hasOwnProperty("attenuationDistance")) {
material.attenuationDistance = data.attenuationDistance;
}
if (data.hasOwnProperty("attenuationColor")) {
const [r, g, b] = data.attenuationColor;
material.attenuation.set(r, g, b).gamma();
}
};
const extensionEmissiveStrength = (data, material, textures) => {
if (data.hasOwnProperty("emissiveStrength")) {
material.emissiveIntensity = data.emissiveStrength;
}
};
const extensionIridescence = (data, material, textures) => {
material.useIridescence = true;
if (data.hasOwnProperty("iridescenceFactor")) {
material.iridescence = data.iridescenceFactor;
}
if (data.hasOwnProperty("iridescenceTexture")) {
material.iridescenceMapChannel = "r";
material.iridescenceMap = textures[data.iridescenceTexture.index];
extractTextureTransform(data.iridescenceTexture, material, ["iridescence"]);
}
if (data.hasOwnProperty("iridescenceIor")) {
material.iridescenceRefractionIndex = data.iridescenceIor;
}
if (data.hasOwnProperty("iridescenceThicknessMinimum")) {
material.iridescenceThicknessMin = data.iridescenceThicknessMinimum;
}
if (data.hasOwnProperty("iridescenceThicknessMaximum")) {
material.iridescenceThicknessMax = data.iridescenceThicknessMaximum;
}
if (data.hasOwnProperty("iridescenceThicknessTexture")) {
material.iridescenceThicknessMapChannel = "g";
material.iridescenceThicknessMap = textures[data.iridescenceThicknessTexture.index];
extractTextureTransform(data.iridescenceThicknessTexture, material, ["iridescenceThickness"]);
}
};
const extensionAnisotropy = (data, material, textures) => {
material.enableGGXSpecular = true;
if (data.hasOwnProperty("anisotropyStrength")) {
material.anisotropyIntensity = data.anisotropyStrength;
} else {
material.anisotropyIntensity = 0;
}
if (data.hasOwnProperty("anisotropyTexture")) {
const anisotropyTexture = data.anisotropyTexture;
material.anisotropyMap = textures[anisotropyTexture.index];
extractTextureTransform(anisotropyTexture, material, ["anisotropy"]);
}
if (data.hasOwnProperty("anisotropyRotation")) {
material.anisotropyRotation = data.anisotropyRotation * math.RAD_TO_DEG;
} else {
material.anisotropyRotation = 0;
}
};
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 (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;
}
const extensions = {
"KHR_materials_clearcoat": extensionClearCoat,
"KHR_materials_emissive_strength": extensionEmissiveStrength,
"KHR_materials_ior": extensionIor,
"KHR_materials_dispersion": extensionDispersion,
"KHR_materials_iridescence": extensionIridescence,
"KHR_materials_pbrSpecularGlossiness": extensionPbrSpecGlossiness,
"KHR_materials_sheen": extensionSheen,
"KHR_materials_specular": extensionSpecular,
"KHR_materials_transmission": extensionTransmission,
"KHR_materials_unlit": extensionUnlit,
"KHR_materials_volume": extensionVolume,
"KHR_materials_anisotropy": extensionAnisotropy
};
if (gltfMaterial.hasOwnProperty("extensions")) {
for (const key in gltfMaterial.extensions) {
const extensionFunc = extensions[key];
if (extensionFunc !== void 0) {
extensionFunc(gltfMaterial.extensions[key], material, textures);
}
}
}
material.update();
return material;
};
const createAnimation = (gltfAnimation, animationIndex, gltfAccessors, bufferViews, nodes, meshes, gltfNodes) => {
const createAnimData = (gltfAccessor) => {
return new AnimData(getNumComponents(gltfAccessor.type), getAccessorDataFloat32(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) {
Debug.warn(`glb-parser: No output data is available for the morph target curve (${entityPath}/graph/weights). Skipping.`);
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 createLight = (gltfLight, node) => {
const lightProps = {
enabled: false,
type: gltfLight.type === "point" ? "omni" : gltfLight.type,
color: gltfLight.hasOwnProperty("color") ? new Color(gltfLight.color) : Color.WHITE,
// when range is not defined, infinity should be used - but that causes infinity in bounds calculations
range: gltfLight.hasOwnProperty("range") ? gltfLight.range : 9999,
falloffMode: LIGHTFALLOFF_INVERSESQUARED,
// TODO: (engine issue #3252) Set intensity to match glTF specification, which uses physically based values:
// - Omni and spot lights use luminous intensity in candela (lm/sr)
// - Directional lights use illuminance in lux (lm/m2).
// Current implementation: clamps specified intensity to 0..2 range
intensity: gltfLight.hasOwnProperty("intensity") ? math.clamp(gltfLight.intensity, 0, 2) : 1
};
if (gltfLight.hasOwnProperty("spot")) {
lightProps.innerConeAngle = gltfLight.spot.hasOwnProperty("innerConeAngle") ? gltfLight.spot.innerConeAngle * math.RAD_TO_DEG : 0;
lightProps.outerConeAngle = gltfLight.spot.hasOwnProperty("outerConeAngle") ? gltfLight.spot.outerConeAngle * math.RAD_TO_DEG : 45;
}
if (gltfLight.hasOwnProperty("intensity")) {
const outerAngleRad = gltfLight.spot?.outerConeAngle ?? Math.PI / 4;
const innerAngleRad = gltfLight.spot?.innerConeAngle ?? 0;
lightProps.luminance = gltfLight.intensity * Light.getLightUnitConversion(lightType