@driftengine/core 4.8.3 → 4.8.4

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.
Files changed (178) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/meshConcat.js +2 -0
  4. package/dist/index.d.ts +10 -4
  5. package/dist/index.js +16 -1
  6. package/dist/math/scalar.js +1 -1
  7. package/dist/render/anisotropicLobe.d.ts +7 -0
  8. package/dist/render/anisotropicLobe.js +33 -0
  9. package/dist/render/backend/api.d.ts +41 -4
  10. package/dist/render/backend/select.js +8 -0
  11. package/dist/render/backend/webgl2/clothTextures.d.ts +27 -0
  12. package/dist/render/backend/webgl2/clothTextures.js +93 -0
  13. package/dist/render/backend/webgl2/renderer.d.ts +136 -3
  14. package/dist/render/backend/webgl2/renderer.js +604 -63
  15. package/dist/render/backend/webgl2/skinScatter.d.ts +50 -0
  16. package/dist/render/backend/webgl2/skinScatter.js +256 -0
  17. package/dist/render/backend/webgpu/buffers.d.ts +25 -11
  18. package/dist/render/backend/webgpu/buffers.js +98 -49
  19. package/dist/render/backend/webgpu/clothSolvePack.d.ts +27 -0
  20. package/dist/render/backend/webgpu/clothSolvePack.js +129 -0
  21. package/dist/render/backend/webgpu/clothTextures.d.ts +68 -0
  22. package/dist/render/backend/webgpu/clothTextures.js +145 -0
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +18 -0
  24. package/dist/render/backend/webgpu/compressedUpload.js +38 -0
  25. package/dist/render/backend/webgpu/computeHost.d.ts +21 -0
  26. package/dist/render/backend/webgpu/computeHost.js +5 -0
  27. package/dist/render/backend/webgpu/depthPass.d.ts +16 -6
  28. package/dist/render/backend/webgpu/depthPass.js +39 -9
  29. package/dist/render/backend/webgpu/drawCommand.js +6 -2
  30. package/dist/render/backend/webgpu/flatPass.d.ts +38 -4
  31. package/dist/render/backend/webgpu/flatPass.js +109 -12
  32. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +74 -0
  33. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +452 -0
  34. package/dist/render/backend/webgpu/motionPass.d.ts +20 -0
  35. package/dist/render/backend/webgpu/motionPass.js +106 -9
  36. package/dist/render/backend/webgpu/postPass.d.ts +3 -1
  37. package/dist/render/backend/webgpu/postPass.js +6 -3
  38. package/dist/render/backend/webgpu/renderer.d.ts +113 -5
  39. package/dist/render/backend/webgpu/renderer.js +498 -48
  40. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +33 -0
  41. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +469 -0
  42. package/dist/render/backend/webgpu/skinScatterPass.d.ts +51 -0
  43. package/dist/render/backend/webgpu/skinScatterPass.js +253 -0
  44. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  45. package/dist/render/backend/webgpu/surfaceTexturePass.js +37 -12
  46. package/dist/render/clothBindingData.d.ts +41 -0
  47. package/dist/render/clothBindingData.js +72 -0
  48. package/dist/render/clothPlacement.d.ts +25 -0
  49. package/dist/render/clothPlacement.js +85 -0
  50. package/dist/render/compressedSource.d.ts +66 -0
  51. package/dist/render/compressedSource.js +138 -0
  52. package/dist/render/cutoutCaster.d.ts +2 -0
  53. package/dist/render/cutoutCaster.js +2 -0
  54. package/dist/render/cutoutDither.d.ts +51 -0
  55. package/dist/render/cutoutDither.js +90 -0
  56. package/dist/render/eyeRefraction.d.ts +35 -0
  57. package/dist/render/eyeRefraction.js +40 -0
  58. package/dist/render/glCompressed.d.ts +27 -0
  59. package/dist/render/glCompressed.js +53 -0
  60. package/dist/render/hairLobes.d.ts +70 -0
  61. package/dist/render/hairLobes.js +120 -0
  62. package/dist/render/lightBudget.d.ts +22 -0
  63. package/dist/render/lightBudget.js +22 -0
  64. package/dist/render/mesh.d.ts +7 -0
  65. package/dist/render/mesh.js +24 -0
  66. package/dist/render/recon/motionVectors.d.ts +4 -2
  67. package/dist/render/recon/motionVectors.js +6 -3
  68. package/dist/render/renderQuality.d.ts +20 -0
  69. package/dist/render/renderQuality.js +5 -0
  70. package/dist/render/rendererHarness.js +4 -0
  71. package/dist/render/sceneTarget.d.ts +12 -0
  72. package/dist/render/sceneTarget.js +14 -0
  73. package/dist/render/shaders/clothBinding.d.ts +22 -0
  74. package/dist/render/shaders/clothBinding.js +80 -0
  75. package/dist/render/shaders/depth.js +42 -6
  76. package/dist/render/shaders/flat/anisotropic.d.ts +15 -0
  77. package/dist/render/shaders/flat/anisotropic.js +68 -0
  78. package/dist/render/shaders/flat/eye.d.ts +1 -0
  79. package/dist/render/shaders/flat/eye.js +81 -0
  80. package/dist/render/shaders/flat/hair.d.ts +1 -0
  81. package/dist/render/shaders/flat/hair.js +101 -0
  82. package/dist/render/shaders/flat/index.js +36 -2
  83. package/dist/render/shaders/flat/main.d.ts +1 -1
  84. package/dist/render/shaders/flat/main.js +113 -17
  85. package/dist/render/shaders/flat/models.d.ts +14 -0
  86. package/dist/render/shaders/flat/models.js +273 -0
  87. package/dist/render/shaders/flat/preamble.js +26 -24
  88. package/dist/render/shaders/flat/skin.d.ts +1 -0
  89. package/dist/render/shaders/flat/skin.js +114 -0
  90. package/dist/render/shaders/generated/depth.wgsl.d.ts +38 -16
  91. package/dist/render/shaders/generated/depth.wgsl.js +38 -16
  92. package/dist/render/shaders/generated/flat.wgsl.d.ts +1790 -1704
  93. package/dist/render/shaders/generated/flat.wgsl.js +1828 -1742
  94. package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +48 -0
  95. package/dist/render/shaders/generated/skinBlur.wgsl.js +54 -0
  96. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  97. package/dist/render/shaders/recon/motion.wgsl.js +102 -0
  98. package/dist/render/shaders/skinBlur.d.ts +1 -0
  99. package/dist/render/shaders/skinBlur.js +103 -0
  100. package/dist/render/shaders/skinning.d.ts +3 -1
  101. package/dist/render/shaders/skinning.js +31 -4
  102. package/dist/render/shadowCasters.d.ts +13 -1
  103. package/dist/render/skinBlur.d.ts +41 -0
  104. package/dist/render/skinBlur.js +49 -0
  105. package/dist/render/skinScatter.d.ts +67 -0
  106. package/dist/render/skinScatter.js +80 -0
  107. package/dist/render/skinnedClothRun.d.ts +26 -0
  108. package/dist/render/skinnedClothRun.js +84 -0
  109. package/dist/render/surfaceModel.d.ts +129 -0
  110. package/dist/render/surfaceModel.js +152 -0
  111. package/dist/render/surfaceTexture.d.ts +44 -4
  112. package/dist/render/surfaceTexture.js +60 -46
  113. package/dist/render/vertexDefaults.js +7 -0
  114. package/package.json +6 -6
  115. package/src/geometry/meshConcat.ts +2 -0
  116. package/src/index.ts +40 -1
  117. package/src/math/scalar.ts +1 -1
  118. package/src/render/anisotropicLobe.ts +41 -0
  119. package/src/render/backend/api.ts +52 -3
  120. package/src/render/backend/select.ts +8 -0
  121. package/src/render/backend/webgl2/clothTextures.ts +120 -0
  122. package/src/render/backend/webgl2/renderer.ts +693 -64
  123. package/src/render/backend/webgl2/skinScatter.ts +309 -0
  124. package/src/render/backend/webgpu/buffers.ts +137 -63
  125. package/src/render/backend/webgpu/clothSolvePack.ts +161 -0
  126. package/src/render/backend/webgpu/clothTextures.ts +194 -0
  127. package/src/render/backend/webgpu/compressedUpload.ts +65 -0
  128. package/src/render/backend/webgpu/computeHost.ts +25 -0
  129. package/src/render/backend/webgpu/depthPass.ts +39 -7
  130. package/src/render/backend/webgpu/drawCommand.ts +6 -2
  131. package/src/render/backend/webgpu/flatPass.ts +145 -7
  132. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +502 -0
  133. package/src/render/backend/webgpu/motionPass.ts +128 -9
  134. package/src/render/backend/webgpu/postPass.ts +5 -1
  135. package/src/render/backend/webgpu/renderer.ts +663 -43
  136. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +470 -0
  137. package/src/render/backend/webgpu/skinScatterPass.ts +309 -0
  138. package/src/render/backend/webgpu/surfaceTexturePass.ts +42 -12
  139. package/src/render/clothBindingData.ts +89 -0
  140. package/src/render/clothPlacement.ts +114 -0
  141. package/src/render/compressedSource.ts +198 -0
  142. package/src/render/cutoutCaster.ts +5 -0
  143. package/src/render/cutoutDither.ts +99 -0
  144. package/src/render/eyeRefraction.ts +72 -0
  145. package/src/render/glCompressed.ts +86 -0
  146. package/src/render/hairLobes.ts +188 -0
  147. package/src/render/lightBudget.ts +25 -0
  148. package/src/render/mesh.ts +23 -0
  149. package/src/render/recon/motionVectors.ts +6 -3
  150. package/src/render/renderQuality.ts +28 -0
  151. package/src/render/rendererHarness.ts +4 -0
  152. package/src/render/sceneTarget.ts +21 -0
  153. package/src/render/shaders/clothBinding.ts +81 -0
  154. package/src/render/shaders/depth.ts +42 -6
  155. package/src/render/shaders/flat/anisotropic.ts +68 -0
  156. package/src/render/shaders/flat/eye.ts +82 -0
  157. package/src/render/shaders/flat/hair.ts +102 -0
  158. package/src/render/shaders/flat/index.ts +36 -2
  159. package/src/render/shaders/flat/main.ts +113 -17
  160. package/src/render/shaders/flat/models.ts +286 -0
  161. package/src/render/shaders/flat/preamble.ts +26 -24
  162. package/src/render/shaders/flat/skin.ts +116 -0
  163. package/src/render/shaders/generated/depth.wgsl.ts +38 -16
  164. package/src/render/shaders/generated/flat.wgsl.ts +1828 -1742
  165. package/src/render/shaders/generated/skinBlur.wgsl.ts +56 -0
  166. package/src/render/shaders/recon/motion.wgsl.ts +102 -0
  167. package/src/render/shaders/skinBlur.ts +105 -0
  168. package/src/render/shaders/skinning.ts +31 -4
  169. package/src/render/shadowCasters.ts +13 -0
  170. package/src/render/skinBlur.ts +53 -0
  171. package/src/render/skinScatter.ts +92 -0
  172. package/src/render/skinnedClothRun.ts +128 -0
  173. package/src/render/surfaceModel.ts +274 -0
  174. package/src/render/surfaceTexture.ts +123 -63
  175. package/src/render/vertexDefaults.ts +7 -0
  176. package/dist/math/exact.d.ts +0 -76
  177. package/dist/math/exact.js +0 -327
  178. package/src/math/exact.ts +0 -340
@@ -0,0 +1,274 @@
1
+ /**
2
+ * A material's shading model: how its surface answers light, when the standard model is not what
3
+ * it is made of — brushed metal, hair, skin, an eye.
4
+ *
5
+ * **A frozen descriptor, made once by a factory, carrying a kind and that model's numbers.** The
6
+ * standard model is `null`, exactly today's shader, and a scene that never names a model compiles
7
+ * nothing of any of them: each is a pipeline constant of its own (`MODEL_ANISOTROPIC` and the rest
8
+ * in `shaders/flat/models.ts`), off in every pipeline but the ones a material asks for.
9
+ *
10
+ * **Two vectors of numbers a material, and no more.** The lit stage's eight-light build is 254 of
11
+ * the 256 uniform vectors an Adreno 740 offers, so a model's numbers are packed into
12
+ * `uModelParams[2]` — the last component of the second says whether a model map is bound — and
13
+ * every model fits. See `packModel` for each layout.
14
+ *
15
+ * **The eye's axis is the one number that moves.** It is the eye's forward direction in the world,
16
+ * which a turning head or a glance changes every frame; the lit stage has no room left for another
17
+ * varying (sixteen, WebGPU's ceiling), so the renderer turns the descriptor's mesh-local axis by
18
+ * the draw's model matrix and the eye's joint each draw, with `eyeAxisInWorld`.
19
+ */
20
+ import type { Vec3 } from '../math/color.ts';
21
+
22
+ /** Strands of brushed metal, satin or hair-like fibre: a highlight stretched across them. */
23
+ export interface AnisotropicModel {
24
+ readonly kind: 'anisotropic';
25
+ /** 0 to 1: how far the highlight is stretched along the tangent. */
26
+ readonly strength: number;
27
+ /** Radians, turning the stretch's direction from the tangent toward the bitangent. */
28
+ readonly rotation: number;
29
+ }
30
+
31
+ /**
32
+ * Hair: three lobes along the strand, which is the mesh's tangent, running from root to tip
33
+ * (`hairLobes.ts`). A model map's green turns each strand's tilt by up to half of it either way and
34
+ * its blue is occlusion.
35
+ */
36
+ export interface HairModel {
37
+ readonly kind: 'hair';
38
+ /**
39
+ * Radians the cuticle's scales tilt the strand's surface: positive moves the white highlight
40
+ * toward the root and the coloured one toward the tip. A card whose tangent runs tip to root
41
+ * takes it negative.
42
+ */
43
+ readonly shift: number;
44
+ /** 0 to 1: the light scattered through many strands, tinted by their colour. */
45
+ readonly scatter: number;
46
+ /** How strongly light from behind glows through: the transmitted lobe. */
47
+ readonly backlit: number;
48
+ }
49
+
50
+ /** Skin: light scattered beneath the surface, reddening its terminator and its thin parts. */
51
+ export interface SkinModel {
52
+ readonly kind: 'skin';
53
+ /** The colour light keeps travelling beneath the surface — the mean free path, as a colour. */
54
+ readonly scatterColor: Vec3;
55
+ /** Metres: how far light travels beneath it before it leaves. */
56
+ readonly radius: number;
57
+ /** 0 to 1: how much a light behind a thin part shows through it. */
58
+ readonly transmission: number;
59
+ /** Which of up to eight profiles the screen-space scattering blurs this skin by. */
60
+ readonly profile: number;
61
+ }
62
+
63
+ /** An eye: an iris seen through a cornea, which refracts it, under a mirror-smooth highlight. */
64
+ export interface EyeModel {
65
+ readonly kind: 'eye';
66
+ /** The iris's radius, in texture coordinates from the centre of the eye's map. */
67
+ readonly irisRadius: number;
68
+ /** Metres from the cornea down to the iris. */
69
+ readonly irisDepth: number;
70
+ /** The cornea's index of refraction. */
71
+ readonly ior: number;
72
+ /** How smooth the cornea's highlight is. */
73
+ readonly corneaRoughness: number;
74
+ /** The eye's forward direction, in the mesh's own space at its bind pose. */
75
+ readonly axis: Vec3;
76
+ /** The joint the eye turns with, or -1 for an eye placed by its model matrix alone. */
77
+ readonly joint: number;
78
+ }
79
+
80
+ /** Every model a material can name, by its `kind`: see `SurfaceMaterial.model`. */
81
+ export type SurfaceModel = AnisotropicModel | HairModel | SkinModel | EyeModel;
82
+ /** A model's name, which also picks its pipeline switch: `SURFACE_MODEL_SWITCH`. */
83
+ export type SurfaceModelKind = SurfaceModel['kind'];
84
+
85
+ /** Every kind, in the order the lit stage declares their switches. */
86
+ export const SURFACE_MODEL_KINDS: readonly SurfaceModelKind[] = [
87
+ 'anisotropic',
88
+ 'hair',
89
+ 'skin',
90
+ 'eye',
91
+ ];
92
+
93
+ /** The lit stage's switch for each kind. */
94
+ export const SURFACE_MODEL_SWITCH: Readonly<Record<SurfaceModelKind, string>> = {
95
+ anisotropic: 'MODEL_ANISOTROPIC',
96
+ hair: 'MODEL_HAIR',
97
+ skin: 'MODEL_SKIN',
98
+ eye: 'MODEL_EYE',
99
+ };
100
+
101
+ /** Floats a material's model takes: `uModelParams[2]`. */
102
+ export const MODEL_PARAM_FLOATS = 8;
103
+
104
+ function check(model: string, name: string, value: number, low: number, high: number): number {
105
+ if (!Number.isFinite(value) || value < low || value > high) {
106
+ throw new Error(`${model}: ${name} must be from ${low} to ${high}, not ${String(value)}`);
107
+ }
108
+ return value;
109
+ }
110
+
111
+ function unit(model: string, name: string, v: Vec3): Vec3 {
112
+ const length = Math.hypot(v[0], v[1], v[2]);
113
+ if (!(length > 1e-6))
114
+ throw new Error(`${model}: ${name} must be a direction, not [${v.join(', ')}]`);
115
+ return [v[0] / length, v[1] / length, v[2] / length];
116
+ }
117
+
118
+ /** Brushed metal and the like, after glTF's `KHR_materials_anisotropy`: a strength and a turn. */
119
+ export function anisotropicModel(
120
+ options: { strength?: number; rotation?: number } = {},
121
+ ): AnisotropicModel {
122
+ return Object.freeze({
123
+ kind: 'anisotropic',
124
+ strength: check('anisotropicModel', 'strength', options.strength ?? 1, 0, 1),
125
+ rotation: check(
126
+ 'anisotropicModel',
127
+ 'rotation',
128
+ options.rotation ?? 0,
129
+ -2 * Math.PI,
130
+ 2 * Math.PI,
131
+ ),
132
+ });
133
+ }
134
+
135
+ /** Hair, along the mesh's tangent from root to tip. A tilt of 3°, within the 2.5° to 5° measured. */
136
+ export function hairModel(
137
+ options: { shift?: number; scatter?: number; backlit?: number } = {},
138
+ ): HairModel {
139
+ return Object.freeze({
140
+ kind: 'hair',
141
+ shift: check('hairModel', 'shift', options.shift ?? (3 * Math.PI) / 180, -0.5, 0.5),
142
+ scatter: check('hairModel', 'scatter', options.scatter ?? 0.7, 0, 1),
143
+ backlit: check('hairModel', 'backlit', options.backlit ?? 1, 0, 4),
144
+ });
145
+ }
146
+
147
+ /** Skin. The defaults are a fair skin's: a red mean free path a little over a centimetre long. */
148
+ export function skinModel(
149
+ options: { scatterColor?: Vec3; radius?: number; transmission?: number; profile?: number } = {},
150
+ ): SkinModel {
151
+ const color = options.scatterColor ?? [0.85, 0.35, 0.22];
152
+ for (let c = 0; c < 3; c++) check('skinModel', 'scatterColor', color[c] as number, 0, 1);
153
+ const profile = check('skinModel', 'profile', options.profile ?? 0, 0, 7);
154
+ if (!Number.isInteger(profile))
155
+ throw new Error(`skinModel: profile must be a whole number, not ${profile}`);
156
+ return Object.freeze({
157
+ kind: 'skin',
158
+ scatterColor: Object.freeze([color[0], color[1], color[2]]) as unknown as Vec3,
159
+ radius: check('skinModel', 'radius', options.radius ?? 0.012, 0, 0.1),
160
+ transmission: check('skinModel', 'transmission', options.transmission ?? 0.5, 0, 1),
161
+ profile,
162
+ });
163
+ }
164
+
165
+ /** An eye. The defaults are a human eye's, its axis along the mesh's +Z. */
166
+ export function eyeModel(
167
+ options: {
168
+ irisRadius?: number;
169
+ irisDepth?: number;
170
+ ior?: number;
171
+ corneaRoughness?: number;
172
+ axis?: Vec3;
173
+ joint?: number;
174
+ } = {},
175
+ ): EyeModel {
176
+ const joint = options.joint ?? -1;
177
+ if (!Number.isInteger(joint) || joint < -1) {
178
+ throw new Error(`eyeModel: joint must be a joint index or -1, not ${String(joint)}`);
179
+ }
180
+ return Object.freeze({
181
+ kind: 'eye',
182
+ irisRadius: check('eyeModel', 'irisRadius', options.irisRadius ?? 0.15, 0.001, 0.5),
183
+ irisDepth: check('eyeModel', 'irisDepth', options.irisDepth ?? 0.003, 0, 0.02),
184
+ ior: check('eyeModel', 'ior', options.ior ?? 1.336, 1, 2),
185
+ corneaRoughness: check('eyeModel', 'corneaRoughness', options.corneaRoughness ?? 0.02, 0, 1),
186
+ axis: Object.freeze(unit('eyeModel', 'axis', options.axis ?? [0, 0, 1])) as unknown as Vec3,
187
+ joint,
188
+ });
189
+ }
190
+
191
+ /**
192
+ * A model's numbers into `out` — eight floats, `uModelParams[0]` then `[1]` — and whether a model
193
+ * map is bound into the last. Zeros for the standard model. An eye's axis goes in with the
194
+ * descriptor's own until the draw turns it: see `eyeAxisInWorld`.
195
+ *
196
+ * - anisotropic: strength, cos and sin of the rotation · −, −, −, map
197
+ * - hair: shift, scatter, backlit, − · −, −, −, map
198
+ * - skin: the scatter colour, radius · transmission, profile, −, map
199
+ * - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
200
+ */
201
+ export function packModel(model: SurfaceModel | null, mapped: boolean, out: Float32Array): void {
202
+ out.fill(0);
203
+ if (model === null) return;
204
+ out[7] = mapped ? 1 : 0;
205
+ switch (model.kind) {
206
+ case 'anisotropic':
207
+ out[0] = model.strength;
208
+ out[1] = Math.cos(model.rotation);
209
+ out[2] = Math.sin(model.rotation);
210
+ return;
211
+ case 'hair':
212
+ out[0] = model.shift;
213
+ out[1] = model.scatter;
214
+ out[2] = model.backlit;
215
+ return;
216
+ case 'skin':
217
+ out[0] = model.scatterColor[0];
218
+ out[1] = model.scatterColor[1];
219
+ out[2] = model.scatterColor[2];
220
+ out[3] = model.radius;
221
+ out[4] = model.transmission;
222
+ out[5] = model.profile;
223
+ return;
224
+ case 'eye':
225
+ out[0] = model.irisRadius;
226
+ out[1] = model.irisDepth;
227
+ out[2] = model.ior;
228
+ out[3] = model.corneaRoughness;
229
+ out[4] = model.axis[0];
230
+ out[5] = model.axis[1];
231
+ out[6] = model.axis[2];
232
+ return;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * An eye's axis in the world for one draw, into `out[at…at+2]`: the descriptor's mesh-local axis
238
+ * turned by its joint's palette entry, where it names one and a palette is set, and then by the
239
+ * draw's model matrix — the turn its vertices take, without the translation. Normalised, because
240
+ * a palette carries a joint's scale.
241
+ */
242
+ export function eyeAxisInWorld(
243
+ model: EyeModel,
244
+ placement: ArrayLike<number>,
245
+ palette: ArrayLike<number> | null,
246
+ out: Float32Array,
247
+ at: number,
248
+ ): void {
249
+ let [x, y, z] = model.axis;
250
+ const j = model.joint * 16;
251
+ if (palette !== null && model.joint >= 0 && j + 16 <= palette.length) {
252
+ const px =
253
+ (palette[j] as number) * x + (palette[j + 4] as number) * y + (palette[j + 8] as number) * z;
254
+ const py =
255
+ (palette[j + 1] as number) * x +
256
+ (palette[j + 5] as number) * y +
257
+ (palette[j + 9] as number) * z;
258
+ const pz =
259
+ (palette[j + 2] as number) * x +
260
+ (palette[j + 6] as number) * y +
261
+ (palette[j + 10] as number) * z;
262
+ x = px;
263
+ y = py;
264
+ z = pz;
265
+ }
266
+ const m = placement;
267
+ const wx = (m[0] as number) * x + (m[4] as number) * y + (m[8] as number) * z;
268
+ const wy = (m[1] as number) * x + (m[5] as number) * y + (m[9] as number) * z;
269
+ const wz = (m[2] as number) * x + (m[6] as number) * y + (m[10] as number) * z;
270
+ const length = Math.hypot(wx, wy, wz) || 1;
271
+ out[at] = wx / length;
272
+ out[at + 1] = wy / length;
273
+ out[at + 2] = wz / length;
274
+ }
@@ -8,9 +8,19 @@
8
8
  * array samples exactly as a 2D texture does. A mesh names its layer per vertex (`MeshData.layers`),
9
9
  * so a merged block wearing forty facades is one draw.
10
10
  */
11
- import { isSourceList, layerSize, refuseArrayUpdate, sourceSize } from './textureSource.ts';
11
+ import type { SurfaceModel } from './surfaceModel.ts';
12
+ import { layerSize, refuseArrayUpdate, sourceSize } from './textureSource.ts';
13
+ import { compressedLayers, planBlocks, refuseBlockUpdate } from './compressedSource.ts';
14
+ import type {
15
+ BlockFormat,
16
+ CompressedTextureFormat,
17
+ CompressedTextureSource,
18
+ SurfaceSource,
19
+ } from './compressedSource.ts';
20
+ import { uploadCompressedArray } from './glCompressed.ts';
12
21
  import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from './surfaceEffects.ts';
13
22
  import type { SurfaceLayerEffect } from './surfaceEffects.ts';
23
+ import type { CutoutMode } from './cutoutDither.ts';
14
24
 
15
25
  /**
16
26
  * How a texture behaves past its edges and between its texels.
@@ -81,8 +91,20 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
81
91
  */
82
92
  uOffset?: number;
83
93
  vOffset?: number;
84
- /** Alpha below which a fragment is discarded. See `uAlbedoCutout`. */
94
+ /** Alpha below which a fragment is discarded. See `uCutout`. */
85
95
  cutout?: number;
96
+ /**
97
+ * How the cutout's edge is drawn: `'hard'`, the default and every cutout before 4.8.4, or
98
+ * `'dithered'`, which keeps a pixel by the share of it the texture covers so a strand of hair, a
99
+ * lash or a fringe of leaves has a soft edge instead of a stair of pixels.
100
+ *
101
+ * **The frame decides how a dithered edge is resolved**, because the material cannot know: under a
102
+ * temporal resolve (TAA or DriftTR) the pattern moves every frame and the resolve averages it;
103
+ * multisampled, the share goes to the GPU as alpha-to-coverage; with neither, the test is hard,
104
+ * since a dither nothing averages is grain. A translucent draw tests hard whatever this says. See
105
+ * `cutoutDither.ts`.
106
+ */
107
+ cutoutMode?: CutoutMode;
86
108
  /**
87
109
  * Whether both faces are seen: glTF's `doubleSided`, what a curtain or a leaf card is. Drawn
88
110
  * without culling, and a back face is lit as its front, with the normal turned to the viewer.
@@ -123,6 +145,19 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
123
145
  * consumer who wants a bound map twice as bright asks here rather than rebuilding the mesh.
124
146
  */
125
147
  emissiveScale?: readonly [number, number, number];
148
+ /**
149
+ * How the surface answers light, when the standard model is not what it is made of: brushed
150
+ * metal, hair, skin, an eye — `anisotropicModel`, `hairModel`, `skinModel`, `eyeModel`. Null or
151
+ * absent is the standard model, exactly as before. **A pipeline of its own**, compiled the first
152
+ * time a draw asks, so a scene that names no model pays for none. See `surfaceModel.ts`.
153
+ */
154
+ model?: SurfaceModel | null;
155
+ /**
156
+ * The model's own channels, in the albedo's coordinates and layers; what each channel means is
157
+ * the model's to say. Absent, each model takes its own neutral. Sampled linear, like any map
158
+ * that is not a colour.
159
+ */
160
+ modelMap?: Texture | null;
126
161
  }
127
162
 
128
163
  export interface SurfaceTextureOptions {
@@ -222,79 +257,45 @@ export class SurfaceTexture {
222
257
  readonly layers: number;
223
258
  /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
224
259
  private effectsTable: WebGLTexture | null = null;
260
+ /** The block format a compressed texture holds, or null for one uploaded from images. */
261
+ private readonly blockFormat: BlockFormat | null;
225
262
 
226
263
  /**
227
264
  * One image, or an array of images that must share one size (`layerSize` refuses otherwise).
228
- * Uploaded unflipped either way; see the constructor body for why.
265
+ * Uploaded unflipped either way; see the constructor body for why. Or blocks, uploaded as they are
266
+ * where `compressed` — the formats this context samples — has theirs; see `compressedSource.ts`.
229
267
  */
230
268
  constructor(
231
269
  gl: WebGL2RenderingContext,
232
- source: TexImageSource | readonly TexImageSource[],
270
+ source: SurfaceSource | readonly SurfaceSource[],
233
271
  options: SurfaceTextureOptions = {},
272
+ compressed: readonly CompressedTextureFormat[] = [],
234
273
  ) {
235
- const sources: readonly TexImageSource[] = isSourceList(source) ? source : [source];
236
- const { width, height } = layerSize(sources);
237
- this.layers = sources.length;
274
+ const listed = (Array.isArray(source) ? source : [source]) as readonly SurfaceSource[];
275
+ const blocks = compressedLayers(listed);
276
+ this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
277
+ /* Refused before anything is allocated, so a refusal leaves no texture behind. */
278
+ const plan = blocks === null ? null : planBlocks(blocks, this.srgb, compressed);
238
279
  const texture = gl.createTexture();
239
280
  if (texture === null) throw new Error('SurfaceTexture: createTexture failed');
240
281
  this.texture = texture;
241
- this.mipmapped = options.mipmap ?? true;
242
- this.srgb = (options.colorSpace ?? 'linear') === 'srgb';
243
-
244
282
  const wrap = (options.wrap ?? 'repeat') === 'repeat' ? gl.REPEAT : gl.CLAMP_TO_EDGE;
245
-
246
283
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
247
- /*
248
- * **Not flipped, and this used to be, which is the bug it is written down for.**
249
- *
250
- * The flip was here with a comment saying every source is a 2D drawing surface whose Y
251
- * runs down from the top-left, so without it "every texture arrives mirrored". That is
252
- * true of a canvas and **WebGL ignores `UNPACK_FLIP_Y_WEBGL` entirely for an
253
- * `ImageBitmap`**, which carries its own orientation. So the two source types the same
254
- * method accepts arrived the opposite way up from each other, and nothing caught it
255
- * because every canvas-sourced texture in this repository was symmetric under a vertical
256
- * flip: eroded noise, a radial halo, a sleeve blurred to 24 pixels and back. One consumer
257
- * painted lettering to a canvas beside a sleeve decoded as a bitmap and the text was
258
- * upside down.
259
- *
260
- * **Made to agree by dropping the flip rather than by extending it**, and the direction
261
- * is a deliberate choice about blast radius rather than a coin toss: `drftLoader` builds
262
- * every model texture from an `ImageBitmap`, so flipping bitmaps to match canvases would
263
- * turn over every painted surface on every loaded model. Dropping it changes only
264
- * canvas-sourced textures, and every one of those that exists today is symmetric.
265
- *
266
- * A caller that was mirroring its own canvas to cancel this must stop.
267
- */
268
- /* Allocated mutable rather than with texStorage3D, so `update` can hand a one-layer texture a
269
- new size, as texImage2D always could. Each layer is then written in place. */
270
- gl.texImage3D(
271
- gl.TEXTURE_2D_ARRAY,
272
- 0,
273
- this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
274
- width,
275
- height,
276
- sources.length,
277
- 0,
278
- gl.RGBA,
279
- gl.UNSIGNED_BYTE,
280
- null,
281
- );
282
- for (let layer = 0; layer < sources.length; layer++) {
283
- gl.texSubImage3D(
284
- gl.TEXTURE_2D_ARRAY,
285
- 0,
286
- 0,
287
- 0,
288
- layer,
289
- width,
290
- height,
291
- 1,
292
- gl.RGBA,
293
- gl.UNSIGNED_BYTE,
294
- sources[layer] as TexImageSource,
295
- );
296
- }
297
284
 
285
+ if (blocks !== null && plan !== null) {
286
+ /* The stored chain, or level 0 alone where no chain was asked for. Nothing is generated:
287
+ `generateMipmap` cannot write a compressed format. */
288
+ this.layers = blocks.length;
289
+ this.blockFormat = (blocks[0] as CompressedTextureSource).format;
290
+ this.mipmapped = (options.mipmap ?? true) && plan.levels > 1;
291
+ uploadCompressedArray(gl, plan.name, blocks, this.mipmapped ? plan.levels : 1);
292
+ } else {
293
+ const sources = listed as readonly TexImageSource[];
294
+ this.layers = sources.length;
295
+ this.blockFormat = null;
296
+ this.mipmapped = options.mipmap ?? true;
297
+ uploadImages(gl, sources, this.srgb);
298
+ }
298
299
  gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
299
300
  gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
300
301
  /* Nearest where the caller said their pixels are the subject; see `filter`. The mip chain is
@@ -314,7 +315,7 @@ export class SurfaceTexture {
314
315
  : gl.LINEAR,
315
316
  );
316
317
 
317
- if (this.mipmapped) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
318
+ if (this.mipmapped && this.blockFormat === null) gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
318
319
  applyAnisotropy(gl, options.anisotropy ?? DEFAULT_ANISOTROPY);
319
320
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
320
321
 
@@ -345,7 +346,6 @@ export class SurfaceTexture {
345
346
  }
346
347
  }
347
348
 
348
- /** Bind the effects table to `unit`, or `fallback` for a texture given none. */
349
349
  /** Whether this texture carries an effects table, which the lit program reads only once one exists. */
350
350
  get hasEffects(): boolean {
351
351
  return this.effectsTable !== null;
@@ -370,6 +370,7 @@ export class SurfaceTexture {
370
370
  update(gl: WebGL2RenderingContext, source: TexImageSource): void {
371
371
  if (this.texture === null) return;
372
372
  refuseArrayUpdate(this.layers);
373
+ refuseBlockUpdate(this.blockFormat);
373
374
  const { width, height } = sourceSize(source);
374
375
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.texture);
375
376
  /* Unflipped, matching the constructor. See it for why, and for what it cost. */
@@ -404,6 +405,65 @@ export class SurfaceTexture {
404
405
  }
405
406
  }
406
407
 
408
+ /** Images into the bound array, level 0 of each layer; the chain, if any, is generated after. */
409
+ function uploadImages(
410
+ gl: WebGL2RenderingContext,
411
+ sources: readonly TexImageSource[],
412
+ srgb: boolean,
413
+ ): void {
414
+ const { width, height } = layerSize(sources);
415
+ /*
416
+ * **Not flipped, and this used to be, which is the bug it is written down for.**
417
+ *
418
+ * The flip was here with a comment saying every source is a 2D drawing surface whose Y
419
+ * runs down from the top-left, so without it "every texture arrives mirrored". That is
420
+ * true of a canvas and **WebGL ignores `UNPACK_FLIP_Y_WEBGL` entirely for an
421
+ * `ImageBitmap`**, which carries its own orientation. So the two source types the same
422
+ * method accepts arrived the opposite way up from each other, and nothing caught it
423
+ * because every canvas-sourced texture in this repository was symmetric under a vertical
424
+ * flip: eroded noise, a radial halo, a sleeve blurred to 24 pixels and back. One consumer
425
+ * painted lettering to a canvas beside a sleeve decoded as a bitmap and the text was
426
+ * upside down.
427
+ *
428
+ * **Made to agree by dropping the flip rather than by extending it**, and the direction
429
+ * is a deliberate choice about blast radius rather than a coin toss: `drftLoader` builds
430
+ * every model texture from an `ImageBitmap`, so flipping bitmaps to match canvases would
431
+ * turn over every painted surface on every loaded model. Dropping it changes only
432
+ * canvas-sourced textures, and every one of those that exists today is symmetric.
433
+ *
434
+ * A caller that was mirroring its own canvas to cancel this must stop.
435
+ */
436
+ /* Allocated mutable rather than with texStorage3D, so `update` can hand a one-layer texture a
437
+ new size, as texImage2D always could. Each layer is then written in place. */
438
+ gl.texImage3D(
439
+ gl.TEXTURE_2D_ARRAY,
440
+ 0,
441
+ srgb ? gl.SRGB8_ALPHA8 : gl.RGBA,
442
+ width,
443
+ height,
444
+ sources.length,
445
+ 0,
446
+ gl.RGBA,
447
+ gl.UNSIGNED_BYTE,
448
+ null,
449
+ );
450
+ for (let layer = 0; layer < sources.length; layer++) {
451
+ gl.texSubImage3D(
452
+ gl.TEXTURE_2D_ARRAY,
453
+ 0,
454
+ 0,
455
+ 0,
456
+ layer,
457
+ width,
458
+ height,
459
+ 1,
460
+ gl.RGBA,
461
+ gl.UNSIGNED_BYTE,
462
+ sources[layer] as TexImageSource,
463
+ );
464
+ }
465
+ }
466
+
407
467
  /**
408
468
  * Set anisotropic filtering when the driver offers it, and stay silent when it does not.
409
469
  *
@@ -67,6 +67,13 @@ export const ABSENT_ATTRIBUTE: Readonly<Record<string, readonly number[]>> = {
67
67
  */
68
68
  joints: [0, 0, 0, 0],
69
69
  weights: [1, 0, 0, 0],
70
+ /**
71
+ * The second four influences, absent: all zero, so a four-influence mesh drawn through a pipeline
72
+ * built for eight adds nothing. **Not the first set's trap**: there a zero sum collapses the
73
+ * vertex, here the first set already sums to one and zero is exactly "no more influences".
74
+ */
75
+ joints2: [0, 0, 0, 0],
76
+ weights2: [0, 0, 0, 0],
70
77
  /**
71
78
  * The four-lane channel: planted, fully sunlit, opaque, and a reserved lane.
72
79
  *
@@ -1,76 +0,0 @@
1
- /**
2
- * Transcendentals that give the same bits on every JavaScript engine.
3
- *
4
- * **Not more accurate than `Math`, and that is not what they are for.** These are within a couple
5
- * of units in the last place of correctly rounded, which is about what a platform `sin` gives you.
6
- * What they are is *reproducible*: ECMAScript declines to specify `sin`, `cos`, `exp`, `acos` and
7
- * eighteen more, so two engines may differ by an ulp, and an ulp is enough to make two peers running
8
- * the same simulation disagree — first invisibly, then completely.
9
- *
10
- * `scripts/determinism.mjs` is the gate that refuses the unspecified ones inside the simulation set
11
- * and names the twenty-two. Its header carries the other half of the argument: `+ - * /` are IEEE
12
- * 754 operations by specification, JavaScript permits no fused-multiply-add contraction, and
13
- * `Math.sqrt` is required correctly rounded and is *not* on that list. **So everything below uses
14
- * only those five operations**, which is the whole design: reproducibility by construction rather
15
- * than by hoping two engines chose the same algorithm.
16
- *
17
- * ---
18
- *
19
- * ## What is here, and what is deliberately not
20
- *
21
- * `exactSin`, `exactCos`, `exactExp`, `exactAcos` and `exactLog`. Five, because five is what the
22
- * engine and its first consumer actually reach for: the terrain binding's slope angle needs `acos`,
23
- * a lockstep fixture rotating anything needs `sin` and `cos`, exponential decay needs `exp`, and a
24
- * detector's box prior is a logit of the patch grid, which needs `log`.
25
- *
26
- * **`atan2` is absent.** It is the obvious fifth and nothing here needs one: `shape.ts` sorts by a
27
- * comparison key computed from two in-plane axes rather than by an angle, and `joints.ts` compares
28
- * a quaternion component against a half-angle's sine. *Reversed by* a site that genuinely needs a
29
- * signed angle from two components, at which point it arrives with its own golden vectors.
30
- *
31
- * ## Nothing in core imports these
32
- *
33
- * Deliberately. `core-only` is the one size figure in this repository that every consumer pays, and
34
- * a module reachable from `createRenderer` is in it whether anybody calls it or not. These are
35
- * reached by the script bindings and by consumers who ask for them, so tree-shaking keeps them out
36
- * of a bundle that does not, which is measured rather than assumed.
37
- *
38
- * ## The coefficients
39
- *
40
- * The polynomial and rational coefficients are fdlibm's, which is the ancestor of most platform
41
- * libms and is public domain. They are written as literals: **a literal is the same number on
42
- * every engine, which is the only property being bought.** Their provenance matters less than their
43
- * fixity — a coefficient somebody re-derived to one more digit is a different function, and
44
- * `exact.test.ts`'s golden bit patterns are what would catch that.
45
- */
46
- /** `Math.sin`, reproducibly. NaN in, NaN out; an infinity answers NaN, as `Math.sin` does. */
47
- export declare function exactSin(x: number): number;
48
- /** `Math.cos`, reproducibly. */
49
- export declare function exactCos(x: number): number;
50
- /**
51
- * `Math.exp`, reproducibly.
52
- *
53
- * Reduce by a multiple of ln 2 into [-ln2/2, ln2/2], sum the Taylor series there, and scale by a
54
- * power of two. **A degree-14 series and not a shorter minimax**, because on that interval the
55
- * fifteenth term is about 4e-20 — below the precision of a double — so the series is exact to the
56
- * representation and there is nothing a fitted polynomial would buy.
57
- */
58
- export declare function exactExp(x: number): number;
59
- /**
60
- * `Math.log`, reproducibly: fdlibm's algorithm, which is also V8's.
61
- *
62
- * Take the power of two out as `k` and leave a significand in [√2/2, √2), so that `f = m - 1` is
63
- * small either side of zero; then log(1 + f) = f - f²/2 + s·(f²/2 + R(s²)) with s = f/(2 + f), and
64
- * `k·ln 2` added back in two parts so the large part loses nothing. The exponent is read and the
65
- * significand rescaled through the double's own words — integer operations, not transcendentals.
66
- */
67
- export declare function exactLog(x: number): number;
68
- /**
69
- * `Math.acos`, reproducibly. Outside [-1, 1] this answers NaN, as `Math.acos` does.
70
- *
71
- * Three branches rather than one, and the split at ±0.5 is the reason it is accurate near the ends:
72
- * `acos` has infinite derivative at ±1, so a polynomial in `x` loses its footing there. Substituting
73
- * `s = sqrt((1 ∓ x)/2)` moves the awkward point to zero, where a polynomial is comfortable — which
74
- * is also why `sqrt` being exactly rounded matters here rather than being a convenience.
75
- */
76
- export declare function exactAcos(x: number): number;