@woosh/meep-engine 2.43.3 → 2.43.5

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 (52) hide show
  1. package/core/binary/BinaryBuffer.js +13 -1
  2. package/core/binary/BitSet.js +2 -2
  3. package/core/collection/array/array_range_equal_strict.js +22 -0
  4. package/core/collection/map/AsyncMapWrapper.js +13 -1
  5. package/core/collection/map/CachedAsyncMap.js +9 -2
  6. package/core/collection/map/CachedAsyncMap.spec.js +47 -0
  7. package/core/color/sRGB_to_linear.js +9 -4
  8. package/core/geom/3d/plane/orient3d_fast.js +3 -0
  9. package/core/geom/3d/plane/orient3d_robust.js +41 -0
  10. package/core/geom/3d/sphere/harmonics/README.md +15 -0
  11. package/core/geom/3d/sphere/harmonics/sh3_add.js +21 -0
  12. package/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js +618 -0
  13. package/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.js +49 -0
  14. package/core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js +53 -0
  15. package/core/geom/3d/tetrahedra/TetrahedralMesh.js +251 -68
  16. package/core/geom/3d/tetrahedra/TetrahedralMesh.spec.js +80 -3
  17. package/core/geom/3d/tetrahedra/build_tetrahedral_mesh_buffer_geometry.js +75 -0
  18. package/core/geom/3d/tetrahedra/delaunay/Cavity.js +5 -1
  19. package/core/geom/3d/tetrahedra/delaunay/compute_delaunay_tetrahedral_mesh.js +30 -31
  20. package/core/geom/3d/tetrahedra/delaunay/fill_in_a_cavity.js +54 -18
  21. package/core/geom/3d/tetrahedra/delaunay/push_boundary_with_validation.js +27 -0
  22. package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity.js +89 -0
  23. package/core/geom/3d/tetrahedra/delaunay/{tetrahedral_mesh_walk_toward_cavity.js → tetrahedral_mesh_walk_towards_containing_tetrahedron.js} +15 -12
  24. package/core/geom/3d/tetrahedra/delaunay/validate_cavity_boundary.js +60 -0
  25. package/core/geom/3d/tetrahedra/{point_in_tetrahedron_circumsphere.js → in_sphere_fast.js} +2 -4
  26. package/core/geom/3d/tetrahedra/in_sphere_robust.js +53 -0
  27. package/core/geom/3d/tetrahedra/prototypeTetrahedraBuilder.js +44 -35
  28. package/core/geom/3d/tetrahedra/validate_tetrahedral_mesh.js +85 -38
  29. package/core/geom/3d/util/make_justified_point_grid.js +31 -0
  30. package/core/process/delay.js +5 -0
  31. package/editor/Editor.js +3 -0
  32. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.js +195 -11
  33. package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.js +16 -0
  34. package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.js +4 -0
  35. package/engine/EngineHarness.js +11 -5
  36. package/engine/ecs/terrain/ecs/TerrainSystem.js +7 -1
  37. package/engine/ecs/transform/copy_three_transform.js +15 -0
  38. package/engine/graphics/ecs/light/Light.js +6 -1
  39. package/engine/graphics/ecs/light/LightSystem.d.ts +1 -1
  40. package/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.js +2 -17
  41. package/engine/graphics/geometry/instancing/InstancedMeshGroup.js +2 -2
  42. package/engine/graphics/micron/plugin/shaded_geometry/MicronShadedGeometryRenderAdapter.js +9 -1
  43. package/engine/graphics/sh3/LightProbeVolume.js +595 -0
  44. package/engine/graphics/sh3/SH3VisualisationMaterial.js +79 -0
  45. package/engine/graphics/sh3/prototypeSH3Probe.js +427 -0
  46. package/engine/graphics/sh3/visualise_probe.js +40 -0
  47. package/engine/graphics/texture/atlas/TextureAtlas.js +15 -3
  48. package/engine/intelligence/blackboard/AbstractBlackboard.d.ts +1 -1
  49. package/package.json +2 -1
  50. package/samples/terrain/from_image_2.js +127 -82
  51. package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity2.js +0 -224
  52. package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_insert_point.js +0 -98
@@ -0,0 +1,618 @@
1
+ import { max2 } from "../../../../math/max2.js";
2
+ import { mat3, vec3 } from "gl-matrix";
3
+ import { array_copy } from "../../../../collection/array/copyArray.js";
4
+ import { min2 } from "../../../../math/min2.js";
5
+ import { assert } from "../../../../assert.js";
6
+
7
+ /*
8
+ @see https://github.com/Bestmaker602/olacziy/blob/212b64ea5f1856b390cdf7629801243f76a4466d/libs/ibl/src/CubemapSH.cpp
9
+ */
10
+
11
+ /**
12
+ *
13
+ * @param {number[]} M 5x5 matrix
14
+ * @param {number[]} x vec5
15
+ * @return {number[]}
16
+ */
17
+ function multiply_5d(M, x) {
18
+ return [
19
+ M[0] * x[0] + M[5] * x[1] + M[10] * x[2] + M[15] * x[3] + M[20] * x[4],
20
+ M[1] * x[0] + M[6] * x[1] + M[11] * x[2] + M[16] * x[3] + M[21] * x[4],
21
+ M[2] * x[0] + M[7] * x[1] + M[12] * x[2] + M[17] * x[3] + M[22] * x[4],
22
+ M[3] * x[0] + M[8] * x[1] + M[13] * x[2] + M[18] * x[3] + M[23] * x[4],
23
+ M[4] * x[0] + M[9] * x[1] + M[14] * x[2] + M[19] * x[3] + M[24] * x[4]
24
+ ];
25
+ }
26
+
27
+
28
+ /**
29
+ * returns n! / d!
30
+ * @param {number} n
31
+ * @param {number} d
32
+ * @return {number}
33
+ */
34
+ function factorial(n, d = 1) {
35
+ let _d = max2(1, d);
36
+ let _n = max2(1, n);
37
+
38
+ let r = 1.0;
39
+
40
+ if (_n === _d) {
41
+ // intentionally left blank
42
+ } else if (_n > _d) {
43
+ for (; _n > _d; _n--) {
44
+ r *= _n;
45
+ }
46
+ } else {
47
+ for (; _d > _n; _d--) {
48
+ r *= _d;
49
+ }
50
+ r = 1.0 / r;
51
+ }
52
+ return r;
53
+ }
54
+
55
+ const F_PI = 3.14159265358979323846264338327950288;
56
+ const F_2_PI = 0.636619772367581343075535053490057448;
57
+ const F_2_SQRTPI = 1.12837916709551257389615890312154517;
58
+ const F_SQRT2 = 1.41421356237309504880168872420969808;
59
+ const F_SQRT1_2 = 0.707106781186547524400844362104849039;
60
+ const M_SQRT_3 = 1.7320508076;
61
+
62
+ /**
63
+ * SH scaling factors:
64
+ * returns sqrt((2*l + 1) / 4*pi) * sqrt( (l-|m|)! / (l+|m|)! )
65
+ */
66
+ function Kml(m, l) {
67
+ m = m < 0 ? -m : m; // abs() is not constexpr
68
+ const K = (2 * l + 1) * factorial((l - m), (l + m));
69
+ return Math.sqrt(K) * (F_2_SQRTPI * 0.25);
70
+ }
71
+
72
+ /**
73
+ * compute Index of spherical harmonics coefficient
74
+ * @param {number} m
75
+ * @param {number} l
76
+ * @return {number}
77
+ */
78
+ function SHindex(m, l) {
79
+ return l * (l + 1) + m;
80
+ }
81
+
82
+ /**
83
+ *
84
+ * @param {number[]} result
85
+ * @param {number} result_offset
86
+ * @param {number} numBands
87
+ */
88
+ function Ki(result, result_offset, numBands) {
89
+ for (let l = 0; l < numBands; l++) {
90
+ result[SHindex(0, l) + result_offset] = Kml(0, l);
91
+ for (let m = 1; m <= l; m++) {
92
+ result[SHindex(m, l) + result_offset] =
93
+ result[SHindex(-m, l) + result_offset] = F_SQRT2 * Kml(m, l);
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * < cos(theta) > SH coefficients pre-multiplied by 1 / K(0,l)
100
+ * @param {number} l
101
+ * @returns {number}
102
+ */
103
+ function computeTruncatedCosSh(l) {
104
+ if (l === 0) {
105
+ return F_PI;
106
+ } else if (l === 1) {
107
+ return 2 * F_PI / 3;
108
+ } else if ((l & 1) !== 0) {
109
+ return 0;
110
+ }
111
+
112
+ const l_2 = l / 2;
113
+ const A0 = ((l_2 & 1) ? 1.0 : -1.0) / ((l + 2) * (l - 1));
114
+ const A1 = factorial(l, l_2) / (factorial(l_2) * (1 << l));
115
+ return 2 * F_PI * A0 * A1;
116
+ }
117
+
118
+ /*
119
+ * Calculates non-normalized SH bases, i.e.:
120
+ * m > 0, cos(m*phi) * P(m,l)
121
+ * m < 0, sin(|m|*phi) * P(|m|,l)
122
+ * m = 0, P(0,l)
123
+ */
124
+ function computeShBasis(SHb, result_offset, numBands, sx, sy, sz) {
125
+
126
+ /*
127
+ * TODO: all the Legendre computation below is identical for all faces, so it
128
+ * might make sense to pre-compute it once. Also note that there is
129
+ * a fair amount of symmetry within a face (which we could take advantage of
130
+ * to reduce the pre-compute table).
131
+ */
132
+
133
+ /*
134
+ * Below, we compute the associated Legendre polynomials using recursion.
135
+ * see: http://mathworld.wolfram.com/AssociatedLegendrePolynomial.html
136
+ *
137
+ * Note [0]: sz == cos(theta) ==> we only need to compute P(sz)
138
+ *
139
+ * Note [1]: We in fact compute P(sz) / sin(theta)^|m|, by removing
140
+ * the "sqrt(1 - sz*sz)" [i.e.: sin(theta)] factor from the recursion.
141
+ * This is later corrected in the ( cos(m*phi), sin(m*phi) ) recursion.
142
+ */
143
+
144
+ // s = (x, y, z) = (sin(theta)*cos(phi), sin(theta)*sin(phi), cos(theta))
145
+
146
+ // handle m=0 separately, since it produces only one coefficient
147
+ let Pml_2 = 0;
148
+ let Pml_1 = 1;
149
+ SHb[result_offset + 0] = Pml_1;
150
+ for (let l = 1; l < numBands; l++) {
151
+ const Pml = ((2 * l - 1.0) * Pml_1 * sz - (l - 1.0) * Pml_2) / l;
152
+ Pml_2 = Pml_1;
153
+ Pml_1 = Pml;
154
+ SHb[result_offset + SHindex(0, l)] = Pml;
155
+ }
156
+ let Pmm = 1;
157
+ for (let m = 1; m < numBands; m++) {
158
+ Pmm = (1.0 - 2 * m) * Pmm; // See [1], divide by sqrt(1 - sz*sz);
159
+ Pml_2 = Pmm;
160
+ Pml_1 = (2 * m + 1.0) * Pmm * sz;
161
+ // l == m
162
+ SHb[result_offset + SHindex(-m, m)] = Pml_2;
163
+ SHb[result_offset + SHindex(m, m)] = Pml_2;
164
+ if (m + 1 < numBands) {
165
+ // l == m+1
166
+ SHb[result_offset + SHindex(-m, m + 1)] = Pml_1;
167
+ SHb[result_offset + SHindex(m, m + 1)] = Pml_1;
168
+ for (let l = m + 2; l < numBands; l++) {
169
+ const Pml = ((2 * l - 1.0) * Pml_1 * sz - (l + m - 1.0) * Pml_2) / (l - m);
170
+ Pml_2 = Pml_1;
171
+ Pml_1 = Pml;
172
+ SHb[result_offset + SHindex(-m, l)] = Pml;
173
+ SHb[result_offset + SHindex(m, l)] = Pml;
174
+ }
175
+ }
176
+ }
177
+
178
+ // At this point, SHb contains the associated Legendre polynomials divided
179
+ // by sin(theta)^|m|. Below we compute the SH basis.
180
+ //
181
+ // ( cos(m*phi), sin(m*phi) ) recursion:
182
+ // cos(m*phi + phi) == cos(m*phi)*cos(phi) - sin(m*phi)*sin(phi)
183
+ // sin(m*phi + phi) == sin(m*phi)*cos(phi) + cos(m*phi)*sin(phi)
184
+ // cos[m+1] == cos[m]*sx - sin[m]*sy
185
+ // sin[m+1] == sin[m]*sx + cos[m]*sy
186
+ //
187
+ // Note that (d.x, d.y) == (cos(phi), sin(phi)) * sin(theta), so the
188
+ // code below actually evaluates:
189
+ // (cos((m*phi), sin(m*phi)) * sin(theta)^|m|
190
+ let Cm = sx;
191
+ let Sm = sy;
192
+ for (let m = 1; m <= numBands; m++) {
193
+ for (let l = m; l < numBands; l++) {
194
+ SHb[result_offset + SHindex(-m, l)] *= Sm;
195
+ SHb[result_offset + SHindex(m, l)] *= Cm;
196
+ }
197
+ const Cm1 = Cm * sx - Sm * sy;
198
+ const Sm1 = Sm * sx + Cm * sy;
199
+ Cm = Cm1;
200
+ Sm = Sm1;
201
+ }
202
+ }
203
+
204
+ /*
205
+ * utilities to rotate very low order spherical harmonics (up to 3rd band)
206
+ * @param {number[]} band1
207
+ * @param {number[]} M 3x3 matrix
208
+ * @returns {number[]}
209
+ */
210
+ function rotateShericalHarmonicBand1(band1, M) {
211
+
212
+ // inverse() is not constexpr -- so we pre-calculate it in mathematica
213
+ //
214
+ // constexpr float3 N0{ 1, 0, 0 };
215
+ // constexpr float3 N1{ 0, 1, 0 };
216
+ // constexpr float3 N2{ 0, 0, 1 };
217
+ //
218
+ // constexpr mat3f A1 = { // this is the projection of N0, N1, N2 to SH space
219
+ // float3{ -N0.y, N0.z, -N0.x },
220
+ // float3{ -N1.y, N1.z, -N1.x },
221
+ // float3{ -N2.y, N2.z, -N2.x }
222
+ // };
223
+ //
224
+ // const mat3f invA1 = inverse(A1);
225
+
226
+ const invA1TimesK = [
227
+ 0, -1, 0,
228
+ 0, 0, 1,
229
+ -1, 0, 0
230
+ ];
231
+
232
+ const R1OverK = [
233
+ -M[1], M[2], -M[0],
234
+ -M[4], M[5], -M[3],
235
+ -M[7], M[8], -M[6]
236
+ ];
237
+
238
+
239
+ mat3.scale(invA1TimesK, invA1TimesK, band1);
240
+
241
+
242
+ mat3.multiply(R1OverK, R1OverK, invA1TimesK );
243
+
244
+ return R1OverK;
245
+ }
246
+
247
+ /**
248
+ * This projects a vec3 to SH2/k space (i.e. we premultiply by 1/k)
249
+ * below can't be constexpr
250
+ * @return {number[]} vec5
251
+ * @param {number} x
252
+ * @param {number} y
253
+ * @param {number} z
254
+ */
255
+ function project_v3_to_sh(x, y, z) {
256
+ return [
257
+ (y * x),
258
+ -(y * z),
259
+ 1 / (2 * M_SQRT_3) * ((3 * z * z - 1)),
260
+ -(z * x),
261
+ 0.5 * ((x * x - y * y))
262
+ ];
263
+ }
264
+
265
+ /**
266
+ *
267
+ * @param {number[]} band2 vec5
268
+ * @param {number[]} M mat3
269
+ * @return {*}
270
+ */
271
+ function rotateShericalHarmonicBand2(band2, M) {
272
+ const n = F_SQRT1_2;
273
+
274
+ // Below we precompute (with help of Mathematica):
275
+ // constexpr float3 N0{ 1, 0, 0 };
276
+ // constexpr float3 N1{ 0, 0, 1 };
277
+ // constexpr float3 N2{ n, n, 0 };
278
+ // constexpr float3 N3{ n, 0, n };
279
+ // constexpr float3 N4{ 0, n, n };
280
+ // constexpr float M_SQRT_PI = 1.7724538509f;
281
+ // constexpr float M_SQRT_15 = 3.8729833462f;
282
+ // constexpr float k = M_SQRT_15 / (2.0f * M_SQRT_PI);
283
+ // --> k * inverse(mat5{project(N0), project(N1), project(N2), project(N3), project(N4)})
284
+ const invATimesK = [
285
+ 0, 1, 2, 0, 0,
286
+ -1, 0, 0, 0, -2,
287
+ 0, M_SQRT_3, 0, 0, 0,
288
+ 1, 1, 0, -2, 0,
289
+ 2, 1, 0, 0, 0
290
+ ];
291
+
292
+ // this is: invA * k * band2
293
+ // 5x5 matrix by vec5 (this a lot of zeroes and constants, which the compiler should eliminate)
294
+ const invATimesKTimesBand2 = multiply_5d(invATimesK, band2);
295
+
296
+ // this is: mat5{project(N0), project(N1), project(N2), project(N3), project(N4)} / k
297
+ // (the 1/k comes from project(), see above)
298
+ const ROverK =
299
+ project_v3_to_sh(M[0], M[1], M[2]) // M * N0
300
+ .concat(project_v3_to_sh(M[6], M[7], M[8])) // M * N1
301
+ .concat(project_v3_to_sh(n * (M[0] + M[3]), n * (M[1] + M[4]), n * (M[2] + M[5]))) // M * N2
302
+ .concat(project_v3_to_sh(n * (M[0] + M[6]), n * (M[1] + M[7]), n * (M[2] + M[8]))) // M * N3
303
+ .concat(project_v3_to_sh(n * (M[3] + M[6]), n * (M[4] + M[7]), n * (M[5] + M[8]))) // M * N4
304
+ ;
305
+
306
+ // notice how "k" disappears
307
+ // this is: (R / k) * (invA * k) * band2 == R * invA * band2
308
+ const result = multiply_5d(ROverK, invATimesKTimesBand2);
309
+
310
+ return result;
311
+ }
312
+
313
+ /*
314
+ * SH from environment with high dynamic range (or high frequencies -- high dynamic range creates
315
+ * high frequencies) exhibit "ringing" and negative values when reconstructed.
316
+ * To mitigate this, we need to low-pass the input image -- or equivalently window the SH by
317
+ * coefficient that tapper towards zero with the band.
318
+ *
319
+ * We use ideas and techniques from
320
+ * Stupid Spherical Harmonics (SH)
321
+ * Deringing Spherical Harmonics
322
+ * by Peter-Pike Sloan
323
+ * https://www.ppsloan.org/publications/shdering.pdf
324
+ *
325
+ */
326
+ function sincWindow(l, w) {
327
+ if (l === 0) {
328
+ return 1.0;
329
+ } else if (l >= w) {
330
+ return 0.0;
331
+ }
332
+
333
+ // we use a sinc window scaled to the desired window size in bands units
334
+ // a sinc window only has zonal harmonics
335
+ let x = ((F_PI) * l) / w;
336
+
337
+ x = Math.sin(x) / x;
338
+
339
+ // The convolution of a SH function f and a ZH function h is just the product of both
340
+ // scaled by 1 / K(0,l) -- the window coefficients include this scale factor.
341
+
342
+ // Taking the window to power N is equivalent to applying the filter N times
343
+ return Math.pow(x, 4);
344
+ }
345
+
346
+ /**
347
+ *
348
+ * @param {number[]|Float32Array} result sh3
349
+ * @param {number[]} sh input
350
+ * @param {number[]} M mat3
351
+ */
352
+ function rotate_sh3_bands(result, sh, M) {
353
+
354
+ const b0 = sh[0];
355
+ const band1 = [sh[1], sh[2], sh[3]];
356
+ const b1 = rotateShericalHarmonicBand1(band1, M);
357
+ const band2 = [sh[4], sh[5], sh[6], sh[7], sh[8]];
358
+ const b2 = rotateShericalHarmonicBand2(band2, M);
359
+
360
+ result[0] = b0;
361
+
362
+ result[1] = b1[0];
363
+ result[2] = b1[1];
364
+ result[3] = b1[2];
365
+
366
+ result[4] = b2[0];
367
+ result[5] = b2[1];
368
+ result[6] = b2[2];
369
+ result[7] = b2[3];
370
+ result[8] = b2[4];
371
+ }
372
+
373
+ /**
374
+ *
375
+ * @param {number[]} input_sh3 sh3
376
+ * @returns {number}
377
+ */
378
+ function shmin(input_sh3) {
379
+ // See "Deringing Spherical Harmonics" by Peter-Pike Sloan
380
+ // https://www.ppsloan.org/publications/shdering.pdf
381
+
382
+ const M_SQRT_PI = 1.7724538509;
383
+ const M_SQRT_3 = 1.7320508076;
384
+ const M_SQRT_5 = 2.2360679775;
385
+ const M_SQRT_15 = 3.8729833462;
386
+ const A = [
387
+ 1.0 / (2.0 * M_SQRT_PI), // 0: 0 0
388
+ -M_SQRT_3 / (2.0 * M_SQRT_PI), // 1: 1 -1
389
+ M_SQRT_3 / (2.0 * M_SQRT_PI), // 2: 1 0
390
+ -M_SQRT_3 / (2.0 * M_SQRT_PI), // 3: 1 1
391
+ M_SQRT_15 / (2.0 * M_SQRT_PI), // 4: 2 -2
392
+ -M_SQRT_15 / (2.0 * M_SQRT_PI), // 5: 2 -1
393
+ M_SQRT_5 / (4.0 * M_SQRT_PI), // 6: 2 0
394
+ -M_SQRT_15 / (2.0 * M_SQRT_PI), // 7: 2 1
395
+ M_SQRT_15 / (4.0 * M_SQRT_PI) // 8: 2 2
396
+ ];
397
+
398
+ // first this to do is to rotate the SH to align Z with the optimal linear direction
399
+ const dir = vec3.fromValues(-input_sh3[3], -input_sh3[1], input_sh3[2]);
400
+
401
+ vec3.normalize(dir, dir);
402
+
403
+ const z_axis = vec3.create();
404
+ vec3.negate(z_axis, dir);
405
+
406
+ const x_axis = vec3.create();
407
+ vec3.cross(x_axis, z_axis, vec3.fromValues(0, 1, 0));
408
+ vec3.normalize(x_axis, x_axis);
409
+
410
+ const y_axis = vec3.create();
411
+ vec3.cross(y_axis, x_axis, z_axis);
412
+
413
+ const M = mat3.create();
414
+ array_copy(x_axis, 0, M, 0, 3);
415
+ array_copy(y_axis, 0, M, 3, 3);
416
+ array_copy(dir, 0, M, 6, 3);
417
+
418
+ mat3.transpose(M, M);
419
+
420
+ const f = new Float32Array(9);
421
+
422
+ rotate_sh3_bands(f, input_sh3, M);
423
+ // here we're guaranteed to have normalize(float3{ -f[3], -f[1], f[2] }) == { 0, 0, 1 }
424
+
425
+
426
+ // Find the min for |m| = 2
427
+ // ------------------------
428
+ //
429
+ // Peter-Pike Sloan shows that the minimum can be expressed as a function
430
+ // of z such as: m2min = -m2max * (1 - z^2) = m2max * z^2 - m2max
431
+ // with m2max = A[8] * std::sqrt(f[8] * f[8] + f[4] * f[4]);
432
+ // We can therefore include this in the ZH min computation (which is function of z^2 as well)
433
+ const m2max = A[8] * Math.sqrt(f[8] * f[8] + f[4] * f[4]);
434
+
435
+ // Find the min of the zonal harmonics
436
+ // -----------------------------------
437
+ //
438
+ // This comes from minimizing the function:
439
+ // ZH(z) = (A[0] * f[0])
440
+ // + (A[2] * f[2]) * z
441
+ // + (A[6] * f[6]) * (3 * s.z * s.z - 1)
442
+ //
443
+ // We do that by finding where it's derivative d/dz is zero:
444
+ // dZH(z)/dz = a * z^2 + b * z + c
445
+ // which is zero for z = -b / 2 * a
446
+ //
447
+ // We also needs to check that -1 < z < 1, otherwise the min is either in z = -1 or 1
448
+ //
449
+ const a = 3 * A[6] * f[6] + m2max;
450
+ const b = A[2] * f[2];
451
+ const c = A[0] * f[0] - A[6] * f[6] - m2max;
452
+
453
+ const zmin = -b / (2.0 * a);
454
+ const m0min_z = a * zmin * zmin + b * zmin + c;
455
+ const m0min_b = min2(a + b + c, a - b + c);
456
+
457
+ const m0min = (a > 0 && zmin >= -1 && zmin <= 1) ? m0min_z : m0min_b;
458
+
459
+ // Find the min for l = 2, |m| = 1
460
+ // -------------------------------
461
+ //
462
+ // Note l = 1, |m| = 1 is guaranteed to be 0 because of the rotation step
463
+ //
464
+ // The function considered is:
465
+ // Y(x, y, z) = A[5] * f[5] * s.y * s.z
466
+ // + A[7] * f[7] * s.z * s.x
467
+ const d = A[4] * Math.sqrt(f[5] * f[5] + f[7] * f[7]);
468
+
469
+ // the |m|=1 function is minimal in -0.5 -- use that to skip the Newton's loop when possible
470
+ let minimum = m0min - 0.5 * d;
471
+
472
+ if (minimum < 0) {
473
+ // We could be negative, to find the minimum we will use Newton's method
474
+ // See https://en.wikipedia.org/wiki/Newton%27s_method_in_optimization
475
+
476
+
477
+ let dz;
478
+ let z = -F_SQRT1_2; // we start guessing at the min of |m|=1 function
479
+ do {
480
+ minimum = func_(z, a, b, c, d); // evaluate our function
481
+ dz = increment_(z, a, b, d); // refine our guess by this amount
482
+ z = z - dz;
483
+ // exit if z goes out of range, or if we have reached enough precision
484
+ } while (Math.abs(z) <= 1 && Math.abs(dz) > 1e-5);
485
+
486
+ if (Math.abs(z) > 1) {
487
+ // z was out of range
488
+ minimum = min2(func_(1, a, b, c, d), func_(-1, a, b, c, d));
489
+ }
490
+ }
491
+
492
+ return minimum;
493
+ }
494
+
495
+ /**
496
+ * this is the function we're trying to minimize
497
+ * @param {number} x
498
+ * @param {number} a
499
+ * @param {number} b
500
+ * @param {number} c
501
+ * @param {number} d
502
+ * @return {number}
503
+ */
504
+ function func_(x, a, b, c, d) {
505
+ // first term accounts for ZH + |m| = 2, second terms for |m| = 1
506
+ return (a * x * x + b * x + c) + (d * x * Math.sqrt(1 - x * x));
507
+ }
508
+
509
+ /**
510
+ * This is func' / func'' -- this was computed with Mathematica
511
+ * @param {number} x
512
+ * @param {number} a
513
+ * @param {number} b
514
+ * @param {number} d
515
+ * @return {number}
516
+ */
517
+ function increment_(x, a, b, d) {
518
+ return (x * x - 1) * (d - 2 * d * x * x + (b + 2 * a * x) * Math.sqrt(1 - x * x))
519
+ / (3 * d * x - 2 * d * x * x * x - 2 * a * Math.pow(1 - x * x, 1.5));
520
+ }
521
+
522
+ /**
523
+ *
524
+ * @param {number[]|Float32Array} f sh3
525
+ * @param {number} cutoff
526
+ * @param {number} numBands
527
+ */
528
+ function windowing(f, cutoff, numBands) {
529
+ for (let l = 0; l < numBands; l++) {
530
+ const w = sincWindow(l, cutoff);
531
+ f[SHindex(0, l)] *= w;
532
+ for (let m = 1; m <= l; m++) {
533
+ f[SHindex(-m, l)] *= w;
534
+ f[SHindex(m, l)] *= w;
535
+ }
536
+ }
537
+ return f;
538
+ }
539
+
540
+ /**
541
+ *
542
+ * @param {number[]} output sh3
543
+ * @param {number} output_offset
544
+ * @param {number[]} input
545
+ * @param {number} input_offset
546
+ * @param {number} numBands
547
+ * @param {number} channel_count
548
+ * @param {number} cutoff
549
+ */
550
+ function windowSH(output, output_offset, input, input_offset, numBands, channel_count, cutoff) {
551
+ assert.isNonNegativeInteger(channel_count, 'channel_count');
552
+ assert.greaterThan(channel_count, 0, 'channel_count must be greater than 0');
553
+
554
+ assert.greaterThanOrEqual(cutoff, 0, 'cutoff must be >= 0');
555
+
556
+ if (cutoff === 0) {
557
+ // auto windowing (default)
558
+
559
+ if (numBands > 3) {
560
+ // auto-windowing works only for 1, 2 or 3 bands
561
+ throw new Error("--sh-window=auto can't work with more than 3 bands. Disabling.");
562
+ }
563
+
564
+ cutoff = numBands * 4 + 1;// start at a large band
565
+ // We need to process each channel separately
566
+ const SH = new Float32Array(9);
567
+
568
+ for (let channel = 0; channel < channel_count; channel++) {
569
+
570
+ for (let i = 0; i < numBands * numBands; i++) {
571
+ SH[i] = output[output_offset + i * channel_count + channel];
572
+ }
573
+
574
+ // find a cut-off band that works
575
+ let l = numBands;
576
+ let r = cutoff;
577
+ for (let i = 0; i < 16 && l + 0.1 < r; i++) {
578
+ const m = 0.5 * (l + r);
579
+ if (shmin(windowing(SH, m, numBands)) < 0) {
580
+ r = m;
581
+ } else {
582
+ l = m;
583
+ }
584
+ }
585
+
586
+ cutoff = min2(cutoff, l);
587
+ }
588
+ }
589
+
590
+ array_copy(input, input_offset, output, output_offset, numBands * numBands * channel_count);
591
+
592
+ for (let l = 0; l < numBands; l++) {
593
+ let w = sincWindow(l, cutoff);
594
+
595
+ for (let i = 0; i < channel_count; i++) {
596
+ output[output_offset + SHindex(0, l) * channel_count + i] = w;
597
+ }
598
+
599
+ for (let m = 1; m <= l; m++) {
600
+ for (let i = 0; i < channel_count; i++) {
601
+ output[output_offset + SHindex(-m, l) * channel_count + i] *= w;
602
+ output[output_offset + SHindex(m, l) * channel_count + i] *= w;
603
+ }
604
+ }
605
+ }
606
+ }
607
+
608
+ /**
609
+ *
610
+ * @param {number[]} result
611
+ * @param {number} result_offset
612
+ * @param {number[]} harmonics
613
+ * @param {number} harmonics_offset
614
+ * @param {number} dimension_count
615
+ */
616
+ export function sh3_dering_optimize_positive(result, result_offset, harmonics, harmonics_offset, dimension_count = 3) {
617
+ windowSH(result, result_offset, harmonics, harmonics_offset, 3, dimension_count, 0);
618
+ }
@@ -0,0 +1,49 @@
1
+ //
2
+
3
+ /**
4
+ * Sample value from a 3-band spherical harmonic defined by 9 coefficients
5
+ *
6
+ * @see https://github.com/mrdoob/three.js/blob/d081c5a3501d272d19375fab1b01fedf9df29b22/src/math/SphericalHarmonics3.js#L55
7
+ * @see https://graphics.stanford.edu/papers/envmap/envmap.pdf
8
+ * @see https://www.ppsloan.org/publications/StupidSH36.pdf
9
+ * @param {number[]} result Result will be written here
10
+ * @param {number} result_offset
11
+ * @param {number[]} harmonics coefficients are read from here
12
+ * @param {number} harmonics_offset offset into coefficients array where to start reading the data
13
+ * @param {number} dimension_count number of encoded dimensions, this is essentially a shortcut for multiple harmonics being read at the same time, such as with RGB values
14
+ * @param {number[]} direction 3d vector read from here, we will read 3 values from here to sample spherical harmonic, direction is assumed to be normalized
15
+ * @param {number} direction_offset offset into direction array
16
+ */
17
+ export function sh3_sample_by_direction(
18
+ result, result_offset,
19
+ harmonics, harmonics_offset,
20
+ dimension_count,
21
+ direction, direction_offset
22
+ ) {
23
+ const x = direction[direction_offset];
24
+ const y = direction[direction_offset + 1];
25
+ const z = direction[direction_offset + 2];
26
+
27
+ let channel_value;
28
+
29
+ for (let i = 0; i < dimension_count; i++) {
30
+
31
+ // band 0
32
+ channel_value = harmonics[harmonics_offset + i] * 0.282095;
33
+
34
+ // band 1
35
+ channel_value += harmonics[harmonics_offset + dimension_count + i] * 0.488603 * y;
36
+ channel_value += harmonics[harmonics_offset + dimension_count * 2 + i] * 0.488603 * z;
37
+ channel_value += harmonics[harmonics_offset + dimension_count * 3 + i] * 0.488603 * x;
38
+
39
+ // band 2
40
+ channel_value += harmonics[harmonics_offset + dimension_count * 4 + i] * 1.092548 * (x * y);
41
+ channel_value += harmonics[harmonics_offset + dimension_count * 5 + i] * 1.092548 * (y * z);
42
+ channel_value += harmonics[harmonics_offset + dimension_count * 6 + i] * 0.315392 * (3.0 * z * z - 1.0);
43
+ channel_value += harmonics[harmonics_offset + dimension_count * 7 + i] * 1.092548 * (x * z);
44
+ channel_value += harmonics[harmonics_offset + dimension_count * 8 + i] * 0.546274 * (x * x - y * y);
45
+
46
+ // write out
47
+ result[result_offset + i] = channel_value;
48
+ }
49
+ }