UNPKG

@loaders.gl/gltf

Version:

Framework-independent loader for the glTF format

237 lines 10.7 kB
/** * https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform/README.md */ import { Vector3, Matrix3 } from '@math.gl/core'; import { getAccessorArrayTypeAndLength } from "../gltf-utils/gltf-utils.js"; import { BYTES, COMPONENTS } from "../gltf-utils/gltf-constants.js"; import { GLTFScenegraph } from "../api/gltf-scenegraph.js"; import { ensureArrayBuffer } from '@loaders.gl/loader-utils'; /** Extension name */ const KHR_TEXTURE_TRANSFORM = 'KHR_texture_transform'; export const name = KHR_TEXTURE_TRANSFORM; const scratchVector = new Vector3(); const scratchRotationMatrix = new Matrix3(); const scratchScaleMatrix = new Matrix3(); /** * The extension entry to process the transformation * @param gltfData gltf buffers and json * @param options GLTFLoader options */ export async function decode(gltfData, options) { const gltfScenegraph = new GLTFScenegraph(gltfData); const hasExtension = gltfScenegraph.hasExtension(KHR_TEXTURE_TRANSFORM); if (!hasExtension || !options.gltf?.loadBuffers) { return; } const materials = gltfData.json.materials || []; for (let i = 0; i < materials.length; i++) { transformTexCoords(i, gltfData); } } /** * Transform TEXCOORD by material * @param materialIndex processing material index * @param gltfData gltf buffers and json */ function transformTexCoords(materialIndex, gltfData) { const material = gltfData.json.materials?.[materialIndex]; const materialTextures = [ material?.pbrMetallicRoughness?.baseColorTexture, material?.emissiveTexture, material?.normalTexture, material?.occlusionTexture, material?.pbrMetallicRoughness?.metallicRoughnessTexture ]; // Save processed texCoords in order no to process the same twice const processedTexCoords = []; for (const textureInfo of materialTextures) { if (textureInfo && textureInfo?.extensions?.[KHR_TEXTURE_TRANSFORM]) { transformPrimitives(gltfData, materialIndex, textureInfo, processedTexCoords); } } } /** * Transform primitives of the particular material * @param gltfData gltf data * @param materialIndex primitives with this material will be transformed * @param texture texture object * @param processedTexCoords storage to save already processed texCoords */ function transformPrimitives(gltfData, materialIndex, texture, processedTexCoords) { const transformParameters = getTransformParameters(texture, processedTexCoords); if (!transformParameters) { return; } const meshes = gltfData.json.meshes || []; for (const mesh of meshes) { for (const primitive of mesh.primitives) { const material = primitive.material; if (Number.isFinite(material) && materialIndex === material) { transformPrimitive(gltfData, primitive, transformParameters); } } } } /** * Get parameters for TEXCOORD transformation * @param texture texture object * @param processedTexCoords storage to save already processed texCoords * @returns texCoord couple and transformation matrix */ function getTransformParameters(texture, processedTexCoords) { const textureInfo = texture.extensions?.[KHR_TEXTURE_TRANSFORM]; const { texCoord: originalTexCoord = 0 } = texture; // If texCoord is not set in the extension, original attribute data will be replaced const { texCoord = originalTexCoord } = textureInfo; // Make sure that couple [originalTexCoord, extensionTexCoord] is not processed twice const isProcessed = processedTexCoords.findIndex(([original, newTexCoord]) => original === originalTexCoord && newTexCoord === texCoord) !== -1; if (!isProcessed) { const matrix = makeTransformationMatrix(textureInfo); if (originalTexCoord !== texCoord) { texture.texCoord = texCoord; } processedTexCoords.push([originalTexCoord, texCoord]); return { originalTexCoord, texCoord, matrix }; } return null; } /** * Transform `TEXCOORD_0` attribute in the primitive * @param gltfData gltf data * @param primitive primitive object * @param transformParameters texCoord couple and transformation matrix */ function transformPrimitive(gltfData, primitive, transformParameters) { const { originalTexCoord, texCoord, matrix } = transformParameters; const texCoordAccessor = primitive.attributes[`TEXCOORD_${originalTexCoord}`]; if (Number.isFinite(texCoordAccessor)) { // Get accessor of the `TEXCOORD_0` attribute const accessor = gltfData.json.accessors?.[texCoordAccessor]; if (accessor && accessor.bufferView !== undefined) { // Get `bufferView` of the `accessor` const bufferView = gltfData.json.bufferViews?.[accessor.bufferView]; if (bufferView) { // Get `arrayBuffer` the `bufferView` look at const { arrayBuffer, byteOffset: bufferByteOffset } = gltfData.buffers[bufferView.buffer]; // Resulting byteOffset is sum of the buffer, accessor and bufferView byte offsets const byteOffset = (bufferByteOffset || 0) + (accessor.byteOffset || 0) + (bufferView.byteOffset || 0); // Deduce TypedArray type and its length from `accessor` and `bufferView` data const { ArrayType, length } = getAccessorArrayTypeAndLength(accessor, bufferView); // Number of bytes each component occupies const bytes = BYTES[accessor.componentType]; // Number of components. For the `TEXCOORD_0` with `VEC2` type, it must return 2 const components = COMPONENTS[accessor.type]; // Multiplier to calculate the address of the `TEXCOORD_0` element in the arrayBuffer const elementAddressScale = bufferView.byteStride || bytes * components; // Data transform to Float32Array const result = new Float32Array(length); for (let i = 0; i < accessor.count; i++) { // Take [u, v] couple from the arrayBuffer const uv = new ArrayType(arrayBuffer, byteOffset + i * elementAddressScale, 2); // Set and transform Vector3 per https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform#overview scratchVector.set(uv[0], uv[1], 1); scratchVector.transformByMatrix3(matrix); // Save result in Float32Array result.set([scratchVector[0], scratchVector[1]], i * components); } // If texCoord the same, replace gltf structural data if (originalTexCoord === texCoord) { updateGltf(accessor, gltfData, result, accessor.bufferView); } else { // If texCoord change, create new attribute createAttribute(texCoord, accessor, primitive, gltfData, result); } } } } } /** * Update GLTF structural objects with new data as we create new `Float32Array` for `TEXCOORD_0`. * @param accessor accessor to change * @param gltfData gltf json and buffers * @param newTexcoordArray typed array with data after transformation */ function updateGltf(accessor, gltfData, newTexCoordArray, originalBufferViewIndex) { accessor.componentType = 5126; accessor.byteOffset = 0; const accessors = gltfData.json.accessors || []; const bufferViewReferenceCount = accessors.reduce((count, currentAccessor) => { return currentAccessor.bufferView === originalBufferViewIndex ? count + 1 : count; }, 0); const shouldCreateNewBufferView = bufferViewReferenceCount > 1; gltfData.buffers.push({ arrayBuffer: ensureArrayBuffer(newTexCoordArray.buffer), byteOffset: 0, byteLength: newTexCoordArray.buffer.byteLength }); const newBufferIndex = gltfData.buffers.length - 1; gltfData.json.bufferViews = gltfData.json.bufferViews || []; if (shouldCreateNewBufferView) { gltfData.json.bufferViews.push({ buffer: newBufferIndex, byteLength: newTexCoordArray.buffer.byteLength, byteOffset: 0 }); accessor.bufferView = gltfData.json.bufferViews.length - 1; return; } const bufferView = gltfData.json.bufferViews[originalBufferViewIndex]; if (!bufferView) { return; } bufferView.buffer = newBufferIndex; bufferView.byteOffset = 0; bufferView.byteLength = newTexCoordArray.buffer.byteLength; if (bufferView.byteStride !== undefined) { delete bufferView.byteStride; } } /** * * @param newTexCoord new `texCoord` value * @param originalAccessor original accessor object, that store data before transformation * @param primitive primitive object * @param gltfData gltf data * @param newTexCoordArray typed array with data after transformation * @returns */ function createAttribute(newTexCoord, originalAccessor, primitive, gltfData, newTexCoordArray) { gltfData.buffers.push({ arrayBuffer: ensureArrayBuffer(newTexCoordArray.buffer), byteOffset: 0, byteLength: newTexCoordArray.buffer.byteLength }); gltfData.json.bufferViews = gltfData.json.bufferViews || []; const bufferViews = gltfData.json.bufferViews; bufferViews.push({ buffer: gltfData.buffers.length - 1, byteLength: newTexCoordArray.buffer.byteLength, byteOffset: 0 }); const accessors = gltfData.json.accessors; if (!accessors) { return; } accessors.push({ bufferView: bufferViews?.length - 1, byteOffset: 0, componentType: 5126, count: originalAccessor.count, type: 'VEC2' }); primitive.attributes[`TEXCOORD_${newTexCoord}`] = accessors.length - 1; } /** * Construct transformation matrix from the extension data (transition, rotation, scale) * @param extensionData extension data * @returns transformation matrix */ function makeTransformationMatrix(extensionData) { const { offset = [0, 0], rotation = 0, scale = [1, 1] } = extensionData; const translationMatrix = new Matrix3().set(1, 0, 0, 0, 1, 0, offset[0], offset[1], 1); const rotationMatrix = scratchRotationMatrix.set(Math.cos(rotation), Math.sin(rotation), 0, -Math.sin(rotation), Math.cos(rotation), 0, 0, 0, 1); const scaleMatrix = scratchScaleMatrix.set(scale[0], 0, 0, 0, scale[1], 0, 0, 0, 1); return translationMatrix.multiplyRight(rotationMatrix).multiplyRight(scaleMatrix); } //# sourceMappingURL=KHR_texture_transform.js.map