@driftengine/assets 4.3.0 → 4.4.1
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/README.md +1 -1
- package/dist/bake/collapseHeap.d.ts +24 -0
- package/dist/bake/collapseHeap.js +80 -0
- package/dist/bake/collapseMeasures.d.ts +31 -0
- package/dist/bake/collapseMeasures.js +183 -0
- package/dist/bake/sceneField.d.ts +6 -0
- package/dist/bake/sceneField.js +48 -0
- package/dist/bake/sdf.d.ts +5 -5
- package/dist/bake/sdf.js +15 -95
- package/dist/bake/simplify.d.ts +42 -0
- package/dist/bake/simplify.js +350 -0
- package/dist/bake/surfaceDepth.d.ts +29 -0
- package/dist/bake/surfaceDepth.js +101 -0
- package/dist/downscale.d.ts +25 -0
- package/dist/downscale.js +50 -0
- package/dist/drawKey.d.ts +13 -1
- package/dist/drawKey.js +5 -3
- package/dist/drftLoader.d.ts +38 -7
- package/dist/drftLoader.js +186 -30
- package/dist/fieldPlacement.d.ts +22 -0
- package/dist/fieldPlacement.js +49 -0
- package/dist/gltf.d.ts +46 -1
- package/dist/gltf.js +101 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.js +15 -1
- package/dist/loadProgress.d.ts +22 -1
- package/dist/orient.d.ts +9 -1
- package/dist/orient.js +60 -2
- package/dist/partMaterial.d.ts +19 -0
- package/dist/partMaterial.js +16 -0
- package/dist/readModel.d.ts +3 -1
- package/dist/repeats.d.ts +39 -0
- package/dist/repeats.js +391 -0
- package/dist/weld.d.ts +8 -0
- package/dist/weld.js +155 -34
- package/package.json +4 -4
- package/src/bake/collapseHeap.ts +81 -0
- package/src/bake/collapseMeasures.ts +215 -0
- package/src/bake/sceneField.ts +50 -0
- package/src/bake/sdf.ts +14 -99
- package/src/bake/simplify.ts +395 -0
- package/src/bake/surfaceDepth.ts +102 -0
- package/src/downscale.ts +61 -0
- package/src/drawKey.ts +17 -3
- package/src/drftLoader.ts +224 -38
- package/src/fieldPlacement.ts +74 -0
- package/src/gltf.ts +139 -3
- package/src/index.ts +24 -1
- package/src/loadProgress.ts +23 -1
- package/src/orient.ts +62 -3
- package/src/partMaterial.ts +42 -0
- package/src/readModel.ts +3 -1
- package/src/repeats.ts +442 -0
- package/src/weld.ts +164 -34
package/dist/gltf.js
CHANGED
|
@@ -173,10 +173,32 @@ function normalMatrix(m) {
|
|
|
173
173
|
* textures may share one image with different samplers. Following the indirection here keeps
|
|
174
174
|
* the material's index meaning what the file said it meant.
|
|
175
175
|
*/
|
|
176
|
+
/**
|
|
177
|
+
* Which image a texture shows: glTF's `source` when there is one, otherwise an image-codec
|
|
178
|
+
* extension's.
|
|
179
|
+
*
|
|
180
|
+
* **The fallback wins when both exist**, because every reader can decode it and the baker
|
|
181
|
+
* identifies whatever bytes it gets. What that gives up is nothing a bake can use today: a WebP
|
|
182
|
+
* extension's image is smaller, but nothing here knows the two are the same picture. What would
|
|
183
|
+
* make it wrong is a baker that decodes AVIF, where the extension's image may be the better source.
|
|
184
|
+
*
|
|
185
|
+
* Basis is refused by name: it is a GPU transcoding format, and this engine uploads decoded images.
|
|
186
|
+
*/
|
|
187
|
+
function textureSource(texture, index, warnings) {
|
|
188
|
+
if (texture?.source !== undefined)
|
|
189
|
+
return texture.source;
|
|
190
|
+
const ext = texture?.extensions;
|
|
191
|
+
if (ext?.KHR_texture_basisu !== undefined) {
|
|
192
|
+
warnings.push(`texture ${index} is KHR_texture_basisu, which this reader does not transcode; export ` +
|
|
193
|
+
'the model with PNG, JPEG or WebP images');
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return ext?.EXT_texture_webp?.source ?? ext?.EXT_texture_avif?.source;
|
|
197
|
+
}
|
|
176
198
|
function readImages(doc, buffers, warnings) {
|
|
177
199
|
const out = [];
|
|
178
200
|
for (let i = 0; i < (doc.textures ?? []).length; i++) {
|
|
179
|
-
const source = doc.textures?.[i]
|
|
201
|
+
const source = textureSource(doc.textures?.[i], i, warnings);
|
|
180
202
|
const image = source === undefined ? undefined : doc.images?.[source];
|
|
181
203
|
if (image === undefined) {
|
|
182
204
|
warnings.push(`texture ${i} names no image this reader can find; it will draw untextured`);
|
|
@@ -385,6 +407,17 @@ function materialOf(material) {
|
|
|
385
407
|
const modulated = ormMap >= 0;
|
|
386
408
|
const metallic = modulated ? 0 : (pbr?.metallicFactor ?? 1);
|
|
387
409
|
const roughness = modulated ? DEFAULT_ROUGHNESS : (pbr?.roughnessFactor ?? 1);
|
|
410
|
+
/*
|
|
411
|
+
* **The same trap reached by the other door: a metal whose colour is an image.** `specular` is
|
|
412
|
+
* this engine's dielectric highlight and it is white, which is a metal's highlight only where the
|
|
413
|
+
* metal is white. Where the base colour is a texture that colour is per texel, the lane cannot
|
|
414
|
+
* say it, and the factor alone blew a dirt decal to white: its file names no metallic and no
|
|
415
|
+
* roughness, so the specification reads a rough metal of dark grime, and every wall it covered
|
|
416
|
+
* drew white under a low sun with the grime dark between. So the lane keeps the dielectric's
|
|
417
|
+
* zero there. What it gives up is the sun's glint on a textured metal with no map, which then
|
|
418
|
+
* shades as the grime or the paint its image says; what would change it is a metallic lane.
|
|
419
|
+
*/
|
|
420
|
+
const highlight = pbr?.baseColorTexture === undefined ? metallic : 0;
|
|
388
421
|
/*
|
|
389
422
|
* **glTF leaves `metallicRoughnessTexture`'s R channel undefined.** The specification assigns G
|
|
390
423
|
* to roughness and B to metallic and says nothing at all about R, so occlusion lives there only
|
|
@@ -402,13 +435,21 @@ function materialOf(material) {
|
|
|
402
435
|
return {
|
|
403
436
|
name: material?.name ?? '',
|
|
404
437
|
color: [base[0] ?? 1, base[1] ?? 1, base[2] ?? 1],
|
|
405
|
-
specular:
|
|
438
|
+
specular: highlight,
|
|
406
439
|
roughness,
|
|
407
440
|
emissive,
|
|
408
441
|
emissiveColor: emissive > 0
|
|
409
442
|
? [emissiveFactor[0] ?? 0, emissiveFactor[1] ?? 0, emissiveFactor[2] ?? 0]
|
|
410
443
|
: [-1, -1, -1],
|
|
411
444
|
opacity: blends ? Math.min(1, Math.max(0, base[3] ?? 1)) : 1,
|
|
445
|
+
/*
|
|
446
|
+
* glTF's `BLEND` says alpha is the factor *times the texture*. With a factor of 1 `opacity` alone
|
|
447
|
+
* cannot say the material blends, and a leaf whose shape is all in its texture's alpha drew as a
|
|
448
|
+
* solid card. So it is said.
|
|
449
|
+
*/
|
|
450
|
+
blend: blends,
|
|
451
|
+
/* Seen from both faces: a curtain, a leaf card. The specification's default is one face. */
|
|
452
|
+
doubleSided: material?.doubleSided === true,
|
|
412
453
|
/* Only in MASK mode: the specification says `alphaCutoff` has no meaning in the other two. */
|
|
413
454
|
cutout: masked ? Math.min(1, Math.max(0, material?.alphaCutoff ?? 0.5)) : 0,
|
|
414
455
|
/* Metalness is glTF's statement about reflection: a metal mirrors its surroundings and
|
|
@@ -424,6 +465,13 @@ function materialOf(material) {
|
|
|
424
465
|
albedo: pbr?.baseColorTexture?.index ?? -1,
|
|
425
466
|
};
|
|
426
467
|
}
|
|
468
|
+
/** The required extensions this reader implements, or follows far enough to fail loudly. */
|
|
469
|
+
const KNOWN_REQUIRED = new Set([
|
|
470
|
+
'KHR_materials_pbrSpecularGlossiness',
|
|
471
|
+
'EXT_texture_webp',
|
|
472
|
+
'EXT_texture_avif',
|
|
473
|
+
'KHR_lights_punctual',
|
|
474
|
+
]);
|
|
427
475
|
/**
|
|
428
476
|
* Turn a document into meshes in world space, one per primitive.
|
|
429
477
|
*
|
|
@@ -441,6 +489,15 @@ export function gltfToMeshes(doc, buffers, options) {
|
|
|
441
489
|
/* One per labelled material, by the ordinal `materials` is about to give it. */
|
|
442
490
|
const substances = [];
|
|
443
491
|
const warnings = [];
|
|
492
|
+
/*
|
|
493
|
+
* A required extension is the file saying it cannot be read correctly without it. One this
|
|
494
|
+
* reader does not know is said by name, rather than drawing whatever falls out.
|
|
495
|
+
*/
|
|
496
|
+
for (const name of doc.extensionsRequired ?? []) {
|
|
497
|
+
if (!KNOWN_REQUIRED.has(name)) {
|
|
498
|
+
warnings.push(`the file requires ${name}, which this reader does not implement`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
444
501
|
const textures = readImages(doc, buffers, warnings);
|
|
445
502
|
/*
|
|
446
503
|
* Said once per material rather than once per primitive that uses it.
|
|
@@ -481,6 +538,9 @@ export function gltfToMeshes(doc, buffers, options) {
|
|
|
481
538
|
* traversal.
|
|
482
539
|
*/
|
|
483
540
|
const nodes = [];
|
|
541
|
+
/* The lights, placed by the same world matrix the walk computes for the geometry. */
|
|
542
|
+
const lights = [];
|
|
543
|
+
const punctual = doc.extensions?.KHR_lights_punctual?.lights ?? [];
|
|
484
544
|
const walk = (nodeIndex, parent, parentNode) => {
|
|
485
545
|
if (seen.has(nodeIndex)) {
|
|
486
546
|
// A cycle would otherwise recurse until the stack gave out, with no useful message.
|
|
@@ -491,6 +551,20 @@ export function gltfToMeshes(doc, buffers, options) {
|
|
|
491
551
|
if (node === undefined)
|
|
492
552
|
throw new DrftError(`gltf: a scene names node ${nodeIndex}, which is absent`);
|
|
493
553
|
const world = multiply(parent, localMatrix(node));
|
|
554
|
+
const hung = node.extensions?.KHR_lights_punctual?.light;
|
|
555
|
+
if (hung !== undefined) {
|
|
556
|
+
const light = punctual[hung];
|
|
557
|
+
if (light === undefined) {
|
|
558
|
+
warnings.push(`node ${nodeIndex} names light ${hung}, and the file has ${punctual.length}`);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
const placed = placeLight(light, world, node.name ?? light.name ?? '');
|
|
562
|
+
if (placed === null)
|
|
563
|
+
warnings.push(`light ${hung}: a type "${light.type}" is not a punctual light`);
|
|
564
|
+
else
|
|
565
|
+
lights.push(placed);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
494
568
|
const entry = nodes.length;
|
|
495
569
|
nodes.push({
|
|
496
570
|
parent: parentNode,
|
|
@@ -571,9 +645,34 @@ export function gltfToMeshes(doc, buffers, options) {
|
|
|
571
645
|
materials,
|
|
572
646
|
textures,
|
|
573
647
|
nodes,
|
|
648
|
+
lights,
|
|
574
649
|
...(substances.length === 0 ? {} : { substances }),
|
|
575
650
|
};
|
|
576
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* One punctual light where its node puts it: the node's origin, and its local −z for the way it
|
|
654
|
+
* points, both through the world matrix. The extension's defaults stand where the file is silent.
|
|
655
|
+
*/
|
|
656
|
+
function placeLight(light, world, name) {
|
|
657
|
+
const kind = light.type;
|
|
658
|
+
if (kind !== 'point' && kind !== 'spot' && kind !== 'directional')
|
|
659
|
+
return null;
|
|
660
|
+
const dx = -(world[8] ?? 0);
|
|
661
|
+
const dy = -(world[9] ?? 0);
|
|
662
|
+
const dz = -(world[10] ?? 1);
|
|
663
|
+
const length = Math.hypot(dx, dy, dz) || 1;
|
|
664
|
+
return {
|
|
665
|
+
kind,
|
|
666
|
+
name,
|
|
667
|
+
position: [world[12] ?? 0, world[13] ?? 0, world[14] ?? 0],
|
|
668
|
+
direction: [dx / length, dy / length, dz / length],
|
|
669
|
+
color: [light.color?.[0] ?? 1, light.color?.[1] ?? 1, light.color?.[2] ?? 1],
|
|
670
|
+
intensity: light.intensity ?? 1,
|
|
671
|
+
range: light.range ?? 0,
|
|
672
|
+
innerConeRad: light.spot?.innerConeAngle ?? 0,
|
|
673
|
+
outerConeRad: light.spot?.outerConeAngle ?? Math.PI / 4,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
577
676
|
function buildPrimitive(doc, buffers, primitive, world, label, warnings,
|
|
578
677
|
/** The skin this primitive is deformed by, or undefined for static geometry. */
|
|
579
678
|
skin,
|
package/dist/index.d.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* world imports none of this and pays for none of it.
|
|
8
8
|
*/
|
|
9
9
|
export { DrftLoader } from './drftLoader.ts';
|
|
10
|
+
export { writePartMaterial } from './partMaterial.ts';
|
|
10
11
|
export { dtexTileBytes, tileGridFromDtex } from './dtexTiles.ts';
|
|
11
12
|
export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
|
|
12
13
|
export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
13
14
|
export type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
|
|
14
15
|
export type { DrftLoadPhase, DrftLoadProgress, DrftPart } from './loadProgress.ts';
|
|
16
|
+
export type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
15
17
|
export { TextureSet, textureColorSpaces } from './drftTextures.ts';
|
|
16
18
|
export { assetCandidates, basenameOf } from './assetPath.ts';
|
|
17
19
|
export { describeImage } from './imageInfo.ts';
|
|
@@ -49,10 +51,15 @@ export type { ModelSource, ModelImport } from './readModel.ts';
|
|
|
49
51
|
export { browserInflate, browserInflateRaw, prepareFbxInflate, prepareZipInflate, } from './fbxInflate.ts';
|
|
50
52
|
export type { AsyncInflate } from './fbxInflate.ts';
|
|
51
53
|
export { weldMesh, dropDefaultAttributes } from './weld.ts';
|
|
54
|
+
export { simplifyMesh } from './bake/simplify.ts';
|
|
55
|
+
export type { SimplifyOptions } from './bake/simplify.ts';
|
|
56
|
+
export { componentCount, findRepeats } from './repeats.ts';
|
|
57
|
+
export type { Repeats } from './repeats.ts';
|
|
52
58
|
export { deriveTangentsFor } from './tangentOrder.ts';
|
|
59
|
+
export { cappedSize, downscaleRgba } from './downscale.ts';
|
|
53
60
|
export { buildCoarseLevel, isOutlineWorthWriting, DEFAULT_COARSE_CELLS } from './coarseLevel.ts';
|
|
54
61
|
export type { CoarseLevelOptions } from './coarseLevel.ts';
|
|
55
|
-
export { orientMeshes, parseUpAxis, describeUpAxis } from './orient.ts';
|
|
62
|
+
export { orientMeshes, orientTransforms, orientLights, parseUpAxis, describeUpAxis, } from './orient.ts';
|
|
56
63
|
export type { UpAxis } from './orient.ts';
|
|
57
64
|
export { readGltfSkins } from './gltfSkin.ts';
|
|
58
65
|
export type { GltfAnimated, GltfSkin } from './gltfSkin.ts';
|
|
@@ -70,6 +77,7 @@ export type { ClusterOptions, ClusterSet } from './bake/cluster.ts';
|
|
|
70
77
|
export { buildClusterDag } from './bake/clusterLod.ts';
|
|
71
78
|
export type { ClusterDag, ClusterLevel } from './bake/clusterLod.ts';
|
|
72
79
|
export { bakeObjectSdf } from './bake/sdf.ts';
|
|
80
|
+
export { bakeSceneField } from './bake/sceneField.ts';
|
|
73
81
|
export { IMPOSTOR_GUTTER, IMPOSTOR_TILE, PROXY_BASE_CELLS, PROXY_MIN_CELLS, buildImpostor, buildProxy, frameOf, octahedralCoord, octahedralDirection, proxyResolution, rasteriseView, sampleImpostor, } from './bake/hlod.ts';
|
|
74
82
|
export type { Impostor, ProxyCell, ViewFrame } from './bake/hlod.ts';
|
|
75
83
|
export type { ObjectSdf } from './bake/sdf.ts';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* consumer writes the words, because what to call a stage is a decision about an audience.
|
|
13
13
|
*/
|
|
14
14
|
export { DrftLoader } from './drftLoader.js';
|
|
15
|
+
/*
|
|
16
|
+
* A part's whole material in the renderer's shape, so a consumer drawing parts cannot copy half of
|
|
17
|
+
* it: a copy that left out the occlusion strength drew a bought model black.
|
|
18
|
+
*/
|
|
19
|
+
export { writePartMaterial } from './partMaterial.js';
|
|
15
20
|
/*
|
|
16
21
|
* **A `DTEX` chunk's tile table as the grid residency asks about.** The other constructor of a
|
|
17
22
|
* `MaterialTileGrid` — `latentTileGrid` cuts one from a latent a baker holds, and this reads one a
|
|
@@ -81,11 +86,19 @@ export { browserInflate, browserInflateRaw, prepareFbxInflate, prepareZipInflate
|
|
|
81
86
|
* on a real asset is roughly six times more than the model has.
|
|
82
87
|
*/
|
|
83
88
|
export { weldMesh, dropDefaultAttributes } from './weld.js';
|
|
89
|
+
export { simplifyMesh } from './bake/simplify.js';
|
|
90
|
+
/*
|
|
91
|
+
* Copies an export merged into one mesh, found again as a prototype and a placement each. The
|
|
92
|
+
* baker writes them as `INST`; see `repeats.ts` for how strict a copy has to be to count.
|
|
93
|
+
*/
|
|
94
|
+
export { componentCount, findRepeats } from './repeats.js';
|
|
84
95
|
/*
|
|
85
96
|
* The frame is derived after the weld and not inside a reader, for the reason `tangentOrder.ts`
|
|
86
97
|
* measures: a soup carrying a frame per corner has nothing left to merge.
|
|
87
98
|
*/
|
|
88
99
|
export { deriveTangentsFor } from './tangentOrder.js';
|
|
100
|
+
/* A decoded image shrunk to a texture cap, which is what the baker's `--max-texture` runs. */
|
|
101
|
+
export { cappedSize, downscaleRgba } from './downscale.js';
|
|
89
102
|
/*
|
|
90
103
|
* One coarse mesh standing in for a whole asset, so a load can open on an outline rather than on
|
|
91
104
|
* nothing. Baked, because a coarse model has to exist as geometry before it can be drawn, and
|
|
@@ -97,7 +110,7 @@ export { buildCoarseLevel, isOutlineWorthWriting, DEFAULT_COARSE_CELLS } from '.
|
|
|
97
110
|
* than guessed: every rule for measuring it from the geometry is wrong on something ordinary,
|
|
98
111
|
* and guessing is what put a car upside down and reported it as verified.
|
|
99
112
|
*/
|
|
100
|
-
export { orientMeshes, parseUpAxis, describeUpAxis } from './orient.js';
|
|
113
|
+
export { orientMeshes, orientTransforms, orientLights, parseUpAxis, describeUpAxis, } from './orient.js';
|
|
101
114
|
/* Skins and clips out of a glTF document. See the module for why it is not part of `gltf.ts`. */
|
|
102
115
|
export { readGltfSkins } from './gltfSkin.js';
|
|
103
116
|
/*
|
|
@@ -165,6 +178,7 @@ export { buildClusterDag } from './bake/clusterLod.js';
|
|
|
165
178
|
* carries, and a field a consumer cannot bake is a pipeline with no input.
|
|
166
179
|
*/
|
|
167
180
|
export { bakeObjectSdf } from './bake/sdf.js';
|
|
181
|
+
export { bakeSceneField } from './bake/sceneField.js';
|
|
168
182
|
/*
|
|
169
183
|
* **The third time, and the one that got furthest before anybody noticed.** `bake/hlod.ts` builds
|
|
170
184
|
* the proxies and impostors `docs/CAPABILITIES.md` lists as shipping, it is tested, it is sized and
|
package/dist/loadProgress.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MeshHandle } from '@driftengine/core';
|
|
1
|
+
import type { MeshHandle, InstancedHandle, MeshInstances } from '@driftengine/core';
|
|
2
2
|
/**
|
|
3
3
|
* The vocabulary of a load in flight: which phase it is in, how far it has got, and what a
|
|
4
4
|
* finished part is.
|
|
@@ -60,6 +60,11 @@ export interface DrftLoadProgress {
|
|
|
60
60
|
readonly message: string;
|
|
61
61
|
}
|
|
62
62
|
/** One drawable piece of a loaded model. */
|
|
63
|
+
/** A part's copies: the instanced batch over its mesh, and the placements already uploaded to it. */
|
|
64
|
+
export interface DrftPartInstances {
|
|
65
|
+
readonly batch: InstancedHandle;
|
|
66
|
+
readonly data: MeshInstances;
|
|
67
|
+
}
|
|
63
68
|
export interface DrftPart {
|
|
64
69
|
readonly mesh: MeshHandle;
|
|
65
70
|
/** Which of the asset's images this surface wears, or -1 for none. */
|
|
@@ -108,6 +113,22 @@ export interface DrftPart {
|
|
|
108
113
|
* nothing at all.
|
|
109
114
|
*/
|
|
110
115
|
readonly cutout: number;
|
|
116
|
+
/**
|
|
117
|
+
* Whether the part blends: its texture's alpha is coverage, so it is drawn translucent whatever its
|
|
118
|
+
* `opacity` says. glTF's `BLEND` with a factor of 1 is the case: a leaf's shape is in its alpha.
|
|
119
|
+
*/
|
|
120
|
+
readonly blend: boolean;
|
|
121
|
+
/** Whether both faces are seen: glTF's `doubleSided`. Drawn without culling, each face lit as the front. */
|
|
122
|
+
readonly doubleSided: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The copies this part is drawn as, or null for a part drawn once.
|
|
125
|
+
*
|
|
126
|
+
* A mesh the file places many times (`INST`, 1.18) is uploaded once and drawn through the
|
|
127
|
+
* engine's instanced path: `drawInstanced(batch, data)`, and `sink.instanced` for a shadow. The
|
|
128
|
+
* placements are uploaded when the part is made and carry the fit, so a caller passes them as
|
|
129
|
+
* they are. Null for every part of a file with no instances, which is every file before 1.18.
|
|
130
|
+
*/
|
|
131
|
+
readonly instances: DrftPartInstances | null;
|
|
111
132
|
/**
|
|
112
133
|
* 0 to 1 as this part arrives, for a caller that would rather it faded in than appeared.
|
|
113
134
|
*
|
package/dist/orient.d.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* is still the winding the rasteriser wants, and the index buffers are passed through
|
|
26
26
|
* untouched.
|
|
27
27
|
*/
|
|
28
|
-
import type { DrftNode, MeshData } from '@driftengine/drft';
|
|
28
|
+
import type { DrftLight, DrftNode, MeshData } from '@driftengine/drft';
|
|
29
29
|
/** Which way is up in the asset, as a signed axis of the engine's own frame. */
|
|
30
30
|
export type UpAxis = '+x' | '-x' | '+y' | '-y' | '+z' | '-z';
|
|
31
31
|
export declare const UP_AXES: readonly UpAxis[];
|
|
@@ -41,6 +41,14 @@ export declare function describeUpAxis(up: UpAxis): string;
|
|
|
41
41
|
* no scale here by construction, which is the other half of why.
|
|
42
42
|
*/
|
|
43
43
|
export declare function orientMeshes(meshes: readonly MeshData[], assetUp: UpAxis): MeshData[];
|
|
44
|
+
/**
|
|
45
|
+
* Column-major 4×4 placements, turned into the engine's frame: `O · M · Oᵀ` for each, with `O` the
|
|
46
|
+
* same turn `orientMeshes` gives the geometry. A copy the file moved along its own +z is then moved
|
|
47
|
+
* along the engine's +y, and a placement that was a rotation stays one of the same angle.
|
|
48
|
+
*/
|
|
49
|
+
export declare function orientTransforms(transforms: Float32Array, assetUp: UpAxis): Float32Array;
|
|
50
|
+
/** Authored lights, turned into the engine's frame by the turn `orientMeshes` gives the geometry. */
|
|
51
|
+
export declare function orientLights(lights: readonly DrftLight[], assetUp: UpAxis): DrftLight[];
|
|
44
52
|
/**
|
|
45
53
|
* Which way round a format's coordinate system is.
|
|
46
54
|
*
|
package/dist/orient.js
CHANGED
|
@@ -61,7 +61,11 @@ export function parseUpAxis(text) {
|
|
|
61
61
|
export function describeUpAxis(up) {
|
|
62
62
|
return TURNS[up].says;
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Turn the first three of every `stride` values, and copy the rest. Three for a position or a
|
|
66
|
+
* normal; four for a tangent, whose fourth is the bitangent's handedness and is not a direction.
|
|
67
|
+
*/
|
|
68
|
+
function turnTriples(source, m, stride = 3) {
|
|
65
69
|
const out = new Float32Array(source.length);
|
|
66
70
|
const m0 = m[0];
|
|
67
71
|
const m1 = m[1];
|
|
@@ -72,13 +76,15 @@ function turnTriples(source, m) {
|
|
|
72
76
|
const m6 = m[6];
|
|
73
77
|
const m7 = m[7];
|
|
74
78
|
const m8 = m[8];
|
|
75
|
-
for (let i = 0; i + 2 < source.length; i +=
|
|
79
|
+
for (let i = 0; i + 2 < source.length; i += stride) {
|
|
76
80
|
const x = source[i];
|
|
77
81
|
const y = source[i + 1];
|
|
78
82
|
const z = source[i + 2];
|
|
79
83
|
out[i] = m0 * x + m1 * y + m2 * z;
|
|
80
84
|
out[i + 1] = m3 * x + m4 * y + m5 * z;
|
|
81
85
|
out[i + 2] = m6 * x + m7 * y + m8 * z;
|
|
86
|
+
for (let k = 3; k < stride; k++)
|
|
87
|
+
out[i + k] = source[i + k];
|
|
82
88
|
}
|
|
83
89
|
return out;
|
|
84
90
|
}
|
|
@@ -95,6 +101,58 @@ export function orientMeshes(meshes, assetUp) {
|
|
|
95
101
|
...mesh,
|
|
96
102
|
positions: turnTriples(mesh.positions, m),
|
|
97
103
|
normals: turnTriples(mesh.normals, m),
|
|
104
|
+
/*
|
|
105
|
+
* **The tangent frame turns with the normals**, and it did not until 2026-09-25: the baker
|
|
106
|
+
* derives tangents before it orients, so a normal-mapped model baked with `--up` kept its
|
|
107
|
+
* tangents in the file's axes and lit its normal maps from the wrong side.
|
|
108
|
+
*/
|
|
109
|
+
...(mesh.tangents === undefined ? {} : { tangents: turnTriples(mesh.tangents, m, 4) }),
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Column-major 4×4 placements, turned into the engine's frame: `O · M · Oᵀ` for each, with `O` the
|
|
114
|
+
* same turn `orientMeshes` gives the geometry. A copy the file moved along its own +z is then moved
|
|
115
|
+
* along the engine's +y, and a placement that was a rotation stays one of the same angle.
|
|
116
|
+
*/
|
|
117
|
+
export function orientTransforms(transforms, assetUp) {
|
|
118
|
+
const o = TURNS[assetUp].m;
|
|
119
|
+
const out = new Float32Array(transforms.length);
|
|
120
|
+
for (let at = 0; at + 15 < transforms.length; at += 16) {
|
|
121
|
+
/* Row-major 3×3 of the placement, read out of its column-major block. */
|
|
122
|
+
for (let r = 0; r < 3; r++) {
|
|
123
|
+
for (let c = 0; c < 3; c++) {
|
|
124
|
+
let sum = 0;
|
|
125
|
+
for (let i = 0; i < 3; i++) {
|
|
126
|
+
for (let j = 0; j < 3; j++) {
|
|
127
|
+
/* (O M Oᵀ)[r][c] = Σ O[r][i] M[i][j] O[c][j] */
|
|
128
|
+
sum +=
|
|
129
|
+
o[r * 3 + i] *
|
|
130
|
+
transforms[at + j * 4 + i] *
|
|
131
|
+
o[c * 3 + j];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
out[at + c * 4 + r] = sum;
|
|
135
|
+
}
|
|
136
|
+
let t = 0;
|
|
137
|
+
for (let i = 0; i < 3; i++)
|
|
138
|
+
t += o[r * 3 + i] * transforms[at + 12 + i];
|
|
139
|
+
out[at + 12 + r] = t;
|
|
140
|
+
}
|
|
141
|
+
out[at + 15] = 1;
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/** Authored lights, turned into the engine's frame by the turn `orientMeshes` gives the geometry. */
|
|
146
|
+
export function orientLights(lights, assetUp) {
|
|
147
|
+
const { m } = TURNS[assetUp];
|
|
148
|
+
const turn = (v) => {
|
|
149
|
+
const t = turnTriples(new Float32Array(v), m);
|
|
150
|
+
return [t[0], t[1], t[2]];
|
|
151
|
+
};
|
|
152
|
+
return lights.map((light) => ({
|
|
153
|
+
...light,
|
|
154
|
+
position: turn(light.position),
|
|
155
|
+
direction: turn(light.direction),
|
|
98
156
|
}));
|
|
99
157
|
}
|
|
100
158
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A loaded part's material, written into the shape the renderer's `setMaterial` takes.
|
|
3
|
+
*
|
|
4
|
+
* **Every field or none**, because a partial copy fails in the one way nobody notices: silently and
|
|
5
|
+
* dark. `SurfaceMaterial` defaults `occlusionStrength` to 1, which is right for a caller who binds
|
|
6
|
+
* an ORM map and says nothing. A glTF material with no occlusion texture is carried as 0, and glTF
|
|
7
|
+
* lets its ORM map's red channel be anything — zero, in a bought scene measured here. A consumer
|
|
8
|
+
* who copied the maps and not the strength drew every surface multiplied by that zero. This is the
|
|
9
|
+
* one place the correspondence is written, so a field added to one side has one place to arrive.
|
|
10
|
+
*
|
|
11
|
+
* Writes into a caller-owned object and allocates nothing, so a scene rewrites one material per
|
|
12
|
+
* part in its frame loop. Opacity and reflectivity are not written: the first is a choice of draw
|
|
13
|
+
* call and the second a separate setter, and both stay the caller's to apply.
|
|
14
|
+
*/
|
|
15
|
+
import type { SurfaceMaterial, SurfaceTextureHandle } from '@driftengine/core';
|
|
16
|
+
import type { TextureSet } from './drftTextures.ts';
|
|
17
|
+
import type { DrftPart } from './loadProgress.ts';
|
|
18
|
+
/** `part`'s maps and factors into `out`, resolving each map through `textures`. */
|
|
19
|
+
export declare function writePartMaterial(part: DrftPart, textures: TextureSet<SurfaceTextureHandle> | null, out: SurfaceMaterial<SurfaceTextureHandle>): SurfaceMaterial<SurfaceTextureHandle>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function at(textures, index) {
|
|
2
|
+
return index >= 0 ? (textures?.at(index) ?? null) : null;
|
|
3
|
+
}
|
|
4
|
+
/** `part`'s maps and factors into `out`, resolving each map through `textures`. */
|
|
5
|
+
export function writePartMaterial(part, textures, out) {
|
|
6
|
+
out.albedo = at(textures, part.albedo);
|
|
7
|
+
out.orm = at(textures, part.orm);
|
|
8
|
+
out.normal = at(textures, part.normal);
|
|
9
|
+
out.emissive = at(textures, part.emissive);
|
|
10
|
+
out.roughnessScale = part.roughnessScale;
|
|
11
|
+
out.metallicScale = part.metallicScale;
|
|
12
|
+
out.occlusionStrength = part.occlusionStrength;
|
|
13
|
+
out.cutout = part.cutout;
|
|
14
|
+
out.doubleSided = part.doubleSided;
|
|
15
|
+
return out;
|
|
16
|
+
}
|
package/dist/readModel.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* decision, and `demo/showroom.ts` runs it in a worker.
|
|
22
22
|
*/
|
|
23
23
|
import type { MeshData } from '@driftengine/drft';
|
|
24
|
-
import type { AnimationClip, DrftMaterial, DrftNode, DrftSkin } from '@driftengine/drft';
|
|
24
|
+
import type { AnimationClip, DrftLight, DrftMaterial, DrftNode, DrftSkin } from '@driftengine/drft';
|
|
25
25
|
import type { AssetReference } from './assetPath.ts';
|
|
26
26
|
import type { Handedness, UpAxis } from './orient.ts';
|
|
27
27
|
import type { Inflate } from './fbx.ts';
|
|
@@ -124,6 +124,8 @@ export interface ModelImport {
|
|
|
124
124
|
* reads it calls `localiseNodes` to get parts it can turn.
|
|
125
125
|
*/
|
|
126
126
|
nodes?: readonly DrftNode[];
|
|
127
|
+
/** The lights the file was authored with, in world space. glTF carries them; nothing else here. */
|
|
128
|
+
lights?: readonly DrftLight[];
|
|
127
129
|
/** Which way round the source's coordinate system was. `orient.ts` is what acts on it. */
|
|
128
130
|
declaredHand?: Handedness;
|
|
129
131
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copies merged into one mesh, found again: a prototype and a rigid placement for each copy.
|
|
3
|
+
*
|
|
4
|
+
* **What an export does to repeated objects, undone.** A modelling tool that flattens a scene writes
|
|
5
|
+
* N copies of an object as one mesh: N runs of the same triangles, one after another, each run's
|
|
6
|
+
* vertices in the same order, each run moved. A bought scene measured here shipped ten thousand
|
|
7
|
+
* candles that way — nineteen million triangles for one candle's worth of shape. Finding the runs
|
|
8
|
+
* turns that back into one mesh and ten thousand matrices.
|
|
9
|
+
*
|
|
10
|
+
* **Strict, and the strictness is the design.** A count N is accepted only when:
|
|
11
|
+
* - the triangle and vertex counts both divide by N;
|
|
12
|
+
* - every run's indices are run 0's, offset by a whole run of vertices;
|
|
13
|
+
* - every run is a rigid transform of run 0 within a tolerance tied to the run's own size, with its
|
|
14
|
+
* normals and tangents turned by the same rotation;
|
|
15
|
+
* - every other attribute is exactly equal.
|
|
16
|
+
*
|
|
17
|
+
* One vertex out refuses the count. What that gives up is copies a tool reordered or re-triangulated,
|
|
18
|
+
* which this cannot see. What would make that wrong is a scene where such copies dominate; finding
|
|
19
|
+
* them needs shape matching, not run matching.
|
|
20
|
+
*
|
|
21
|
+
* **Rigid only.** A scaled copy is refused rather than instanced with a scale, because a scale
|
|
22
|
+
* changes what the normals mean and nothing downstream is asked to renormalise them. Skinned and
|
|
23
|
+
* morphed meshes are refused outright: their copies are not placements of one shape.
|
|
24
|
+
*/
|
|
25
|
+
import type { MeshData } from '@driftengine/drft';
|
|
26
|
+
export interface Repeats {
|
|
27
|
+
/** Run 0: one copy's geometry, with its own indices from zero. */
|
|
28
|
+
readonly prototype: MeshData;
|
|
29
|
+
readonly count: number;
|
|
30
|
+
/** One column-major 4×4 matrix a copy, taking the prototype to where that copy was. Copy 0 is identity. */
|
|
31
|
+
readonly transforms: Float32Array;
|
|
32
|
+
}
|
|
33
|
+
/** How many connected pieces the indices make of the mesh. Unreferenced vertices count as their own. */
|
|
34
|
+
export declare function componentCount(mesh: MeshData): number;
|
|
35
|
+
/**
|
|
36
|
+
* The prototype and placements for the largest candidate count that `mesh` is exactly that many
|
|
37
|
+
* rigid copies of, or null when none is.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findRepeats(mesh: MeshData, candidates: readonly number[]): Repeats | null;
|