UNPKG

pex-renderer

Version:

Physically Based Renderer for Pex

1,024 lines (892 loc) 31.6 kB
const path = require('path') const { loadJSON, loadImage, loadBinary } = require('pex-io') const { quat, mat4, utils } = require('pex-math') // Constants // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#specifying-extensions const SUPPORTED_EXTENSIONS = [ 'KHR_materials_unlit', 'KHR_materials_pbrSpecularGlossiness', 'KHR_texture_transform' ] const WEBGL_CONSTANTS = { // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants#Buffers ELEMENT_ARRAY_BUFFER: 34963, // 0x8893 ARRAY_BUFFER: 34962, // 0x8892 // https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants#Data_types BYTE: 5120, // 0x1400 UNSIGNED_BYTE: 5121, // 0x1401 SHORT: 5122, // 0x1402 UNSIGNED_SHORT: 5123, // 0x1403 UNSIGNED_INT: 5125, // 0x1405 FLOAT: 5126 // 0x1406 } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays#Typed_array_views const WEBGL_TYPED_ARRAY_BY_COMPONENT_TYPES = { [WEBGL_CONSTANTS.BYTE]: Int8Array, [WEBGL_CONSTANTS.UNSIGNED_BYTE]: Uint8Array, [WEBGL_CONSTANTS.SHORT]: Int16Array, [WEBGL_CONSTANTS.UNSIGNED_SHORT]: Uint16Array, [WEBGL_CONSTANTS.UNSIGNED_INT]: Uint32Array, [WEBGL_CONSTANTS.FLOAT]: Float32Array } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessor-element-size const GLTF_ACCESSOR_COMPONENT_TYPE_SIZE = { [WEBGL_CONSTANTS.BYTE]: 1, [WEBGL_CONSTANTS.UNSIGNED_BYTE]: 1, [WEBGL_CONSTANTS.SHORT]: 2, [WEBGL_CONSTANTS.UNSIGNED_SHORT]: 2, [WEBGL_CONSTANTS.UNSIGNED_INT]: 4, [WEBGL_CONSTANTS.FLOAT]: 4 } const GLTF_ACCESSOR_TYPE_COMPONENTS_NUMBER = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#header const MAGIC = 0x46546c67 // glTF // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#chunks const CHUNK_TYPE = { JSON: 0x4e4f534a, BIN: 0x004e4942 } const PEX_ATTRIBUTE_NAME_MAP = { POSITION: 'positions', NORMAL: 'normals', TANGENT: 'tangents', TEXCOORD_0: 'texCoords', TEXCOORD_1: 'texCoords1', JOINTS_0: 'joints', WEIGHTS_0: 'weights', COLOR_0: 'vertexColors' } function linearToSrgb(color) { return [ Math.pow(color[0], 1.0 / 2.2), Math.pow(color[1], 1.0 / 2.2), Math.pow(color[2], 1.0 / 2.2), color.length == 4 ? color[3] : 1 ] } // Build // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/accessor.schema.json function getAccessor(accessor, bufferViews) { if (accessor._data) return accessor const numberOfComponents = GLTF_ACCESSOR_TYPE_COMPONENTS_NUMBER[accessor.type] if (accessor.byteOffset === undefined) accessor.byteOffset = 0 accessor._bufferView = bufferViews[accessor.bufferView] const TypedArrayConstructor = WEBGL_TYPED_ARRAY_BY_COMPONENT_TYPES[accessor.componentType] const byteSize = GLTF_ACCESSOR_COMPONENT_TYPE_SIZE[accessor.componentType] const data = new TypedArrayConstructor( accessor._bufferView._data.slice( accessor.byteOffset, accessor.byteOffset + accessor.count * numberOfComponents * byteSize ) ) accessor._data = data // Sparse accessors // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/accessor.sparse.schema.json if (accessor.sparse !== undefined) { const TypedArrayIndicesConstructor = WEBGL_TYPED_ARRAY_BY_COMPONENT_TYPES[ accessor.sparse.indices.componentType ] // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/accessor.sparse.indices.schema.json const sparseIndices = new TypedArrayIndicesConstructor( bufferViews[accessor.sparse.indices.bufferView]._data, accessor.sparse.indices.byteOffset || 0, accessor.sparse.count ) // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/accessor.sparse.values.schema.json const sparseValues = new TypedArrayConstructor( bufferViews[accessor.sparse.values.bufferView]._data, accessor.sparse.values.byteOffset || 0, accessor.sparse.count * numberOfComponents ) if (accessor._data !== null) { accessor._data = accessor._data.slice() } let valuesIndex = 0 for ( let indicesIndex = 0; indicesIndex < sparseIndices.length; indicesIndex++ ) { let dataIndex = sparseIndices[indicesIndex] * numberOfComponents for ( let componentIndex = 0; componentIndex < numberOfComponents; componentIndex++ ) { accessor._data[dataIndex++] = sparseValues[valuesIndex++] } } } return accessor } function getPexMaterialTexture(materialTexture, gltf, ctx, encoding) { // Retrieve glTF root object properties // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/texture.schema.json const texture = gltf.textures[materialTexture.index] // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/image.schema.json const image = gltf.images[texture.source] // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/sampler.schema.json const sampler = gltf.samplers && gltf.samplers[texture.sampler] ? gltf.samplers[texture.sampler] : {} sampler.minFilter = sampler.minFilter || ctx.Filter.LinearMipmapLinear sampler.magFilter = sampler.magFilter || ctx.Filter.Linear sampler.wrapS = sampler.wrapS || ctx.Wrap.Repeat sampler.wrapT = sampler.wrapT || ctx.Wrap.Repeat const hasMipMap = sampler.minFilter !== ctx.Filter.Nearest && sampler.minFilter !== ctx.Filter.Linear if (!texture._tex) { let img = image._img if (!utils.isPowerOfTwo(img.width) || !utils.isPowerOfTwo(img.height)) { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#samplers if ( sampler.wrapS !== ctx.Wrap.Clamp || sampler.wrapT !== ctx.Wrap.Clamp || hasMipMap ) { const canvas2d = document.createElement('canvas') canvas2d.width = utils.nextPowerOfTwo(img.width) canvas2d.height = utils.nextPowerOfTwo(img.height) const ctx2d = canvas2d.getContext('2d') ctx2d.drawImage(img, 0, 0, canvas2d.width, canvas2d.height) img = canvas2d } } texture._tex = ctx.texture2D({ data: img, width: img.width, height: img.height, encoding: encoding || ctx.Encoding.Linear, pixelFormat: ctx.PixelFormat.RGBA8, wrapS: sampler.wrapS, wrapT: sampler.wrapT, min: sampler.minFilter, mag: sampler.magFilter }) if (hasMipMap) ctx.update(texture._tex, { mipmap: true, aniso: 16 }) } // https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_texture_transform/schema/KHR_texture_transform.textureInfo.schema.json const textureTransform = materialTexture.extensions && materialTexture.extensions.KHR_texture_transform // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/textureInfo.schema.json const texCoord = materialTexture.texCoord return !texCoord && !textureTransform ? texture._tex : { texture: texture._tex, // textureInfo texCoord: texCoord || 0, // textureTransform.texCoord: Overrides the textureInfo texCoord value if supplied. ...(textureTransform || {}) } } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/material.schema.json function handleMaterial(material, gltf, ctx) { let materialProps = { baseColor: [1, 1, 1, 1], roughness: 1, metallic: 1, castShadows: true, receiveShadows: true, cullFace: !material.doubleSided } // Metallic/Roughness workflow // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/material.pbrMetallicRoughness.schema.json const pbrMetallicRoughness = material.pbrMetallicRoughness if (pbrMetallicRoughness) { materialProps = { ...materialProps, baseColor: [1, 1, 1, 1], roughness: 1, metallic: 1 } if (pbrMetallicRoughness.baseColorFactor) { materialProps.baseColor = linearToSrgb( pbrMetallicRoughness.baseColorFactor ) } if (pbrMetallicRoughness.baseColorTexture) { materialProps.baseColorMap = getPexMaterialTexture( pbrMetallicRoughness.baseColorTexture, gltf, ctx, ctx.Encoding.SRGB ) } if (pbrMetallicRoughness.metallicFactor !== undefined) { materialProps.metallic = pbrMetallicRoughness.metallicFactor } if (pbrMetallicRoughness.roughnessFactor !== undefined) { materialProps.roughness = pbrMetallicRoughness.roughnessFactor } if (pbrMetallicRoughness.metallicRoughnessTexture) { materialProps.metallicRoughnessMap = getPexMaterialTexture( pbrMetallicRoughness.metallicRoughnessTexture, gltf, ctx ) } } // Specular/Glossiness workflow // https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness/schema/glTF.KHR_materials_pbrSpecularGlossiness.schema.json const pbrSpecularGlossiness = material.extensions ? material.extensions.KHR_materials_pbrSpecularGlossiness : null if (pbrSpecularGlossiness) { materialProps = { ...materialProps, useSpecularGlossinessWorkflow: true, diffuse: [1, 1, 1, 1], specular: [1, 1, 1], glossiness: 1 } if (pbrSpecularGlossiness.diffuseFactor) { materialProps.diffuse = linearToSrgb(pbrSpecularGlossiness.diffuseFactor) } if (pbrSpecularGlossiness.specularFactor) { materialProps.specular = linearToSrgb( pbrSpecularGlossiness.specularFactor ) } if (pbrSpecularGlossiness.glossinessFactor !== undefined) { materialProps.glossiness = pbrSpecularGlossiness.glossinessFactor } if (pbrSpecularGlossiness.diffuseTexture) { materialProps.diffuseMap = getPexMaterialTexture( pbrSpecularGlossiness.diffuseTexture, gltf, ctx, ctx.Encoding.SRGB ) } if (pbrSpecularGlossiness.specularGlossinessTexture) { materialProps.specularGlossinessMap = getPexMaterialTexture( pbrSpecularGlossiness.specularGlossinessTexture, gltf, ctx, ctx.Encoding.SRGB ) } } // Additional Maps // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/material.normalTextureInfo.schema.json if (material.normalTexture) { materialProps.normalMap = getPexMaterialTexture( material.normalTexture, gltf, ctx ) } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/material.occlusionTextureInfo.schema.json if (material.occlusionTexture) { materialProps.occlusionMap = getPexMaterialTexture( material.occlusionTexture, gltf, ctx ) } if (material.emissiveTexture) { materialProps.emissiveColorMap = getPexMaterialTexture( material.emissiveTexture, gltf, ctx, ctx.Encoding.SRGB ) } if (material.emissiveFactor) { materialProps = { ...materialProps, emissiveColor: linearToSrgb(material.emissiveFactor) } } // Alpha Coverage if (material.alphaMode === 'BLEND') { materialProps = { ...materialProps, depthWrite: false, blend: true, blendSrcRGBFactor: ctx.BlendFactor.SrcAlpha, blendSrcAlphaFactor: ctx.BlendFactor.One, blendDstRGBFactor: ctx.BlendFactor.OneMinusSrcAlpha, blendDstAlphaFactor: ctx.BlendFactor.One } } if (material.alphaMode === 'MASK') { materialProps.alphaTest = material.alphaCutoff || 0.5 } // KHR_materials_unlit if (material.extensions && material.extensions.KHR_materials_unlit) { materialProps = { ...materialProps, unlit: true } } return materialProps } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/mesh.primitive.schema.json function handlePrimitive(primitive, gltf, ctx) { let geometryProps = {} // Format attributes for pex-context const attributes = Object.keys(primitive.attributes).reduce( (attributes, name) => { const attributeName = PEX_ATTRIBUTE_NAME_MAP[name] if (!attributeName) console.warn(`glTF Loader: Unknown attribute '${name}'`) const accessor = getAccessor( gltf.accessors[primitive.attributes[name]], gltf.bufferViews ) if (accessor.sparse) { attributes[attributeName] = accessor._data } else { if (!accessor._bufferView._vertexBuffer) { accessor._bufferView._vertexBuffer = ctx.vertexBuffer( accessor._bufferView._data ) } attributes[attributeName] = { buffer: accessor._bufferView._vertexBuffer, offset: accessor.byteOffset, type: accessor.componentType, stride: accessor._bufferView.byteStride } } return attributes }, {} ) const positionAccessor = gltf.accessors[primitive.attributes.POSITION] const indicesAccessor = gltf.accessors[primitive.indices] && getAccessor(gltf.accessors[primitive.indices], gltf.bufferViews) // Create geometry geometryProps = { ...geometryProps, ...attributes, bounds: [positionAccessor.min, positionAccessor.max] } if (indicesAccessor) { if (!indicesAccessor._bufferView._indexBuffer) { indicesAccessor._bufferView._indexBuffer = ctx.indexBuffer( indicesAccessor._bufferView._data ) } geometryProps = { ...geometryProps, indices: { buffer: indicesAccessor._bufferView._indexBuffer, offset: indicesAccessor.byteOffset, type: indicesAccessor.componentType }, count: indicesAccessor.count } } else { geometryProps = { ...geometryProps, count: positionAccessor._data.length / 3 } } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#primitivemode if (primitive.mode) { geometryProps = { ...geometryProps, primitive: primitive.mode } } return geometryProps } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/mesh.schema.json function handleMesh(mesh, gltf, ctx, renderer) { return mesh.primitives.map((primitive) => { const geometryCmp = renderer.geometry( handlePrimitive(primitive, gltf, ctx, renderer) ) const materialCmp = primitive.material !== undefined ? renderer.material( handleMaterial( gltf.materials[primitive.material], gltf, ctx, renderer ) ) : renderer.material() const components = [geometryCmp, materialCmp] // Create morph if (primitive.targets) { let sources = {} const targets = primitive.targets.reduce((targets, target) => { const targetKeys = Object.keys(target) targetKeys.forEach((targetKey) => { const targetName = PEX_ATTRIBUTE_NAME_MAP[targetKey] || targetKey targets[targetName] = targets[targetName] || [] const accessor = getAccessor( gltf.accessors[target[targetKey]], gltf.bufferViews ) targets[targetName].push(accessor._data) if (!sources[targetName]) { const sourceAccessor = getAccessor( gltf.accessors[primitive.attributes[targetKey]], gltf.bufferViews ) sources[targetName] = sourceAccessor._data } }) return targets }, {}) const morphCmp = renderer.morph({ sources, targets, weights: mesh.weights }) components.push(morphCmp) } return components }) } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/node.schema.json function handleNode(node, gltf, i, ctx, renderer, options) { const components = [] let transform if (node.matrix) { const mn = mat4.create() const scale = [ Math.hypot(node.matrix[0], node.matrix[1], node.matrix[2]), Math.hypot(node.matrix[4], node.matrix[5], node.matrix[6]), Math.hypot(node.matrix[8], node.matrix[9], node.matrix[10]) ] for (const col of [0, 1, 2]) { mn[col] = node.matrix[col] / scale[0] mn[col + 4] = node.matrix[col + 4] / scale[1] mn[col + 8] = node.matrix[col + 8] / scale[2] } transform = { position: [node.matrix[12], node.matrix[13], node.matrix[14]], rotation: quat.fromMat4(quat.create(), mn), scale } } else { transform = { position: node.translation || [0, 0, 0], rotation: node.rotation || [0, 0, 0, 1], scale: node.scale || [1, 1, 1] } } components.push(renderer.transform(transform)) if (options.includeCameras && Number.isInteger(node.camera)) { const camera = gltf.cameras[node.camera] const enabled = options.enabledCameras.includes(node.camera) // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/camera.schema.json if (camera.type === 'orthographic') { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/camera.orthographic.schema.json components.push( renderer.camera({ enabled, name: camera.name || `camera_${node.camera}`, projection: 'orthographic', near: camera.orthographic.znear, far: camera.orthographic.zfar, left: -camera.orthographic.xmag / 2, right: camera.orthographic.xmag / 2, top: camera.orthographic.ymag / 2, bottom: camera.orthographic.ymag / 2 }) ) } else { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/camera.perspective.schema.json components.push( renderer.camera({ enabled, name: camera.name || `camera_${node.camera}`, near: camera.perspective.znear, far: camera.perspective.zfar || Infinity, fov: camera.perspective.yfov, aspect: camera.perspective.aspectRatio || ctx.gl.drawingBufferWidth / ctx.gl.drawingBufferHeight }) ) } } node.entity = renderer.entity(components) node.entity.name = node.name || `node_${i}` // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/skin.schema.json let skinCmp = null if (Number.isInteger(node.skin)) { const skin = gltf.skins[node.skin] const accessor = getAccessor( gltf.accessors[skin.inverseBindMatrices], gltf.bufferViews ) let inverseBindMatrices = [] for (let i = 0; i < accessor._data.length; i += 16) { inverseBindMatrices.push(accessor._data.slice(i, i + 16)) } skinCmp = renderer.skin({ inverseBindMatrices: inverseBindMatrices }) } if (Number.isInteger(node.mesh)) { const primitives = handleMesh(gltf.meshes[node.mesh], gltf, ctx, renderer) if (primitives.length === 1) { primitives[0].forEach((component) => { node.entity.addComponent(component) }) if (skinCmp) node.entity.addComponent(skinCmp) return node.entity } else { // create sub nodes for each primitive const primitiveNodes = primitives.map((components, j) => { const subEntity = renderer.entity(components) subEntity.name = `node_${i}_${j}` subEntity.transform.set({ parent: node.entity.transform }) // TODO: should skin component be shared? if (skinCmp) subEntity.addComponent(skinCmp) return subEntity }) const nodes = [node.entity].concat(primitiveNodes) return nodes } } return node.entity } function handleAnimation(animation, gltf, renderer) { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/animation.schema.json // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/animation.channel.schema.json const channels = animation.channels.map((channel) => { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/animation.sampler.schema.json const sampler = animation.samplers[channel.sampler] const input = getAccessor(gltf.accessors[sampler.input], gltf.bufferViews) const output = getAccessor(gltf.accessors[sampler.output], gltf.bufferViews) // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/animation.channel.target.schema.json const target = gltf.nodes[channel.target.node].entity const outputData = [] const od = output._data let offset = GLTF_ACCESSOR_TYPE_COMPONENTS_NUMBER[output.type] if (channel.target.path === 'weights') { offset = target.getComponent('Morph').weights.length } for (let i = 0; i < od.length; i += offset) { if (offset === 1) { outputData.push([od[i]]) } if (offset === 2) { outputData.push([od[i], od[i + 1]]) } if (offset === 3) { outputData.push([od[i], od[i + 1], od[i + 2]]) } if (offset === 4) { outputData.push([od[i], od[i + 1], od[i + 2], od[i + 3]]) } } return { input: input._data, output: outputData, interpolation: sampler.interpolation, target: target, path: channel.target.path } }) return renderer.animation({ channels: channels, autoplay: true, loop: true }) } // LOADER // ============================================================================= function uint8ArrayToArrayBuffer(arr) { return arr.buffer.slice(arr.byteOffset, arr.byteLength + arr.byteOffset) } class BinaryReader { constructor(arrayBuffer) { this._arrayBuffer = arrayBuffer this._dataView = new DataView(arrayBuffer) this._byteOffset = 0 } getPosition() { return this._byteOffset } getLength() { return this._arrayBuffer.byteLength } readUint32() { const value = this._dataView.getUint32(this._byteOffset, true) this._byteOffset += 4 return value } readUint8Array(length) { const value = new Uint8Array(this._arrayBuffer, this._byteOffset, length) this._byteOffset += length return value } skipBytes(length) { this._byteOffset += length } } function unpackBinary(data) { const binaryReader = new BinaryReader(data) // Check header // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#header // uint32 magic // uint32 version // uint32 length const magic = binaryReader.readUint32() if (magic !== MAGIC) throw new Error(`Unexpected magic: ${magic}`) const version = binaryReader.readUint32() if (version !== 2) throw new Error(`Unsupported version: ${version} `) const length = binaryReader.readUint32() if (length !== binaryReader.getLength()) { throw new Error( `Length in header does not match actual data length: ${length} != ${binaryReader.getLength()}` ) } // Decode chunks // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#chunks // uint32 chunkLength // uint32 chunkType // ubyte[] chunkData // JSON const chunkLength = binaryReader.readUint32() const chunkType = binaryReader.readUint32() if (chunkType !== CHUNK_TYPE.JSON) throw new Error('First chunk format is not JSON') // Decode Buffer to Text const buffer = binaryReader.readUint8Array(chunkLength) let json if (typeof TextDecoder !== 'undefined') { json = new TextDecoder().decode(buffer) } else { let result = '' const length = buffer.byteLength for (let i = 0; i < length; i++) { result += String.fromCharCode(buffer[i]) } json = result } // BIN let bin = null while (binaryReader.getPosition() < binaryReader.getLength()) { const chunkLength = binaryReader.readUint32() const chunkType = binaryReader.readUint32() switch (chunkType) { case CHUNK_TYPE.JSON: { throw new Error('Unexpected JSON chunk') } case CHUNK_TYPE.BIN: { bin = binaryReader.readUint8Array(chunkLength) break } default: { binaryReader.skipBytes(chunkLength) break } } } return { json: json, bin: bin } } function loadData(data) { if (data instanceof ArrayBuffer) { const unpacked = unpackBinary(data) return { json: JSON.parse(unpacked.json), bin: uint8ArrayToArrayBuffer(unpacked.bin) } } return { json: data } } function isBase64(uri) { return uri.length < 5 ? false : uri.substr(0, 5) === 'data:' } function decodeBase64(uri) { const decodedString = atob(uri.split(',')[1]) const bufferLength = decodedString.length const bufferView = new Uint8Array(new ArrayBuffer(bufferLength)) for (let i = 0; i < bufferLength; i++) { bufferView[i] = decodedString.charCodeAt(i) } return bufferView.buffer } const DEFAULT_OPTIONS = { enabledCameras: [0], enabledScene: undefined, includeCameras: false, includeLights: false } async function loadGltf(url, renderer, options = {}) { const opts = Object.assign({}, DEFAULT_OPTIONS, options) const ctx = renderer._ctx // Load and unpack data // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification const extension = path.extname(url) const basePath = path.dirname(url) const isBinary = extension === '.glb' // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/glTF.schema.json const { json, bin } = loadData( isBinary ? await loadBinary(url) : await loadJSON(url) ) // Check required extensions // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#specifying-extensions if (json.extensionsRequired) { const unsupportedExtensions = json.extensionsRequired.filter( (extension) => !SUPPORTED_EXTENSIONS.includes(extension) ) if (unsupportedExtensions.length) { console.warn('glTF loader: unsupported extensions', unsupportedExtensions) } } // Check asset version // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/asset.schema.json const version = parseInt(json.asset.version) if (!version || version < 2) { console.warn( `glTF Loader: Invalid or unsupported version: ${json.asset.version}` ) } // Data setup // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#binary-data-storage // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/buffer.schema.json for (let buffer of json.buffers) { if (isBinary) { buffer._data = bin } else { if (isBase64(buffer.uri)) { buffer._data = decodeBase64(buffer.uri) } else { buffer._data = await loadBinary([basePath, buffer.uri].join('/')) } } } // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/bufferView.schema.json for (let bufferView of json.bufferViews) { const bufferData = json.buffers[bufferView.buffer]._data if (bufferView.byteOffset === undefined) bufferView.byteOffset = 0 bufferView._data = bufferData.slice( bufferView.byteOffset, bufferView.byteOffset + bufferView.byteLength ) // Set buffer if target is present // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#bufferviewtarget if (bufferView.target === WEBGL_CONSTANTS.ELEMENT_ARRAY_BUFFER) { bufferView._indexBuffer = ctx.indexBuffer(bufferView._data) } else if (bufferView.target === WEBGL_CONSTANTS.ARRAY_BUFFER) { bufferView._vertexBuffer = ctx.vertexBuffer(bufferView._data) } } // Load images // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/image.schema.json if (json.images) { for (let image of json.images) { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#uris if (isBinary || image.bufferView) { const bufferView = json.bufferViews[image.bufferView] bufferView.byteOffset = bufferView.byteOffset || 0 const buffer = json.buffers[bufferView.buffer] const data = buffer._data.slice( bufferView.byteOffset, bufferView.byteOffset + bufferView.byteLength ) const blob = new Blob([data], { type: image.mimeType }) const uri = URL.createObjectURL(blob) image._img = await loadImage({ url: uri, crossOrigin: 'anonymous' }) } else if (isBase64(image.uri)) { image._img = await loadImage({ url: image.uri, crossOrigin: 'anonymous' }) } else { // TODO why are we replacing uri encoded spaces? image._img = await loadImage({ url: [basePath, image.uri].join('/').replace(/%/g, '%25'), crossOrigin: 'anonymous' }) } } } // Load scene // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/schema/scene.schema.json let scenes = (json.scenes || [{}]).map((scene, index) => { // Create scene root entity scene.root = renderer.entity([ renderer.transform({ enabled: opts.enabledScene || index === (json.scene || 0) }) ]) scene.root.name = scene.name || `scene_${index}` // Add scene entities for each node and its children // TODO: scene.entities is just convenience. We could use a user-friendly entity traverse. scene.entities = json.nodes.reduce((entities, node, i) => { const result = handleNode(node, json, i, ctx, renderer, opts) if (result.length) { result.forEach((primitive) => entities.push(primitive)) } else { entities.push(result) } return entities }, []) // Build pex-renderer hierarchy json.nodes.forEach((node, index) => { const parentNode = json.nodes[index] const parentTransform = parentNode.entity.transform // Default to scene root if (!parentNode.entity.transform.parent) { parentNode.entity.transform.set({ parent: scene.root.transform }) } if (node.children) { node.children.forEach((childIndex) => { const child = json.nodes[childIndex] const childTransform = child.entity.transform childTransform.set({ parent: parentTransform }) }) } }) json.nodes.forEach((node) => { if (node.skin !== undefined) { const skin = json.skins[node.skin] const joints = skin.joints.map((i) => json.nodes[i].entity) if (json.meshes[node.mesh].primitives.length === 1) { node.entity.getComponent('Skin').set({ joints: joints }) } else { node.entity.transform.children.forEach((child) => { // FIXME: currently we share the same Skin component // so this code is redundant after first child child.entity.getComponent('Skin').set({ joints: joints }) }) } } }) if (json.animations) { json.animations.forEach((animation) => { const animationComponent = handleAnimation(animation, json, renderer) scene.root.addComponent(animationComponent) }) } renderer.update() return scene }) return scenes } module.exports = loadGltf