@gltf-transform/functions
Advanced tools
Comparing version 3.5.1 to 3.6.0
@@ -1,2 +0,2 @@ | ||
import type { Transform } from '@gltf-transform/core'; | ||
import type { Accessor, Primitive, Transform } from '@gltf-transform/core'; | ||
/** Options for the {@link dequantize} function. */ | ||
@@ -15,4 +15,31 @@ export interface DequantizeOptions { | ||
* | ||
* Example: | ||
* | ||
* ```javascript | ||
* import { dequantizePrimitive } from '@gltf-transform/functions'; | ||
* | ||
* await document.transform(dequantize()); | ||
* ``` | ||
* | ||
* @category Transforms | ||
*/ | ||
export declare function dequantize(_options?: DequantizeOptions): Transform; | ||
/** | ||
* Dequantize a single {@link Primitive}, converting all vertex attributes to float32. Dequantization | ||
* will increase the size of the mesh on disk and in memory, but may be necessary for compatibility | ||
* with applications that don't support quantization. | ||
* | ||
* Example: | ||
* | ||
* ```javascript | ||
* import { dequantizePrimitive } from '@gltf-transform/functions'; | ||
* | ||
* const mesh = document.getRoot().listMeshes().find((mesh) => mesh.getName() === 'MyMesh'); | ||
* | ||
* for (const prim of mesh.listPrimitives()) { | ||
* dequantizePrimitive(prim); | ||
* } | ||
* ``` | ||
*/ | ||
export declare function dequantizePrimitive(prim: Primitive, options: Required<DequantizeOptions>): void; | ||
export declare function dequantizeAttribute(semantic: string, attribute: Accessor, options: Required<DequantizeOptions>): void; |
@@ -5,3 +5,3 @@ export * from './center.js'; | ||
export * from './dedup.js'; | ||
export * from './dequantize.js'; | ||
export { dequantize, dequantizePrimitive, DequantizeOptions } from './dequantize.js'; | ||
export * from './draco.js'; | ||
@@ -8,0 +8,0 @@ export * from './flatten.js'; |
@@ -7,5 +7,7 @@ import { Texture, Transform, vec2 } from '@gltf-transform/core'; | ||
/** Instance of the Sharp encoder, which must be installed from the | ||
* 'sharp' package and provided by the caller. | ||
* 'sharp' package and provided by the caller. When not provided, a | ||
* platform-specific fallback implementation will be used, and most | ||
* quality- and compression-related options are ignored. | ||
*/ | ||
encoder: unknown; | ||
encoder?: unknown; | ||
/** | ||
@@ -32,15 +34,27 @@ * Target image format. If specified, included textures in other formats | ||
quality?: number | null; | ||
/** Level of CPU effort to reduce file size, 0-100. PNG, WebP, and AVIF only. Default: auto. */ | ||
/** | ||
* Level of CPU effort to reduce file size, 0-100. PNG, WebP, and AVIF | ||
* only. Supported only when a Sharp encoder is provided. Default: auto. | ||
*/ | ||
effort?: number | null; | ||
/** Use lossless compression mode. WebP and AVIF only. Default: false. */ | ||
/** | ||
* Use lossless compression mode. WebP and AVIF only. Supported only when a | ||
* Sharp encoder is provided. Default: false. | ||
*/ | ||
lossless?: boolean; | ||
/** Use near lossless compression mode. WebP only. Default: false. */ | ||
/** | ||
* Use near lossless compression mode. WebP only. Supported only when a | ||
* Sharp encoder is provided. Default: false. | ||
*/ | ||
nearLossless?: boolean; | ||
} | ||
export type CompressTextureOptions = Omit<TextureCompressOptions, 'pattern' | 'formats' | 'slots'>; | ||
export declare const TEXTURE_COMPRESS_DEFAULTS: Required<Omit<TextureCompressOptions, 'resize' | 'targetFormat' | 'encoder'>>; | ||
export declare const TEXTURE_COMPRESS_DEFAULTS: Omit<TextureCompressOptions, 'resize' | 'targetFormat' | 'encoder'>; | ||
/** | ||
* Optimizes images, optionally resizing or converting to JPEG, PNG, WebP, or AVIF formats. | ||
* | ||
* Requires `sharp`, and is available only in Node.js environments. | ||
* For best results use a Node.js environment, install the `sharp` module, and | ||
* provide an encoder. When the encoder is omitted — `sharp` works only in Node.js — | ||
* the implementation will use a platform-specific fallback encoder, and most | ||
* quality- and compression-related options are ignored. | ||
* | ||
@@ -66,2 +80,8 @@ * Example: | ||
* ); | ||
* | ||
* // (C) Resize and convert images to WebP in a browser, without a Sharp | ||
* // encoder. Most quality- and compression-related options are ignored. | ||
* await document.transform( | ||
* textureCompress({ targetFormat: 'webp', resize: [1024, 1024] }) | ||
* ); | ||
* ``` | ||
@@ -75,3 +95,6 @@ * | ||
* | ||
* Requires `sharp`, and is available only in Node.js environments. | ||
* For best results use a Node.js environment, install the `sharp` module, and | ||
* provide an encoder. When the encoder is omitted — `sharp` works only in Node.js — | ||
* the implementation will use a platform-specific fallback encoder, and most | ||
* quality- and compression-related options are ignored. | ||
* | ||
@@ -87,2 +110,3 @@ * Example: | ||
* | ||
* // (A) Node.js. | ||
* await compressTexture(texture, { | ||
@@ -93,2 +117,8 @@ * encoder: sharp, | ||
* }); | ||
* | ||
* // (B) Web. | ||
* await compressTexture(texture, { | ||
* targetFormat: 'webp', | ||
* resize: [1024, 1024] | ||
* }); | ||
* ``` | ||
@@ -95,0 +125,0 @@ */ |
@@ -1,2 +0,2 @@ | ||
import type { Transform, vec2 } from '@gltf-transform/core'; | ||
import { type Transform, type vec2 } from '@gltf-transform/core'; | ||
/** Options for the {@link textureResize} function. */ | ||
@@ -30,6 +30,8 @@ export interface TextureResizeOptions { | ||
* package, which works in Web and Node.js environments. For a faster and more robust implementation | ||
* based on Sharp (available only in Node.js), use {@link textureCompress} with the 'resize' option. | ||
* in Node.js, use {@link textureCompress}, providing a Sharp encoder and 'resize' options instead. | ||
* | ||
* @deprecated Prefer {@link textureCompress}, instead. | ||
* @privateRemarks TODO(v4): Remove this function, using `textureCompress()` instead. | ||
* @category Transforms | ||
*/ | ||
export declare function textureResize(_options?: TextureResizeOptions): Transform; |
@@ -1,2 +0,2 @@ | ||
import type { mat4, Primitive } from '@gltf-transform/core'; | ||
import { mat4, Primitive } from '@gltf-transform/core'; | ||
/** | ||
@@ -3,0 +3,0 @@ * Applies a transform matrix to a {@link Primitive}. |
import type { NdArray } from 'ndarray'; | ||
import { Accessor, Primitive, Property, Texture, Transform, TransformContext } from '@gltf-transform/core'; | ||
import { Accessor, Primitive, Property, Texture, Transform, TransformContext, vec2 } from '@gltf-transform/core'; | ||
/** | ||
@@ -45,2 +45,4 @@ * Prepares a function used in an {@link Document.transform} pipeline. Use of this wrapper is | ||
/** @hidden */ | ||
export declare function shallowEqualsArray(a: ArrayLike<unknown> | null, b: ArrayLike<unknown> | null): boolean; | ||
/** @hidden */ | ||
export declare function remapAttribute(attribute: Accessor, remap: Uint32Array, dstCount: number): void; | ||
@@ -58,1 +60,3 @@ /** @hidden */ | ||
export declare function createPrimGroupKey(prim: Primitive): string; | ||
/** @hidden */ | ||
export declare function fitWithin(size: vec2, limit: vec2): vec2; |
@@ -52,3 +52,3 @@ import { Document, Primitive, Transform } from '@gltf-transform/core'; | ||
* of the AABB's longest dimension. Other vertex attributes are also compared | ||
* during welding, with attribute-specific thresholds. For --tolerance=0, geometry | ||
* during welding, with attribute-specific thresholds. For tolerance=0, geometry | ||
* is indexed in place, without merging. | ||
@@ -65,6 +65,8 @@ * | ||
* for (const prim of mesh.listPrimitives()) { | ||
* weldPrimitive(document, prim, {tolerance: 0.0001}); | ||
* weldPrimitive(prim, {tolerance: 0.0001}); | ||
* } | ||
* ``` | ||
* | ||
* @privateRemarks TODO(v4): Remove the "Document" parameter. | ||
*/ | ||
export declare function weldPrimitive(doc: Document, prim: Primitive, options: Required<WeldOptions>): void; | ||
export declare function weldPrimitive(a: Document | Primitive, b?: Primitive | WeldOptions, c?: Required<WeldOptions>): void; |
{ | ||
"name": "@gltf-transform/functions", | ||
"version": "3.5.1", | ||
"version": "3.6.0", | ||
"repository": "github:donmccurdy/glTF-Transform", | ||
@@ -39,4 +39,4 @@ "homepage": "https://gltf-transform.dev/functions.html", | ||
"dependencies": { | ||
"@gltf-transform/core": "^3.5.1", | ||
"@gltf-transform/extensions": "^3.5.1", | ||
"@gltf-transform/core": "^3.6.0", | ||
"@gltf-transform/extensions": "^3.6.0", | ||
"ktx-parse": "^0.6.0", | ||
@@ -57,3 +57,3 @@ "ndarray": "^1.0.19", | ||
}, | ||
"gitHead": "2b74ae533f9aced2e044963794c3a28593645962" | ||
"gitHead": "1de21dc41949a103d2fc7a4c9a0b516f8ed8173b" | ||
} |
@@ -17,4 +17,26 @@ # @gltf-transform/functions | ||
<h2>Commercial Use</h2> | ||
<p> | ||
<b>Using glTF Transform for a personal project?</b> That's great! Sponsorship is neither expected nor required. Feel | ||
free to share screenshots if you've made something you're excited about — I enjoy seeing those! | ||
</p> | ||
<p> | ||
<b>Using glTF Transform in for-profit work?</b> That's wonderful! Your support is important to keep glTF Transform | ||
maintained, independent, and open source under MIT License. Please consider a | ||
<a href="https://gltf-transform.dev/pro" target="_blank">subscription</a> | ||
or | ||
<a href="https://github.com/sponsors/donmccurdy" target="_blank">GitHub sponsorship</a>. | ||
</p> | ||
<p> | ||
<i> | ||
Learn more in the | ||
<a href="https://gltf-transform.dev/pro" target="_blank"> glTF Transform Pro </a> FAQs</i | ||
>. | ||
</p> | ||
## License | ||
Copyright 2023, MIT License. |
@@ -16,3 +16,3 @@ import { | ||
} from '@gltf-transform/core'; | ||
import { createTransform } from './utils.js'; | ||
import { createTransform, shallowEqualsArray } from './utils.js'; | ||
@@ -309,3 +309,3 @@ const NAME = 'dedup'; | ||
const duplicates = new Map<Skin, Skin>(); | ||
const skip = new Set(['name']); | ||
const skip = new Set(['name', 'joints']); | ||
@@ -321,3 +321,5 @@ for (let i = 0; i < skins.length; i++) { | ||
if (a.equals(b, skip)) { | ||
// Check joints with shallow equality, not deep equality. | ||
// See: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/RecursiveSkeletons | ||
if (a.equals(b, skip) && shallowEqualsArray(a.listJoints(), b.listJoints())) { | ||
duplicates.set(b, a); | ||
@@ -324,0 +326,0 @@ } |
@@ -25,2 +25,10 @@ import type { Accessor, Document, Primitive, Transform } from '@gltf-transform/core'; | ||
* | ||
* Example: | ||
* | ||
* ```javascript | ||
* import { dequantizePrimitive } from '@gltf-transform/functions'; | ||
* | ||
* await document.transform(dequantize()); | ||
* ``` | ||
* | ||
* @category Transforms | ||
@@ -43,3 +51,20 @@ */ | ||
function dequantizePrimitive(prim: Primitive, options: Required<DequantizeOptions>): void { | ||
/** | ||
* Dequantize a single {@link Primitive}, converting all vertex attributes to float32. Dequantization | ||
* will increase the size of the mesh on disk and in memory, but may be necessary for compatibility | ||
* with applications that don't support quantization. | ||
* | ||
* Example: | ||
* | ||
* ```javascript | ||
* import { dequantizePrimitive } from '@gltf-transform/functions'; | ||
* | ||
* const mesh = document.getRoot().listMeshes().find((mesh) => mesh.getName() === 'MyMesh'); | ||
* | ||
* for (const prim of mesh.listPrimitives()) { | ||
* dequantizePrimitive(prim); | ||
* } | ||
* ``` | ||
*/ | ||
export function dequantizePrimitive(prim: Primitive, options: Required<DequantizeOptions>): void { | ||
for (const semantic of prim.listSemantics()) { | ||
@@ -55,3 +80,3 @@ dequantizeAttribute(semantic, prim.getAttribute(semantic)!, options); | ||
function dequantizeAttribute(semantic: string, attribute: Accessor, options: Required<DequantizeOptions>): void { | ||
export function dequantizeAttribute(semantic: string, attribute: Accessor, options: Required<DequantizeOptions>): void { | ||
if (!attribute.getArray()) return; | ||
@@ -58,0 +83,0 @@ if (!options.pattern.test(semantic)) return; |
@@ -5,3 +5,3 @@ export * from './center.js'; | ||
export * from './dedup.js'; | ||
export * from './dequantize.js'; | ||
export { dequantize, dequantizePrimitive, DequantizeOptions } from './dequantize.js'; | ||
export * from './draco.js'; | ||
@@ -8,0 +8,0 @@ export * from './flatten.js'; |
@@ -1,2 +0,2 @@ | ||
import { Document, ILogger, MathUtils, Mesh, Node, Transform, vec3, vec4 } from '@gltf-transform/core'; | ||
import { Document, ILogger, MathUtils, Mesh, Node, Primitive, Transform, vec3, vec4 } from '@gltf-transform/core'; | ||
import { InstancedMesh, EXTMeshGPUInstancing } from '@gltf-transform/extensions'; | ||
@@ -69,2 +69,6 @@ import { createTransform } from './utils.js'; | ||
// Cannot preserve volumetric effects when instancing with varying scale. | ||
// See: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/AttenuationTest | ||
if (mesh.listPrimitives().some(hasVolume) && nodes.some(hasScale)) continue; | ||
const batch = createBatch(doc, batchExtension, mesh, nodes.length); | ||
@@ -147,2 +151,12 @@ const batchTranslation = batch.getAttribute('TRANSLATION')!; | ||
function hasVolume(prim: Primitive) { | ||
const material = prim.getMaterial(); | ||
return !!(material && material.getExtension('KHR_materials_volume')); | ||
} | ||
function hasScale(node: Node) { | ||
const scale = node.getWorldScale(); | ||
return !MathUtils.eq(scale, [1, 1, 1]); | ||
} | ||
function createBatch(doc: Document, batchExtension: EXTMeshGPUInstancing, mesh: Mesh, count: number): InstancedMesh { | ||
@@ -149,0 +163,0 @@ const buffer = mesh.listPrimitives()[0].getAttribute('POSITION')!.getBuffer(); |
@@ -43,3 +43,3 @@ import { Document, Primitive, ComponentTypeToTypedArray } from '@gltf-transform/core'; | ||
'Requires ≥2 Primitives, sharing the same Material ' + | ||
'and Mode, with compatible vertex attributes and indices.' | ||
'and Mode, with compatible vertex attributes and indices.', | ||
); | ||
@@ -46,0 +46,0 @@ } |
@@ -17,2 +17,3 @@ import { | ||
import { createPrimGroupKey, createTransform, formatLong, isUsed } from './utils.js'; | ||
import { dequantizeAttribute } from './dequantize.js'; | ||
@@ -100,3 +101,3 @@ const NAME = 'join'; | ||
keepAttributes: true, | ||
}) | ||
}), | ||
); | ||
@@ -148,2 +149,4 @@ | ||
dequantizeTransformableAttributes(prim); | ||
let key = createPrimGroupKey(prim); | ||
@@ -225,3 +228,3 @@ | ||
`${NAME}: Joined Primitives (${prims.length}) containing ` + | ||
`${formatLong(dstVertexCount)} vertices under Node "${dstNode.getName()}".` | ||
`${formatLong(dstVertexCount)} vertices under Node "${dstNode.getName()}".`, | ||
); | ||
@@ -251,1 +254,16 @@ } | ||
} | ||
/** | ||
* Dequantize attributes that would be affected by {@link transformPrimitive}, | ||
* to avoid invalidating our primitive group keys. | ||
* | ||
* See: https://github.com/donmccurdy/glTF-Transform/issues/844 | ||
*/ | ||
function dequantizeTransformableAttributes(prim: Primitive) { | ||
for (const semantic of ['POSITION', 'NORMAL', 'TANGENT']) { | ||
const attribute = prim.getAttribute(semantic); | ||
if (attribute && attribute.getComponentSize() < 4) { | ||
dequantizeAttribute(semantic, attribute, { pattern: /.*/ }); | ||
} | ||
} | ||
} |
@@ -13,2 +13,3 @@ import { Accessor, Document, Primitive, PropertyType, Transform, TransformContext } from '@gltf-transform/core'; | ||
import { dedup } from './dedup.js'; | ||
import { prune } from './prune.js'; | ||
@@ -92,3 +93,3 @@ const NAME = 'simplify'; | ||
logger.warn( | ||
`${NAME}: Skipping primitive of mesh "${mesh.getName()}": Requires TRIANGLES draw mode.` | ||
`${NAME}: Skipping primitive of mesh "${mesh.getName()}": Requires TRIANGLES draw mode.`, | ||
); | ||
@@ -98,5 +99,14 @@ continue; | ||
simplifyPrimitive(document, prim, options); | ||
if (prim.getIndices()!.getCount() === 0) prim.dispose(); | ||
} | ||
if (mesh.listPrimitives().length === 0) mesh.dispose(); | ||
} | ||
// Where simplification removes meshes, we may need to prune leaf nodes. | ||
await document.transform( | ||
prune({ keepLeaves: false, propertyTypes: [PropertyType.ACCESSOR, PropertyType.NODE] }), | ||
); | ||
// Where multiple primitive indices point into the same vertex streams, simplification | ||
@@ -156,3 +166,3 @@ // may write duplicate streams. Find and remove the duplicates after processing. | ||
options.error, | ||
options.lockBorder ? ['LockBorder'] : [] | ||
options.lockBorder ? ['LockBorder'] : [], | ||
); | ||
@@ -159,0 +169,0 @@ |
@@ -6,4 +6,7 @@ import { BufferUtils, Document, ImageUtils, Texture, TextureChannel, Transform, vec2 } from '@gltf-transform/core'; | ||
import type sharp from 'sharp'; | ||
import { createTransform, formatBytes } from './utils.js'; | ||
import { createTransform, fitWithin, formatBytes } from './utils.js'; | ||
import { TextureResizeFilter } from './texture-resize.js'; | ||
import { getPixels, savePixels } from 'ndarray-pixels'; | ||
import ndarray from 'ndarray'; | ||
import { lanczos2, lanczos3 } from 'ndarray-lanczos'; | ||
@@ -18,5 +21,7 @@ const NAME = 'textureCompress'; | ||
/** Instance of the Sharp encoder, which must be installed from the | ||
* 'sharp' package and provided by the caller. | ||
* 'sharp' package and provided by the caller. When not provided, a | ||
* platform-specific fallback implementation will be used, and most | ||
* quality- and compression-related options are ignored. | ||
*/ | ||
encoder: unknown; | ||
encoder?: unknown; | ||
/** | ||
@@ -44,7 +49,16 @@ * Target image format. If specified, included textures in other formats | ||
quality?: number | null; | ||
/** Level of CPU effort to reduce file size, 0-100. PNG, WebP, and AVIF only. Default: auto. */ | ||
/** | ||
* Level of CPU effort to reduce file size, 0-100. PNG, WebP, and AVIF | ||
* only. Supported only when a Sharp encoder is provided. Default: auto. | ||
*/ | ||
effort?: number | null; | ||
/** Use lossless compression mode. WebP and AVIF only. Default: false. */ | ||
/** | ||
* Use lossless compression mode. WebP and AVIF only. Supported only when a | ||
* Sharp encoder is provided. Default: false. | ||
*/ | ||
lossless?: boolean; | ||
/** Use near lossless compression mode. WebP only. Default: false. */ | ||
/** | ||
* Use near lossless compression mode. WebP only. Supported only when a | ||
* Sharp encoder is provided. Default: false. | ||
*/ | ||
nearLossless?: boolean; | ||
@@ -55,13 +69,13 @@ } | ||
export const TEXTURE_COMPRESS_DEFAULTS: Required<Omit<TextureCompressOptions, 'resize' | 'targetFormat' | 'encoder'>> = | ||
{ | ||
resizeFilter: TextureResizeFilter.LANCZOS3, | ||
pattern: null, | ||
formats: null, | ||
slots: null, | ||
quality: null, | ||
effort: null, | ||
lossless: false, | ||
nearLossless: false, | ||
}; | ||
// IMPORTANT: No defaults for quality flags, see https://github.com/donmccurdy/glTF-Transform/issues/969. | ||
export const TEXTURE_COMPRESS_DEFAULTS: Omit<TextureCompressOptions, 'resize' | 'targetFormat' | 'encoder'> = { | ||
resizeFilter: TextureResizeFilter.LANCZOS3, | ||
pattern: undefined, | ||
formats: undefined, | ||
slots: undefined, | ||
quality: undefined, | ||
effort: undefined, | ||
lossless: false, | ||
nearLossless: false, | ||
}; | ||
@@ -71,3 +85,6 @@ /** | ||
* | ||
* Requires `sharp`, and is available only in Node.js environments. | ||
* For best results use a Node.js environment, install the `sharp` module, and | ||
* provide an encoder. When the encoder is omitted — `sharp` works only in Node.js — | ||
* the implementation will use a platform-specific fallback encoder, and most | ||
* quality- and compression-related options are ignored. | ||
* | ||
@@ -93,2 +110,8 @@ * Example: | ||
* ); | ||
* | ||
* // (C) Resize and convert images to WebP in a browser, without a Sharp | ||
* // encoder. Most quality- and compression-related options are ignored. | ||
* await document.transform( | ||
* textureCompress({ targetFormat: 'webp', resize: [1024, 1024] }) | ||
* ); | ||
* ``` | ||
@@ -100,3 +123,2 @@ * | ||
const options = { ...TEXTURE_COMPRESS_DEFAULTS, ..._options } as Required<TextureCompressOptions>; | ||
const encoder = options.encoder as typeof sharp | null; | ||
const targetFormat = options.targetFormat as Format | undefined; | ||
@@ -107,6 +129,2 @@ const patternRe = options.pattern; | ||
if (!encoder) { | ||
throw new Error(`${targetFormat}: encoder dependency required — install "sharp".`); | ||
} | ||
return createTransform(NAME, async (document: Document): Promise<void> => { | ||
@@ -161,3 +179,3 @@ const logger = document.getLogger(); | ||
logger.debug(`${prefix}: Size = ${formatBytes(srcByteLength)} → ${formatBytes(dstByteLength)}${flag}`); | ||
}) | ||
}), | ||
); | ||
@@ -188,3 +206,6 @@ | ||
* | ||
* Requires `sharp`, and is available only in Node.js environments. | ||
* For best results use a Node.js environment, install the `sharp` module, and | ||
* provide an encoder. When the encoder is omitted — `sharp` works only in Node.js — | ||
* the implementation will use a platform-specific fallback encoder, and most | ||
* quality- and compression-related options are ignored. | ||
* | ||
@@ -200,2 +221,3 @@ * Example: | ||
* | ||
* // (A) Node.js. | ||
* await compressTexture(texture, { | ||
@@ -206,2 +228,8 @@ * encoder: sharp, | ||
* }); | ||
* | ||
* // (B) Web. | ||
* await compressTexture(texture, { | ||
* targetFormat: 'webp', | ||
* resize: [1024, 1024] | ||
* }); | ||
* ``` | ||
@@ -213,6 +241,2 @@ */ | ||
if (!encoder) { | ||
throw new Error(`${options.targetFormat}: encoder dependency required — install "sharp".`); | ||
} | ||
const srcFormat = getFormat(texture); | ||
@@ -223,4 +247,36 @@ const dstFormat = options.targetFormat || srcFormat; | ||
const srcImage = texture.getImage()!; | ||
const dstImage = encoder | ||
? await _encodeWithSharp(srcImage, srcMimeType, dstMimeType, options) | ||
: await _encodeWithNdarrayPixels(srcImage, srcMimeType, dstMimeType, options); | ||
const srcByteLength = srcImage.byteLength; | ||
const dstByteLength = dstImage.byteLength; | ||
if (srcMimeType === dstMimeType && dstByteLength >= srcByteLength && !options.resize) { | ||
// Skip if src/dst formats match and dst is larger than the original. | ||
return; | ||
} else if (srcMimeType === dstMimeType) { | ||
// Overwrite if src/dst formats match and dst is smaller than the original. | ||
texture.setImage(dstImage); | ||
} else { | ||
// Overwrite, then update path and MIME type if src/dst formats differ. | ||
const srcExtension = ImageUtils.mimeTypeToExtension(srcMimeType); | ||
const dstExtension = ImageUtils.mimeTypeToExtension(dstMimeType); | ||
const dstURI = texture.getURI().replace(new RegExp(`\\.${srcExtension}$`), `.${dstExtension}`); | ||
texture.setImage(dstImage).setMimeType(dstMimeType).setURI(dstURI); | ||
} | ||
} | ||
async function _encodeWithSharp( | ||
srcImage: Uint8Array, | ||
_srcMimeType: string, | ||
dstMimeType: string, | ||
options: Required<CompressTextureOptions>, | ||
): Promise<Uint8Array> { | ||
const encoder = options.encoder as typeof sharp; | ||
let encoderOptions: sharp.JpegOptions | sharp.PngOptions | sharp.WebpOptions | sharp.AvifOptions = {}; | ||
const dstFormat = getFormatFromMimeType(dstMimeType); | ||
switch (dstFormat) { | ||
@@ -253,6 +309,4 @@ case 'jpeg': | ||
const srcImage = texture.getImage()!; | ||
const instance = encoder(srcImage).toFormat(dstFormat, encoderOptions); | ||
// Resize. | ||
if (options.resize) { | ||
@@ -266,24 +320,31 @@ instance.resize(options.resize[0], options.resize[1], { | ||
const dstImage = BufferUtils.toView(await instance.toBuffer()); | ||
return BufferUtils.toView(await instance.toBuffer()); | ||
} | ||
const srcByteLength = srcImage.byteLength; | ||
const dstByteLength = dstImage.byteLength; | ||
async function _encodeWithNdarrayPixels( | ||
srcImage: Uint8Array, | ||
srcMimeType: string, | ||
dstMimeType: string, | ||
options: Required<CompressTextureOptions>, | ||
): Promise<Uint8Array> { | ||
const srcPixels = (await getPixels(srcImage, srcMimeType)) as ndarray.NdArray<Uint8Array>; | ||
if (srcMimeType === dstMimeType && dstByteLength >= srcByteLength) { | ||
// Skip if src/dst formats match and dst is larger than the original. | ||
return; | ||
} else if (srcMimeType === dstMimeType) { | ||
// Overwrite if src/dst formats match and dst is smaller than the original. | ||
texture.setImage(dstImage); | ||
} else { | ||
// Overwrite, then update path and MIME type if src/dst formats differ. | ||
const srcExtension = ImageUtils.mimeTypeToExtension(srcMimeType); | ||
const dstExtension = ImageUtils.mimeTypeToExtension(dstMimeType); | ||
const dstURI = texture.getURI().replace(new RegExp(`\\.${srcExtension}$`), `.${dstExtension}`); | ||
texture.setImage(dstImage).setMimeType(dstMimeType).setURI(dstURI); | ||
if (options.resize) { | ||
const [w, h] = srcPixels.shape; | ||
const dstSize = fitWithin([w, h], options.resize); | ||
const dstPixels = ndarray(new Uint8Array(dstSize[0] * dstSize[1] * 4), [...dstSize, 4]); | ||
options.resizeFilter === TextureResizeFilter.LANCZOS3 | ||
? lanczos3(srcPixels, dstPixels) | ||
: lanczos2(srcPixels, dstPixels); | ||
return savePixels(dstPixels, dstMimeType); | ||
} | ||
return savePixels(srcPixels, dstMimeType); | ||
} | ||
function getFormat(texture: Texture): Format { | ||
const mimeType = texture.getMimeType(); | ||
return getFormatFromMimeType(texture.getMimeType()); | ||
} | ||
function getFormatFromMimeType(mimeType: string): Format { | ||
const format = mimeType.split('/').pop() as Format | undefined; | ||
@@ -296,5 +357,5 @@ if (!format || !FORMATS.includes(format)) { | ||
function remap(value: number | null | undefined, srcMax: number, dstMax: number): number | null { | ||
if (value == null) return null; | ||
function remap(value: number | null | undefined, srcMax: number, dstMax: number): number | undefined { | ||
if (value == null) return undefined; | ||
return Math.round((value / srcMax) * dstMax); | ||
} |
import ndarray from 'ndarray'; | ||
import { lanczos2, lanczos3 } from 'ndarray-lanczos'; | ||
import { getPixels, savePixels } from 'ndarray-pixels'; | ||
import type { Document, Transform, vec2 } from '@gltf-transform/core'; | ||
import { MathUtils, type Document, type Transform, type vec2 } from '@gltf-transform/core'; | ||
import { listTextureSlots } from './list-texture-slots.js'; | ||
import { createTransform } from './utils.js'; | ||
import { createTransform, fitWithin } from './utils.js'; | ||
@@ -46,4 +46,6 @@ const NAME = 'textureResize'; | ||
* package, which works in Web and Node.js environments. For a faster and more robust implementation | ||
* based on Sharp (available only in Node.js), use {@link textureCompress} with the 'resize' option. | ||
* in Node.js, use {@link textureCompress}, providing a Sharp encoder and 'resize' options instead. | ||
* | ||
* @deprecated Prefer {@link textureCompress}, instead. | ||
* @privateRemarks TODO(v4): Remove this function, using `textureCompress()` instead. | ||
* @category Transforms | ||
@@ -77,6 +79,6 @@ */ | ||
const [maxWidth, maxHeight] = options.size; | ||
const [srcWidth, srcHeight] = texture.getSize()!; | ||
const srcSize = texture.getSize()!; | ||
const dstSize = fitWithin(srcSize, options.size); | ||
if (srcWidth <= maxWidth && srcHeight <= maxHeight) { | ||
if (MathUtils.eq(srcSize, dstSize)) { | ||
logger.debug(`${NAME}: Skipping, not within size range.`); | ||
@@ -86,18 +88,5 @@ continue; | ||
let dstWidth = srcWidth; | ||
let dstHeight = srcHeight; | ||
if (dstWidth > maxWidth) { | ||
dstHeight = Math.floor(dstHeight * (maxWidth / dstWidth)); | ||
dstWidth = maxWidth; | ||
} | ||
if (dstHeight > maxHeight) { | ||
dstWidth = Math.floor(dstWidth * (maxHeight / dstHeight)); | ||
dstHeight = maxHeight; | ||
} | ||
const srcImage = texture.getImage()!; | ||
const srcPixels = (await getPixels(srcImage, texture.getMimeType())) as ndarray.NdArray<Uint8Array>; | ||
const dstPixels = ndarray(new Uint8Array(dstWidth * dstHeight * 4), [dstWidth, dstHeight, 4]); | ||
const dstPixels = ndarray(new Uint8Array(dstSize[0] * dstSize[1] * 4), [...dstSize, 4]); | ||
@@ -104,0 +93,0 @@ logger.debug(`${NAME}: Resizing "${uri || name}", ${srcPixels.shape} → ${dstPixels.shape}...`); |
@@ -1,2 +0,2 @@ | ||
import type { vec3, vec4, mat4, Accessor, Primitive } from '@gltf-transform/core'; | ||
import { vec3, vec4, mat4, Accessor, Primitive } from '@gltf-transform/core'; | ||
import { create as createMat3, fromMat4, invert, transpose } from 'gl-matrix/mat3'; | ||
@@ -6,2 +6,4 @@ import { create as createVec3, normalize as normalizeVec3, transformMat3, transformMat4 } from 'gl-matrix/vec3'; | ||
import { createIndices } from './utils.js'; | ||
import { weldPrimitive } from './weld.js'; | ||
import { determinant } from 'gl-matrix/mat4'; | ||
@@ -66,2 +68,8 @@ /** | ||
// Reverse winding order if scale is negative. | ||
// See: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/NegativeScaleTest | ||
if (determinant(matrix) < 0) { | ||
reversePrimitiveWindingOrder(prim); | ||
} | ||
// Update mask. | ||
@@ -140,1 +148,14 @@ for (let i = 0; i < indices.length; i++) skipIndices.add(indices[i]); | ||
} | ||
function reversePrimitiveWindingOrder(prim: Primitive) { | ||
if (prim.getMode() !== Primitive.Mode.TRIANGLES) return; | ||
if (!prim.getIndices()) weldPrimitive(prim, { tolerance: 0 }); | ||
const indices = prim.getIndices()!; | ||
for (let i = 0, il = indices.getCount(); i < il; i += 3) { | ||
const a = indices.getScalar(i); | ||
const c = indices.getScalar(i + 2); | ||
indices.setScalar(i, c); | ||
indices.setScalar(i + 2, a); | ||
} | ||
} |
@@ -12,2 +12,3 @@ import type { NdArray } from 'ndarray'; | ||
TransformContext, | ||
vec2, | ||
} from '@gltf-transform/core'; | ||
@@ -41,3 +42,3 @@ | ||
target: Texture, | ||
fn: (pixels: NdArray, i: number, j: number) => void | ||
fn: (pixels: NdArray, i: number, j: number) => void, | ||
): Promise<Texture | null> { | ||
@@ -171,2 +172,13 @@ if (!source) return null; | ||
/** @hidden */ | ||
export function shallowEqualsArray(a: ArrayLike<unknown> | null, b: ArrayLike<unknown> | null) { | ||
if (a == null && b == null) return true; | ||
if (a == null || b == null) return false; | ||
if (a.length !== b.length) return false; | ||
for (let i = 0; i < a.length; i++) { | ||
if (a[i] !== b[i]) return false; | ||
} | ||
return true; | ||
} | ||
/** @hidden */ | ||
export function remapAttribute(attribute: Accessor, remap: Uint32Array, dstCount: number) { | ||
@@ -241,1 +253,24 @@ const elementSize = attribute.getElementSize(); | ||
} | ||
/** @hidden */ | ||
export function fitWithin(size: vec2, limit: vec2): vec2 { | ||
const [maxWidth, maxHeight] = limit; | ||
const [srcWidth, srcHeight] = size; | ||
if (srcWidth <= maxWidth && srcHeight <= maxHeight) return size; | ||
let dstWidth = srcWidth; | ||
let dstHeight = srcHeight; | ||
if (dstWidth > maxWidth) { | ||
dstHeight = Math.floor(dstHeight * (maxWidth / dstWidth)); | ||
dstWidth = maxWidth; | ||
} | ||
if (dstHeight > maxHeight) { | ||
dstWidth = Math.floor(dstWidth * (maxHeight / dstHeight)); | ||
dstHeight = maxHeight; | ||
} | ||
return [dstWidth, dstHeight]; | ||
} |
@@ -104,17 +104,4 @@ import { | ||
export function weld(_options: WeldOptions = WELD_DEFAULTS): Transform { | ||
const options = { ...WELD_DEFAULTS, ..._options } as Required<WeldOptions>; | ||
const options = expandWeldOptions(_options); | ||
if (options.tolerance < 0 || options.tolerance > 0.1) { | ||
throw new Error(`${NAME}: Requires 0 ≤ tolerance ≤ 0.1`); | ||
} | ||
if (options.toleranceNormal < 0 || options.toleranceNormal > Math.PI / 2) { | ||
throw new Error(`${NAME}: Requires 0 ≤ toleranceNormal ≤ ${(Math.PI / 2).toFixed(2)}`); | ||
} | ||
if (options.tolerance > 0) { | ||
options.tolerance = Math.max(options.tolerance, Number.EPSILON); | ||
options.toleranceNormal = Math.max(options.toleranceNormal, Number.EPSILON); | ||
} | ||
return createTransform(NAME, async (doc: Document): Promise<void> => { | ||
@@ -133,6 +120,6 @@ const logger = doc.getLogger(); | ||
if (options.tolerance > 0) { | ||
// If tolerance is greater than 0, welding may remove a mesh, so we prune | ||
await doc.transform(prune({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.NODE] })); | ||
} | ||
if (options.tolerance > 0) { | ||
// If tolerance is greater than 0, welding may remove a mesh, so we prune | ||
await doc.transform(prune({ propertyTypes: [PropertyType.ACCESSOR, PropertyType.NODE] })); | ||
} | ||
@@ -154,3 +141,3 @@ await doc.transform(dedup({ propertyTypes: [PropertyType.ACCESSOR] })); | ||
* of the AABB's longest dimension. Other vertex attributes are also compared | ||
* during welding, with attribute-specific thresholds. For --tolerance=0, geometry | ||
* during welding, with attribute-specific thresholds. For tolerance=0, geometry | ||
* is indexed in place, without merging. | ||
@@ -167,14 +154,35 @@ * | ||
* for (const prim of mesh.listPrimitives()) { | ||
* weldPrimitive(document, prim, {tolerance: 0.0001}); | ||
* weldPrimitive(prim, {tolerance: 0.0001}); | ||
* } | ||
* ``` | ||
* | ||
* @privateRemarks TODO(v4): Remove the "Document" parameter. | ||
*/ | ||
export function weldPrimitive(doc: Document, prim: Primitive, options: Required<WeldOptions>): void { | ||
if (prim.getIndices() && !options.overwrite) return; | ||
if (prim.getMode() === Primitive.Mode.POINTS) return; | ||
if (options.tolerance === 0) { | ||
_indexPrimitive(doc, prim); | ||
export function weldPrimitive( | ||
a: Document | Primitive, | ||
b: Primitive | WeldOptions = WELD_DEFAULTS, | ||
c = WELD_DEFAULTS, | ||
): void { | ||
let _document: Document; | ||
let _prim: Primitive; | ||
let _options: Required<WeldOptions>; | ||
if (a instanceof Primitive) { | ||
const graph = a.getGraph(); | ||
_document = Document.fromGraph(graph)!; | ||
_prim = a; | ||
_options = expandWeldOptions(b as WeldOptions); | ||
} else { | ||
_weldPrimitive(doc, prim, options); | ||
_document = a; | ||
_prim = b as Primitive; | ||
_options = expandWeldOptions(c as WeldOptions); | ||
} | ||
if (_prim.getIndices() && !_options.overwrite) return; | ||
if (_prim.getMode() === Primitive.Mode.POINTS) return; | ||
if (_options.tolerance === 0) { | ||
_indexPrimitive(_document, _prim); | ||
} else { | ||
_weldPrimitive(_document, _prim, _options); | ||
} | ||
} | ||
@@ -327,3 +335,3 @@ | ||
reorder: number[], | ||
dstCount: number | ||
dstCount: number, | ||
): void { | ||
@@ -411,1 +419,20 @@ const dstAttrArray = createArrayOfType(srcAttr.getArray()!, dstCount * srcAttr.getElementSize()); | ||
} | ||
function expandWeldOptions(_options: WeldOptions): Required<WeldOptions> { | ||
const options = { ...WELD_DEFAULTS, ..._options } as Required<WeldOptions>; | ||
if (options.tolerance < 0 || options.tolerance > 0.1) { | ||
throw new Error(`${NAME}: Requires 0 ≤ tolerance ≤ 0.1`); | ||
} | ||
if (options.toleranceNormal < 0 || options.toleranceNormal > Math.PI / 2) { | ||
throw new Error(`${NAME}: Requires 0 ≤ toleranceNormal ≤ ${(Math.PI / 2).toFixed(2)}`); | ||
} | ||
if (options.tolerance > 0) { | ||
options.tolerance = Math.max(options.tolerance, Number.EPSILON); | ||
options.toleranceNormal = Math.max(options.toleranceNormal, Number.EPSILON); | ||
} | ||
return options; | ||
} |
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
1389027
7647
42