@driftengine/assets 4.4.1 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Model readers for seven formats, and the streaming loader that uploads them.
4
4
 
5
- **Cost: 15.6 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
5
+ **Cost: 15.9 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
7
7
 
8
8
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
package/dist/drawKey.d.ts CHANGED
@@ -23,6 +23,15 @@ export interface DrawSurfaceOverride {
23
23
  * gallery and read as a black blotch. 0 says dielectric.
24
24
  */
25
25
  readonly metallicScale?: number;
26
+ /**
27
+ * Glass, in place of what the file says: the share of light that passes, how milky it is, and
28
+ * the colour it takes (`GlassOptions` in the renderer). For a model whose panes arrive as an
29
+ * opaque dark material, because its format or its exporter had no word for glass. Transmission
30
+ * above zero draws the part blended.
31
+ */
32
+ readonly transmission?: number;
33
+ readonly frost?: number;
34
+ readonly tint?: readonly [number, number, number];
26
35
  }
27
36
  /**
28
37
  * The fields a merged group carries, resolved from a material and its override.
@@ -47,6 +56,13 @@ export interface DrawGrouping {
47
56
  /** Whether both faces are drawn and lit. A merge of a two-sided part and a one-sided one would cull
48
57
  * one of them or unhide the back of the other, so it is part of the key. */
49
58
  readonly doubleSided: boolean;
59
+ /**
60
+ * Glass. Zero transmission is not glass, and then frost is 0 and the tint white whatever the
61
+ * file or the override said, so a stray value on an opaque surface never splits a draw.
62
+ */
63
+ readonly transmission: number;
64
+ readonly frost: number;
65
+ readonly tint: readonly [number, number, number];
50
66
  }
51
67
  /**
52
68
  * Resolve what a part will actually be drawn with.
@@ -56,7 +72,7 @@ export interface DrawGrouping {
56
72
  * 0, which is the identity that lets a file saying nothing about a property draw the same as one
57
73
  * that did not have the property to say.
58
74
  *
59
- * The override wins over the material for the three fields it may carry, because a consumer dressing
75
+ * The override wins over the material for the fields it may carry, because a consumer dressing
60
76
  * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
61
77
  * a fact about the *image* a material wears, so nothing outside the file has anything to say
62
78
  * about it.
package/dist/drawKey.js CHANGED
@@ -1,3 +1,4 @@
1
+ const CLEAR = [1, 1, 1];
1
2
  /**
2
3
  * Resolve what a part will actually be drawn with.
3
4
  *
@@ -6,12 +7,14 @@
6
7
  * 0, which is the identity that lets a file saying nothing about a property draw the same as one
7
8
  * that did not have the property to say.
8
9
  *
9
- * The override wins over the material for the three fields it may carry, because a consumer dressing
10
+ * The override wins over the material for the fields it may carry, because a consumer dressing
10
11
  * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
11
12
  * a fact about the *image* a material wears, so nothing outside the file has anything to say
12
13
  * about it.
13
14
  */
14
15
  export function resolveDrawGrouping(material, override) {
16
+ const transmission = Math.max(0, override?.transmission ?? material?.transmission ?? 0);
17
+ const glass = transmission > 0;
15
18
  return {
16
19
  albedo: material?.albedo ?? -1,
17
20
  orm: material?.ormMap ?? -1,
@@ -23,8 +26,11 @@ export function resolveDrawGrouping(material, override) {
23
26
  metallicScale: override?.metallicScale ?? material?.metallicScale ?? 1,
24
27
  occlusionStrength: material?.occlusionStrength ?? 0,
25
28
  cutout: material?.cutout ?? 0,
26
- blend: material?.blend === true,
29
+ blend: material?.blend === true || glass,
27
30
  doubleSided: material?.doubleSided === true,
31
+ transmission,
32
+ frost: glass ? (override?.frost ?? material?.frost ?? 0) : 0,
33
+ tint: glass ? (override?.tint ?? material?.tint ?? CLEAR) : CLEAR,
28
34
  };
29
35
  }
30
36
  /**
@@ -40,5 +46,5 @@ export function resolveDrawGrouping(material, override) {
40
46
  */
41
47
  export function drawKeyOf(material, override) {
42
48
  const g = resolveDrawGrouping(material, override);
43
- return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}`;
49
+ return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}:${g.transmission}:${g.frost}:${g.tint[0]},${g.tint[1]},${g.tint[2]}`;
44
50
  }
@@ -742,6 +742,7 @@ export class DrftLoader {
742
742
  cutout: 0,
743
743
  blend: false,
744
744
  doubleSided: false,
745
+ glass: null,
745
746
  reveal: this.outlinePart?.reveal ?? 0,
746
747
  instances: null,
747
748
  };
@@ -796,6 +797,7 @@ export class DrftLoader {
796
797
  cutout,
797
798
  blend,
798
799
  doubleSided,
800
+ glass: glassOf(g),
799
801
  });
800
802
  }
801
803
  const key = drawKeyOf(material, override);
@@ -814,6 +816,7 @@ export class DrftLoader {
814
816
  cutout,
815
817
  blend,
816
818
  doubleSided,
819
+ glass: glassOf(g),
817
820
  members: [],
818
821
  };
819
822
  this.groups.set(key, group);
@@ -854,6 +857,7 @@ export class DrftLoader {
854
857
  cutout,
855
858
  blend,
856
859
  doubleSided,
860
+ glass: group.glass,
857
861
  reveal: 0,
858
862
  instances: null,
859
863
  };
@@ -947,6 +951,7 @@ export class DrftLoader {
947
951
  cutout: group.cutout,
948
952
  blend: group.blend,
949
953
  doubleSided: group.doubleSided,
954
+ glass: group.glass,
950
955
  reveal: 1,
951
956
  instances: null,
952
957
  });
@@ -1068,6 +1073,12 @@ export class DrftLoader {
1068
1073
  };
1069
1074
  }
1070
1075
  }
1076
+ /** A group's glass as the renderer takes it, or null for a group that lets no light through. */
1077
+ function glassOf(g) {
1078
+ if (g.transmission <= 0)
1079
+ return null;
1080
+ return { transmission: g.transmission, frost: g.frost, tint: [g.tint[0], g.tint[1], g.tint[2]] };
1081
+ }
1071
1082
  /**
1072
1083
  * The MIME type a codec's bytes carry, or `null` for one the browser has no decoder for.
1073
1084
  *
package/dist/gltf.d.ts CHANGED
@@ -80,16 +80,35 @@ interface GltfSpecularGlossiness {
80
80
  texCoord?: number;
81
81
  };
82
82
  }
83
+ /**
84
+ * The three extensions that together say a material is glass: how much light passes
85
+ * (`KHR_materials_transmission`), how much of it is scattered on the way and the colour it takes
86
+ * (`KHR_materials_diffuse_transmission`), and the colour a thickness of it absorbs towards
87
+ * (`KHR_materials_volume`). Only the fields this reader maps are named.
88
+ */
89
+ interface GltfTransmission {
90
+ transmissionFactor?: number;
91
+ }
92
+ interface GltfDiffuseTransmission {
93
+ diffuseTransmissionFactor?: number;
94
+ diffuseTransmissionColorFactor?: number[];
95
+ }
96
+ interface GltfVolume {
97
+ attenuationColor?: number[];
98
+ }
83
99
  interface GltfMaterial {
84
100
  pbrMetallicRoughness?: GltfPbrMetallicRoughness;
85
101
  /**
86
- * The material extensions this reader knows. One, and everything else is ignored.
102
+ * The material extensions this reader knows, and everything else is ignored.
87
103
  *
88
- * Read as an optional field rather than as `Record<string, unknown>` so that adding a second
89
- * costs a name here and a branch in `pbrOf`, and so that a typo in either is a compile error.
104
+ * Read as optional fields rather than as `Record<string, unknown>` so that adding one costs a
105
+ * name here and a branch where it is read, and so that a typo in either is a compile error.
90
106
  */
91
107
  extensions?: {
92
108
  KHR_materials_pbrSpecularGlossiness?: GltfSpecularGlossiness;
109
+ KHR_materials_transmission?: GltfTransmission;
110
+ KHR_materials_diffuse_transmission?: GltfDiffuseTransmission;
111
+ KHR_materials_volume?: GltfVolume;
93
112
  };
94
113
  normalTexture?: {
95
114
  index: number;
package/dist/gltf.js CHANGED
@@ -428,6 +428,7 @@ function materialOf(material) {
428
428
  * A separate occlusion image is dropped rather than loaded into a second unit. One map, one
429
429
  * unit, and `textureUnitBudget.test.ts` says the next unit belongs to emissive.
430
430
  */
431
+ const glass = glassOf(material, modulated ? undefined : roughness);
431
432
  const occlusion = material?.occlusionTexture;
432
433
  const occlusionStrength = modulated && occlusion !== undefined && occlusion.index === ormMap
433
434
  ? (occlusion.strength ?? 1)
@@ -447,7 +448,7 @@ function materialOf(material) {
447
448
  * cannot say the material blends, and a leaf whose shape is all in its texture's alpha drew as a
448
449
  * solid card. So it is said.
449
450
  */
450
- blend: blends,
451
+ blend: blends || glass.transmission > 0,
451
452
  /* Seen from both faces: a curtain, a leaf card. The specification's default is one face. */
452
453
  doubleSided: material?.doubleSided === true,
453
454
  /* Only in MASK mode: the specification says `alphaCutoff` has no meaning in the other two. */
@@ -463,8 +464,42 @@ function materialOf(material) {
463
464
  metallicScale: modulated ? (pbr?.metallicFactor ?? 1) : 1,
464
465
  occlusionStrength,
465
466
  albedo: pbr?.baseColorTexture?.index ?? -1,
467
+ transmission: glass.transmission,
468
+ frost: glass.frost,
469
+ tint: glass.tint,
466
470
  };
467
471
  }
472
+ const unit = (value, fallback) => Math.min(1, Math.max(0, value ?? fallback));
473
+ /**
474
+ * What a material's extensions say about letting light through, as this engine's glass.
475
+ *
476
+ * **Rough glass is frosted glass.** `KHR_materials_transmission` says its roughness blurs what is
477
+ * seen through the surface, and that blur is exactly what `frost` is, so a transmissive material's
478
+ * roughness raises its frost and a diffusely transmitting share raises it too — whichever says
479
+ * more. Only transmission makes a material glass: a surface stating diffuse transmission alone is a
480
+ * leaf or a sheet of paper, which this engine's glass would draw see-through and let every shadow
481
+ * pass, so it stays the opaque surface it was. What that gives up is a paper shade stated by that
482
+ * extension alone, which arrives opaque; a per-material override is the door for it.
483
+ *
484
+ * `roughness` is `undefined` where a map carries the roughness per texel: the factor then only
485
+ * scales the map, so it says nothing about how rough the glass is, and frost comes from the
486
+ * diffuse share alone rather than from a number the file did not state.
487
+ */
488
+ function glassOf(material, roughness) {
489
+ const ext = material?.extensions;
490
+ const transmission = unit(ext?.KHR_materials_transmission?.transmissionFactor, 0);
491
+ if (transmission <= 0)
492
+ return { transmission: 0, frost: 0, tint: [1, 1, 1] };
493
+ const diffuse = ext?.KHR_materials_diffuse_transmission;
494
+ const frost = Math.max(unit(diffuse?.diffuseTransmissionFactor, 0), unit(roughness, 0));
495
+ const colour = diffuse?.diffuseTransmissionColorFactor ?? ext?.KHR_materials_volume?.attenuationColor;
496
+ const tint = [
497
+ unit(colour?.[0], 1),
498
+ unit(colour?.[1], 1),
499
+ unit(colour?.[2], 1),
500
+ ];
501
+ return { transmission, frost, tint };
502
+ }
468
503
  /** The required extensions this reader implements, or follows far enough to fail loudly. */
469
504
  const KNOWN_REQUIRED = new Set([
470
505
  'KHR_materials_pbrSpecularGlossiness',
@@ -1,4 +1,4 @@
1
- import type { MeshHandle, InstancedHandle, MeshInstances } from '@driftengine/core';
1
+ import type { GlassOptions, 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.
@@ -120,6 +120,13 @@ export interface DrftPart {
120
120
  readonly blend: boolean;
121
121
  /** Whether both faces are seen: glTF's `doubleSided`. Drawn without culling, each face lit as the front. */
122
122
  readonly doubleSided: boolean;
123
+ /**
124
+ * The glass this part is, from its file or from `DrftLoaderOptions.surface`, or null for a part
125
+ * that lets no light through. A glass part is also `blend`, so a caller drawing blended parts
126
+ * translucent passes this as the draw's `glass` and every pane arrives as the file or the
127
+ * override said. One object per merged group, shared by its parts, so it is not rebuilt per draw.
128
+ */
129
+ readonly glass: GlassOptions | null;
123
130
  /**
124
131
  * The copies this part is drawn as, or null for a part drawn once.
125
132
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/assets",
3
- "version": "4.4.1",
3
+ "version": "4.5.0",
4
4
  "description": "Model readers and the streaming loader for the .drft container",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,11 +27,11 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "dependencies": {
30
- "@driftengine/drft": "4.4.1",
31
- "@driftengine/texture": "4.4.1"
30
+ "@driftengine/drft": "4.5.0",
31
+ "@driftengine/texture": "4.5.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@driftengine/core": "4.4.1"
34
+ "@driftengine/core": "4.5.0"
35
35
  },
36
36
  "author": "Drift Technologies",
37
37
  "repository": {
package/src/drawKey.ts CHANGED
@@ -25,6 +25,15 @@ export interface DrawSurfaceOverride {
25
25
  * gallery and read as a black blotch. 0 says dielectric.
26
26
  */
27
27
  readonly metallicScale?: number;
28
+ /**
29
+ * Glass, in place of what the file says: the share of light that passes, how milky it is, and
30
+ * the colour it takes (`GlassOptions` in the renderer). For a model whose panes arrive as an
31
+ * opaque dark material, because its format or its exporter had no word for glass. Transmission
32
+ * above zero draws the part blended.
33
+ */
34
+ readonly transmission?: number;
35
+ readonly frost?: number;
36
+ readonly tint?: readonly [number, number, number];
28
37
  }
29
38
 
30
39
  /**
@@ -50,8 +59,17 @@ export interface DrawGrouping {
50
59
  /** Whether both faces are drawn and lit. A merge of a two-sided part and a one-sided one would cull
51
60
  * one of them or unhide the back of the other, so it is part of the key. */
52
61
  readonly doubleSided: boolean;
62
+ /**
63
+ * Glass. Zero transmission is not glass, and then frost is 0 and the tint white whatever the
64
+ * file or the override said, so a stray value on an opaque surface never splits a draw.
65
+ */
66
+ readonly transmission: number;
67
+ readonly frost: number;
68
+ readonly tint: readonly [number, number, number];
53
69
  }
54
70
 
71
+ const CLEAR: readonly [number, number, number] = [1, 1, 1];
72
+
55
73
  /**
56
74
  * Resolve what a part will actually be drawn with.
57
75
  *
@@ -60,7 +78,7 @@ export interface DrawGrouping {
60
78
  * 0, which is the identity that lets a file saying nothing about a property draw the same as one
61
79
  * that did not have the property to say.
62
80
  *
63
- * The override wins over the material for the three fields it may carry, because a consumer dressing
81
+ * The override wins over the material for the fields it may carry, because a consumer dressing
64
82
  * a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
65
83
  * a fact about the *image* a material wears, so nothing outside the file has anything to say
66
84
  * about it.
@@ -69,6 +87,8 @@ export function resolveDrawGrouping(
69
87
  material: DrftMaterial | undefined,
70
88
  override?: DrawSurfaceOverride,
71
89
  ): DrawGrouping {
90
+ const transmission = Math.max(0, override?.transmission ?? material?.transmission ?? 0);
91
+ const glass = transmission > 0;
72
92
  return {
73
93
  albedo: material?.albedo ?? -1,
74
94
  orm: material?.ormMap ?? -1,
@@ -80,8 +100,11 @@ export function resolveDrawGrouping(
80
100
  metallicScale: override?.metallicScale ?? material?.metallicScale ?? 1,
81
101
  occlusionStrength: material?.occlusionStrength ?? 0,
82
102
  cutout: material?.cutout ?? 0,
83
- blend: material?.blend === true,
103
+ blend: material?.blend === true || glass,
84
104
  doubleSided: material?.doubleSided === true,
105
+ transmission,
106
+ frost: glass ? (override?.frost ?? material?.frost ?? 0) : 0,
107
+ tint: glass ? (override?.tint ?? material?.tint ?? CLEAR) : CLEAR,
85
108
  };
86
109
  }
87
110
 
@@ -101,5 +124,5 @@ export function drawKeyOf(
101
124
  override?: DrawSurfaceOverride,
102
125
  ): string {
103
126
  const g = resolveDrawGrouping(material, override);
104
- return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}`;
127
+ return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}:${g.transmission}:${g.frost}:${g.tint[0]},${g.tint[1]},${g.tint[2]}`;
105
128
  }
package/src/drftLoader.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { MeshData } from '@driftengine/drft';
4
4
  import type { MeshHandle, RendererApi } from '@driftengine/core';
5
- import type { SurfaceTextureHandle } from '@driftengine/core';
5
+ import type { GlassOptions, SurfaceTextureHandle } from '@driftengine/core';
6
6
  import { MeshBuilder, concatMeshes, createMeshInstances, placeMesh } from '@driftengine/core';
7
7
  import type { DrftMaterial } from '@driftengine/drft';
8
8
  import type { DrftSdfvEntry } from '@driftengine/drft';
@@ -11,7 +11,7 @@ import type { DrftFieldPlacement } from './fieldPlacement.ts';
11
11
  import type { DrftTexture } from '@driftengine/drft';
12
12
  import { TextureSet, textureColorSpaces } from './drftTextures.ts';
13
13
  import { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
14
- import type { DrawSurfaceOverride } from './drawKey.ts';
14
+ import type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
15
15
  import {
16
16
  streamDrft,
17
17
  CODEC_JPEG,
@@ -260,6 +260,8 @@ export class DrftLoader {
260
260
  cutout: number;
261
261
  blend: boolean;
262
262
  doubleSided: boolean;
263
+ /** Built once when the group is made, and shared by every part drawn from it. */
264
+ glass: GlassOptions | null;
263
265
  /** The group's parts, placed, joined once when the group is built. See `concatMeshes`. */
264
266
  members: MeshData[];
265
267
  }
@@ -964,6 +966,7 @@ export class DrftLoader {
964
966
  cutout: 0,
965
967
  blend: false,
966
968
  doubleSided: false,
969
+ glass: null,
967
970
  reveal: this.outlinePart?.reveal ?? 0,
968
971
  instances: null,
969
972
  };
@@ -1033,6 +1036,7 @@ export class DrftLoader {
1033
1036
  cutout,
1034
1037
  blend,
1035
1038
  doubleSided,
1039
+ glass: glassOf(g),
1036
1040
  });
1037
1041
  }
1038
1042
 
@@ -1052,6 +1056,7 @@ export class DrftLoader {
1052
1056
  cutout,
1053
1057
  blend,
1054
1058
  doubleSided,
1059
+ glass: glassOf(g),
1055
1060
  members: [],
1056
1061
  };
1057
1062
  this.groups.set(key, group);
@@ -1093,6 +1098,7 @@ export class DrftLoader {
1093
1098
  cutout,
1094
1099
  blend,
1095
1100
  doubleSided,
1101
+ glass: group.glass,
1096
1102
  reveal: 0,
1097
1103
  instances: null,
1098
1104
  };
@@ -1190,6 +1196,7 @@ export class DrftLoader {
1190
1196
  cutout: group.cutout,
1191
1197
  blend: group.blend,
1192
1198
  doubleSided: group.doubleSided,
1199
+ glass: group.glass,
1193
1200
  reveal: 1,
1194
1201
  instances: null,
1195
1202
  });
@@ -1309,6 +1316,12 @@ export class DrftLoader {
1309
1316
  }
1310
1317
  }
1311
1318
 
1319
+ /** A group's glass as the renderer takes it, or null for a group that lets no light through. */
1320
+ function glassOf(g: DrawGrouping): GlassOptions | null {
1321
+ if (g.transmission <= 0) return null;
1322
+ return { transmission: g.transmission, frost: g.frost, tint: [g.tint[0], g.tint[1], g.tint[2]] };
1323
+ }
1324
+
1312
1325
  /**
1313
1326
  * The MIME type a codec's bytes carry, or `null` for one the browser has no decoder for.
1314
1327
  *
package/src/gltf.ts CHANGED
@@ -77,15 +77,36 @@ interface GltfSpecularGlossiness {
77
77
  /** RGB the specular colour, A the glossiness. Both multiply the factors above. */
78
78
  specularGlossinessTexture?: { index: number; texCoord?: number };
79
79
  }
80
+ /**
81
+ * The three extensions that together say a material is glass: how much light passes
82
+ * (`KHR_materials_transmission`), how much of it is scattered on the way and the colour it takes
83
+ * (`KHR_materials_diffuse_transmission`), and the colour a thickness of it absorbs towards
84
+ * (`KHR_materials_volume`). Only the fields this reader maps are named.
85
+ */
86
+ interface GltfTransmission {
87
+ transmissionFactor?: number;
88
+ }
89
+ interface GltfDiffuseTransmission {
90
+ diffuseTransmissionFactor?: number;
91
+ diffuseTransmissionColorFactor?: number[];
92
+ }
93
+ interface GltfVolume {
94
+ attenuationColor?: number[];
95
+ }
80
96
  interface GltfMaterial {
81
97
  pbrMetallicRoughness?: GltfPbrMetallicRoughness;
82
98
  /**
83
- * The material extensions this reader knows. One, and everything else is ignored.
99
+ * The material extensions this reader knows, and everything else is ignored.
84
100
  *
85
- * Read as an optional field rather than as `Record<string, unknown>` so that adding a second
86
- * costs a name here and a branch in `pbrOf`, and so that a typo in either is a compile error.
101
+ * Read as optional fields rather than as `Record<string, unknown>` so that adding one costs a
102
+ * name here and a branch where it is read, and so that a typo in either is a compile error.
87
103
  */
88
- extensions?: { KHR_materials_pbrSpecularGlossiness?: GltfSpecularGlossiness };
104
+ extensions?: {
105
+ KHR_materials_pbrSpecularGlossiness?: GltfSpecularGlossiness;
106
+ KHR_materials_transmission?: GltfTransmission;
107
+ KHR_materials_diffuse_transmission?: GltfDiffuseTransmission;
108
+ KHR_materials_volume?: GltfVolume;
109
+ };
89
110
  normalTexture?: { index: number; texCoord?: number; scale?: number };
90
111
  /** `strength` is a mix from 1, not a multiply: 0 means unoccluded. */
91
112
  occlusionTexture?: { index: number; texCoord?: number; strength?: number };
@@ -640,6 +661,8 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
640
661
  * A separate occlusion image is dropped rather than loaded into a second unit. One map, one
641
662
  * unit, and `textureUnitBudget.test.ts` says the next unit belongs to emissive.
642
663
  */
664
+ const glass = glassOf(material, modulated ? undefined : roughness);
665
+
643
666
  const occlusion = material?.occlusionTexture;
644
667
  const occlusionStrength =
645
668
  modulated && occlusion !== undefined && occlusion.index === ormMap
@@ -662,7 +685,7 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
662
685
  * cannot say the material blends, and a leaf whose shape is all in its texture's alpha drew as a
663
686
  * solid card. So it is said.
664
687
  */
665
- blend: blends,
688
+ blend: blends || glass.transmission > 0,
666
689
  /* Seen from both faces: a curtain, a leaf card. The specification's default is one face. */
667
690
  doubleSided: material?.doubleSided === true,
668
691
  /* Only in MASK mode: the specification says `alphaCutoff` has no meaning in the other two. */
@@ -678,9 +701,49 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
678
701
  metallicScale: modulated ? (pbr?.metallicFactor ?? 1) : 1,
679
702
  occlusionStrength,
680
703
  albedo: pbr?.baseColorTexture?.index ?? -1,
704
+ transmission: glass.transmission,
705
+ frost: glass.frost,
706
+ tint: glass.tint,
681
707
  };
682
708
  }
683
709
 
710
+ const unit = (value: number | undefined, fallback: number): number =>
711
+ Math.min(1, Math.max(0, value ?? fallback));
712
+
713
+ /**
714
+ * What a material's extensions say about letting light through, as this engine's glass.
715
+ *
716
+ * **Rough glass is frosted glass.** `KHR_materials_transmission` says its roughness blurs what is
717
+ * seen through the surface, and that blur is exactly what `frost` is, so a transmissive material's
718
+ * roughness raises its frost and a diffusely transmitting share raises it too — whichever says
719
+ * more. Only transmission makes a material glass: a surface stating diffuse transmission alone is a
720
+ * leaf or a sheet of paper, which this engine's glass would draw see-through and let every shadow
721
+ * pass, so it stays the opaque surface it was. What that gives up is a paper shade stated by that
722
+ * extension alone, which arrives opaque; a per-material override is the door for it.
723
+ *
724
+ * `roughness` is `undefined` where a map carries the roughness per texel: the factor then only
725
+ * scales the map, so it says nothing about how rough the glass is, and frost comes from the
726
+ * diffuse share alone rather than from a number the file did not state.
727
+ */
728
+ function glassOf(
729
+ material: GltfMaterial | undefined,
730
+ roughness: number | undefined,
731
+ ): { transmission: number; frost: number; tint: [number, number, number] } {
732
+ const ext = material?.extensions;
733
+ const transmission = unit(ext?.KHR_materials_transmission?.transmissionFactor, 0);
734
+ if (transmission <= 0) return { transmission: 0, frost: 0, tint: [1, 1, 1] };
735
+ const diffuse = ext?.KHR_materials_diffuse_transmission;
736
+ const frost = Math.max(unit(diffuse?.diffuseTransmissionFactor, 0), unit(roughness, 0));
737
+ const colour =
738
+ diffuse?.diffuseTransmissionColorFactor ?? ext?.KHR_materials_volume?.attenuationColor;
739
+ const tint: [number, number, number] = [
740
+ unit(colour?.[0], 1),
741
+ unit(colour?.[1], 1),
742
+ unit(colour?.[2], 1),
743
+ ];
744
+ return { transmission, frost, tint };
745
+ }
746
+
684
747
  /** What a caller can tell the reader about what it is going to do with the result. */
685
748
  export interface GltfReadOptions {
686
749
  /**
@@ -1,4 +1,4 @@
1
- import type { MeshHandle, InstancedHandle, MeshInstances } from '@driftengine/core';
1
+ import type { GlassOptions, 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
@@ -134,6 +134,13 @@ export interface DrftPart {
134
134
  readonly blend: boolean;
135
135
  /** Whether both faces are seen: glTF's `doubleSided`. Drawn without culling, each face lit as the front. */
136
136
  readonly doubleSided: boolean;
137
+ /**
138
+ * The glass this part is, from its file or from `DrftLoaderOptions.surface`, or null for a part
139
+ * that lets no light through. A glass part is also `blend`, so a caller drawing blended parts
140
+ * translucent passes this as the draw's `glass` and every pane arrives as the file or the
141
+ * override said. One object per merged group, shared by its parts, so it is not rebuilt per draw.
142
+ */
143
+ readonly glass: GlassOptions | null;
137
144
  /**
138
145
  * The copies this part is drawn as, or null for a part drawn once.
139
146
  *