@driftengine/assets 4.12.0 → 4.13.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Model readers, the loader that streams a `.drft` file onto the screen, and the textures a phone
4
4
  samples.
5
5
 
6
- **Cost: 27.1 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it
6
+ **Cost: 29.2 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it
7
7
  drifts more than 3% — the number is a fact about the build rather than a claim in a document.
8
8
 
9
9
  - **Readers.** glTF and GLB, OBJ, STL, USD and 3MF, and experimentally FBX and Blender's `.blend`,
@@ -1,6 +1,6 @@
1
1
  /** Loading a `.drft` into drawable parts progressively, without stalling the frame. */
2
2
  import type { MeshData } from '@driftengine/drft';
3
- import type { RendererApi } from '@driftengine/core';
3
+ import type { RendererApi, ShadowCasters, Vec3 } from '@driftengine/core';
4
4
  import type { SurfaceTextureHandle } from '@driftengine/core';
5
5
  import type { DrftMaterial } from '@driftengine/drft';
6
6
  import type { DrftFieldPlacement } from './fieldPlacement.ts';
@@ -328,6 +328,8 @@ export declare class DrftLoader {
328
328
  private arrived;
329
329
  /** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
330
330
  private readonly regionStore;
331
+ /** What `draw`, `casters` and `prepare` do with the parts. See `partDraws.ts`. */
332
+ private readonly partDraws;
331
333
  constructor(renderer: RendererApi, options?: DrftLoaderOptions);
332
334
  get progress(): DrftLoadProgress;
333
335
  /**
@@ -444,6 +446,41 @@ export declare class DrftLoader {
444
446
  readonly y: number;
445
447
  readonly z: number;
446
448
  } | null;
449
+ /**
450
+ * Draw this frame's parts into the open mesh pass, each by every rule a baked container needs:
451
+ * its whole material, its copies through the instanced path, a blended part translucent and
452
+ * writing no depth, glass as glass, its own reflectivity. Returns the draws issued.
453
+ *
454
+ * **The one call a model wants**, because each rule missed is a wrong picture rather than an
455
+ * error: a repeated arch drawn once, a decal as a black patch, foliage without its alpha test.
456
+ * What it gives up, and what drawing `parts` by hand is for, is a per-part override; see
457
+ * `PartDraws`.
458
+ */
459
+ draw(): number;
460
+ /**
461
+ * Every part that stands in the light, for `drawShadowCasters` or a mirror's replay: all but a
462
+ * decal, which would shadow its own surface, each with its material so a cutout casts the shape
463
+ * in its texture, glass as glass and copies as copies. Always this frame's parts.
464
+ */
465
+ readonly casters: ShadowCasters;
466
+ /**
467
+ * Compile what every part's draw will take, off the frame, and wait for it. Call it once the load
468
+ * has finished, behind whatever is showing then; one frame drawn there as well compiles the
469
+ * shadow passes' own pipelines, which this cannot reach, and the first frame a player sees then
470
+ * waits on nothing.
471
+ */
472
+ prepare(): Promise<void>;
473
+ /**
474
+ * A directional light's matrix covering the model whole, as fitted, into `out`, through
475
+ * `computeLightMatrixForBounds`; returns the depth span for `shadowDepthSpan`, or null until
476
+ * `HEAD` has said how large the model is.
477
+ *
478
+ * **What it gives up** is the bounds' own: they are the file's, fitted, so a `transform` that
479
+ * moves a mesh moves it past them, and a model far larger than what the viewer sees spreads the
480
+ * map over all of it — a city wants the map around the viewer instead, through
481
+ * `computeLightMatrix`.
482
+ */
483
+ shadowFit(lightDir: Vec3, shadowMapSize: number, out: Float32Array): number | null;
447
484
  /**
448
485
  * Start loading, and resolve when the last byte has been read.
449
486
  *
@@ -1,5 +1,6 @@
1
1
  /** Loading a `.drft` into drawable parts progressively, without stalling the frame. */
2
- import { MeshBuilder, concatMeshes, placeMesh } from '@driftengine/core';
2
+ import { MeshBuilder, computeLightMatrixForBounds, concatMeshes, placeMesh, } from '@driftengine/core';
3
+ import { PartDraws } from './partDraws.js';
3
4
  import { placeFields } from './fieldPlacement.js';
4
5
  import { TextureSet, textureColorSpaces } from './drftTextures.js';
5
6
  import { bcPlan, createBcDecoder } from './bcLoad.js';
@@ -11,6 +12,9 @@ import { drawKeyOf, resolveDrawGrouping } from './drawKey.js';
11
12
  import { streamDrft, CODEC_JPEG, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError, expandAssembly, } from '@driftengine/drft';
12
13
  import { RegionStore, fitCopies } from './regionStore.js';
13
14
  import { DEFAULT_REVEAL_SEC, DEFAULT_UPLOAD_MS_PER_FRAME, DEFAULT_UPLOADS_PER_FRAME, isDocumentResponse, mayBeginMore, } from './uploadBudget.js';
15
+ /** `shadowFit`'s fitted bounds, rewritten in place on each call. */
16
+ const FIT_MIN = [0, 0, 0];
17
+ const FIT_MAX = [0, 0, 0];
14
18
  /**
15
19
  * A `.drft` loaded progressively: the model builds up on screen instead of appearing.
16
20
  *
@@ -162,10 +166,13 @@ export class DrftLoader {
162
166
  arrived = 0;
163
167
  /** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
164
168
  regionStore;
169
+ /** What `draw`, `casters` and `prepare` do with the parts. See `partDraws.ts`. */
170
+ partDraws;
165
171
  constructor(renderer, options = {}) {
166
172
  this.renderer = renderer;
167
173
  this.options = options;
168
174
  this.regionStore = new RegionStore(renderer, () => this.fit);
175
+ this.partDraws = new PartDraws(renderer);
169
176
  }
170
177
  get progress() {
171
178
  return this.state;
@@ -388,6 +395,58 @@ export class DrftLoader {
388
395
  get placement() {
389
396
  return this.fit;
390
397
  }
398
+ /**
399
+ * Draw this frame's parts into the open mesh pass, each by every rule a baked container needs:
400
+ * its whole material, its copies through the instanced path, a blended part translucent and
401
+ * writing no depth, glass as glass, its own reflectivity. Returns the draws issued.
402
+ *
403
+ * **The one call a model wants**, because each rule missed is a wrong picture rather than an
404
+ * error: a repeated arch drawn once, a decal as a black patch, foliage without its alpha test.
405
+ * What it gives up, and what drawing `parts` by hand is for, is a per-part override; see
406
+ * `PartDraws`.
407
+ */
408
+ draw() {
409
+ return this.partDraws.draw(this.revealed, this.textureSet);
410
+ }
411
+ /**
412
+ * Every part that stands in the light, for `drawShadowCasters` or a mirror's replay: all but a
413
+ * decal, which would shadow its own surface, each with its material so a cutout casts the shape
414
+ * in its texture, glass as glass and copies as copies. Always this frame's parts.
415
+ */
416
+ casters = (sink) => this.partDraws.cast(sink, this.revealed, this.textureSet);
417
+ /**
418
+ * Compile what every part's draw will take, off the frame, and wait for it. Call it once the load
419
+ * has finished, behind whatever is showing then; one frame drawn there as well compiles the
420
+ * shadow passes' own pipelines, which this cannot reach, and the first frame a player sees then
421
+ * waits on nothing.
422
+ */
423
+ prepare() {
424
+ return this.partDraws.prepare(this.revealed, this.textureSet);
425
+ }
426
+ /**
427
+ * A directional light's matrix covering the model whole, as fitted, into `out`, through
428
+ * `computeLightMatrixForBounds`; returns the depth span for `shadowDepthSpan`, or null until
429
+ * `HEAD` has said how large the model is.
430
+ *
431
+ * **What it gives up** is the bounds' own: they are the file's, fitted, so a `transform` that
432
+ * moves a mesh moves it past them, and a model far larger than what the viewer sees spreads the
433
+ * map over all of it — a city wants the map around the viewer instead, through
434
+ * `computeLightMatrix`.
435
+ */
436
+ shadowFit(lightDir, shadowMapSize, out) {
437
+ const fit = this.fit;
438
+ const b = this.bounds;
439
+ if (fit === null || b.length < 6)
440
+ return null;
441
+ const { scale, x, y, z } = fit;
442
+ FIT_MIN[0] = b[0] * scale + x;
443
+ FIT_MIN[1] = b[1] * scale + y;
444
+ FIT_MIN[2] = b[2] * scale + z;
445
+ FIT_MAX[0] = b[3] * scale + x;
446
+ FIT_MAX[1] = b[4] * scale + y;
447
+ FIT_MAX[2] = b[5] * scale + z;
448
+ return computeLightMatrixForBounds(lightDir, FIT_MIN, FIT_MAX, shadowMapSize, out);
449
+ }
391
450
  /**
392
451
  * Start loading, and resolve when the last byte has been read.
393
452
  *
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
  export { DrftLoader } from './drftLoader.ts';
10
10
  export { writePartMaterial } from './partMaterial.ts';
11
+ export { PartDraws } from './partDraws.ts';
11
12
  export { dtexTileBytes, tileGridFromDtex } from './dtexTiles.ts';
12
13
  export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
13
14
  export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
package/dist/index.js CHANGED
@@ -17,6 +17,11 @@ export { DrftLoader } from './drftLoader.js';
17
17
  * it: a copy that left out the occlusion strength drew a bought model black.
18
18
  */
19
19
  export { writePartMaterial } from './partMaterial.js';
20
+ /*
21
+ * Parts drawn, cast and prepared by every rule a container needs, which `DrftLoader.draw` is: here
22
+ * for a caller drawing parts of its own choosing, several loaders' or a list it filtered.
23
+ */
24
+ export { PartDraws } from './partDraws.js';
20
25
  /*
21
26
  * **A `DTEX` chunk's tile table as the grid residency asks about.** The other constructor of a
22
27
  * `MaterialTileGrid` — `latentTileGrid` cuts one from a latent a baker holds, and this reads one a
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A loaded model's parts drawn, cast and prepared by the rules a baked container needs, so a caller
3
+ * gets them right by calling one method rather than by knowing each of them.
4
+ *
5
+ * **Every rule here is one that fails as a picture rather than an error**, which is why they are in
6
+ * one place: a part's whole material (`writePartMaterial`), or foliage loses its alpha test and a
7
+ * one-sided card vanishes from behind; a part's copies through the instanced path, or a repeated arch
8
+ * is drawn once; a blended part translucent whatever its opacity and writing no depth, since its
9
+ * alpha is in its texture, or a decal draws as a black patch on the stone; glass as glass, its options
10
+ * kept per pane so a frame allocates none; each part's own reflectivity; and in the shadow, every
11
+ * part but a decal, which would shadow its own surface, with its material so a cutout casts the
12
+ * shape in its texture. A part fades in on its `reveal`, translucent while it does.
13
+ *
14
+ * **What it gives up**: one material and one reflectivity a part, as the container says, with no
15
+ * per-part override; a caller who wants one (a map switched off, a part left out) draws the parts
16
+ * itself, which `writePartMaterial` and the fields of `DrftPart` are for. It sets no camera and opens
17
+ * no pass: it draws into whichever mesh pass is open, as `drawMesh` does.
18
+ */
19
+ import type { RendererApi, ShadowCasterSink, SurfaceTextureHandle } from '@driftengine/core';
20
+ import type { TextureSet } from './drftTextures.ts';
21
+ import type { DrftPart } from './loadProgress.ts';
22
+ /** Parts drawn, cast and prepared by every rule a container needs; `DrftLoader.draw` is one. */
23
+ export declare class PartDraws {
24
+ private readonly renderer;
25
+ private readonly material;
26
+ private readonly casterMaterial;
27
+ /** Each pane's draw options, made once: a frame drawing glass allocates nothing. */
28
+ private readonly glassDraws;
29
+ constructor(renderer: RendererApi);
30
+ /** Draw every part into the open mesh pass. Returns the draws issued. */
31
+ draw(parts: readonly DrftPart[], textures: TextureSet<SurfaceTextureHandle> | null): number;
32
+ /** Offer every part that stands in the light to a shadow pass, or to a mirror's replay. */
33
+ cast(sink: ShadowCasterSink, parts: readonly DrftPart[], textures: TextureSet<SurfaceTextureHandle> | null): void;
34
+ /**
35
+ * Compile, off the frame, what every part's draw will take, then wait for it: each in its own
36
+ * material, translucent as well where it will be drawn so, through its batch where it has copies.
37
+ * What it cannot reach is the shadow passes' and a probe's own pipelines, which one frame drawn
38
+ * behind a loading screen compiles; see `RendererApi.prepareMesh`.
39
+ */
40
+ prepare(parts: readonly DrftPart[], textures: TextureSet<SurfaceTextureHandle> | null): Promise<void>;
41
+ /** A part's translucent draw options: glass as glass, a blended part writing no depth. */
42
+ private optionsFor;
43
+ }
@@ -0,0 +1,100 @@
1
+ import { writePartMaterial } from './partMaterial.js';
2
+ const IDENTITY = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
3
+ /** A blended part's draw: its alpha is in its texture, and a depth it wrote would hide what is behind. */
4
+ const BLENDED = Object.freeze({ depthWrite: false });
5
+ const OPAQUE_PREPARED = Object.freeze({ translucent: false });
6
+ const TRANSLUCENT_PREPARED = Object.freeze({ translucent: true });
7
+ /** Parts drawn, cast and prepared by every rule a container needs; `DrftLoader.draw` is one. */
8
+ export class PartDraws {
9
+ renderer;
10
+ material = {};
11
+ casterMaterial = {};
12
+ /** Each pane's draw options, made once: a frame drawing glass allocates nothing. */
13
+ glassDraws = new Map();
14
+ constructor(renderer) {
15
+ this.renderer = renderer;
16
+ }
17
+ /** Draw every part into the open mesh pass. Returns the draws issued. */
18
+ draw(parts, textures) {
19
+ const renderer = this.renderer;
20
+ let draws = 0;
21
+ for (const part of parts) {
22
+ renderer.setMaterial(writePartMaterial(part, textures, this.material));
23
+ renderer.setSurfaceReflectivity(part.reflectivity);
24
+ const opacity = part.opacity * part.reveal;
25
+ const options = this.optionsFor(part);
26
+ const instances = part.instances;
27
+ if (instances !== null) {
28
+ if (translucent(part, opacity)) {
29
+ renderer.drawTranslucentInstanced(instances.batch, instances.data, opacity, options);
30
+ }
31
+ else {
32
+ renderer.drawInstanced(instances.batch, instances.data);
33
+ }
34
+ }
35
+ else if (translucent(part, opacity)) {
36
+ renderer.drawTranslucentMesh(part.mesh, IDENTITY, opacity, options);
37
+ }
38
+ else {
39
+ renderer.drawMesh(part.mesh, IDENTITY);
40
+ }
41
+ draws++;
42
+ }
43
+ /* Nothing of the last part is left for the caller's own draws: no material, no mirror. */
44
+ renderer.setMaterial(null);
45
+ renderer.setSurfaceReflectivity(0);
46
+ return draws;
47
+ }
48
+ /** Offer every part that stands in the light to a shadow pass, or to a mirror's replay. */
49
+ cast(sink, parts, textures) {
50
+ const material = this.casterMaterial;
51
+ for (const part of parts) {
52
+ /* A blended part that is not glass is a decal, which would shadow the surface it lies on. */
53
+ if (part.blend && part.glass === null)
54
+ continue;
55
+ writePartMaterial(part, textures, material);
56
+ material.glass = part.glass ?? undefined;
57
+ if (part.instances !== null) {
58
+ sink.instanced?.(part.instances.batch, part.instances.data, material);
59
+ }
60
+ else {
61
+ sink.mesh(part.mesh, IDENTITY, material);
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * Compile, off the frame, what every part's draw will take, then wait for it: each in its own
67
+ * material, translucent as well where it will be drawn so, through its batch where it has copies.
68
+ * What it cannot reach is the shadow passes' and a probe's own pipelines, which one frame drawn
69
+ * behind a loading screen compiles; see `RendererApi.prepareMesh`.
70
+ */
71
+ async prepare(parts, textures) {
72
+ const renderer = this.renderer;
73
+ for (const part of parts) {
74
+ const material = writePartMaterial(part, textures, this.material);
75
+ const options = translucent(part, part.opacity) ? TRANSLUCENT_PREPARED : OPAQUE_PREPARED;
76
+ if (part.instances !== null)
77
+ renderer.prepareInstanced(part.instances.batch, material, options);
78
+ else
79
+ renderer.prepareMesh(part.mesh, material, options);
80
+ }
81
+ await renderer.ready();
82
+ }
83
+ /** A part's translucent draw options: glass as glass, a blended part writing no depth. */
84
+ optionsFor(part) {
85
+ const glass = part.glass;
86
+ if (glass !== null) {
87
+ let options = this.glassDraws.get(glass);
88
+ if (options === undefined) {
89
+ options = Object.freeze({ ...BLENDED, glass });
90
+ this.glassDraws.set(glass, options);
91
+ }
92
+ return options;
93
+ }
94
+ return part.blend ? BLENDED : undefined;
95
+ }
96
+ }
97
+ /** A blended part is drawn translucent whatever its opacity: its alpha is in its texture. */
98
+ function translucent(part, opacity) {
99
+ return opacity < 1 || part.blend;
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/assets",
3
- "version": "4.12.0",
3
+ "version": "4.13.1",
4
4
  "description": "Model readers and the streaming loader for the .drft container",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -32,11 +32,11 @@
32
32
  ],
33
33
  "sideEffects": false,
34
34
  "dependencies": {
35
- "@driftengine/drft": "4.12.0",
36
- "@driftengine/texture": "4.12.0"
35
+ "@driftengine/drft": "4.13.1",
36
+ "@driftengine/texture": "4.13.1"
37
37
  },
38
38
  "peerDependencies": {
39
- "@driftengine/core": "4.12.0"
39
+ "@driftengine/core": "4.13.1"
40
40
  },
41
41
  "author": "Drift Technologies",
42
42
  "repository": {
package/src/drftLoader.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  /** Loading a `.drft` into drawable parts progressively, without stalling the frame. */
2
2
 
3
3
  import type { MeshData } from '@driftengine/drft';
4
- import type { MeshHandle, RendererApi } from '@driftengine/core';
4
+ import type { MeshHandle, RendererApi, ShadowCasters, Vec3 } from '@driftengine/core';
5
5
  import type {
6
6
  CompressedTextureSource,
7
7
  GlassOptions,
8
8
  SurfaceTextureHandle,
9
9
  } from '@driftengine/core';
10
- import { MeshBuilder, concatMeshes, placeMesh } from '@driftengine/core';
10
+ import {
11
+ MeshBuilder,
12
+ computeLightMatrixForBounds,
13
+ concatMeshes,
14
+ placeMesh,
15
+ } from '@driftengine/core';
16
+ import { PartDraws } from './partDraws.ts';
11
17
  import type { DrftMaterial } from '@driftengine/drft';
12
18
  import type { DrftSdfvEntry } from '@driftengine/drft';
13
19
  import { placeFields } from './fieldPlacement.ts';
@@ -226,6 +232,10 @@ export interface DrftLoaderOptions {
226
232
  readonly onMesh?: (mesh: MeshData, ordinal: number) => boolean;
227
233
  }
228
234
 
235
+ /** `shadowFit`'s fitted bounds, rewritten in place on each call. */
236
+ const FIT_MIN: Vec3 = [0, 0, 0];
237
+ const FIT_MAX: Vec3 = [0, 0, 0];
238
+
229
239
  /**
230
240
  * A `.drft` loaded progressively: the model builds up on screen instead of appearing.
231
241
  *
@@ -411,11 +421,14 @@ export class DrftLoader {
411
421
  private arrived = 0;
412
422
  /** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
413
423
  private readonly regionStore: RegionStore;
424
+ /** What `draw`, `casters` and `prepare` do with the parts. See `partDraws.ts`. */
425
+ private readonly partDraws: PartDraws;
414
426
 
415
427
  constructor(renderer: RendererApi, options: DrftLoaderOptions = {}) {
416
428
  this.renderer = renderer;
417
429
  this.options = options;
418
430
  this.regionStore = new RegionStore(renderer, () => this.fit);
431
+ this.partDraws = new PartDraws(renderer);
419
432
  }
420
433
 
421
434
  get progress(): DrftLoadProgress {
@@ -657,6 +670,62 @@ export class DrftLoader {
657
670
  return this.fit;
658
671
  }
659
672
 
673
+ /**
674
+ * Draw this frame's parts into the open mesh pass, each by every rule a baked container needs:
675
+ * its whole material, its copies through the instanced path, a blended part translucent and
676
+ * writing no depth, glass as glass, its own reflectivity. Returns the draws issued.
677
+ *
678
+ * **The one call a model wants**, because each rule missed is a wrong picture rather than an
679
+ * error: a repeated arch drawn once, a decal as a black patch, foliage without its alpha test.
680
+ * What it gives up, and what drawing `parts` by hand is for, is a per-part override; see
681
+ * `PartDraws`.
682
+ */
683
+ draw(): number {
684
+ return this.partDraws.draw(this.revealed, this.textureSet);
685
+ }
686
+
687
+ /**
688
+ * Every part that stands in the light, for `drawShadowCasters` or a mirror's replay: all but a
689
+ * decal, which would shadow its own surface, each with its material so a cutout casts the shape
690
+ * in its texture, glass as glass and copies as copies. Always this frame's parts.
691
+ */
692
+ readonly casters: ShadowCasters = (sink) =>
693
+ this.partDraws.cast(sink, this.revealed, this.textureSet);
694
+
695
+ /**
696
+ * Compile what every part's draw will take, off the frame, and wait for it. Call it once the load
697
+ * has finished, behind whatever is showing then; one frame drawn there as well compiles the
698
+ * shadow passes' own pipelines, which this cannot reach, and the first frame a player sees then
699
+ * waits on nothing.
700
+ */
701
+ prepare(): Promise<void> {
702
+ return this.partDraws.prepare(this.revealed, this.textureSet);
703
+ }
704
+
705
+ /**
706
+ * A directional light's matrix covering the model whole, as fitted, into `out`, through
707
+ * `computeLightMatrixForBounds`; returns the depth span for `shadowDepthSpan`, or null until
708
+ * `HEAD` has said how large the model is.
709
+ *
710
+ * **What it gives up** is the bounds' own: they are the file's, fitted, so a `transform` that
711
+ * moves a mesh moves it past them, and a model far larger than what the viewer sees spreads the
712
+ * map over all of it — a city wants the map around the viewer instead, through
713
+ * `computeLightMatrix`.
714
+ */
715
+ shadowFit(lightDir: Vec3, shadowMapSize: number, out: Float32Array): number | null {
716
+ const fit = this.fit;
717
+ const b = this.bounds;
718
+ if (fit === null || b.length < 6) return null;
719
+ const { scale, x, y, z } = fit;
720
+ FIT_MIN[0] = (b[0] as number) * scale + x;
721
+ FIT_MIN[1] = (b[1] as number) * scale + y;
722
+ FIT_MIN[2] = (b[2] as number) * scale + z;
723
+ FIT_MAX[0] = (b[3] as number) * scale + x;
724
+ FIT_MAX[1] = (b[4] as number) * scale + y;
725
+ FIT_MAX[2] = (b[5] as number) * scale + z;
726
+ return computeLightMatrixForBounds(lightDir, FIT_MIN, FIT_MAX, shadowMapSize, out);
727
+ }
728
+
660
729
  /**
661
730
  * Start loading, and resolve when the last byte has been read.
662
731
  *
package/src/index.ts CHANGED
@@ -18,6 +18,11 @@ export { DrftLoader } from './drftLoader.ts';
18
18
  * it: a copy that left out the occlusion strength drew a bought model black.
19
19
  */
20
20
  export { writePartMaterial } from './partMaterial.ts';
21
+ /*
22
+ * Parts drawn, cast and prepared by every rule a container needs, which `DrftLoader.draw` is: here
23
+ * for a caller drawing parts of its own choosing, several loaders' or a list it filtered.
24
+ */
25
+ export { PartDraws } from './partDraws.ts';
21
26
  /*
22
27
  * **A `DTEX` chunk's tile table as the grid residency asks about.** The other constructor of a
23
28
  * `MaterialTileGrid` — `latentTileGrid` cuts one from a latent a baker holds, and this reads one a
@@ -0,0 +1,138 @@
1
+ /**
2
+ * A loaded model's parts drawn, cast and prepared by the rules a baked container needs, so a caller
3
+ * gets them right by calling one method rather than by knowing each of them.
4
+ *
5
+ * **Every rule here is one that fails as a picture rather than an error**, which is why they are in
6
+ * one place: a part's whole material (`writePartMaterial`), or foliage loses its alpha test and a
7
+ * one-sided card vanishes from behind; a part's copies through the instanced path, or a repeated arch
8
+ * is drawn once; a blended part translucent whatever its opacity and writing no depth, since its
9
+ * alpha is in its texture, or a decal draws as a black patch on the stone; glass as glass, its options
10
+ * kept per pane so a frame allocates none; each part's own reflectivity; and in the shadow, every
11
+ * part but a decal, which would shadow its own surface, with its material so a cutout casts the
12
+ * shape in its texture. A part fades in on its `reveal`, translucent while it does.
13
+ *
14
+ * **What it gives up**: one material and one reflectivity a part, as the container says, with no
15
+ * per-part override; a caller who wants one (a map switched off, a part left out) draws the parts
16
+ * itself, which `writePartMaterial` and the fields of `DrftPart` are for. It sets no camera and opens
17
+ * no pass: it draws into whichever mesh pass is open, as `drawMesh` does.
18
+ */
19
+ import type {
20
+ GlassOptions,
21
+ RendererApi,
22
+ ShadowCasterSink,
23
+ SurfaceMaterial,
24
+ SurfaceTextureHandle,
25
+ TranslucentMeshOptions,
26
+ } from '@driftengine/core';
27
+ import type { TextureSet } from './drftTextures.ts';
28
+ import type { DrftPart } from './loadProgress.ts';
29
+ import { writePartMaterial } from './partMaterial.ts';
30
+
31
+ const IDENTITY = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
32
+
33
+ /** A blended part's draw: its alpha is in its texture, and a depth it wrote would hide what is behind. */
34
+ const BLENDED: TranslucentMeshOptions = Object.freeze({ depthWrite: false });
35
+ const OPAQUE_PREPARED = Object.freeze({ translucent: false });
36
+ const TRANSLUCENT_PREPARED = Object.freeze({ translucent: true });
37
+
38
+ /** A caster's material: a part's, and its glass, rewritten for each part. */
39
+ type CasterMaterial = SurfaceMaterial<SurfaceTextureHandle> & { glass?: GlassOptions };
40
+
41
+ /** Parts drawn, cast and prepared by every rule a container needs; `DrftLoader.draw` is one. */
42
+ export class PartDraws {
43
+ private readonly material: SurfaceMaterial<SurfaceTextureHandle> = {};
44
+ private readonly casterMaterial: CasterMaterial = {};
45
+ /** Each pane's draw options, made once: a frame drawing glass allocates nothing. */
46
+ private readonly glassDraws = new Map<GlassOptions, TranslucentMeshOptions>();
47
+
48
+ constructor(private readonly renderer: RendererApi) {}
49
+
50
+ /** Draw every part into the open mesh pass. Returns the draws issued. */
51
+ draw(parts: readonly DrftPart[], textures: TextureSet<SurfaceTextureHandle> | null): number {
52
+ const renderer = this.renderer;
53
+ let draws = 0;
54
+ for (const part of parts) {
55
+ renderer.setMaterial(writePartMaterial(part, textures, this.material));
56
+ renderer.setSurfaceReflectivity(part.reflectivity);
57
+ const opacity = part.opacity * part.reveal;
58
+ const options = this.optionsFor(part);
59
+ const instances = part.instances;
60
+ if (instances !== null) {
61
+ if (translucent(part, opacity)) {
62
+ renderer.drawTranslucentInstanced(instances.batch, instances.data, opacity, options);
63
+ } else {
64
+ renderer.drawInstanced(instances.batch, instances.data);
65
+ }
66
+ } else if (translucent(part, opacity)) {
67
+ renderer.drawTranslucentMesh(part.mesh, IDENTITY, opacity, options);
68
+ } else {
69
+ renderer.drawMesh(part.mesh, IDENTITY);
70
+ }
71
+ draws++;
72
+ }
73
+ /* Nothing of the last part is left for the caller's own draws: no material, no mirror. */
74
+ renderer.setMaterial(null);
75
+ renderer.setSurfaceReflectivity(0);
76
+ return draws;
77
+ }
78
+
79
+ /** Offer every part that stands in the light to a shadow pass, or to a mirror's replay. */
80
+ cast(
81
+ sink: ShadowCasterSink,
82
+ parts: readonly DrftPart[],
83
+ textures: TextureSet<SurfaceTextureHandle> | null,
84
+ ): void {
85
+ const material = this.casterMaterial;
86
+ for (const part of parts) {
87
+ /* A blended part that is not glass is a decal, which would shadow the surface it lies on. */
88
+ if (part.blend && part.glass === null) continue;
89
+ writePartMaterial(part, textures, material);
90
+ material.glass = part.glass ?? undefined;
91
+ if (part.instances !== null) {
92
+ sink.instanced?.(part.instances.batch, part.instances.data, material);
93
+ } else {
94
+ sink.mesh(part.mesh, IDENTITY, material);
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Compile, off the frame, what every part's draw will take, then wait for it: each in its own
101
+ * material, translucent as well where it will be drawn so, through its batch where it has copies.
102
+ * What it cannot reach is the shadow passes' and a probe's own pipelines, which one frame drawn
103
+ * behind a loading screen compiles; see `RendererApi.prepareMesh`.
104
+ */
105
+ async prepare(
106
+ parts: readonly DrftPart[],
107
+ textures: TextureSet<SurfaceTextureHandle> | null,
108
+ ): Promise<void> {
109
+ const renderer = this.renderer;
110
+ for (const part of parts) {
111
+ const material = writePartMaterial(part, textures, this.material);
112
+ const options = translucent(part, part.opacity) ? TRANSLUCENT_PREPARED : OPAQUE_PREPARED;
113
+ if (part.instances !== null)
114
+ renderer.prepareInstanced(part.instances.batch, material, options);
115
+ else renderer.prepareMesh(part.mesh, material, options);
116
+ }
117
+ await renderer.ready();
118
+ }
119
+
120
+ /** A part's translucent draw options: glass as glass, a blended part writing no depth. */
121
+ private optionsFor(part: DrftPart): TranslucentMeshOptions | undefined {
122
+ const glass = part.glass;
123
+ if (glass !== null) {
124
+ let options = this.glassDraws.get(glass);
125
+ if (options === undefined) {
126
+ options = Object.freeze({ ...BLENDED, glass });
127
+ this.glassDraws.set(glass, options);
128
+ }
129
+ return options;
130
+ }
131
+ return part.blend ? BLENDED : undefined;
132
+ }
133
+ }
134
+
135
+ /** A blended part is drawn translucent whatever its opacity: its alpha is in its texture. */
136
+ function translucent(part: DrftPart, opacity: number): boolean {
137
+ return opacity < 1 || part.blend;
138
+ }