@loaders.gl/gltf
Version:
Framework-independent loader for the glTF format
303 lines (286 loc) • 11.3 kB
text/typescript
/**
* https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_transform/README.md
*/
import {Vector3, Matrix3} from '@math.gl/core';
import type {GLTFWithBuffers} from '../types/gltf-types';
import type {
GLTFMeshPrimitive,
GLTFAccessor,
GLTFBufferView,
GLTFMaterialNormalTextureInfo,
GLTFMaterialOcclusionTextureInfo,
GLTFTextureInfo
} from '../types/gltf-json-schema';
import type {GLTFLoaderOptions} from '../../gltf-loader';
import {getAccessorArrayTypeAndLength} from '../gltf-utils/gltf-utils';
import {BYTES, COMPONENTS} from '../gltf-utils/gltf-constants';
import {} from '../types/gltf-json-schema';
import {GLTFScenegraph} from '../api/gltf-scenegraph';
/** 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();
/** Extension textureInfo https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform#gltf-schema-updates */
type TextureInfo = {
/** The offset of the UV coordinate origin as a factor of the texture dimensions. */
offset?: [number, number];
/** Rotate the UVs by this many radians counter-clockwise around the origin. This is equivalent to a similar rotation of the image clockwise. */
rotation?: number;
/** The scale factor applied to the components of the UV coordinates. */
scale?: [number, number];
/** Overrides the textureInfo texCoord value if supplied, and if this extension is supported. */
texCoord?: number;
};
/** Intersection of all GLTF textures */
type CompoundGLTFTextureInfo = GLTFTextureInfo &
GLTFMaterialNormalTextureInfo &
GLTFMaterialOcclusionTextureInfo;
/** Parameters for TEXCOORD transformation */
type TransformParameters = {
/** Original texCoord value https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#_textureinfo_texcoord */
originalTexCoord: number;
/** New texCoord value from extension https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform#gltf-schema-updates */
texCoord: number;
/** Transformation matrix */
matrix: Matrix3;
};
/**
* The extension entry to process the transformation
* @param gltfData gltf buffers and json
* @param options GLTFLoader options
*/
export async function decode(gltfData: GLTFWithBuffers, options: GLTFLoaderOptions) {
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: number, gltfData: GLTFWithBuffers): void {
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: [number, number][] = [];
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: GLTFWithBuffers,
materialIndex: number,
texture: CompoundGLTFTextureInfo,
processedTexCoords: [number, number][]
) {
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: CompoundGLTFTextureInfo,
processedTexCoords: [number, number][]
): TransformParameters | null {
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: GLTFWithBuffers,
primitive: GLTFMeshPrimitive,
transformParameters: 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) {
// 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, bufferView, gltfData.buffers, result);
} 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 bufferView bufferView to change
* @param buffers binary buffers
* @param newTexcoordArray typed array with data after transformation
*/
function updateGltf(
accessor: GLTFAccessor,
bufferView: GLTFBufferView,
buffers: {arrayBuffer: ArrayBuffer; byteOffset: number; byteLength: number}[],
newTexCoordArray: Float32Array
): void {
accessor.componentType = 5126;
buffers.push({
arrayBuffer: newTexCoordArray.buffer,
byteOffset: 0,
byteLength: newTexCoordArray.buffer.byteLength
});
bufferView.buffer = buffers.length - 1;
bufferView.byteLength = newTexCoordArray.buffer.byteLength;
bufferView.byteOffset = 0;
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: number,
originalAccessor: GLTFAccessor,
primitive: GLTFMeshPrimitive,
gltfData: GLTFWithBuffers,
newTexCoordArray: Float32Array
) {
gltfData.buffers.push({
arrayBuffer: newTexCoordArray.buffer,
byteOffset: 0,
byteLength: newTexCoordArray.buffer.byteLength
});
const bufferViews = gltfData.json.bufferViews;
if (!bufferViews) {
return;
}
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: TextureInfo): Matrix3 {
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);
}