@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.
- package/core/binary/BinaryBuffer.js +13 -1
- package/core/binary/BitSet.js +2 -2
- package/core/collection/array/array_range_equal_strict.js +22 -0
- package/core/collection/map/AsyncMapWrapper.js +13 -1
- package/core/collection/map/CachedAsyncMap.js +9 -2
- package/core/collection/map/CachedAsyncMap.spec.js +47 -0
- package/core/color/sRGB_to_linear.js +9 -4
- package/core/geom/3d/plane/orient3d_fast.js +3 -0
- package/core/geom/3d/plane/orient3d_robust.js +41 -0
- package/core/geom/3d/sphere/harmonics/README.md +15 -0
- package/core/geom/3d/sphere/harmonics/sh3_add.js +21 -0
- package/core/geom/3d/sphere/harmonics/sh3_dering_optimize_positive.js +618 -0
- package/core/geom/3d/sphere/harmonics/sh3_sample_by_direction.js +49 -0
- package/core/geom/3d/sphere/harmonics/sh3_sample_irradiance_by_direction.js +53 -0
- package/core/geom/3d/tetrahedra/TetrahedralMesh.js +251 -68
- package/core/geom/3d/tetrahedra/TetrahedralMesh.spec.js +80 -3
- package/core/geom/3d/tetrahedra/build_tetrahedral_mesh_buffer_geometry.js +75 -0
- package/core/geom/3d/tetrahedra/delaunay/Cavity.js +5 -1
- package/core/geom/3d/tetrahedra/delaunay/compute_delaunay_tetrahedral_mesh.js +30 -31
- package/core/geom/3d/tetrahedra/delaunay/fill_in_a_cavity.js +54 -18
- package/core/geom/3d/tetrahedra/delaunay/push_boundary_with_validation.js +27 -0
- package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity.js +89 -0
- package/core/geom/3d/tetrahedra/delaunay/{tetrahedral_mesh_walk_toward_cavity.js → tetrahedral_mesh_walk_towards_containing_tetrahedron.js} +15 -12
- package/core/geom/3d/tetrahedra/delaunay/validate_cavity_boundary.js +60 -0
- package/core/geom/3d/tetrahedra/{point_in_tetrahedron_circumsphere.js → in_sphere_fast.js} +2 -4
- package/core/geom/3d/tetrahedra/in_sphere_robust.js +53 -0
- package/core/geom/3d/tetrahedra/prototypeTetrahedraBuilder.js +44 -35
- package/core/geom/3d/tetrahedra/validate_tetrahedral_mesh.js +85 -38
- package/core/geom/3d/util/make_justified_point_grid.js +31 -0
- package/core/process/delay.js +5 -0
- package/editor/Editor.js +3 -0
- package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.js +195 -11
- package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.js +16 -0
- package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.js +4 -0
- package/engine/EngineHarness.js +11 -5
- package/engine/ecs/terrain/ecs/TerrainSystem.js +7 -1
- package/engine/ecs/transform/copy_three_transform.js +15 -0
- package/engine/graphics/ecs/light/Light.js +6 -1
- package/engine/graphics/ecs/light/LightSystem.d.ts +1 -1
- package/engine/graphics/ecs/mesh-v2/three_object_to_entity_composition.js +2 -17
- package/engine/graphics/geometry/instancing/InstancedMeshGroup.js +2 -2
- package/engine/graphics/micron/plugin/shaded_geometry/MicronShadedGeometryRenderAdapter.js +9 -1
- package/engine/graphics/sh3/LightProbeVolume.js +595 -0
- package/engine/graphics/sh3/SH3VisualisationMaterial.js +79 -0
- package/engine/graphics/sh3/prototypeSH3Probe.js +427 -0
- package/engine/graphics/sh3/visualise_probe.js +40 -0
- package/engine/graphics/texture/atlas/TextureAtlas.js +15 -3
- package/engine/intelligence/blackboard/AbstractBlackboard.d.ts +1 -1
- package/package.json +2 -1
- package/samples/terrain/from_image_2.js +127 -82
- package/core/geom/3d/tetrahedra/delaunay/tetrahedral_mesh_compute_cavity2.js +0 -224
- 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
|
+
}
|