@aura3d/assets 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GLTFAnimationRuntime.d.ts +85 -0
- package/dist/GLTFAnimationRuntime.d.ts.map +1 -1
- package/dist/GLTFAnimationRuntime.js +143 -0
- package/dist/GLTFAnimationRuntime.js.map +1 -1
- package/dist/GLTFAutoFit.d.ts +61 -0
- package/dist/GLTFAutoFit.d.ts.map +1 -0
- package/dist/GLTFAutoFit.js +77 -0
- package/dist/GLTFAutoFit.js.map +1 -0
- package/dist/GLTFLoader.d.ts +21 -0
- package/dist/GLTFLoader.d.ts.map +1 -1
- package/dist/GLTFLoader.js +60 -15
- package/dist/GLTFLoader.js.map +1 -1
- package/dist/GLTFRenderResources.d.ts +9 -1
- package/dist/GLTFRenderResources.d.ts.map +1 -1
- package/dist/GLTFRenderResources.js +93 -4
- package/dist/GLTFRenderResources.js.map +1 -1
- package/dist/browser-index.d.ts +6 -4
- package/dist/browser-index.d.ts.map +1 -1
- package/dist/browser-index.js +3 -2
- package/dist/browser-index.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/GLTFAnimationRuntime.ts +195 -0
- package/src/GLTFAutoFit.ts +133 -0
- package/src/GLTFLoader.ts +74 -16
- package/src/GLTFRenderResources.ts +103 -4
- package/src/browser-index.ts +9 -4
- package/src/index.ts +12 -4
- package/tests/assets.test.ts +83 -1
package/src/GLTFLoader.ts
CHANGED
|
@@ -387,6 +387,8 @@ export interface GLTFRuntimeMaterialKey {
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
export interface GLTFMorphTargetAsset {
|
|
390
|
+
/** Morph target / blendshape name (from `mesh.extras.targetNames`), when authored. */
|
|
391
|
+
readonly name?: string;
|
|
390
392
|
readonly positions: readonly (readonly [number, number, number])[];
|
|
391
393
|
readonly normals: readonly (readonly [number, number, number])[];
|
|
392
394
|
readonly tangents: readonly (readonly [number, number, number])[];
|
|
@@ -842,13 +844,15 @@ export class GLTFLoader implements AssetLoader<GLTFAsset> {
|
|
|
842
844
|
const typedTangents = tangents.map((tangent) => [tangent[0] ?? 0, tangent[1] ?? 0, tangent[2] ?? 0, tangent[3] ?? 1] as const);
|
|
843
845
|
const typedColors = colors.map((color) => [color[0] ?? 1, color[1] ?? 1, color[2] ?? 1, color[3] ?? 1] as const);
|
|
844
846
|
const typedJoints = joints.map((joint) => [joint[0] ?? 0, joint[1] ?? 0, joint[2] ?? 0, joint[3] ?? 0] as const);
|
|
847
|
+
const weightNormalization: SkinWeightNormalizationStats = { degenerate: 0, rescaled: 0 };
|
|
845
848
|
const typedWeights = weights.map((weight) => normalizeSkinWeights([
|
|
846
849
|
weight[0] ?? 0,
|
|
847
850
|
weight[1] ?? 0,
|
|
848
851
|
weight[2] ?? 0,
|
|
849
852
|
weight[3] ?? 0
|
|
850
|
-
],
|
|
851
|
-
|
|
853
|
+
], weightNormalization));
|
|
854
|
+
reportSkinWeightNormalization(meshIndex, primitiveIndex, weightNormalization);
|
|
855
|
+
const morphTargets = readMorphTargets(json, buffers, primitive, positions.length, meshIndex, primitiveIndex, accessorCache, readMorphTargetNames(mesh));
|
|
852
856
|
const resolvedPrimitive = resolvePrimitiveMode(primitive, indices?.map((index) => index[0] ?? 0), positions.length, meshIndex, primitiveIndex);
|
|
853
857
|
const geometry = createGeometryAsset(typedPositions, resolvedPrimitive.indices);
|
|
854
858
|
return {
|
|
@@ -1459,7 +1463,8 @@ function readMorphTargets(
|
|
|
1459
1463
|
vertexCount: number,
|
|
1460
1464
|
meshIndex: number,
|
|
1461
1465
|
primitiveIndex: number,
|
|
1462
|
-
accessorCache: GLTFAccessorReadCache
|
|
1466
|
+
accessorCache: GLTFAccessorReadCache,
|
|
1467
|
+
targetNames: readonly string[] = []
|
|
1463
1468
|
): readonly GLTFMorphTargetAsset[] {
|
|
1464
1469
|
if (primitive.targets === undefined) {
|
|
1465
1470
|
return [];
|
|
@@ -1481,7 +1486,9 @@ function readMorphTargets(
|
|
|
1481
1486
|
if (tangents.length > 0 && tangents.length !== vertexCount) {
|
|
1482
1487
|
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} morph target ${targetIndex} TANGENT count mismatch`);
|
|
1483
1488
|
}
|
|
1489
|
+
const name = targetNames[targetIndex];
|
|
1484
1490
|
return {
|
|
1491
|
+
...(typeof name === "string" && name.length > 0 ? { name } : {}),
|
|
1485
1492
|
positions: positions.map((position) => [position[0] ?? 0, position[1] ?? 0, position[2] ?? 0] as const),
|
|
1486
1493
|
normals: normals.map((normal) => [normal[0] ?? 0, normal[1] ?? 0, normal[2] ?? 0] as const),
|
|
1487
1494
|
tangents: tangents.map((tangent) => [tangent[0] ?? 0, tangent[1] ?? 0, tangent[2] ?? 0] as const)
|
|
@@ -1489,6 +1496,17 @@ function readMorphTargets(
|
|
|
1489
1496
|
});
|
|
1490
1497
|
}
|
|
1491
1498
|
|
|
1499
|
+
// glTF stores morph-target names in `mesh.extras.targetNames` (a parallel array). Read them so the
|
|
1500
|
+
// runtime can expose named morph influences instead of bare indices.
|
|
1501
|
+
function readMorphTargetNames(mesh: GLTFMesh): readonly string[] {
|
|
1502
|
+
const extras = (mesh as { extras?: Record<string, unknown> }).extras;
|
|
1503
|
+
const names = extras?.targetNames;
|
|
1504
|
+
if (Array.isArray(names) && names.every((n) => typeof n === "string")) {
|
|
1505
|
+
return names as readonly string[];
|
|
1506
|
+
}
|
|
1507
|
+
return [];
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1492
1510
|
function validateMorphTargetDescriptor(
|
|
1493
1511
|
target: Readonly<Record<string, number>>,
|
|
1494
1512
|
meshIndex: number,
|
|
@@ -1866,27 +1884,67 @@ function validateQuantizedAttributeComponentType(
|
|
|
1866
1884
|
);
|
|
1867
1885
|
}
|
|
1868
1886
|
|
|
1869
|
-
|
|
1887
|
+
export interface SkinWeightNormalizationStats {
|
|
1888
|
+
degenerate: number;
|
|
1889
|
+
rescaled: number;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const SKIN_WEIGHT_DEGENERATE_EPSILON = 1e-6;
|
|
1893
|
+
|
|
1894
|
+
/**
|
|
1895
|
+
* Normalize a single vertex's 4 skin weights so they sum to 1.
|
|
1896
|
+
*
|
|
1897
|
+
* Real-world catalog GLBs (Objaverse/Sketchfab) frequently ship denormalized,
|
|
1898
|
+
* negative, or all-zero WEIGHTS_0 values. Rather than hard-failing the whole
|
|
1899
|
+
* asset, sanitize per vertex: negative/non-finite components are clamped to 0,
|
|
1900
|
+
* a degenerate (near-zero) sum collapses to a rigid binding `[1, 0, 0, 0]`, and
|
|
1901
|
+
* any other sum is rescaled to 1. The optional `stats` accumulator lets callers
|
|
1902
|
+
* surface a single diagnostic per primitive instead of one per vertex.
|
|
1903
|
+
*
|
|
1904
|
+
* Component proportions are preserved regardless of the source accessor
|
|
1905
|
+
* componentType (FLOAT 5126, or normalized UNSIGNED_BYTE 5121 / UNSIGNED_SHORT
|
|
1906
|
+
* 5123), so integer-encoded weights normalize correctly without extra handling.
|
|
1907
|
+
*/
|
|
1908
|
+
export function normalizeSkinWeights(
|
|
1870
1909
|
weights: readonly [number, number, number, number],
|
|
1871
|
-
|
|
1872
|
-
primitiveIndex: number
|
|
1910
|
+
stats?: SkinWeightNormalizationStats
|
|
1873
1911
|
): readonly [number, number, number, number] {
|
|
1874
|
-
|
|
1875
|
-
|
|
1912
|
+
const sanitized: [number, number, number, number] = [
|
|
1913
|
+
Number.isFinite(weights[0]) && weights[0] > 0 ? weights[0] : 0,
|
|
1914
|
+
Number.isFinite(weights[1]) && weights[1] > 0 ? weights[1] : 0,
|
|
1915
|
+
Number.isFinite(weights[2]) && weights[2] > 0 ? weights[2] : 0,
|
|
1916
|
+
Number.isFinite(weights[3]) && weights[3] > 0 ? weights[3] : 0
|
|
1917
|
+
];
|
|
1918
|
+
const sum = sanitized[0] + sanitized[1] + sanitized[2] + sanitized[3];
|
|
1919
|
+
if (sum < SKIN_WEIGHT_DEGENERATE_EPSILON) {
|
|
1920
|
+
if (stats) stats.degenerate += 1;
|
|
1921
|
+
return [1, 0, 0, 0] as const;
|
|
1876
1922
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
throw new Error(`glTF mesh ${meshIndex} primitive ${primitiveIndex} WEIGHTS_0 must sum to a positive value`);
|
|
1923
|
+
if (Math.abs(sum - 1) <= 1e-5) {
|
|
1924
|
+
return sanitized as readonly [number, number, number, number];
|
|
1880
1925
|
}
|
|
1881
|
-
if (
|
|
1926
|
+
if (stats) stats.rescaled += 1;
|
|
1882
1927
|
return [
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1928
|
+
sanitized[0] / sum,
|
|
1929
|
+
sanitized[1] / sum,
|
|
1930
|
+
sanitized[2] / sum,
|
|
1931
|
+
sanitized[3] / sum
|
|
1887
1932
|
] as const;
|
|
1888
1933
|
}
|
|
1889
1934
|
|
|
1935
|
+
function reportSkinWeightNormalization(
|
|
1936
|
+
meshIndex: number,
|
|
1937
|
+
primitiveIndex: number,
|
|
1938
|
+
stats: SkinWeightNormalizationStats
|
|
1939
|
+
): void {
|
|
1940
|
+
if (stats.degenerate === 0 && stats.rescaled === 0) return;
|
|
1941
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
1942
|
+
console.warn(
|
|
1943
|
+
`glTF mesh ${meshIndex} primitive ${primitiveIndex} WEIGHTS_0 normalized: ` +
|
|
1944
|
+
`${stats.degenerate} degenerate vertex(es) set to [1,0,0,0], ${stats.rescaled} rescaled to sum 1`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1890
1948
|
function createCameraAssets(json: GLTFJson): readonly GLTFCameraAsset[] {
|
|
1891
1949
|
return (json.cameras ?? []).map((camera, index) => {
|
|
1892
1950
|
if (camera.type === "perspective") {
|
|
@@ -80,6 +80,13 @@ export interface GLTFRenderResourceOptions {
|
|
|
80
80
|
readonly sceneIndex?: GLTFSceneCreateOptions["sceneIndex"];
|
|
81
81
|
readonly sceneName?: GLTFSceneCreateOptions["sceneName"];
|
|
82
82
|
readonly materialRenderStateOverrides?: readonly GLTFMaterialRenderStateOverride[];
|
|
83
|
+
/**
|
|
84
|
+
* When `true`, a texture that fails to fetch/decode aborts the whole asset load
|
|
85
|
+
* (legacy behavior). When omitted/`false` (default), the loader logs a warning
|
|
86
|
+
* and substitutes a neutral 1x1 texture so the material falls back to its
|
|
87
|
+
* `baseColorFactor` and the rest of the asset still loads.
|
|
88
|
+
*/
|
|
89
|
+
readonly failOnMissingTexture?: boolean;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
export interface GLTFMaterialRenderStateOverride {
|
|
@@ -392,7 +399,16 @@ export async function createGLTFRenderResources(
|
|
|
392
399
|
return texture;
|
|
393
400
|
} catch (error) {
|
|
394
401
|
textureByImage.delete(cacheKey);
|
|
395
|
-
|
|
402
|
+
if (options.failOnMissingTexture) {
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
406
|
+
warnMissingGLTFTexture(textureAsset.name, image.uri, reason);
|
|
407
|
+
const fallback = createFallbackTexture(textureAsset.name, colorSpace);
|
|
408
|
+
const fallbackPromise = Promise.resolve(fallback);
|
|
409
|
+
textureByImage.set(cacheKey, fallbackPromise);
|
|
410
|
+
setTextureLibraryEntry(textureLibrary, textureAsset.name, colorSpace, fallback);
|
|
411
|
+
return fallback;
|
|
396
412
|
}
|
|
397
413
|
};
|
|
398
414
|
|
|
@@ -1534,7 +1550,15 @@ function createDefaultGLTFMaterial(mesh: GLTFMeshAsset, options: { readonly inst
|
|
|
1534
1550
|
return new PBRMaterial(defaults);
|
|
1535
1551
|
}
|
|
1536
1552
|
|
|
1537
|
-
|
|
1553
|
+
/**
|
|
1554
|
+
* baseColorFactor alpha at or above this value is treated as fully opaque when
|
|
1555
|
+
* deciding whether a `BLEND` material is a spurious-blend (ghost) case. 8-bit
|
|
1556
|
+
* authoring rounds 255/255 to 1.0 but tools occasionally emit 0.996 (254/255),
|
|
1557
|
+
* so we allow a small epsilon while still catching genuine fades.
|
|
1558
|
+
*/
|
|
1559
|
+
const BLEND_OPAQUE_ALPHA_THRESHOLD = 0.996;
|
|
1560
|
+
|
|
1561
|
+
export function renderStateForGLTFMaterial(
|
|
1538
1562
|
material: GLTFMaterialAsset,
|
|
1539
1563
|
overrides: readonly GLTFMaterialRenderStateOverride[] = []
|
|
1540
1564
|
): Partial<RenderState> {
|
|
@@ -1550,9 +1574,40 @@ function renderStateForGLTFMaterial(
|
|
|
1550
1574
|
}
|
|
1551
1575
|
|
|
1552
1576
|
function requiresTransparentRenderState(material: GLTFMaterialAsset): boolean {
|
|
1553
|
-
if (material.alphaMode === "BLEND") return
|
|
1577
|
+
if (material.alphaMode === "BLEND") return !isEffectivelyOpaqueBlendMaterial(material);
|
|
1554
1578
|
if (material.alphaMode !== "OPAQUE") return false;
|
|
1555
1579
|
if (usesUnbackedScalarTransmission(material)) return false;
|
|
1580
|
+
return materialHasTransmissionOrVolume(material);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* Many catalog GLBs (notably Sketchfab exports) spuriously tag fully opaque
|
|
1585
|
+
* materials with `alphaMode: "BLEND"`. The standard glTF blend equation over a
|
|
1586
|
+
* fully opaque surface (baseColorFactor alpha == 1, no texel alpha in use) is a
|
|
1587
|
+
* no-op, but enabling blend disables depth writes — so those characters render
|
|
1588
|
+
* as translucent "ghosts" with broken depth sorting.
|
|
1589
|
+
*
|
|
1590
|
+
* When a BLEND material is provably opaque we treat it as OPAQUE (blend off,
|
|
1591
|
+
* depthWrite on). The check is deliberately conservative: anything that could
|
|
1592
|
+
* carry real transparency — alpha factor < 1, transmission/volume, or a
|
|
1593
|
+
* meaningful alphaCutoff — keeps blending.
|
|
1594
|
+
*
|
|
1595
|
+
* Known edge case: a baseColorTexture's own alpha channel cannot be inspected
|
|
1596
|
+
* here (images are decoded asynchronously and channel-usage is not tracked on
|
|
1597
|
+
* the material), so a BLEND material that relies purely on per-texel texture
|
|
1598
|
+
* alpha while keeping baseColorFactor alpha == 1 would be flattened to opaque.
|
|
1599
|
+
* That pattern is rare in the wild (such assets normally use MASK or set the
|
|
1600
|
+
* factor alpha < 1), and the override path (renderStateOverrides /
|
|
1601
|
+
* GLTFMaterialRenderStateOverride) remains available to force blend back on.
|
|
1602
|
+
*/
|
|
1603
|
+
function isEffectivelyOpaqueBlendMaterial(material: GLTFMaterialAsset): boolean {
|
|
1604
|
+
if (material.alphaMode !== "BLEND") return false;
|
|
1605
|
+
if (material.baseColorFactor[3] < BLEND_OPAQUE_ALPHA_THRESHOLD) return false;
|
|
1606
|
+
if (materialHasTransmissionOrVolume(material)) return false;
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function materialHasTransmissionOrVolume(material: GLTFMaterialAsset): boolean {
|
|
1556
1611
|
return (material.transmission?.factor ?? 0) > 0.001
|
|
1557
1612
|
|| (material.diffuseTransmission?.factor ?? 0) > 0.001
|
|
1558
1613
|
|| (material.volume?.thicknessFactor ?? 0) > 0.001
|
|
@@ -2096,10 +2151,54 @@ function isKTX2BasisImage(image: GLTFImageAsset): boolean {
|
|
|
2096
2151
|
return image.mimeType === "image/ktx2" || /\.ktx2(?:[?#]|$)/i.test(image.uri ?? "");
|
|
2097
2152
|
}
|
|
2098
2153
|
|
|
2154
|
+
function createFallbackTexture(label: string, colorSpace: GLTFTextureColorSpace): Texture {
|
|
2155
|
+
return new Texture({
|
|
2156
|
+
width: 1,
|
|
2157
|
+
height: 1,
|
|
2158
|
+
colorSpace,
|
|
2159
|
+
label: `${label} (missing-texture-fallback)`,
|
|
2160
|
+
data: new Uint8Array([255, 255, 255, 255])
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
function warnMissingGLTFTexture(textureName: string, imageUri: string | undefined, reason: string): void {
|
|
2165
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
2166
|
+
const source = imageUri ? ` (${imageUri})` : "";
|
|
2167
|
+
console.warn(
|
|
2168
|
+
`glTF texture "${textureName}"${source} could not be loaded; falling back to baseColorFactor. Reason: ${reason}`
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2099
2172
|
function resolveImageUrl(assetUrl: string, imageUri: string): string {
|
|
2100
2173
|
if (/^(?:data:|blob:|https?:|file:)/i.test(imageUri)) return imageUri;
|
|
2101
2174
|
if (assetUrl.startsWith("data:")) {
|
|
2102
2175
|
throw new Error(`Relative glTF image uri ${imageUri} cannot be resolved from a data URL asset`);
|
|
2103
2176
|
}
|
|
2104
|
-
return new URL(imageUri, assetUrl).toString();
|
|
2177
|
+
return new URL(imageUri, resolveAbsoluteAssetBase(assetUrl)).toString();
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* Derive an absolute base URL for resolving relative texture URIs.
|
|
2182
|
+
*
|
|
2183
|
+
* Catalog assets are often loaded from root-relative paths (e.g.
|
|
2184
|
+
* `/aura-assets/x.glb`). `new URL(uri, base)` requires `base` to be absolute, so
|
|
2185
|
+
* a bare relative `assetUrl` throws `Failed to construct 'URL': Invalid base URL`.
|
|
2186
|
+
* If the asset URL is already absolute we use it directly; otherwise we resolve it
|
|
2187
|
+
* against the document/origin base when running in a browser, falling back to a
|
|
2188
|
+
* synthetic `file:///` origin so resolution never throws in non-browser contexts.
|
|
2189
|
+
*/
|
|
2190
|
+
function resolveAbsoluteAssetBase(assetUrl: string): string {
|
|
2191
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(assetUrl)) return assetUrl;
|
|
2192
|
+
const documentBase =
|
|
2193
|
+
(typeof document !== "undefined" && document.baseURI) ||
|
|
2194
|
+
(typeof location !== "undefined" && location.href) ||
|
|
2195
|
+
undefined;
|
|
2196
|
+
if (documentBase) {
|
|
2197
|
+
try {
|
|
2198
|
+
return new URL(assetUrl, documentBase).toString();
|
|
2199
|
+
} catch {
|
|
2200
|
+
// fall through to synthetic base below
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return new URL(assetUrl, "file:///").toString();
|
|
2105
2204
|
}
|
package/src/browser-index.ts
CHANGED
|
@@ -25,7 +25,9 @@ export type {
|
|
|
25
25
|
GLTFSceneAnalysisEvidence,
|
|
26
26
|
GLTFSceneAnalysisOptions
|
|
27
27
|
} from "./SceneAnalysisFixtures";
|
|
28
|
-
export { GLTFLoader } from "./GLTFLoader";
|
|
28
|
+
export { GLTFLoader, normalizeSkinWeights } from "./GLTFLoader";
|
|
29
|
+
export { autoFitGLTFScene, computeAutoFitTransform } from "./GLTFAutoFit";
|
|
30
|
+
export type { AutoFitOptions, AutoFitTransform, GLTFUpAxis } from "./GLTFAutoFit";
|
|
29
31
|
export type {
|
|
30
32
|
GLTFAsset,
|
|
31
33
|
GLTFCameraAsset,
|
|
@@ -57,7 +59,8 @@ export type {
|
|
|
57
59
|
GLTFTextureAsset,
|
|
58
60
|
GLTFTransmissionMaterialExtension,
|
|
59
61
|
GLTFVolumeMaterialExtension,
|
|
60
|
-
SerializedGLTFAsset
|
|
62
|
+
SerializedGLTFAsset,
|
|
63
|
+
SkinWeightNormalizationStats
|
|
61
64
|
} from "./GLTFLoader";
|
|
62
65
|
export { inspectGLTFAsset } from "./AssetInspection";
|
|
63
66
|
export type {
|
|
@@ -89,7 +92,7 @@ export type {
|
|
|
89
92
|
GLTFExtensionSupportFamily,
|
|
90
93
|
GLTFExtensionSupportStatus
|
|
91
94
|
} from "./GLTFExtensionSupport";
|
|
92
|
-
export { createGLTFSceneAnimationMixer, createGLTFSceneAnimationRuntime, GLTFImportedSkeletonIKController, GLTFSceneAnimationCloneSampler, GLTFSceneAnimationMixerBinding, GLTFSceneAnimationRuntime, GLTFSceneMorphTargetController } from "./GLTFAnimationRuntime";
|
|
95
|
+
export { createGLTFSceneAnimationMixer, createGLTFSceneAnimationRuntime, GLTFImportedSkeletonIKController, GLTFSceneAnimationCloneSampler, GLTFSceneAnimationMixerBinding, GLTFSceneAnimationRuntime, GLTFSceneMorphTargetController, resolveGLTFClipName } from "./GLTFAnimationRuntime";
|
|
93
96
|
export type {
|
|
94
97
|
GLTFImportedSkeletonIKControllerOptions,
|
|
95
98
|
GLTFImportedSkeletonIKControllerSnapshot,
|
|
@@ -108,7 +111,9 @@ export type {
|
|
|
108
111
|
GLTFSceneAnimationRuntimeOptions,
|
|
109
112
|
GLTFSceneAnimationRuntimeSnapshot,
|
|
110
113
|
GLTFSceneMorphTargetControllerOptions,
|
|
111
|
-
GLTFSceneMorphTargetControllerSnapshot
|
|
114
|
+
GLTFSceneMorphTargetControllerSnapshot,
|
|
115
|
+
GLTFScenePose,
|
|
116
|
+
GLTFScenePoseBoneTransform
|
|
112
117
|
} from "./GLTFAnimationRuntime";
|
|
113
118
|
export {
|
|
114
119
|
DEFAULT_GLTF_RENDER_ENVIRONMENT_LIGHTING,
|
package/src/index.ts
CHANGED
|
@@ -49,13 +49,15 @@ export type {
|
|
|
49
49
|
GLTFExtensionSupportFamily,
|
|
50
50
|
GLTFExtensionSupportStatus
|
|
51
51
|
} from "./GLTFExtensionSupport";
|
|
52
|
-
export { createGLTFSceneAnimationMixer, createGLTFSceneAnimationRuntime, GLTFImportedSkeletonIKController, GLTFSceneAnimationCloneSampler, GLTFSceneAnimationMixerBinding, GLTFSceneAnimationRuntime, GLTFSceneMorphTargetController } from "./GLTFAnimationRuntime";
|
|
52
|
+
export { createGLTFSceneAnimationMixer, createGLTFSceneAnimationRuntime, GLTFImportedSkeletonIKController, GLTFSceneAnimationCloneSampler, GLTFSceneAnimationMixerBinding, GLTFSceneAnimationRuntime, GLTFSceneMorphTargetController, resolveGLTFClipName } from "./GLTFAnimationRuntime";
|
|
53
53
|
export type {
|
|
54
54
|
GLTFImportedSkeletonIKControllerOptions,
|
|
55
55
|
GLTFImportedSkeletonIKControllerSnapshot,
|
|
56
56
|
GLTFImportedSkeletonIKOptions,
|
|
57
57
|
GLTFImportedSkeletonIKResult,
|
|
58
58
|
GLTFSceneAnimationApplyResult,
|
|
59
|
+
GLTFSceneAnimationClipSample,
|
|
60
|
+
GLTFSceneAnimationClipBoneMask,
|
|
59
61
|
GLTFSceneAnimationActionSnapshot,
|
|
60
62
|
GLTFSceneAnimationCloneSample,
|
|
61
63
|
GLTFSceneAnimationCloneSampleResult,
|
|
@@ -68,7 +70,9 @@ export type {
|
|
|
68
70
|
GLTFSceneAnimationRuntimeOptions,
|
|
69
71
|
GLTFSceneAnimationRuntimeSnapshot,
|
|
70
72
|
GLTFSceneMorphTargetControllerOptions,
|
|
71
|
-
GLTFSceneMorphTargetControllerSnapshot
|
|
73
|
+
GLTFSceneMorphTargetControllerSnapshot,
|
|
74
|
+
GLTFScenePose,
|
|
75
|
+
GLTFScenePoseBoneTransform
|
|
72
76
|
} from "./GLTFAnimationRuntime";
|
|
73
77
|
export { AssetHandle } from "./AssetHandle";
|
|
74
78
|
export type { AssetHandleOptions, AssetHandleStatus } from "./AssetHandle";
|
|
@@ -225,7 +229,9 @@ export type {
|
|
|
225
229
|
} from "./CarConceptMaterialStability";
|
|
226
230
|
export { ImageLoader } from "./ImageLoader";
|
|
227
231
|
export type { ImageAsset } from "./ImageLoader";
|
|
228
|
-
export { GLTFLoader } from "./GLTFLoader";
|
|
232
|
+
export { GLTFLoader, normalizeSkinWeights } from "./GLTFLoader";
|
|
233
|
+
export { autoFitGLTFScene, computeAutoFitTransform } from "./GLTFAutoFit";
|
|
234
|
+
export type { AutoFitOptions, AutoFitTransform, GLTFUpAxis } from "./GLTFAutoFit";
|
|
229
235
|
export type {
|
|
230
236
|
GLTFAsset,
|
|
231
237
|
GLTFCameraAsset,
|
|
@@ -238,6 +244,7 @@ export type {
|
|
|
238
244
|
GLTFLightAsset,
|
|
239
245
|
GLTFLoaderOptions,
|
|
240
246
|
GLTFLoaderDiagnostics,
|
|
247
|
+
SkinWeightNormalizationStats,
|
|
241
248
|
GLTFMaterialAsset,
|
|
242
249
|
GLTFMaterialVariantAsset,
|
|
243
250
|
GLTFMaterialVariantMappingAsset,
|
|
@@ -267,7 +274,8 @@ export {
|
|
|
267
274
|
createDefaultGLTFHdrStudioPreviewEnvironmentLighting,
|
|
268
275
|
createGLTFRenderResourceDiagnostics,
|
|
269
276
|
createGLTFRenderResources,
|
|
270
|
-
createGLTFRenderSource
|
|
277
|
+
createGLTFRenderSource,
|
|
278
|
+
renderStateForGLTFMaterial
|
|
271
279
|
} from "./GLTFRenderResources";
|
|
272
280
|
export type {
|
|
273
281
|
DecodedGLTFImage,
|
package/tests/assets.test.ts
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
AssetManager,
|
|
5
|
+
computeAutoFitTransform,
|
|
6
|
+
GLTFLoader,
|
|
7
|
+
normalizeSkinWeights,
|
|
8
|
+
OBJLoader,
|
|
9
|
+
resolveGLTFClipName,
|
|
10
|
+
type AssetLoader
|
|
11
|
+
} from "../src/index";
|
|
12
|
+
|
|
13
|
+
test("normalizeSkinWeights collapses degenerate (zero-sum) weights to a rigid binding", () => {
|
|
14
|
+
const stats = { degenerate: 0, rescaled: 0 };
|
|
15
|
+
assert.deepEqual(normalizeSkinWeights([0, 0, 0, 0], stats), [1, 0, 0, 0]);
|
|
16
|
+
assert.equal(stats.degenerate, 1);
|
|
17
|
+
assert.equal(stats.rescaled, 0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("normalizeSkinWeights rescales denormalized weights to sum 1 (FLOAT)", () => {
|
|
21
|
+
const stats = { degenerate: 0, rescaled: 0 };
|
|
22
|
+
const result = normalizeSkinWeights([2, 2, 0, 0], stats);
|
|
23
|
+
assert.deepEqual(result, [0.5, 0.5, 0, 0]);
|
|
24
|
+
assert.equal(stats.rescaled, 1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("normalizeSkinWeights rescales integer-encoded weights regardless of componentType scale", () => {
|
|
28
|
+
// UNSIGNED_BYTE-style raw values (sum 255) and UNSIGNED_SHORT-style (sum 65535)
|
|
29
|
+
// both normalize by proportion to the same result.
|
|
30
|
+
assert.deepEqual(normalizeSkinWeights([255, 0, 0, 0]), [1, 0, 0, 0]);
|
|
31
|
+
assert.deepEqual(normalizeSkinWeights([32767, 32768, 0, 0]).map((value) => Math.round(value * 100) / 100), [0.5, 0.5, 0, 0]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("normalizeSkinWeights clamps negative and non-finite components to zero", () => {
|
|
35
|
+
assert.deepEqual(normalizeSkinWeights([1, -1, Number.NaN, Number.POSITIVE_INFINITY]), [1, 0, 0, 0]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("normalizeSkinWeights leaves already-normalized weights untouched", () => {
|
|
39
|
+
const stats = { degenerate: 0, rescaled: 0 };
|
|
40
|
+
assert.deepEqual(normalizeSkinWeights([0.6, 0.4, 0, 0], stats), [0.6, 0.4, 0, 0]);
|
|
41
|
+
assert.equal(stats.degenerate, 0);
|
|
42
|
+
assert.equal(stats.rescaled, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("resolveGLTFClipName matches exact (case/format-insensitive) first", () => {
|
|
46
|
+
assert.equal(resolveGLTFClipName("Idle", ["Walk", "idle", "Run"]), "idle");
|
|
47
|
+
assert.equal(resolveGLTFClipName("walk-cycle", ["Run", "Walk_Cycle"]), "Walk_Cycle");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("resolveGLTFClipName resolves via synonym groups", () => {
|
|
51
|
+
assert.equal(resolveGLTFClipName("idle", ["Loops", "Sprint"]), "Loops");
|
|
52
|
+
assert.equal(resolveGLTFClipName("run", ["Loops", "Sprint"]), "Sprint");
|
|
53
|
+
assert.equal(resolveGLTFClipName("wave", ["Greet", "Static"]), "Greet");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("resolveGLTFClipName falls back to substring then first clip", () => {
|
|
57
|
+
assert.equal(resolveGLTFClipName("walk", ["WalkForward", "Run"]), "WalkForward");
|
|
58
|
+
assert.equal(resolveGLTFClipName("nonexistent", ["Take 001", "Other"]), "Take 001");
|
|
59
|
+
assert.equal(resolveGLTFClipName("anything", []), undefined);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("computeAutoFitTransform scales tall and tiny models to the target height", () => {
|
|
63
|
+
const big = computeAutoFitTransform({ min: [-900, 0, -900], max: [900, 1821, 900] }, { targetHeight: 1.6 });
|
|
64
|
+
assert.ok(Math.abs(big.fittedHeight - 1.6) < 1e-6);
|
|
65
|
+
assert.ok(big.scale < 0.01);
|
|
66
|
+
|
|
67
|
+
const tiny = computeAutoFitTransform({ min: [-0.5, 0, -0.5], max: [0.5, 1, 0.5] });
|
|
68
|
+
assert.ok(Math.abs(tiny.fittedHeight - 1.6) < 1e-6);
|
|
69
|
+
assert.ok(tiny.scale > 1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("computeAutoFitTransform recenters feet to y=0 and centers x/z", () => {
|
|
73
|
+
const fit = computeAutoFitTransform({ min: [2, 4, 6], max: [4, 6, 8] }, { targetHeight: 2 });
|
|
74
|
+
// height 2 -> scale 1; center (3,5,7), floor y=4
|
|
75
|
+
assert.equal(fit.scale, 1);
|
|
76
|
+
assert.deepEqual(fit.translation, [-3, -4, -7]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("computeAutoFitTransform applies z-up correction", () => {
|
|
80
|
+
const fit = computeAutoFitTransform({ min: [-1, -1, 0], max: [1, 1, 4] }, { upAxis: "z", targetHeight: 4 });
|
|
81
|
+
assert.equal(fit.upAxisCorrection, "z-to-y");
|
|
82
|
+
// after z->y rotation the source z-extent (4) becomes height -> scale 1
|
|
83
|
+
assert.ok(Math.abs(fit.fittedHeight - 4) < 1e-6);
|
|
84
|
+
assert.notDeepEqual(fit.rotation, [0, 0, 0, 1]);
|
|
85
|
+
});
|
|
4
86
|
|
|
5
87
|
test("AssetManager shares duplicate in-flight loads and releases cached handles", async () => {
|
|
6
88
|
let loads = 0;
|