@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/src/gltf.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* world matrix on the way through, and the graph does not survive into the engine.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import type { DrftNode, MeshData } from '@driftengine/drft';
|
|
20
|
+
import type { DrftLight, DrftNode, MeshData } from '@driftengine/drft';
|
|
21
21
|
import { DrftError } from '@driftengine/drft';
|
|
22
22
|
import type { DrftMaterial } from '@driftengine/drft';
|
|
23
23
|
import { generateTangents } from '@driftengine/core';
|
|
@@ -93,6 +93,7 @@ interface GltfMaterial {
|
|
|
93
93
|
emissiveFactor?: number[];
|
|
94
94
|
/** `OPAQUE`, `MASK` or `BLEND`. Absent means opaque, which the specification states. */
|
|
95
95
|
alphaMode?: string;
|
|
96
|
+
doubleSided?: boolean;
|
|
96
97
|
alphaCutoff?: number;
|
|
97
98
|
name?: string;
|
|
98
99
|
/**
|
|
@@ -118,6 +119,16 @@ interface GltfImage {
|
|
|
118
119
|
interface GltfTexture {
|
|
119
120
|
source?: number;
|
|
120
121
|
sampler?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Where an image in a newer codec is named. glTF's own `source` is the fallback, and a file that
|
|
124
|
+
* *requires* the extension usually carries none, so the extension's index is the only route to
|
|
125
|
+
* the picture.
|
|
126
|
+
*/
|
|
127
|
+
extensions?: {
|
|
128
|
+
EXT_texture_webp?: { source?: number };
|
|
129
|
+
EXT_texture_avif?: { source?: number };
|
|
130
|
+
KHR_texture_basisu?: { source?: number };
|
|
131
|
+
};
|
|
121
132
|
}
|
|
122
133
|
interface GltfNode {
|
|
123
134
|
mesh?: number;
|
|
@@ -129,6 +140,17 @@ interface GltfNode {
|
|
|
129
140
|
rotation?: number[];
|
|
130
141
|
scale?: number[];
|
|
131
142
|
name?: string;
|
|
143
|
+
/** Which of the document's punctual lights hangs on this node. */
|
|
144
|
+
extensions?: { KHR_lights_punctual?: { light?: number } };
|
|
145
|
+
}
|
|
146
|
+
/** One `KHR_lights_punctual` light, as the extension states it. */
|
|
147
|
+
interface GltfPunctualLight {
|
|
148
|
+
type?: string;
|
|
149
|
+
name?: string;
|
|
150
|
+
color?: number[];
|
|
151
|
+
intensity?: number;
|
|
152
|
+
range?: number;
|
|
153
|
+
spot?: { innerConeAngle?: number; outerConeAngle?: number };
|
|
132
154
|
}
|
|
133
155
|
export interface GltfDocument {
|
|
134
156
|
asset?: { version?: string; generator?: string };
|
|
@@ -142,9 +164,14 @@ export interface GltfDocument {
|
|
|
142
164
|
materials?: GltfMaterial[];
|
|
143
165
|
images?: GltfImage[];
|
|
144
166
|
textures?: GltfTexture[];
|
|
167
|
+
/** Extensions the file states it cannot be read correctly without. */
|
|
168
|
+
extensionsRequired?: string[];
|
|
169
|
+
extensionsUsed?: string[];
|
|
145
170
|
/* Read by `gltfSkin.ts`; ignored entirely by the mesh path, which flattens. */
|
|
146
171
|
skins?: GltfSkinDef[];
|
|
147
172
|
animations?: GltfAnimationDef[];
|
|
173
|
+
/** The document's own lights, which nodes name by index. */
|
|
174
|
+
extensions?: { KHR_lights_punctual?: { lights?: GltfPunctualLight[] } };
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
/** A skin, as glTF declares one. `skeleton` is a hint this reader does not need and ignores. */
|
|
@@ -323,6 +350,34 @@ function normalMatrix(m: readonly number[]): number[] {
|
|
|
323
350
|
* textures may share one image with different samplers. Following the indirection here keeps
|
|
324
351
|
* the material's index meaning what the file said it meant.
|
|
325
352
|
*/
|
|
353
|
+
/**
|
|
354
|
+
* Which image a texture shows: glTF's `source` when there is one, otherwise an image-codec
|
|
355
|
+
* extension's.
|
|
356
|
+
*
|
|
357
|
+
* **The fallback wins when both exist**, because every reader can decode it and the baker
|
|
358
|
+
* identifies whatever bytes it gets. What that gives up is nothing a bake can use today: a WebP
|
|
359
|
+
* extension's image is smaller, but nothing here knows the two are the same picture. What would
|
|
360
|
+
* make it wrong is a baker that decodes AVIF, where the extension's image may be the better source.
|
|
361
|
+
*
|
|
362
|
+
* Basis is refused by name: it is a GPU transcoding format, and this engine uploads decoded images.
|
|
363
|
+
*/
|
|
364
|
+
function textureSource(
|
|
365
|
+
texture: GltfTexture | undefined,
|
|
366
|
+
index: number,
|
|
367
|
+
warnings: string[],
|
|
368
|
+
): number | undefined {
|
|
369
|
+
if (texture?.source !== undefined) return texture.source;
|
|
370
|
+
const ext = texture?.extensions;
|
|
371
|
+
if (ext?.KHR_texture_basisu !== undefined) {
|
|
372
|
+
warnings.push(
|
|
373
|
+
`texture ${index} is KHR_texture_basisu, which this reader does not transcode; export ` +
|
|
374
|
+
'the model with PNG, JPEG or WebP images',
|
|
375
|
+
);
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
return ext?.EXT_texture_webp?.source ?? ext?.EXT_texture_avif?.source;
|
|
379
|
+
}
|
|
380
|
+
|
|
326
381
|
function readImages(
|
|
327
382
|
doc: GltfDocument,
|
|
328
383
|
buffers: readonly Uint8Array[],
|
|
@@ -330,7 +385,7 @@ function readImages(
|
|
|
330
385
|
): AssetReference[] {
|
|
331
386
|
const out: AssetReference[] = [];
|
|
332
387
|
for (let i = 0; i < (doc.textures ?? []).length; i++) {
|
|
333
|
-
const source = doc.textures?.[i]
|
|
388
|
+
const source = textureSource(doc.textures?.[i], i, warnings);
|
|
334
389
|
const image = source === undefined ? undefined : doc.images?.[source];
|
|
335
390
|
if (image === undefined) {
|
|
336
391
|
warnings.push(`texture ${i} names no image this reader can find; it will draw untextured`);
|
|
@@ -563,6 +618,17 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
|
|
|
563
618
|
const modulated = ormMap >= 0;
|
|
564
619
|
const metallic = modulated ? 0 : (pbr?.metallicFactor ?? 1);
|
|
565
620
|
const roughness = modulated ? DEFAULT_ROUGHNESS : (pbr?.roughnessFactor ?? 1);
|
|
621
|
+
/*
|
|
622
|
+
* **The same trap reached by the other door: a metal whose colour is an image.** `specular` is
|
|
623
|
+
* this engine's dielectric highlight and it is white, which is a metal's highlight only where the
|
|
624
|
+
* metal is white. Where the base colour is a texture that colour is per texel, the lane cannot
|
|
625
|
+
* say it, and the factor alone blew a dirt decal to white: its file names no metallic and no
|
|
626
|
+
* roughness, so the specification reads a rough metal of dark grime, and every wall it covered
|
|
627
|
+
* drew white under a low sun with the grime dark between. So the lane keeps the dielectric's
|
|
628
|
+
* zero there. What it gives up is the sun's glint on a textured metal with no map, which then
|
|
629
|
+
* shades as the grime or the paint its image says; what would change it is a metallic lane.
|
|
630
|
+
*/
|
|
631
|
+
const highlight = pbr?.baseColorTexture === undefined ? metallic : 0;
|
|
566
632
|
|
|
567
633
|
/*
|
|
568
634
|
* **glTF leaves `metallicRoughnessTexture`'s R channel undefined.** The specification assigns G
|
|
@@ -583,7 +649,7 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
|
|
|
583
649
|
return {
|
|
584
650
|
name: material?.name ?? '',
|
|
585
651
|
color: [base[0] ?? 1, base[1] ?? 1, base[2] ?? 1],
|
|
586
|
-
specular:
|
|
652
|
+
specular: highlight,
|
|
587
653
|
roughness,
|
|
588
654
|
emissive,
|
|
589
655
|
emissiveColor:
|
|
@@ -591,6 +657,14 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
|
|
|
591
657
|
? [emissiveFactor[0] ?? 0, emissiveFactor[1] ?? 0, emissiveFactor[2] ?? 0]
|
|
592
658
|
: [-1, -1, -1],
|
|
593
659
|
opacity: blends ? Math.min(1, Math.max(0, base[3] ?? 1)) : 1,
|
|
660
|
+
/*
|
|
661
|
+
* glTF's `BLEND` says alpha is the factor *times the texture*. With a factor of 1 `opacity` alone
|
|
662
|
+
* cannot say the material blends, and a leaf whose shape is all in its texture's alpha drew as a
|
|
663
|
+
* solid card. So it is said.
|
|
664
|
+
*/
|
|
665
|
+
blend: blends,
|
|
666
|
+
/* Seen from both faces: a curtain, a leaf card. The specification's default is one face. */
|
|
667
|
+
doubleSided: material?.doubleSided === true,
|
|
594
668
|
/* Only in MASK mode: the specification says `alphaCutoff` has no meaning in the other two. */
|
|
595
669
|
cutout: masked ? Math.min(1, Math.max(0, material?.alphaCutoff ?? 0.5)) : 0,
|
|
596
670
|
/* Metalness is glTF's statement about reflection: a metal mirrors its surroundings and
|
|
@@ -627,6 +701,14 @@ export interface GltfReadOptions {
|
|
|
627
701
|
readonly deriveTangents?: boolean;
|
|
628
702
|
}
|
|
629
703
|
|
|
704
|
+
/** The required extensions this reader implements, or follows far enough to fail loudly. */
|
|
705
|
+
const KNOWN_REQUIRED = new Set([
|
|
706
|
+
'KHR_materials_pbrSpecularGlossiness',
|
|
707
|
+
'EXT_texture_webp',
|
|
708
|
+
'EXT_texture_avif',
|
|
709
|
+
'KHR_lights_punctual',
|
|
710
|
+
]);
|
|
711
|
+
|
|
630
712
|
/**
|
|
631
713
|
* Turn a document into meshes in world space, one per primitive.
|
|
632
714
|
*
|
|
@@ -644,6 +726,7 @@ export function gltfToMeshes(
|
|
|
644
726
|
materials: DrftMaterial[];
|
|
645
727
|
textures: AssetReference[];
|
|
646
728
|
nodes: DrftNode[];
|
|
729
|
+
lights: DrftLight[];
|
|
647
730
|
} {
|
|
648
731
|
const version = doc.asset?.version ?? '';
|
|
649
732
|
if (!version.startsWith('2')) {
|
|
@@ -655,6 +738,15 @@ export function gltfToMeshes(
|
|
|
655
738
|
/* One per labelled material, by the ordinal `materials` is about to give it. */
|
|
656
739
|
const substances: { material: number; substance: string }[] = [];
|
|
657
740
|
const warnings: string[] = [];
|
|
741
|
+
/*
|
|
742
|
+
* A required extension is the file saying it cannot be read correctly without it. One this
|
|
743
|
+
* reader does not know is said by name, rather than drawing whatever falls out.
|
|
744
|
+
*/
|
|
745
|
+
for (const name of doc.extensionsRequired ?? []) {
|
|
746
|
+
if (!KNOWN_REQUIRED.has(name)) {
|
|
747
|
+
warnings.push(`the file requires ${name}, which this reader does not implement`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
658
750
|
const textures = readImages(doc, buffers, warnings);
|
|
659
751
|
/*
|
|
660
752
|
* Said once per material rather than once per primitive that uses it.
|
|
@@ -696,6 +788,9 @@ export function gltfToMeshes(
|
|
|
696
788
|
* traversal.
|
|
697
789
|
*/
|
|
698
790
|
const nodes: DrftNode[] = [];
|
|
791
|
+
/* The lights, placed by the same world matrix the walk computes for the geometry. */
|
|
792
|
+
const lights: DrftLight[] = [];
|
|
793
|
+
const punctual = doc.extensions?.KHR_lights_punctual?.lights ?? [];
|
|
699
794
|
|
|
700
795
|
const walk = (nodeIndex: number, parent: readonly number[], parentNode: number): void => {
|
|
701
796
|
if (seen.has(nodeIndex)) {
|
|
@@ -708,6 +803,18 @@ export function gltfToMeshes(
|
|
|
708
803
|
if (node === undefined)
|
|
709
804
|
throw new DrftError(`gltf: a scene names node ${nodeIndex}, which is absent`);
|
|
710
805
|
const world = multiply(parent, localMatrix(node));
|
|
806
|
+
const hung = node.extensions?.KHR_lights_punctual?.light;
|
|
807
|
+
if (hung !== undefined) {
|
|
808
|
+
const light = punctual[hung];
|
|
809
|
+
if (light === undefined) {
|
|
810
|
+
warnings.push(`node ${nodeIndex} names light ${hung}, and the file has ${punctual.length}`);
|
|
811
|
+
} else {
|
|
812
|
+
const placed = placeLight(light, world, node.name ?? light.name ?? '');
|
|
813
|
+
if (placed === null)
|
|
814
|
+
warnings.push(`light ${hung}: a type "${light.type}" is not a punctual light`);
|
|
815
|
+
else lights.push(placed);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
711
818
|
|
|
712
819
|
const entry = nodes.length;
|
|
713
820
|
nodes.push({
|
|
@@ -795,10 +902,39 @@ export function gltfToMeshes(
|
|
|
795
902
|
materials,
|
|
796
903
|
textures,
|
|
797
904
|
nodes,
|
|
905
|
+
lights,
|
|
798
906
|
...(substances.length === 0 ? {} : { substances }),
|
|
799
907
|
};
|
|
800
908
|
}
|
|
801
909
|
|
|
910
|
+
/**
|
|
911
|
+
* One punctual light where its node puts it: the node's origin, and its local −z for the way it
|
|
912
|
+
* points, both through the world matrix. The extension's defaults stand where the file is silent.
|
|
913
|
+
*/
|
|
914
|
+
function placeLight(
|
|
915
|
+
light: GltfPunctualLight,
|
|
916
|
+
world: readonly number[],
|
|
917
|
+
name: string,
|
|
918
|
+
): DrftLight | null {
|
|
919
|
+
const kind = light.type;
|
|
920
|
+
if (kind !== 'point' && kind !== 'spot' && kind !== 'directional') return null;
|
|
921
|
+
const dx = -(world[8] ?? 0);
|
|
922
|
+
const dy = -(world[9] ?? 0);
|
|
923
|
+
const dz = -(world[10] ?? 1);
|
|
924
|
+
const length = Math.hypot(dx, dy, dz) || 1;
|
|
925
|
+
return {
|
|
926
|
+
kind,
|
|
927
|
+
name,
|
|
928
|
+
position: [world[12] ?? 0, world[13] ?? 0, world[14] ?? 0],
|
|
929
|
+
direction: [dx / length, dy / length, dz / length],
|
|
930
|
+
color: [light.color?.[0] ?? 1, light.color?.[1] ?? 1, light.color?.[2] ?? 1],
|
|
931
|
+
intensity: light.intensity ?? 1,
|
|
932
|
+
range: light.range ?? 0,
|
|
933
|
+
innerConeRad: light.spot?.innerConeAngle ?? 0,
|
|
934
|
+
outerConeRad: light.spot?.outerConeAngle ?? Math.PI / 4,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
802
938
|
function buildPrimitive(
|
|
803
939
|
doc: GltfDocument,
|
|
804
940
|
buffers: readonly Uint8Array[],
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* consumer writes the words, because what to call a stage is a decision about an audience.
|
|
14
14
|
*/
|
|
15
15
|
export { DrftLoader } from './drftLoader.ts';
|
|
16
|
+
/*
|
|
17
|
+
* A part's whole material in the renderer's shape, so a consumer drawing parts cannot copy half of
|
|
18
|
+
* it: a copy that left out the occlusion strength drew a bought model black.
|
|
19
|
+
*/
|
|
20
|
+
export { writePartMaterial } from './partMaterial.ts';
|
|
16
21
|
/*
|
|
17
22
|
* **A `DTEX` chunk's tile table as the grid residency asks about.** The other constructor of a
|
|
18
23
|
* `MaterialTileGrid` — `latentTileGrid` cuts one from a latent a baker holds, and this reads one a
|
|
@@ -28,6 +33,7 @@ export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
|
|
|
28
33
|
export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
29
34
|
export type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
|
|
30
35
|
export type { DrftLoadPhase, DrftLoadProgress, DrftPart } from './loadProgress.ts';
|
|
36
|
+
export type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
31
37
|
/* Addressing a texture by name, so a consumer never holds an ordinal. */
|
|
32
38
|
export { TextureSet, textureColorSpaces } from './drftTextures.ts';
|
|
33
39
|
export { assetCandidates, basenameOf } from './assetPath.ts';
|
|
@@ -105,11 +111,21 @@ export type { AsyncInflate } from './fbxInflate.ts';
|
|
|
105
111
|
* on a real asset is roughly six times more than the model has.
|
|
106
112
|
*/
|
|
107
113
|
export { weldMesh, dropDefaultAttributes } from './weld.ts';
|
|
114
|
+
export { simplifyMesh } from './bake/simplify.ts';
|
|
115
|
+
export type { SimplifyOptions } from './bake/simplify.ts';
|
|
116
|
+
/*
|
|
117
|
+
* Copies an export merged into one mesh, found again as a prototype and a placement each. The
|
|
118
|
+
* baker writes them as `INST`; see `repeats.ts` for how strict a copy has to be to count.
|
|
119
|
+
*/
|
|
120
|
+
export { componentCount, findRepeats } from './repeats.ts';
|
|
121
|
+
export type { Repeats } from './repeats.ts';
|
|
108
122
|
/*
|
|
109
123
|
* The frame is derived after the weld and not inside a reader, for the reason `tangentOrder.ts`
|
|
110
124
|
* measures: a soup carrying a frame per corner has nothing left to merge.
|
|
111
125
|
*/
|
|
112
126
|
export { deriveTangentsFor } from './tangentOrder.ts';
|
|
127
|
+
/* A decoded image shrunk to a texture cap, which is what the baker's `--max-texture` runs. */
|
|
128
|
+
export { cappedSize, downscaleRgba } from './downscale.ts';
|
|
113
129
|
/*
|
|
114
130
|
* One coarse mesh standing in for a whole asset, so a load can open on an outline rather than on
|
|
115
131
|
* nothing. Baked, because a coarse model has to exist as geometry before it can be drawn, and
|
|
@@ -122,7 +138,13 @@ export type { CoarseLevelOptions } from './coarseLevel.ts';
|
|
|
122
138
|
* than guessed: every rule for measuring it from the geometry is wrong on something ordinary,
|
|
123
139
|
* and guessing is what put a car upside down and reported it as verified.
|
|
124
140
|
*/
|
|
125
|
-
export {
|
|
141
|
+
export {
|
|
142
|
+
orientMeshes,
|
|
143
|
+
orientTransforms,
|
|
144
|
+
orientLights,
|
|
145
|
+
parseUpAxis,
|
|
146
|
+
describeUpAxis,
|
|
147
|
+
} from './orient.ts';
|
|
126
148
|
export type { UpAxis } from './orient.ts';
|
|
127
149
|
|
|
128
150
|
/* Skins and clips out of a glTF document. See the module for why it is not part of `gltf.ts`. */
|
|
@@ -199,6 +221,7 @@ export type { ClusterDag, ClusterLevel } from './bake/clusterLod.ts';
|
|
|
199
221
|
* carries, and a field a consumer cannot bake is a pipeline with no input.
|
|
200
222
|
*/
|
|
201
223
|
export { bakeObjectSdf } from './bake/sdf.ts';
|
|
224
|
+
export { bakeSceneField } from './bake/sceneField.ts';
|
|
202
225
|
/*
|
|
203
226
|
* **The third time, and the one that got furthest before anybody noticed.** `bake/hlod.ts` builds
|
|
204
227
|
* the proxies and impostors `docs/CAPABILITIES.md` lists as shipping, it is tested, it is sized and
|
package/src/loadProgress.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
|
/**
|
|
4
4
|
* The vocabulary of a load in flight: which phase it is in, how far it has got, and what a
|
|
@@ -73,6 +73,12 @@ export interface DrftLoadProgress {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** One drawable piece of a loaded model. */
|
|
76
|
+
/** A part's copies: the instanced batch over its mesh, and the placements already uploaded to it. */
|
|
77
|
+
export interface DrftPartInstances {
|
|
78
|
+
readonly batch: InstancedHandle;
|
|
79
|
+
readonly data: MeshInstances;
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
export interface DrftPart {
|
|
77
83
|
readonly mesh: MeshHandle;
|
|
78
84
|
/** Which of the asset's images this surface wears, or -1 for none. */
|
|
@@ -121,6 +127,22 @@ export interface DrftPart {
|
|
|
121
127
|
* nothing at all.
|
|
122
128
|
*/
|
|
123
129
|
readonly cutout: number;
|
|
130
|
+
/**
|
|
131
|
+
* Whether the part blends: its texture's alpha is coverage, so it is drawn translucent whatever its
|
|
132
|
+
* `opacity` says. glTF's `BLEND` with a factor of 1 is the case: a leaf's shape is in its alpha.
|
|
133
|
+
*/
|
|
134
|
+
readonly blend: boolean;
|
|
135
|
+
/** Whether both faces are seen: glTF's `doubleSided`. Drawn without culling, each face lit as the front. */
|
|
136
|
+
readonly doubleSided: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The copies this part is drawn as, or null for a part drawn once.
|
|
139
|
+
*
|
|
140
|
+
* A mesh the file places many times (`INST`, 1.18) is uploaded once and drawn through the
|
|
141
|
+
* engine's instanced path: `drawInstanced(batch, data)`, and `sink.instanced` for a shadow. The
|
|
142
|
+
* placements are uploaded when the part is made and carry the fit, so a caller passes them as
|
|
143
|
+
* they are. Null for every part of a file with no instances, which is every file before 1.18.
|
|
144
|
+
*/
|
|
145
|
+
readonly instances: DrftPartInstances | null;
|
|
124
146
|
/**
|
|
125
147
|
* 0 to 1 as this part arrives, for a caller that would rather it faded in than appeared.
|
|
126
148
|
*
|
package/src/orient.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* untouched.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import type { DrftNode, MeshData } from '@driftengine/drft';
|
|
29
|
+
import type { DrftLight, DrftNode, MeshData } from '@driftengine/drft';
|
|
30
30
|
import { DrftError } from '@driftengine/drft';
|
|
31
31
|
|
|
32
32
|
/** Which way is up in the asset, as a signed axis of the engine's own frame. */
|
|
@@ -71,7 +71,11 @@ export function describeUpAxis(up: UpAxis): string {
|
|
|
71
71
|
return (TURNS[up] as { says: string }).says;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Turn the first three of every `stride` values, and copy the rest. Three for a position or a
|
|
76
|
+
* normal; four for a tangent, whose fourth is the bitangent's handedness and is not a direction.
|
|
77
|
+
*/
|
|
78
|
+
function turnTriples(source: Float32Array, m: readonly number[], stride = 3): Float32Array {
|
|
75
79
|
const out = new Float32Array(source.length);
|
|
76
80
|
const m0 = m[0] as number;
|
|
77
81
|
const m1 = m[1] as number;
|
|
@@ -82,13 +86,14 @@ function turnTriples(source: Float32Array, m: readonly number[]): Float32Array {
|
|
|
82
86
|
const m6 = m[6] as number;
|
|
83
87
|
const m7 = m[7] as number;
|
|
84
88
|
const m8 = m[8] as number;
|
|
85
|
-
for (let i = 0; i + 2 < source.length; i +=
|
|
89
|
+
for (let i = 0; i + 2 < source.length; i += stride) {
|
|
86
90
|
const x = source[i] as number;
|
|
87
91
|
const y = source[i + 1] as number;
|
|
88
92
|
const z = source[i + 2] as number;
|
|
89
93
|
out[i] = m0 * x + m1 * y + m2 * z;
|
|
90
94
|
out[i + 1] = m3 * x + m4 * y + m5 * z;
|
|
91
95
|
out[i + 2] = m6 * x + m7 * y + m8 * z;
|
|
96
|
+
for (let k = 3; k < stride; k++) out[i + k] = source[i + k] as number;
|
|
92
97
|
}
|
|
93
98
|
return out;
|
|
94
99
|
}
|
|
@@ -106,6 +111,60 @@ export function orientMeshes(meshes: readonly MeshData[], assetUp: UpAxis): Mesh
|
|
|
106
111
|
...mesh,
|
|
107
112
|
positions: turnTriples(mesh.positions, m),
|
|
108
113
|
normals: turnTriples(mesh.normals, m),
|
|
114
|
+
/*
|
|
115
|
+
* **The tangent frame turns with the normals**, and it did not until 2026-09-25: the baker
|
|
116
|
+
* derives tangents before it orients, so a normal-mapped model baked with `--up` kept its
|
|
117
|
+
* tangents in the file's axes and lit its normal maps from the wrong side.
|
|
118
|
+
*/
|
|
119
|
+
...(mesh.tangents === undefined ? {} : { tangents: turnTriples(mesh.tangents, m, 4) }),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Column-major 4×4 placements, turned into the engine's frame: `O · M · Oᵀ` for each, with `O` the
|
|
125
|
+
* same turn `orientMeshes` gives the geometry. A copy the file moved along its own +z is then moved
|
|
126
|
+
* along the engine's +y, and a placement that was a rotation stays one of the same angle.
|
|
127
|
+
*/
|
|
128
|
+
export function orientTransforms(transforms: Float32Array, assetUp: UpAxis): Float32Array {
|
|
129
|
+
const o = TURNS[assetUp].m;
|
|
130
|
+
const out = new Float32Array(transforms.length);
|
|
131
|
+
for (let at = 0; at + 15 < transforms.length; at += 16) {
|
|
132
|
+
/* Row-major 3×3 of the placement, read out of its column-major block. */
|
|
133
|
+
for (let r = 0; r < 3; r++) {
|
|
134
|
+
for (let c = 0; c < 3; c++) {
|
|
135
|
+
let sum = 0;
|
|
136
|
+
for (let i = 0; i < 3; i++) {
|
|
137
|
+
for (let j = 0; j < 3; j++) {
|
|
138
|
+
/* (O M Oᵀ)[r][c] = Σ O[r][i] M[i][j] O[c][j] */
|
|
139
|
+
sum +=
|
|
140
|
+
(o[r * 3 + i] as number) *
|
|
141
|
+
(transforms[at + j * 4 + i] as number) *
|
|
142
|
+
(o[c * 3 + j] as number);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
out[at + c * 4 + r] = sum;
|
|
146
|
+
}
|
|
147
|
+
let t = 0;
|
|
148
|
+
for (let i = 0; i < 3; i++)
|
|
149
|
+
t += (o[r * 3 + i] as number) * (transforms[at + 12 + i] as number);
|
|
150
|
+
out[at + 12 + r] = t;
|
|
151
|
+
}
|
|
152
|
+
out[at + 15] = 1;
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Authored lights, turned into the engine's frame by the turn `orientMeshes` gives the geometry. */
|
|
158
|
+
export function orientLights(lights: readonly DrftLight[], assetUp: UpAxis): DrftLight[] {
|
|
159
|
+
const { m } = TURNS[assetUp];
|
|
160
|
+
const turn = (v: readonly [number, number, number]): [number, number, number] => {
|
|
161
|
+
const t = turnTriples(new Float32Array(v), m);
|
|
162
|
+
return [t[0] as number, t[1] as number, t[2] as number];
|
|
163
|
+
};
|
|
164
|
+
return lights.map((light) => ({
|
|
165
|
+
...light,
|
|
166
|
+
position: turn(light.position),
|
|
167
|
+
direction: turn(light.direction),
|
|
109
168
|
}));
|
|
110
169
|
}
|
|
111
170
|
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
|
|
19
|
+
function at(
|
|
20
|
+
textures: TextureSet<SurfaceTextureHandle> | null,
|
|
21
|
+
index: number,
|
|
22
|
+
): SurfaceTextureHandle | null {
|
|
23
|
+
return index >= 0 ? (textures?.at(index) ?? null) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `part`'s maps and factors into `out`, resolving each map through `textures`. */
|
|
27
|
+
export function writePartMaterial(
|
|
28
|
+
part: DrftPart,
|
|
29
|
+
textures: TextureSet<SurfaceTextureHandle> | null,
|
|
30
|
+
out: SurfaceMaterial<SurfaceTextureHandle>,
|
|
31
|
+
): SurfaceMaterial<SurfaceTextureHandle> {
|
|
32
|
+
out.albedo = at(textures, part.albedo);
|
|
33
|
+
out.orm = at(textures, part.orm);
|
|
34
|
+
out.normal = at(textures, part.normal);
|
|
35
|
+
out.emissive = at(textures, part.emissive);
|
|
36
|
+
out.roughnessScale = part.roughnessScale;
|
|
37
|
+
out.metallicScale = part.metallicScale;
|
|
38
|
+
out.occlusionStrength = part.occlusionStrength;
|
|
39
|
+
out.cutout = part.cutout;
|
|
40
|
+
out.doubleSided = part.doubleSided;
|
|
41
|
+
return out;
|
|
42
|
+
}
|
package/src/readModel.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import type { MeshData } from '@driftengine/drft';
|
|
25
|
-
import type { AnimationClip, DrftMaterial, DrftNode, DrftSkin } from '@driftengine/drft';
|
|
25
|
+
import type { AnimationClip, DrftLight, DrftMaterial, DrftNode, DrftSkin } from '@driftengine/drft';
|
|
26
26
|
import { DrftError } from '@driftengine/drft';
|
|
27
27
|
import type { AssetReference } from './assetPath.ts';
|
|
28
28
|
import { assetCandidates } from './assetPath.ts';
|
|
@@ -150,6 +150,8 @@ export interface ModelImport {
|
|
|
150
150
|
* reads it calls `localiseNodes` to get parts it can turn.
|
|
151
151
|
*/
|
|
152
152
|
nodes?: readonly DrftNode[];
|
|
153
|
+
/** The lights the file was authored with, in world space. glTF carries them; nothing else here. */
|
|
154
|
+
lights?: readonly DrftLight[];
|
|
153
155
|
/** Which way round the source's coordinate system was. `orient.ts` is what acts on it. */
|
|
154
156
|
declaredHand?: Handedness;
|
|
155
157
|
}
|