@gltf-transform/functions
Advanced tools
Comparing version 3.7.5 to 3.8.0
import type { Transform } from '@gltf-transform/core'; | ||
export interface MeshoptOptions { | ||
import { QuantizeOptions } from './quantize.js'; | ||
export interface MeshoptOptions extends Omit<QuantizeOptions, 'pattern' | 'patternTargets'> { | ||
encoder: unknown; | ||
@@ -4,0 +5,0 @@ level?: 'medium' | 'high'; |
@@ -9,2 +9,4 @@ import { Transform } from '@gltf-transform/core'; | ||
keepAttributes?: boolean; | ||
/** Whether to keep single-color textures that can be converted to material factors. */ | ||
keepSolidTextures?: boolean; | ||
} | ||
@@ -11,0 +13,0 @@ /** |
@@ -6,2 +6,4 @@ import { Transform } from '@gltf-transform/core'; | ||
pattern?: RegExp; | ||
/** Pattern (regex) used to filter morph target semantics for quantization. Default: `options.pattern`. */ | ||
patternTargets?: RegExp; | ||
/** Bounds for quantization grid. */ | ||
@@ -24,3 +26,3 @@ quantizationVolume?: 'mesh' | 'scene'; | ||
} | ||
export declare const QUANTIZE_DEFAULTS: Required<QuantizeOptions>; | ||
export declare const QUANTIZE_DEFAULTS: Required<Omit<QuantizeOptions, 'patternTargets'>>; | ||
/** | ||
@@ -27,0 +29,0 @@ * References: |
import { Texture, Transform, vec2 } from '@gltf-transform/core'; | ||
import { TextureResizeFilter } from './texture-resize.js'; | ||
type Format = (typeof FORMATS)[number]; | ||
declare const FORMATS: readonly ["jpeg", "png", "webp", "avif"]; | ||
type Format = (typeof TEXTURE_COMPRESS_SUPPORTED_FORMATS)[number]; | ||
export declare const TEXTURE_COMPRESS_SUPPORTED_FORMATS: readonly ["jpeg", "png", "webp", "avif"]; | ||
export interface TextureCompressOptions { | ||
@@ -27,5 +27,12 @@ /** Instance of the Sharp encoder, which must be installed from the | ||
pattern?: RegExp | null; | ||
/** Pattern matching the format(s) to be compressed or converted. */ | ||
/** | ||
* Pattern matching the format(s) to be compressed or converted. Some examples | ||
* of formats include "jpeg" and "png". | ||
*/ | ||
formats?: RegExp | null; | ||
/** Pattern matching the material texture slot(s) to be compressed or converted. */ | ||
/** | ||
* Pattern matching the material texture slot(s) to be compressed or converted. | ||
* Some examples of slot names include "baseColorTexture", "occlusionTexture", | ||
* "metallicRoughnessTexture", and "normalTexture". | ||
*/ | ||
slots?: RegExp | null; | ||
@@ -32,0 +39,0 @@ /** Quality, 1-100. Default: auto. */ |
{ | ||
"name": "@gltf-transform/functions", | ||
"version": "3.7.5", | ||
"version": "3.8.0", | ||
"repository": "github:donmccurdy/glTF-Transform", | ||
@@ -39,4 +39,4 @@ "homepage": "https://gltf-transform.dev/functions.html", | ||
"dependencies": { | ||
"@gltf-transform/core": "^3.7.5", | ||
"@gltf-transform/extensions": "^3.7.5", | ||
"@gltf-transform/core": "^3.8.0", | ||
"@gltf-transform/extensions": "^3.8.0", | ||
"ktx-parse": "^0.6.0", | ||
@@ -57,3 +57,3 @@ "ndarray": "^1.0.19", | ||
}, | ||
"gitHead": "bf05150bcaa3df7add9c86ba8ffaffae3fdd94a7" | ||
"gitHead": "94ea01f4c998c85cb25d56a057f7326d36a81786" | ||
} |
@@ -5,6 +5,6 @@ import type { Document, Transform } from '@gltf-transform/core'; | ||
import { reorder } from './reorder.js'; | ||
import { quantize } from './quantize.js'; | ||
import { QUANTIZE_DEFAULTS, QuantizeOptions, quantize } from './quantize.js'; | ||
import { createTransform } from './utils.js'; | ||
export interface MeshoptOptions { | ||
export interface MeshoptOptions extends Omit<QuantizeOptions, 'pattern' | 'patternTargets'> { | ||
encoder: unknown; | ||
@@ -14,3 +14,6 @@ level?: 'medium' | 'high'; | ||
export const MESHOPT_DEFAULTS: Required<Omit<MeshoptOptions, 'encoder'>> = { level: 'high' }; | ||
export const MESHOPT_DEFAULTS: Required<Omit<MeshoptOptions, 'encoder'>> = { | ||
level: 'high', | ||
...QUANTIZE_DEFAULTS, | ||
}; | ||
@@ -53,2 +56,20 @@ const NAME = 'meshopt'; | ||
return createTransform(NAME, async (document: Document): Promise<void> => { | ||
let pattern: RegExp; | ||
let patternTargets: RegExp; | ||
let quantizeNormal = options.quantizeNormal; | ||
// IMPORTANT: Vertex attributes should be quantized in 'high' mode IFF they are | ||
// _not_ filtered in 'packages/extensions/src/ext-meshopt-compression/encoder.ts'. | ||
// Note that normals and tangents use octahedral filters, but _morph_ normals | ||
// and tangents do not. | ||
// See: https://github.com/donmccurdy/glTF-Transform/issues/1142 | ||
if (options.level === 'medium') { | ||
pattern = /.*/; | ||
patternTargets = /.*/; | ||
} else { | ||
pattern = /^(POSITION|TEXCOORD|JOINTS|WEIGHTS)(_\d+)?$/; | ||
patternTargets = /^(POSITION|TEXCOORD|JOINTS|WEIGHTS|NORMAL|TANGENT)(_\d+)?$/; | ||
quantizeNormal = Math.min(quantizeNormal, 8); // See meshopt::getMeshoptFilter. | ||
} | ||
await document.transform( | ||
@@ -60,10 +81,7 @@ reorder({ | ||
quantize({ | ||
// IMPORTANT: Vertex attributes should be quantized in 'high' mode IFF they are | ||
// _not_ filtered in 'packages/extensions/src/ext-meshopt-compression/encoder.ts'. | ||
pattern: options.level === 'medium' ? /.*/ : /^(POSITION|TEXCOORD|JOINTS|WEIGHTS)(_\d+)?$/, | ||
quantizePosition: 14, | ||
quantizeTexcoord: 12, | ||
quantizeColor: 8, | ||
quantizeNormal: 8, | ||
}) | ||
...options, | ||
pattern, | ||
patternTargets, | ||
quantizeNormal, | ||
}), | ||
); | ||
@@ -70,0 +88,0 @@ |
341
src/prune.ts
import { | ||
AnimationChannel, | ||
ColorUtils, | ||
Document, | ||
ExtensionProperty, | ||
Graph, | ||
ILogger, | ||
Material, | ||
Node, | ||
Primitive, | ||
PrimitiveTarget, | ||
Property, | ||
PropertyType, | ||
Root, | ||
Transform, | ||
Node, | ||
Scene, | ||
ExtensionProperty, | ||
Material, | ||
Primitive, | ||
PrimitiveTarget, | ||
Texture, | ||
TextureInfo, | ||
Transform, | ||
vec3, | ||
vec4, | ||
} from '@gltf-transform/core'; | ||
import { mul as mulVec3 } from 'gl-matrix/vec3'; | ||
import { add, create, len, mul, scale, sub } from 'gl-matrix/vec4'; | ||
import { NdArray } from 'ndarray'; | ||
import { getPixels } from 'ndarray-pixels'; | ||
import { getTextureColorSpace } from './get-texture-color-space.js'; | ||
import { listTextureInfoByMaterial } from './list-texture-info.js'; | ||
import { listTextureSlots } from './list-texture-slots.js'; | ||
import { createTransform } from './utils.js'; | ||
import { listTextureInfoByMaterial } from './list-texture-info.js'; | ||
const NAME = 'prune'; | ||
const EPS = 3 / 255; | ||
export interface PruneOptions { | ||
@@ -30,2 +42,4 @@ /** List of {@link PropertyType} identifiers to be de-duplicated.*/ | ||
keepAttributes?: boolean; | ||
/** Whether to keep single-color textures that can be converted to material factors. */ | ||
keepSolidTextures?: boolean; | ||
} | ||
@@ -48,2 +62,3 @@ const PRUNE_DEFAULTS: Required<PruneOptions> = { | ||
keepAttributes: true, | ||
keepSolidTextures: true, | ||
}; | ||
@@ -75,8 +90,8 @@ | ||
return createTransform(NAME, (doc: Document): void => { | ||
const logger = doc.getLogger(); | ||
const root = doc.getRoot(); | ||
const graph = doc.getGraph(); | ||
return createTransform(NAME, async (document: Document): Promise<void> => { | ||
const logger = document.getLogger(); | ||
const root = document.getRoot(); | ||
const graph = document.getGraph(); | ||
const disposed: Record<string, number> = {}; | ||
const counter = new DisposeCounter(); | ||
@@ -90,18 +105,42 @@ // Prune top-down, so that low-level properties like accessors can be removed if the | ||
if (mesh.listPrimitives().length > 0) continue; | ||
mesh.dispose(); | ||
markDisposed(mesh); | ||
counter.dispose(mesh); | ||
} | ||
} | ||
if (propertyTypes.has(PropertyType.NODE) && !options.keepLeaves) root.listScenes().forEach(nodeTreeShake); | ||
if (propertyTypes.has(PropertyType.NODE)) root.listNodes().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.SKIN)) root.listSkins().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.MESH)) root.listMeshes().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.CAMERA)) root.listCameras().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.NODE)) { | ||
if (!options.keepLeaves) { | ||
for (const scene of root.listScenes()) { | ||
nodeTreeShake(graph, scene, counter); | ||
} | ||
} | ||
for (const node of root.listNodes()) { | ||
treeShake(node, counter); | ||
} | ||
} | ||
if (propertyTypes.has(PropertyType.SKIN)) { | ||
for (const skin of root.listSkins()) { | ||
treeShake(skin, counter); | ||
} | ||
} | ||
if (propertyTypes.has(PropertyType.MESH)) { | ||
for (const mesh of root.listMeshes()) { | ||
treeShake(mesh, counter); | ||
} | ||
} | ||
if (propertyTypes.has(PropertyType.CAMERA)) { | ||
for (const camera of root.listCameras()) { | ||
treeShake(camera, counter); | ||
} | ||
} | ||
if (propertyTypes.has(PropertyType.PRIMITIVE)) { | ||
indirectTreeShake(graph, PropertyType.PRIMITIVE); | ||
indirectTreeShake(graph, PropertyType.PRIMITIVE, counter); | ||
} | ||
if (propertyTypes.has(PropertyType.PRIMITIVE_TARGET)) { | ||
indirectTreeShake(graph, PropertyType.PRIMITIVE_TARGET); | ||
indirectTreeShake(graph, PropertyType.PRIMITIVE_TARGET, counter); | ||
} | ||
@@ -115,3 +154,3 @@ | ||
const material = prim.getMaterial(); | ||
const required = listRequiredSemantics(doc, material); | ||
const required = listRequiredSemantics(document, material); | ||
const unused = listUnusedSemantics(prim, required); | ||
@@ -140,4 +179,3 @@ pruneAttributes(prim, unused); | ||
if (!channel.getTargetNode()) { | ||
channel.dispose(); | ||
markDisposed(channel); | ||
counter.dispose(channel); | ||
} | ||
@@ -147,6 +185,6 @@ } | ||
const samplers = anim.listSamplers(); | ||
treeShake(anim); | ||
samplers.forEach(treeShake); | ||
treeShake(anim, counter); | ||
samplers.forEach((sampler) => treeShake(sampler, counter)); | ||
} else { | ||
anim.listSamplers().forEach(treeShake); | ||
anim.listSamplers().forEach((sampler) => treeShake(sampler, counter)); | ||
} | ||
@@ -156,7 +194,21 @@ } | ||
if (propertyTypes.has(PropertyType.MATERIAL)) root.listMaterials().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.TEXTURE)) root.listTextures().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.ACCESSOR)) root.listAccessors().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.BUFFER)) root.listBuffers().forEach(treeShake); | ||
if (propertyTypes.has(PropertyType.MATERIAL)) { | ||
root.listMaterials().forEach((material) => treeShake(material, counter)); | ||
} | ||
if (propertyTypes.has(PropertyType.TEXTURE)) { | ||
root.listTextures().forEach((texture) => treeShake(texture, counter)); | ||
if (!options.keepSolidTextures) { | ||
await pruneSolidTextures(document, counter); | ||
} | ||
} | ||
if (propertyTypes.has(PropertyType.ACCESSOR)) { | ||
root.listAccessors().forEach((accessor) => treeShake(accessor, counter)); | ||
} | ||
if (propertyTypes.has(PropertyType.BUFFER)) { | ||
root.listBuffers().forEach((buffer) => treeShake(buffer, counter)); | ||
} | ||
// TODO(bug): This process does not identify unused ExtensionProperty instances. That could | ||
@@ -167,5 +219,6 @@ // be a future enhancement, either tracking unlinked properties as if they were connected | ||
if (Object.keys(disposed).length) { | ||
const str = Object.keys(disposed) | ||
.map((t) => `${t} (${disposed[t]})`) | ||
if (!counter.empty()) { | ||
const str = counter | ||
.entries() | ||
.map(([type, count]) => `${type} (${count})`) | ||
.join(', '); | ||
@@ -178,60 +231,83 @@ logger.info(`${NAME}: Removed types... ${str}`); | ||
logger.debug(`${NAME}: Complete.`); | ||
}); | ||
} | ||
// | ||
/********************************************************************************************** | ||
* Utility for disposing properties and reporting statistics afterward. | ||
*/ | ||
/** Disposes of the given property if it is unused. */ | ||
function treeShake(prop: Property): void { | ||
// Consider a property unused if it has no references from another property, excluding | ||
// types Root and AnimationChannel. | ||
const parents = prop.listParents().filter((p) => !(p instanceof Root || p instanceof AnimationChannel)); | ||
if (!parents.length) { | ||
prop.dispose(); | ||
markDisposed(prop); | ||
} | ||
} | ||
class DisposeCounter { | ||
public readonly disposed: Record<string, number> = {}; | ||
/** | ||
* For property types the Root does not maintain references to, we'll need to search the | ||
* graph. It's possible that objects may have been constructed without any outbound links, | ||
* but since they're not on the graph they don't need to be tree-shaken. | ||
*/ | ||
function indirectTreeShake(graph: Graph<Property>, propertyType: string): void { | ||
graph | ||
.listEdges() | ||
.map((edge) => edge.getParent()) | ||
.filter((parent) => parent.propertyType === propertyType) | ||
.forEach(treeShake); | ||
} | ||
empty(): boolean { | ||
for (const key in this.disposed) return false; | ||
return true; | ||
} | ||
/** Iteratively prunes leaf Nodes without contents. */ | ||
function nodeTreeShake(prop: Node | Scene): void { | ||
prop.listChildren().forEach(nodeTreeShake); | ||
entries(): [string, number][] { | ||
return Object.entries(this.disposed); | ||
} | ||
if (prop instanceof Scene) return; | ||
/** Records properties disposed by type. */ | ||
dispose(prop: Property): void { | ||
this.disposed[prop.propertyType] = this.disposed[prop.propertyType] || 0; | ||
this.disposed[prop.propertyType]++; | ||
prop.dispose(); | ||
} | ||
} | ||
const isUsed = graph.listParentEdges(prop).some((e) => { | ||
const ptype = e.getParent().propertyType; | ||
return ptype !== PropertyType.ROOT && ptype !== PropertyType.SCENE && ptype !== PropertyType.NODE; | ||
}); | ||
const isEmpty = graph.listChildren(prop).length === 0; | ||
if (isEmpty && !isUsed) { | ||
prop.dispose(); | ||
markDisposed(prop); | ||
} | ||
} | ||
/********************************************************************************************** | ||
* Helper functions for the {@link prune} transform. | ||
* | ||
* IMPORTANT: These functions were previously declared in function scope, but | ||
* broke in the CommonJS build due to a buggy Babel transform. See: | ||
* https://github.com/donmccurdy/glTF-Transform/issues/1140 | ||
*/ | ||
function pruneAttributes(prim: Primitive | PrimitiveTarget, unused: string[]) { | ||
for (const semantic of unused) { | ||
prim.setAttribute(semantic, null); | ||
} | ||
/** Disposes of the given property if it is unused. */ | ||
function treeShake(prop: Property, counter: DisposeCounter): void { | ||
// Consider a property unused if it has no references from another property, excluding | ||
// types Root and AnimationChannel. | ||
const parents = prop.listParents().filter((p) => !(p instanceof Root || p instanceof AnimationChannel)); | ||
if (!parents.length) { | ||
counter.dispose(prop); | ||
} | ||
} | ||
/** | ||
* For property types the Root does not maintain references to, we'll need to search the | ||
* graph. It's possible that objects may have been constructed without any outbound links, | ||
* but since they're not on the graph they don't need to be tree-shaken. | ||
*/ | ||
function indirectTreeShake(graph: Graph<Property>, propertyType: string, counter: DisposeCounter): void { | ||
for (const edge of graph.listEdges()) { | ||
const parent = edge.getParent(); | ||
if (parent.propertyType === propertyType) { | ||
treeShake(parent, counter); | ||
} | ||
} | ||
} | ||
/** Records properties disposed by type. */ | ||
function markDisposed(prop: Property): void { | ||
disposed[prop.propertyType] = disposed[prop.propertyType] || 0; | ||
disposed[prop.propertyType]++; | ||
} | ||
/** Iteratively prunes leaf Nodes without contents. */ | ||
function nodeTreeShake(graph: Graph<Property>, prop: Node | Scene, counter: DisposeCounter): void { | ||
prop.listChildren().forEach((child) => nodeTreeShake(graph, child, counter)); | ||
if (prop instanceof Scene) return; | ||
const isUsed = graph.listParentEdges(prop).some((e) => { | ||
const ptype = e.getParent().propertyType; | ||
return ptype !== PropertyType.ROOT && ptype !== PropertyType.SCENE && ptype !== PropertyType.NODE; | ||
}); | ||
const isEmpty = graph.listChildren(prop).length === 0; | ||
if (isEmpty && !isUsed) { | ||
counter.dispose(prop); | ||
} | ||
} | ||
function pruneAttributes(prim: Primitive | PrimitiveTarget, unused: string[]) { | ||
for (const semantic of unused) { | ||
prim.setAttribute(semantic, null); | ||
} | ||
} | ||
/** | ||
@@ -261,3 +337,3 @@ * Lists vertex attribute semantics that are unused when rendering a given primitive. | ||
material: Material | ExtensionProperty | null, | ||
semantics = new Set<string>() | ||
semantics = new Set<string>(), | ||
): Set<string> { | ||
@@ -348,1 +424,104 @@ if (!material) return semantics; | ||
} | ||
/********************************************************************************************** | ||
* Prune solid (single-color) textures. | ||
*/ | ||
async function pruneSolidTextures(document: Document, counter: DisposeCounter): Promise<void> { | ||
const root = document.getRoot(); | ||
const graph = document.getGraph(); | ||
const logger = document.getLogger(); | ||
const textures = root.listTextures(); | ||
const pending = textures.map(async (texture) => { | ||
const factor = await getTextureFactor(texture); | ||
if (!factor) return; | ||
if (getTextureColorSpace(texture) === 'srgb') { | ||
ColorUtils.convertSRGBToLinear(factor, factor); | ||
} | ||
const name = texture.getName() || texture.getURI(); | ||
const size = texture.getSize()?.join('x'); | ||
const slots = listTextureSlots(texture); | ||
for (const edge of graph.listParentEdges(texture)) { | ||
const parent = edge.getParent(); | ||
if (parent !== root && applyMaterialFactor(parent as Material, factor, edge.getName(), logger)) { | ||
edge.dispose(); | ||
} | ||
} | ||
if (texture.listParents().length === 1) { | ||
counter.dispose(texture); | ||
logger.debug(`${NAME}: Removed solid-color texture "${name}" (${size}px ${slots.join(', ')})`); | ||
} | ||
}); | ||
await Promise.all(pending); | ||
} | ||
function applyMaterialFactor( | ||
material: Material | ExtensionProperty, | ||
factor: vec4, | ||
slot: string, | ||
logger: ILogger, | ||
): boolean { | ||
if (material instanceof Material) { | ||
switch (slot) { | ||
case 'baseColorTexture': | ||
material.setBaseColorFactor(mul(factor, factor, material.getBaseColorFactor()) as vec4); | ||
return true; | ||
case 'emissiveTexture': | ||
material.setEmissiveFactor( | ||
mulVec3([0, 0, 0], factor.slice(0, 3) as vec3, material.getEmissiveFactor()) as vec3, | ||
); | ||
return true; | ||
case 'occlusionTexture': | ||
return Math.abs(factor[0] - 1) <= EPS; | ||
case 'metallicRoughnessTexture': | ||
material.setRoughnessFactor(factor[1] * material.getRoughnessFactor()); | ||
material.setMetallicFactor(factor[2] * material.getMetallicFactor()); | ||
return true; | ||
case 'normalTexture': | ||
return len(sub(create(), factor, [0.5, 0.5, 1, 1])) <= EPS; | ||
} | ||
} | ||
logger.warn(`${NAME}: Detected single-color ${slot} texture. Pruning ${slot} not yet supported.`); | ||
return false; | ||
} | ||
async function getTextureFactor(texture: Texture): Promise<vec4 | null> { | ||
const pixels = await maybeGetPixels(texture); | ||
if (!pixels) return null; | ||
const min: vec4 = [Infinity, Infinity, Infinity, Infinity]; | ||
const max: vec4 = [-Infinity, -Infinity, -Infinity, -Infinity]; | ||
const target: vec4 = [0, 0, 0, 0]; | ||
const [width, height] = pixels.shape; | ||
for (let i = 0; i < width; i++) { | ||
for (let j = 0; j < height; j++) { | ||
for (let k = 0; k < 4; k++) { | ||
min[k] = Math.min(min[k], pixels.get(i, j, k)); | ||
max[k] = Math.max(max[k], pixels.get(i, j, k)); | ||
} | ||
} | ||
if (len(sub(target, max, min)) / 255 > EPS) { | ||
return null; | ||
} | ||
} | ||
return scale(target, add(target, max, min), 0.5 / 255) as vec4; | ||
} | ||
async function maybeGetPixels(texture: Texture): Promise<NdArray<Uint8Array> | null> { | ||
try { | ||
return await getPixels(texture.getImage()!, texture.getMimeType()); | ||
} catch (e) { | ||
return null; | ||
} | ||
} |
@@ -45,2 +45,4 @@ import { | ||
pattern?: RegExp; | ||
/** Pattern (regex) used to filter morph target semantics for quantization. Default: `options.pattern`. */ | ||
patternTargets?: RegExp; | ||
/** Bounds for quantization grid. */ | ||
@@ -64,3 +66,3 @@ quantizationVolume?: 'mesh' | 'scene'; | ||
export const QUANTIZE_DEFAULTS: Required<QuantizeOptions> = { | ||
export const QUANTIZE_DEFAULTS: Required<Omit<QuantizeOptions, 'patternTargets'>> = { | ||
pattern: /.*/, | ||
@@ -94,2 +96,4 @@ quantizationVolume: 'mesh', | ||
options.patternTargets = options.patternTargets || options.pattern; | ||
return createTransform(NAME, async (doc: Document): Promise<void> => { | ||
@@ -128,3 +132,3 @@ const logger = doc.getLogger(); | ||
prune({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.SKIN, PropertyType.MATERIAL] }), | ||
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.MATERIAL, PropertyType.SKIN] }) | ||
dedup({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.MATERIAL, PropertyType.SKIN] }), | ||
); | ||
@@ -140,10 +144,13 @@ | ||
nodeTransform: VectorTransform<vec3>, | ||
options: Required<QuantizeOptions> | ||
options: Required<QuantizeOptions>, | ||
): void { | ||
const isTarget = prim instanceof PrimitiveTarget; | ||
const logger = doc.getLogger(); | ||
for (const semantic of prim.listSemantics()) { | ||
if (!options.pattern.test(semantic)) continue; | ||
if (!isTarget && !options.pattern.test(semantic)) continue; | ||
if (isTarget && !options.patternTargets.test(semantic)) continue; | ||
const srcAttribute = prim.getAttribute(semantic)!; | ||
const { bits, ctor } = getQuantizationSettings(semantic, srcAttribute, logger, options); | ||
@@ -201,3 +208,3 @@ | ||
(max[1] - min[1]) / 2, | ||
(max[2] - min[2]) / 2 | ||
(max[2] - min[2]) / 2, | ||
); | ||
@@ -303,3 +310,3 @@ | ||
instanceScale ? (instanceScale.getElement(i, s) as vec3) : S_IDENTITY, | ||
instanceMatrix | ||
instanceMatrix, | ||
); | ||
@@ -357,2 +364,3 @@ | ||
const hi = 2 * quantBits - storageBits; | ||
const range = [signBits > 0 ? -1 : 0, 1] as vec2; | ||
@@ -362,4 +370,7 @@ for (let i = 0, di = 0, el: number[] = []; i < attribute.getCount(); i++) { | ||
for (let j = 0; j < el.length; j++) { | ||
// Clamp to range. | ||
let value = clamp(el[j], range); | ||
// Map [0.0 ... 1.0] to [0 ... scale]. | ||
let value = Math.round(Math.abs(el[j]) * scale); | ||
value = Math.round(Math.abs(value) * scale); | ||
@@ -382,3 +393,3 @@ // Replicate msb to missing lsb. | ||
logger: ILogger, | ||
options: Required<QuantizeOptions> | ||
options: Required<QuantizeOptions>, | ||
): { bits: number; ctor?: TypedArrayConstructor } { | ||
@@ -509,1 +520,5 @@ const min = attribute.getMinNormalized([]); | ||
} | ||
function clamp(value: number, range: vec2): number { | ||
return Math.min(Math.max(value, range[0]), range[1]); | ||
} |
@@ -14,4 +14,4 @@ import { BufferUtils, Document, ImageUtils, Texture, TextureChannel, Transform, vec2 } from '@gltf-transform/core'; | ||
type Format = (typeof FORMATS)[number]; | ||
const FORMATS = ['jpeg', 'png', 'webp', 'avif'] as const; | ||
type Format = (typeof TEXTURE_COMPRESS_SUPPORTED_FORMATS)[number]; | ||
export const TEXTURE_COMPRESS_SUPPORTED_FORMATS = ['jpeg', 'png', 'webp', 'avif'] as const; | ||
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/avif']; | ||
@@ -41,5 +41,12 @@ | ||
pattern?: RegExp | null; | ||
/** Pattern matching the format(s) to be compressed or converted. */ | ||
/** | ||
* Pattern matching the format(s) to be compressed or converted. Some examples | ||
* of formats include "jpeg" and "png". | ||
*/ | ||
formats?: RegExp | null; | ||
/** Pattern matching the material texture slot(s) to be compressed or converted. */ | ||
/** | ||
* Pattern matching the material texture slot(s) to be compressed or converted. | ||
* Some examples of slot names include "baseColorTexture", "occlusionTexture", | ||
* "metallicRoughnessTexture", and "normalTexture". | ||
*/ | ||
slots?: RegExp | null; | ||
@@ -337,3 +344,3 @@ | ||
const format = mimeType.split('/').pop() as Format | undefined; | ||
if (!format || !FORMATS.includes(format)) { | ||
if (!format || !TEXTURE_COMPRESS_SUPPORTED_FORMATS.includes(format)) { | ||
throw new Error(`Unknown MIME type "${mimeType}".`); | ||
@@ -340,0 +347,0 @@ } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1431745
7899