UNPKG

gpu-curtains

Version:

gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.

1,105 lines (1,104 loc) 71.4 kB
import { isCameraRenderer } from '../../core/renderers/utils.mjs'; import { Sampler } from '../../core/samplers/Sampler.mjs'; import { MediaTexture } from '../../core/textures/MediaTexture.mjs'; import { Object3D } from '../../core/objects3D/Object3D.mjs'; import { Box3 } from '../../math/Box3.mjs'; import { Vec3 } from '../../math/Vec3.mjs'; import { Mat3 } from '../../math/Mat3.mjs'; import { Mat4 } from '../../math/Mat4.mjs'; import { Geometry } from '../../core/geometries/Geometry.mjs'; import { IndexedGeometry } from '../../core/geometries/IndexedGeometry.mjs'; import { LitMesh } from '../meshes/LitMesh.mjs'; import { OrthographicCamera } from '../../core/cameras/OrthographicCamera.mjs'; import { PerspectiveCamera } from '../../core/cameras/PerspectiveCamera.mjs'; import { BufferBinding } from '../../core/bindings/BufferBinding.mjs'; import { KeyframesAnimation } from '../animations/KeyframesAnimation.mjs'; import { TargetsAnimationsManager } from '../animations/TargetsAnimationsManager.mjs'; import { Vec2 } from '../../math/Vec2.mjs'; import { RenderMaterial } from '../../core/materials/RenderMaterial.mjs'; import { DirectionalLight } from '../../core/lights/DirectionalLight.mjs'; import { PointLight } from '../../core/lights/PointLight.mjs'; import { SpotLight } from '../../core/lights/SpotLight.mjs'; var __typeError = (msg) => { throw TypeError(msg); }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var _primitiveInstances, _GLTFScenesManager_instances, getAccessorArray_fn, getSparseAccessorIndicesAndValues_fn, parsePrimitiveProperty_fn; const GL = typeof window !== "undefined" && WebGLRenderingContext || { BYTE: 5120, UNSIGNED_BYTE: 5121, SHORT: 5122, UNSIGNED_SHORT: 5123, UNSIGNED_INT: 5125, FLOAT: 5126, TRIANGLES: 4, TRIANGLE_STRIP: 5, LINES: 1, LINE_STRIP: 3, POINTS: 0, CLAMP_TO_EDGE: 33071, MIRRORED_REPEAT: 33648, NEAREST: 9728, LINEAR: 9729, LINEAR_MIPMAP_NEAREST: 9985, NEAREST_MIPMAP_LINEAR: 9986, LINEAR_MIPMAP_LINEAR: 9987 }; const _GLTFScenesManager = class _GLTFScenesManager { /** * {@link GLTFScenesManager} constructor. * @param parameters - parameters used to create our {@link GLTFScenesManager}. * @param parameters.renderer - our {@link CameraRenderer} class object. * @param parameters.gltf - The {@link GLTFLoader.gltf | gltf} object used. */ constructor({ renderer, gltf }) { __privateAdd(this, _GLTFScenesManager_instances); /** The {@link PrimitiveInstances} Map, to group similar {@link LitMesh} by instances. */ __privateAdd(this, _primitiveInstances); renderer = isCameraRenderer(renderer, "GLTFScenesManager"); this.renderer = renderer; this.gltf = gltf; __privateSet(this, _primitiveInstances, /* @__PURE__ */ new Map()); this.scenesManager = { node: new Object3D(), nodes: /* @__PURE__ */ new Map(), boundingBox: new Box3(), samplers: [], materialsTextures: [], materialsParams: [], scenes: [], meshes: [], meshesDescriptors: [], animations: [], cameras: [], skins: [], lights: [] }; this.createSamplers(); this.createMaterialTextures(); this.createMaterialsParams(); this.createLights(); this.createAnimations(); this.createScenes(); } /** * Get an attribute type, bufferFormat and size from its {@link GLTF.AccessorType | accessor type}. * @param type - {@link GLTF.AccessorType | accessor type} to use. * @returns - corresponding type, bufferFormat and size. */ static getVertexAttributeParamsFromType(type) { switch (type) { case "VEC2": return { type: "vec2f", bufferFormat: "float32x2", size: 2 }; case "VEC3": return { type: "vec3f", bufferFormat: "float32x3", size: 3 }; case "VEC4": return { type: "vec4f", bufferFormat: "float32x4", size: 4 }; case "MAT2": return { type: "mat2x2f", bufferFormat: "float32x2", // not used size: 6 }; case "MAT3": return { type: "mat3x3f", bufferFormat: "float32x3", // not used size: 9 }; case "MAT4": return { type: "mat4x4f", bufferFormat: "float32x4", // not used size: 16 }; case "SCALAR": default: return { type: "f32", bufferFormat: "float32", size: 1 }; } } /** * Get the corresponding typed array constructor based on the {@link GLTF.AccessorComponentType | accessor component type}. * @param componentType - {@link GLTF.AccessorComponentType | accessor component type} to use. * @returns - corresponding typed array constructor. */ static getTypedArrayConstructorFromComponentType(componentType) { switch (componentType) { case GL.BYTE: return Int8Array; case GL.UNSIGNED_BYTE: return Uint8Array; case GL.SHORT: return Int16Array; case GL.UNSIGNED_SHORT: return Uint16Array; case GL.UNSIGNED_INT: return Uint32Array; case GL.FLOAT: // GL.FLOAT default: return Float32Array; } } /** * Get the {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createRenderPipeline#topology | GPUPrimitiveTopology} based on the {@link GLTF.MeshPrimitiveMode | WebGL primitive mode}. * @param mode - {@link GLTF.MeshPrimitiveMode | WebGL primitive mode} to use. * @returns - corresponding {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createRenderPipeline#topology | GPUPrimitiveTopology}. */ static gpuPrimitiveTopologyForMode(mode) { switch (mode) { case GL.TRIANGLES: return "triangle-list"; case GL.TRIANGLE_STRIP: return "triangle-strip"; case GL.LINES: return "line-list"; case GL.LINE_STRIP: return "line-strip"; case GL.POINTS: return "point-list"; } } /** * Get the {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createSampler#addressmodeu | GPUAddressMode} based on the {@link GLTF.TextureWrapMode | WebGL texture wrap mode}. * @param wrap - {@link GLTF.TextureWrapMode | WebGL texture wrap mode} to use. * @returns - corresponding {@link https://developer.mozilla.org/en-US/docs/Web/API/GPUDevice/createSampler#addressmodeu | GPUAddressMode}. */ static gpuAddressModeForWrap(wrap) { switch (wrap) { case GL.CLAMP_TO_EDGE: return "clamp-to-edge"; case GL.MIRRORED_REPEAT: return "mirror-repeat"; default: return "repeat"; } } /** * Create the {@link scenesManager} {@link TargetsAnimationsManager} if any animation is present in the {@link gltf}. */ createAnimations() { this.gltf.animations?.forEach((animation, index) => { this.scenesManager.animations.push( new TargetsAnimationsManager(this.renderer, { label: animation.name ?? "Animation " + index }) ); }); } /** * Create the {@link ScenesManager.lights | lights} defined by the `KHR_lights_punctual` extension if any. */ createLights() { if (this.gltf.extensions && this.gltf.extensions["KHR_lights_punctual"]) { for (const light of this.gltf.extensions["KHR_lights_punctual"].lights) { if (light.type === "spot") { const innerConeAngle = light.spot.innerConeAngle !== void 0 ? light.spot.innerConeAngle : 0; const outerConeAngle = light.spot.outerConeAngle !== void 0 ? light.spot.outerConeAngle : Math.PI / 4; this.scenesManager.lights.push( new SpotLight(this.renderer, { ...light.name !== void 0 && { label: light.name }, color: light.color !== void 0 ? new Vec3(light.color[0], light.color[1], light.color[2]) : new Vec3(1), intensity: light.intensity !== void 0 ? light.intensity : 1, range: light.range !== void 0 ? light.range : 0, angle: outerConeAngle, penumbra: 1 - innerConeAngle / outerConeAngle }) ); } else if (light.type === "directional") { this.scenesManager.lights.push( new DirectionalLight(this.renderer, { ...light.name !== void 0 && { label: light.name }, color: light.color !== void 0 ? new Vec3(light.color[0], light.color[1], light.color[2]) : new Vec3(1), intensity: light.intensity !== void 0 ? light.intensity : 1 }) ); } else if (light.type === "point") { this.scenesManager.lights.push( new PointLight(this.renderer, { ...light.name !== void 0 && { label: light.name }, color: light.color !== void 0 ? new Vec3(light.color[0], light.color[1], light.color[2]) : new Vec3(1), intensity: light.intensity !== void 0 ? light.intensity : 1, range: light.range !== void 0 ? light.range : 0 }) ); } } } } /** * Create the {@link Sampler} and add them to the {@link ScenesManager.samplers | scenesManager samplers array}. */ createSamplers() { if (this.gltf.samplers) { for (const [index, sampler] of Object.entries(this.gltf.samplers)) { const descriptor = { label: "glTF sampler " + index, name: "gltfSampler" + index, // TODO better name? addressModeU: _GLTFScenesManager.gpuAddressModeForWrap(sampler.wrapS), addressModeV: _GLTFScenesManager.gpuAddressModeForWrap(sampler.wrapT) }; if (!sampler.magFilter || sampler.magFilter === GL.LINEAR) { descriptor.magFilter = "linear"; } switch (sampler.minFilter) { case GL.NEAREST: break; case GL.LINEAR: case GL.LINEAR_MIPMAP_NEAREST: descriptor.minFilter = "linear"; break; case GL.NEAREST_MIPMAP_LINEAR: descriptor.mipmapFilter = "linear"; break; case GL.LINEAR_MIPMAP_LINEAR: default: descriptor.minFilter = "linear"; descriptor.mipmapFilter = "linear"; break; } this.scenesManager.samplers.push(new Sampler(this.renderer, descriptor)); } } else { this.scenesManager.samplers.push( new Sampler(this.renderer, { label: "Default sampler", name: "defaultSampler", magFilter: "linear", minFilter: "linear", mipmapFilter: "linear" }) ); } } /** * Create a {@link MediaTexture} based on the options. * @param material - material using that texture. * @param image - image source of the texture. * @param name - name of the texture. * @param useTransform - Whether the {@link MediaTexture} should handle transformations. * @returns - newly created {@link MediaTexture}. */ createTexture(material, image, name, useTransform = false) { const format = (() => { switch (name) { case "baseColorTexture": case "emissiveTexture": case "specularTexture": case "specularColorTexture": return "rgba8unorm-srgb"; case "occlusionTexture": case "transmissionTexture": return "r8unorm"; case "thicknessTexture": return "rg8unorm"; default: return "rgba8unorm"; } })(); const texture = new MediaTexture(this.renderer, { label: material.name ? material.name + ": " + name : name, name, format, visibility: ["fragment"], generateMips: true, // generate mips by default fixedSize: { width: image.width, height: image.height }, useTransform }); texture.useImageBitmap(image); return texture; } /** * Create the {ScenesManager.materialsTextures | scenesManager materialsTextures array} and each associated {@link types/gltf/GLTFScenesManager.MaterialTextureDescriptor | MaterialTextureDescriptor} and their respective {@link Texture}. */ createMaterialTextures() { this.scenesManager.materialsTextures = []; const createdTextures = []; if (this.gltf.materials) { for (const [materialIndex, material] of Object.entries(this.gltf.materials)) { const materialTextures = { material: materialIndex, texturesDescriptors: [] }; const getUVAttributeName = (texture) => { if (!texture.texCoord) return "uv"; return texture.texCoord !== 0 ? "uv" + texture.texCoord : "uv"; }; const createTexture = (gltfTextureInfo, name) => { const index = gltfTextureInfo.index; const gltfTexture = this.gltf.textures[index]; const source = gltfTexture.extensions && gltfTexture.extensions["EXT_texture_webp"] ? gltfTexture.extensions["EXT_texture_webp"].source : gltfTexture.source; const samplerIndex = this.gltf.textures.find((t) => { const src = t.extensions && t.extensions["EXT_texture_webp"] ? t.extensions["EXT_texture_webp"].source : t.source; return src === index; })?.sampler; const sampler = this.scenesManager.samplers[samplerIndex ?? 0]; const textureTransform = gltfTextureInfo.extensions && gltfTextureInfo.extensions["KHR_texture_transform"]; const texCoordAttributeName = getUVAttributeName( textureTransform && textureTransform.texCoord !== void 0 ? textureTransform : gltfTextureInfo ); const hasTexture = createdTextures.find((createdTexture) => createdTexture.index === index); if (hasTexture) { const reusedTexture = new MediaTexture(this.renderer, { label: material.name ? material.name + ": " + name : name, name, visibility: ["fragment"], generateMips: true, // generate mips by default fromTexture: hasTexture.texture, ...textureTransform && { useTransform: true } }); if (textureTransform) { const { offset, rotation, scale } = textureTransform; if (offset !== void 0) reusedTexture.offset.set(offset[0], offset[1]); if (rotation !== void 0) reusedTexture.rotation = rotation; if (scale !== void 0) reusedTexture.scale.set(scale[0], scale[1]); } materialTextures.texturesDescriptors.push({ texture: reusedTexture, sampler, texCoordAttributeName }); return; } const image = this.gltf.imagesBitmaps[source]; const texture = this.createTexture(material, image, name, !!textureTransform); if (textureTransform) { const { offset, rotation, scale } = textureTransform; if (offset !== void 0) texture.offset.set(offset[0], offset[1]); if (rotation !== void 0) texture.rotation = rotation; if (scale !== void 0) texture.scale.set(scale[0], scale[1]); } materialTextures.texturesDescriptors.push({ texture, sampler, texCoordAttributeName }); createdTextures.push({ index, texture }); }; this.scenesManager.materialsTextures[materialIndex] = materialTextures; if (material.pbrMetallicRoughness) { if (material.pbrMetallicRoughness.baseColorTexture && material.pbrMetallicRoughness.baseColorTexture.index !== void 0) { createTexture(material.pbrMetallicRoughness.baseColorTexture, "baseColorTexture"); } if (material.pbrMetallicRoughness.metallicRoughnessTexture && material.pbrMetallicRoughness.metallicRoughnessTexture.index !== void 0) { createTexture(material.pbrMetallicRoughness.metallicRoughnessTexture, "metallicRoughnessTexture"); } } if (material.normalTexture && material.normalTexture.index !== void 0) { createTexture(material.normalTexture, "normalTexture"); } if (material.occlusionTexture && material.occlusionTexture.index !== void 0) { createTexture(material.occlusionTexture, "occlusionTexture"); } if (material.emissiveTexture && material.emissiveTexture.index !== void 0) { createTexture(material.emissiveTexture, "emissiveTexture"); } const { extensions } = material; const transmission = extensions && extensions.KHR_materials_transmission || null; const specular = extensions && extensions.KHR_materials_specular || null; const volume = extensions && extensions.KHR_materials_volume || null; if (transmission && transmission.transmissionTexture && transmission.transmissionTexture.index !== void 0) { createTexture(transmission.transmissionTexture, "transmissionTexture"); } if (specular && (specular.specularTexture || specular.specularColorTexture)) { const { specularTexture, specularColorTexture } = specular; if (specularTexture && specularColorTexture) { if (specularTexture.index !== void 0 && specularColorTexture.index !== void 0 && specularTexture.index === specularColorTexture.index) { createTexture(specular.specularTexture, "specularTexture"); } else { if (specularTexture && specularTexture.index !== void 0) { createTexture(specular.specularTexture, "specularFactorTexture"); } if (specularColorTexture && specularColorTexture.index !== void 0) { createTexture(specular.specularColorTexture, "specularColorTexture"); } } } } if (volume && volume.thicknessTexture && volume.thicknessTexture.index !== void 0) { createTexture(volume.thicknessTexture, "thicknessTexture"); } } } } /** * Get the {@link MeshDescriptorMaterialParams} for a given {@link GLTF.IMeshPrimitive.material | glTF primitive material index}. * @param materialIndex - {@link GLTF.IMeshPrimitive.material | glTF primitive material index}. * @param label - Optional label to use for the {@link RenderMaterial} created. * @returns - Created {@link MeshDescriptorMaterialParams}. */ getMaterialBaseParameters(materialIndex, label = null) { const materialParams = {}; const material = this.gltf.materials && this.gltf.materials[materialIndex] || {}; if (label) { materialParams.label = label + (material.name ? " " + material.name : ""); } else if (material.name) { materialParams.label = material.name; } const { extensions } = material; const dispersion = extensions && extensions.KHR_materials_dispersion || null; const emissiveStrength = extensions && extensions.KHR_materials_emissive_strength || null; const ior = extensions && extensions.KHR_materials_ior || null; const transmission = extensions && extensions.KHR_materials_transmission || null; const specular = extensions && extensions.KHR_materials_specular || null; const volume = extensions && extensions.KHR_materials_volume || null; const litMeshMaterialParams = { colorSpace: "linear", color: material.pbrMetallicRoughness && material.pbrMetallicRoughness.baseColorFactor !== void 0 ? new Vec3( material.pbrMetallicRoughness.baseColorFactor[0], material.pbrMetallicRoughness.baseColorFactor[1], material.pbrMetallicRoughness.baseColorFactor[2] ) : new Vec3(1), opacity: material.pbrMetallicRoughness && material.pbrMetallicRoughness.baseColorFactor !== void 0 ? material.pbrMetallicRoughness.baseColorFactor[3] : 1, alphaCutoff: material.alphaCutoff !== void 0 ? material.alphaCutoff : material.alphaMode === "MASK" ? 0.5 : 0, metallic: material.pbrMetallicRoughness?.metallicFactor === void 0 ? 1 : material.pbrMetallicRoughness.metallicFactor, roughness: material.pbrMetallicRoughness?.roughnessFactor === void 0 ? 1 : material.pbrMetallicRoughness.roughnessFactor, normalScale: material.normalTexture?.scale === void 0 ? new Vec2(1) : new Vec2(material.normalTexture.scale), occlusionIntensity: material.occlusionTexture?.strength === void 0 ? 1 : material.occlusionTexture.strength, emissiveIntensity: emissiveStrength && emissiveStrength.emissiveStrength !== void 0 ? emissiveStrength.emissiveStrength : 1, emissiveColor: material.emissiveFactor !== void 0 ? new Vec3(material.emissiveFactor[0], material.emissiveFactor[1], material.emissiveFactor[2]) : new Vec3(0), specularIntensity: specular && specular.specularFactor !== void 0 ? specular.specularFactor : 1, specularColor: specular && specular.specularColorFactor !== void 0 ? new Vec3(specular.specularColorFactor[0], specular.specularColorFactor[1], specular.specularColorFactor[2]) : new Vec3(1), transmission: transmission && transmission.transmissionFactor !== void 0 ? transmission.transmissionFactor : 0, ior: ior && ior.ior !== void 0 ? ior.ior : 1.5, dispersion: dispersion && dispersion.dispersion !== void 0 ? dispersion.dispersion : 0, thickness: volume && volume.thicknessFactor !== void 0 ? volume.thicknessFactor : 0, attenuationDistance: volume && volume.attenuationDistance !== void 0 ? volume.attenuationDistance : Infinity, attenuationColor: volume && volume.attenuationColor !== void 0 ? new Vec3(volume.attenuationColor[0], volume.attenuationColor[1], volume.attenuationColor[2]) : new Vec3(1) }; materialParams.material = litMeshMaterialParams; materialParams.cullMode = material.doubleSided ? "none" : "back"; if (material.alphaMode === "BLEND") { materialParams.transparent = true; materialParams.targets = [ { blend: { color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha" }, alpha: { // This just prevents the canvas from having alpha "holes" in it. srcFactor: "one", dstFactor: "one" } } } ]; } return materialParams; } /** * Create all the {@link MeshDescriptorMaterialParams} from the {@link GLTF.IMaterial | glTF materials}. */ createMaterialsParams() { this.gltf.materials?.forEach((material, index) => { this.scenesManager.materialsParams.push(this.getMaterialBaseParameters(index)); }); } /** * Create a {@link ChildDescriptor} from a parent {@link ChildDescriptor} and a {@link GLTF.INode | glTF Node} * @param parent - parent {@link ChildDescriptor} to use. * @param node - {@link GLTF.INode | glTF Node} to use. * @param index - Index of the {@link GLTF.INode | glTF Node} to use. */ createNode(parent, node, index) { const child = { index, name: node.name, node: new Object3D(), children: [] }; this.scenesManager.nodes.set(index, child.node); parent.children.push(child); child.node.parent = parent.node; if (node.matrix) { child.node.modelMatrix.setFromArray(new Float32Array(node.matrix)); child.node.matrices.model.shouldUpdate = false; } else { if (node.translation) child.node.position.set(node.translation[0], node.translation[1], node.translation[2]); if (node.scale) child.node.scale.set(node.scale[0], node.scale[1], node.scale[2]); if (node.rotation) child.node.quaternion.setFromArray(new Float32Array(node.rotation)); } if (node.children) { node.children.forEach((childNodeIndex) => { const childNode = this.gltf.nodes[childNodeIndex]; this.createNode(child, childNode, childNodeIndex); }); } let instancesDescriptor = null; if (node.mesh !== void 0) { let instanceAttributes = null; if (node.extensions && node.extensions.EXT_mesh_gpu_instancing) { const { attributes } = node.extensions.EXT_mesh_gpu_instancing; instanceAttributes = { count: 0, nodesTransformations: {} }; for (const attribute of Object.entries(attributes)) { const accessor = this.gltf.accessors[attribute[1]]; const attributeValues = __privateMethod(this, _GLTFScenesManager_instances, getAccessorArray_fn).call(this, accessor); instanceAttributes.count = accessor.count; instanceAttributes.nodesTransformations[attribute[0].toLowerCase()] = attributeValues; } } const mesh = this.gltf.meshes[node.mesh]; mesh.primitives.forEach((primitive, primitiveIndex) => { const meshDescriptor = { parent: child.node, texturesDescriptors: [], variantName: "Default", parameters: { label: mesh.name ? mesh.name + " " + primitiveIndex : "glTF mesh " + primitiveIndex }, nodes: [], extensionsUsed: [], alternateDescriptors: /* @__PURE__ */ new Map(), alternateMaterials: /* @__PURE__ */ new Map() }; instancesDescriptor = __privateGet(this, _primitiveInstances).get(primitive); if (!instancesDescriptor) { instancesDescriptor = { instances: [], // instances nodes: [], // node transform meshDescriptor }; __privateGet(this, _primitiveInstances).set(primitive, instancesDescriptor); } instancesDescriptor.instances.push(node); instancesDescriptor.nodes.push(child.node); if (instanceAttributes && instanceAttributes.count) { for (let i = 0; i < instanceAttributes.count; i++) { const instanceNode = new Object3D(); if (instanceAttributes.nodesTransformations) { const { translation, scale, rotation } = instanceAttributes.nodesTransformations; if (translation) { instanceNode.position.set(translation[i * 3], translation[i * 3 + 1], translation[i * 3 + 2]); } if (scale) { instanceNode.scale.set(scale[i * 3], scale[i * 3 + 1], scale[i * 3 + 2]); } if (rotation) { instanceNode.quaternion.setFromArray( Float32Array.from([rotation[i * 4], rotation[i * 4 + 1], rotation[i * 4 + 2], rotation[i * 4 + 3]]) ); } } instanceNode.parent = child.node; instancesDescriptor.instances.push(node); instancesDescriptor.nodes.push(instanceNode); } } }); } if (node.extensions && node.extensions.KHR_lights_punctual) { const light = this.scenesManager.lights[node.extensions.KHR_lights_punctual.light]; light.position.set(0, 0, 0); if (light instanceof DirectionalLight || light instanceof SpotLight) { light.target.set(0, 0, -1); } light.parent = child.node; } if (node.camera !== void 0) { const gltfCamera = this.gltf.cameras[node.camera]; if (gltfCamera.type === "perspective") { let width, height; if (gltfCamera.perspective.aspectRatio !== void 0) { const minSize = Math.min(this.renderer.boundingRect.width, this.renderer.boundingRect.height); width = minSize / gltfCamera.perspective.aspectRatio; height = minSize * gltfCamera.perspective.aspectRatio; } else { width = this.renderer.boundingRect.width; height = this.renderer.boundingRect.height; } const fov = gltfCamera.perspective.yfov * 180 / Math.PI; const camera = new PerspectiveCamera({ fov, near: gltfCamera.perspective.znear, far: gltfCamera.perspective.zfar, width, height, pixelRatio: this.renderer.pixelRatio, ...gltfCamera.perspective.aspectRatio !== void 0 && { forceAspect: gltfCamera.perspective.aspectRatio } }); camera.parent = child.node; this.scenesManager.cameras.push(camera); } else if (gltfCamera.type === "orthographic") { const camera = new OrthographicCamera({ near: gltfCamera.orthographic.znear, far: gltfCamera.orthographic.zfar, left: -gltfCamera.orthographic.xmag, right: gltfCamera.orthographic.xmag, top: gltfCamera.orthographic.ymag, bottom: -gltfCamera.orthographic.ymag }); camera.parent = child.node; this.scenesManager.cameras.push(camera); } } if (this.gltf.animations) { this.scenesManager.animations.forEach((targetsAnimation, i) => { const animation = this.gltf.animations[i]; const channels = animation.channels.filter((channel) => channel.target.node === index); if (channels && channels.length) { targetsAnimation.addTarget(child.node); channels.forEach((channel) => { const sampler = animation.samplers[channel.sampler]; const path = channel.target.path; const inputAccessor = this.gltf.accessors[sampler.input]; const keyframes = __privateMethod(this, _GLTFScenesManager_instances, getAccessorArray_fn).call(this, inputAccessor); const outputAccessor = this.gltf.accessors[sampler.output]; const values = __privateMethod(this, _GLTFScenesManager_instances, getAccessorArray_fn).call(this, outputAccessor); const animName = node.name ? `${node.name} animation` : `${channel.target.path} animation ${index}`; const keyframesAnimation = new KeyframesAnimation({ label: animation.name ? `${animation.name} ${animName}` : `Animation ${i} ${animName}`, inputIndex: sampler.input, keyframes, values, path, interpolation: sampler.interpolation }); targetsAnimation.addTargetAnimation(child.node, keyframesAnimation); }); } }); } } /** * Get a clean attribute name based on a glTF attribute name. * @param gltfAttributeName - glTF attribute name. * @returns - Attribute name conform to our expectations. */ static getCleanAttributeName(gltfAttributeName) { return gltfAttributeName === "TEXCOORD_0" ? "uv" : gltfAttributeName.replace("_", "").replace("TEXCOORD", "uv").toLowerCase(); } /** * Sort an array of {@link VertexBufferAttributeParams} by an array of attribute names. * @param attributesNames - array of attribute names to use for sorting. * @param attributes - {@link VertexBufferAttributeParams} array to sort. */ sortAttributesByNames(attributesNames, attributes) { attributes.sort((a, b) => { let aIndex = attributesNames.findIndex((attrName) => attrName === a.name); aIndex = aIndex === -1 ? Infinity : aIndex; let bIndex = attributesNames.findIndex((attrName) => attrName === b.name); bIndex = bIndex === -1 ? Infinity : bIndex; return aIndex - bIndex; }); } /** * Create the mesh {@link Geometry} based on the given {@link gltf} primitive and {@link PrimitiveInstanceDescriptor}. * @param primitive - {@link gltf} primitive to use to create the {@link Geometry}. * @param primitiveInstance - {@link PrimitiveInstanceDescriptor} to use to create the {@link Geometry}. */ createGeometry(primitive, primitiveInstance) { const { instances, meshDescriptor } = primitiveInstance; const geometryBBox = new Box3(); for (const [attribName, accessorIndex] of Object.entries(primitive.attributes)) { if (attribName === "POSITION") { const accessor = this.gltf.accessors[accessorIndex]; if (geometryBBox) { geometryBBox.min.min(new Vec3(accessor.min[0], accessor.min[1], accessor.min[2])); geometryBBox.max.max(new Vec3(accessor.max[0], accessor.max[1], accessor.max[2])); } } } let defaultAttributes = []; let interleavedArray = __privateMethod(this, _GLTFScenesManager_instances, parsePrimitiveProperty_fn).call(this, primitive.attributes, defaultAttributes); const isIndexedGeometry = "indices" in primitive; let indicesArray = null; let indicesConstructor = null; if (isIndexedGeometry) { const accessor = this.gltf.accessors[primitive.indices]; const bufferView = this.gltf.bufferViews[accessor.bufferView]; indicesConstructor = _GLTFScenesManager.getTypedArrayConstructorFromComponentType(accessor.componentType); const bytesPerElement = indicesConstructor.name === "Uint8Array" ? Uint16Array.BYTES_PER_ELEMENT : indicesConstructor.BYTES_PER_ELEMENT; const arrayOffset = accessor.byteOffset + bufferView.byteOffset; const arrayBuffer = this.gltf.arrayBuffers[bufferView.buffer]; const arrayLength = Math.ceil(accessor.count / bytesPerElement) * bytesPerElement; indicesArray = indicesConstructor.name === "Uint8Array" ? Uint16Array.from(new indicesConstructor(arrayBuffer, arrayOffset, arrayLength)) : new indicesConstructor(arrayBuffer, arrayOffset, arrayLength); if (accessor.sparse) { const { indices, values } = __privateMethod(this, _GLTFScenesManager_instances, getSparseAccessorIndicesAndValues_fn).call(this, accessor); for (let i = 0; i < indices.length; i++) { indicesArray[indices[i]] = values[i]; } } } const hasNormal = defaultAttributes.find((attribute) => attribute.name === "normal"); if (!hasNormal) { defaultAttributes = defaultAttributes.filter((attr) => attr.name !== "tangent"); interleavedArray = null; } if (!interleavedArray) { this.sortAttributesByNames(["position", "uv", "normal"], defaultAttributes); } const geometryAttributes = { instancesCount: instances.length, topology: _GLTFScenesManager.gpuPrimitiveTopologyForMode(primitive.mode), vertexBuffers: [ { name: "attributes", stepMode: "vertex", // explicitly set the stepMode even if not mandatory attributes: defaultAttributes, ...interleavedArray && { array: interleavedArray } // interleaved array! } ] }; const GeometryConstructor = isIndexedGeometry ? IndexedGeometry : Geometry; meshDescriptor.parameters.geometry = new GeometryConstructor(geometryAttributes); if (isIndexedGeometry && indicesConstructor && indicesArray) { meshDescriptor.parameters.geometry.setIndexBuffer({ bufferFormat: indicesConstructor.name === "Uint32Array" ? "uint32" : "uint16", array: indicesArray }); } if (!hasNormal) { meshDescriptor.parameters.geometry.computeGeometry(); } meshDescriptor.parameters.geometry.boundingBox = geometryBBox; } /** * Create the {@link SkinDefinition | skins definitions} for each {@link gltf} skins. */ createSkins() { if (this.gltf.skins) { this.gltf.skins.forEach((skin, skinIndex) => { const skinnedMeshNode = this.gltf.nodes.find( (node) => node.skin !== void 0 && node.mesh !== void 0 && node.skin === skinIndex ); const meshIndex = skinnedMeshNode.mesh; let matrices; if (skin.inverseBindMatrices) { const matricesAccessor = this.gltf.accessors[skin.inverseBindMatrices]; matrices = __privateMethod(this, _GLTFScenesManager_instances, getAccessorArray_fn).call(this, matricesAccessor); } else { matrices = new Float32Array(16 * skin.joints.length); for (let i = 0; i < skin.joints.length * 16; i += 16) { matrices[i] = 1; matrices[i + 5] = 1; matrices[i + 10] = 1; matrices[i + 15] = 1; } } const binding = new BufferBinding({ label: "Skin " + skinIndex, name: "skin" + skinIndex, bindingType: "storage", visibility: ["vertex"], childrenBindings: [ { binding: new BufferBinding({ label: "Joints " + skinIndex, name: "joints", bindingType: "storage", visibility: ["vertex"], struct: { jointMatrix: { type: "mat4x4f", value: new Float32Array(16) }, normalMatrix: { type: "mat4x4f", value: new Float32Array(16) } } }), count: skin.joints.length, forceArray: true // needs to be always iterable } ] }); for (let i = 0; i < skin.joints.length; i++) { for (let j = 0; j < 16; j++) { binding.childrenBindings[i].inputs.jointMatrix.value[j] = matrices[i * 16 + j]; binding.childrenBindings[i].inputs.normalMatrix.value[j] = matrices[i * 16 + j]; } binding.childrenBindings[i].inputs.jointMatrix.shouldUpdate = true; binding.childrenBindings[i].inputs.normalMatrix.shouldUpdate = true; } const joints = skin.joints.map((joint) => this.scenesManager.nodes.get(joint)); const jointMatrix = new Mat4(); const normalMatrix = new Mat4(); const parentNodeIndex = this.gltf.nodes.findIndex( (node) => node.mesh !== void 0 && node.skin !== void 0 && node.mesh === meshIndex ); if (parentNodeIndex !== -1) { const parentNode = this.scenesManager.nodes.get(parentNodeIndex); const parentInverseWorldMatrix = new Mat4(); const _updateWorldMatrix = parentNode.updateWorldMatrix.bind(parentNode); parentNode.updateWorldMatrix = (updateParents, updateChildren) => { _updateWorldMatrix(updateParents, updateChildren); parentInverseWorldMatrix.copy(parentNode.worldMatrix).invert(); }; if (this.scenesManager.animations.length) { for (const animation of this.scenesManager.animations) { joints.forEach((object, jointIndex) => { const updateJointMatrix = () => { if (animation.isPlaying) { jointMatrix.setFromArray(matrices, jointIndex * 16).premultiply(object.worldMatrix).premultiply(parentInverseWorldMatrix); } else { jointMatrix.identity(); } normalMatrix.copy(jointMatrix).invert().transpose(); for (let i = 0; i < 16; i++) { binding.childrenBindings[jointIndex].inputs.jointMatrix.value[i] = jointMatrix.elements[i]; binding.childrenBindings[jointIndex].inputs.normalMatrix.value[i] = normalMatrix.elements[i]; } binding.childrenBindings[jointIndex].inputs.jointMatrix.shouldUpdate = true; binding.childrenBindings[jointIndex].inputs.normalMatrix.shouldUpdate = true; }; const node = this.gltf.nodes[jointIndex]; const animName = node.name ? `${node.name} skin animation` : `skin animation ${jointIndex}`; const emptyAnimation = new KeyframesAnimation({ label: animation.label ? `${animation.label} ${animName}` : `Animation ${animName}` }); emptyAnimation.onAfterUpdate = updateJointMatrix; animation.addTargetAnimation(object, emptyAnimation); }); } } else { joints.forEach((object, jointIndex) => { jointMatrix.setFromArray(matrices, jointIndex * 16).premultiply(object.worldMatrix).premultiply(parentInverseWorldMatrix); normalMatrix.copy(jointMatrix).invert().transpose(); for (let i = 0; i < 16; i++) { binding.childrenBindings[jointIndex].inputs.jointMatrix.value[i] = jointMatrix.elements[i]; binding.childrenBindings[jointIndex].inputs.normalMatrix.value[i] = normalMatrix.elements[i]; } binding.childrenBindings[jointIndex].inputs.jointMatrix.shouldUpdate = true; binding.childrenBindings[jointIndex].inputs.normalMatrix.shouldUpdate = true; }); } this.scenesManager.skins.push({ parentNode, joints, inverseBindMatrices: matrices, jointMatrix, normalMatrix, parentInverseWorldMatrix, binding }); } }); } } /** * Create the mesh material parameters based on the given {@link gltf} primitive and {@link PrimitiveInstanceDescriptor}. * @param primitive - {@link gltf} primitive to use to create the material parameters. * @param primitiveInstance - {@link PrimitiveInstanceDescriptor} to use to create the material parameters. */ createMaterial(primitive, primitiveInstance) { const { instances, nodes, meshDescriptor } = primitiveInstance; const instancesCount = instances.length; const meshIndex = instances[0].mesh; if (primitive.targets) { const bindings = []; const weights = this.gltf.meshes[meshIndex].weights; let weightAnimation; for (const animation of this.scenesManager.animations) { weightAnimation = animation.getAnimationByObject3DAndPath(meshDescriptor.parent, "weights"); if (weightAnimation) break; } primitive.targets.forEach((target, index) => { const targetAttributes = []; __privateMethod(this, _GLTFScenesManager_instances, parsePrimitiveProperty_fn).call(this, target, targetAttributes); const struct = targetAttributes.reduce( (acc, attribute) => { return acc = { ...acc, ...{ [attribute.name]: { type: `array<${attribute.type}>`, value: attribute.array } } }; }, { weight: { type: "f32", value: weights && weights.length ? weights[index] : 0 } } ); const targetBinding = new BufferBinding({ label: "Morph target " + index, name: "morphTarget" + index, bindingType: "storage", visibility: ["vertex"], struct }); if (weightAnimation) { weightAnimation.addWeightBindingInput(targetBinding.inputs.weight); } bindings.push(targetBinding); }); if (!meshDescriptor.parameters.bindings) { meshDescriptor.parameters.bindings = []; } meshDescriptor.parameters.bindings = [...meshDescriptor.parameters.bindings, ...bindings]; } if (this.gltf.skins) { this.gltf.skins.forEach((skin, skinIndex) => { if (!meshDescriptor.parameters.bindings) { meshDescriptor.parameters.bindings = []; } instances.forEach((node, instanceIndex) => { if (node.skin !== void 0 && node.skin === skinIndex) { const skinDef = this.scenesManager.skins[skinIndex]; meshDescriptor.parameters.bindings = [...meshDescriptor.parameters.bindings, skinDef.binding]; if (instanceIndex > 0) { const tempBbox = meshDescriptor.parameters.geometry.boundingBox.clone(); const tempMat4 = new Mat4(); skinDef.joints.forEach((object, jointIndex) => { tempMat4.setFromArray(skinDef.inverseBindMatrices, jointIndex * 16); const transformedBbox = tempBbox.applyMat4(tempMat4).applyMat4(object.worldMatrix); this.scenesManager.boundingBox.min.min(transformedBbox.min); this.scenesManager.boundingBox.max.max(transformedBbox.max); }); } } }); }); } const defaultMaterialParams = this.scenesManager.materialsParams[primitive.material]; const materialTextures = this.scenesManager.materialsTextures[primitive.material]; meshDescriptor.texturesDescriptors = materialTextures?.texturesDescriptors || []; meshDescriptor.parameters = { ...meshDescriptor.parameters, ...defaultMaterialParams }; const material = this.gltf.materials && this.gltf.materials[primitive.material] || {}; const { extensions } = material; if (extensions) { for (const extension of Object.keys(extensions)) { if (extension === "KHR_materials_unlit" && this.gltf.extensionsRequired && this.gltf.extensionsRequired.includes(extension)) { meshDescriptor.extensionsUsed.push(extension); } else { meshDescriptor.extensionsUsed.push(extension); } } } const dispersion = extensions && extensions.KHR_materials_dispersion || null; const transmission = extensions && extensions.KHR_materials_transmission || null; const volume = extensions && extensions.KHR_materials_volume || null; const hasTransmission = transmission || volume || dispersion; const useTransmission = this.gltf.extensionsUsed && (this.gltf.extensionsUsed.includes("KHR_materials_transmission") || this.gltf.extensionsUsed.includes("KHR_materials_volume") || this.gltf.extensionsUsed.includes("KHR_materials_dispersion")); if (useTransmission && hasTransmission) { meshDescriptor.parameters.transmissive = true; } if (useTransmission && hasTransmission) { this.renderer.createTransmissionTarget(); meshDescriptor.texturesDescriptors.push({ texture: this.renderer.transmissionTarget.texture, sampler: this.renderer.transmissionTarget.sampler }); } meshDescriptor.parameters.material = { ...meshDescriptor.parameters.material, ...meshDescriptor.texturesDescriptors.reduce((acc, descriptor) => { return { ...acc, [descriptor.texture.options.name]: descriptor }; }, {}) }; if (instancesCount > 1) { const instanceMatricesBinding = new BufferBinding({ label: "Instance matrices", name: "matrices", visibility: ["vertex", "fragment"], bindingType: "storage", struct: { model: { type: "mat4x4f", value: new Mat4() }, normal: { type: "mat3x3f", value: new Mat3() } } }); const instancesBinding = new BufferBinding({ label: "Instances", name: "instances", visibility: ["vertex", "fragment"], bindingType: "storage", childrenBindings: [ { binding: instanceMatricesBinding, count: instancesCount, forceArray: true } ] }); instancesBinding.childrenBindings.forEach((binding, index) => { const instanceNode = nodes[index]; const updateInstanceMatrices = () => { binding.inputs.model.value.copy(instanceNode.worldMatrix); binding.inputs.normal.value.getNormalMatrix(instanceNode.worldMatrix); binding.inputs.model.shouldUpdate = true; binding.inputs.normal.shouldUpdate = true; }; const _updateWorldMatrix = instanceNode.updateWorldMatrix.bind(instanceNode); instanceNode.updateWorldMatrix = () => { _updateWorldMatrix(); updateInstanceMatrices(); }; updateInstanceMatrices(); }); if (!meshDescriptor.parameters.bindings) { meshDescriptor.parameters.bindings = []; } meshDescriptor.parameters.bindings.push(instancesBinding); } for (let i = 0; i < nodes.length; i++) { const tempBbox = meshDescriptor.parameters.geometry.boundingBox.clone(); const transformedBbox = tempBbox.applyMat4(meshDescriptor.nodes[i].worldMatrix); this.scenesManager.boundingBox.min.min(transformedBbox.min); this.scenesManager.boundingBox.max.max(transformedBbox.max); } this.scenesManager.boundingBox.max.max(new Vec3(1e-3)); if (primitive.extensions) { if (primitive.extensions["KHR_materials_variants"] && this.gltf.extensionsUsed && this.gltf.extensionsUsed.includes("KHR_materials_variants")) { meshDescriptor.extensionsUsed.push("KHR_materials_variants"); this.gltf.extensions["KHR_materials_variants"].variants.forEach((variant, index) => { const variantMaterial = primitive.extensions["KHR_materials_variants"].mappings.find( (mapping) => mapping.variants && mapping.variants.includes(index) ); if (variantMaterial) { const gltfVariantMaterial = this.gltf.materials[variantMaterial.material]; const variantMaterialParams = this.scenesManager.materialsParams[variantMaterial.material]; const materialTextures2 = this.scenesManager.materialsTextures[variantMaterial.material]; const