@loaders.gl/gltf
Version:
Framework-independent loader for the glTF format
237 lines • 10.7 kB
JavaScript
/**
* 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