@woosh/meep-engine 2.161.0 → 2.163.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,7 +82,7 @@ export default defineConfig({
82
82
  ```
83
83
 
84
84
  ### Reliability
85
- * **Tests:** 7,244 tests with 90%+ core coverage.
85
+ * **Tests:** 7,554 tests with 90%+ core coverage.
86
86
 
87
87
  * **Stability:** Exhaustively tested corner cases over a decade of development.
88
88
 
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.161.0",
9
+ "version": "2.163.0",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -3,8 +3,8 @@
3
3
  * Assumes linear RGB space
4
4
  * Assumes D65 standard illuminant
5
5
  * @see https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
6
- * @param {vec3} out
7
- * @param {vec3} input
6
+ * @param {number[]|Float32Array|Float64Array} out
7
+ * @param {number[]|Float32Array|Float64Array} input
8
8
  */
9
- export function rgb_to_xyz(out: vec3, input: vec3): void;
9
+ export function rgb_to_xyz(out: number[] | Float32Array | Float64Array, input: number[] | Float32Array | Float64Array): void;
10
10
  //# sourceMappingURL=rgb_to_xyz.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"rgb_to_xyz.d.ts","sourceRoot":"","sources":["../../../../../src/core/color/xyz/rgb_to_xyz.js"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,yDASC"}
1
+ {"version":3,"file":"rgb_to_xyz.d.ts","sourceRoot":"","sources":["../../../../../src/core/color/xyz/rgb_to_xyz.js"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,gCAHW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,SAClC,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,QAW5C"}
@@ -3,8 +3,8 @@
3
3
  * Assumes linear RGB space
4
4
  * Assumes D65 standard illuminant
5
5
  * @see https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
6
- * @param {vec3} out
7
- * @param {vec3} input
6
+ * @param {number[]|Float32Array|Float64Array} out
7
+ * @param {number[]|Float32Array|Float64Array} input
8
8
  */
9
9
  export function rgb_to_xyz(out, input) {
10
10
 
@@ -2,8 +2,8 @@
2
2
  * CIE color model to linear RGB
3
3
  * Assumes D65 standard illuminant
4
4
  * @see https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
5
- * @param {vec3|number[]|Float32Array} out
6
- * @param {vec3|number[]|Float32Array} input
5
+ * @param {number[]|Float32Array|Float64Array} out
6
+ * @param {number[]|Float32Array|Float64Array} input
7
7
  */
8
- export function xyz_to_rgb(out: vec3 | number[] | Float32Array, input: vec3 | number[] | Float32Array): void;
8
+ export function xyz_to_rgb(out: number[] | Float32Array | Float64Array, input: number[] | Float32Array | Float64Array): void;
9
9
  //# sourceMappingURL=xyz_to_rgb.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"xyz_to_rgb.d.ts","sourceRoot":"","sources":["../../../../../src/core/color/xyz/xyz_to_rgb.js"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,gCAHW,OAAK,MAAM,EAAE,GAAC,YAAY,SAC1B,OAAK,MAAM,EAAE,GAAC,YAAY,QAUpC"}
1
+ {"version":3,"file":"xyz_to_rgb.d.ts","sourceRoot":"","sources":["../../../../../src/core/color/xyz/xyz_to_rgb.js"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,gCAHW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,SAClC,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,QAU5C"}
@@ -2,8 +2,8 @@
2
2
  * CIE color model to linear RGB
3
3
  * Assumes D65 standard illuminant
4
4
  * @see https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
5
- * @param {vec3|number[]|Float32Array} out
6
- * @param {vec3|number[]|Float32Array} input
5
+ * @param {number[]|Float32Array|Float64Array} out
6
+ * @param {number[]|Float32Array|Float64Array} input
7
7
  */
8
8
  export function xyz_to_rgb(out, input) {
9
9
  const x = input[0];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Scale a 3d vector by a scalar, writing the result into `output`.
3
+ *
4
+ * @param {number[]|Float32Array} output
5
+ * @param {number} output_offset
6
+ * @param {number[]|Float32Array} input
7
+ * @param {number} input_offset
8
+ * @param {number} scalar
9
+ */
10
+ export function v3_array_scale(output: number[] | Float32Array, output_offset: number, input: number[] | Float32Array, input_offset: number, scalar: number): void;
11
+ //# sourceMappingURL=v3_array_scale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v3_array_scale.d.ts","sourceRoot":"","sources":["../../../../../src/core/geom/vec3/v3_array_scale.js"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,uCANW,MAAM,EAAE,GAAC,YAAY,iBACrB,MAAM,SACN,MAAM,EAAE,GAAC,YAAY,gBACrB,MAAM,UACN,MAAM,QAUhB"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Scale a 3d vector by a scalar, writing the result into `output`.
3
+ *
4
+ * @param {number[]|Float32Array} output
5
+ * @param {number} output_offset
6
+ * @param {number[]|Float32Array} input
7
+ * @param {number} input_offset
8
+ * @param {number} scalar
9
+ */
10
+ export function v3_array_scale(
11
+ output, output_offset,
12
+ input, input_offset,
13
+ scalar,
14
+ ) {
15
+ output[output_offset] = input[input_offset] * scalar;
16
+ output[output_offset + 1] = input[input_offset + 1] * scalar;
17
+ output[output_offset + 2] = input[input_offset + 2] * scalar;
18
+ }
@@ -270,11 +270,11 @@ export class EngineHarness {
270
270
  */
271
271
  static async buildBasics({
272
272
  engine,
273
- focus = new Vector3(10, 0, 10),
273
+ focus = new Vector3(0, 0, 0),
274
274
  heightMap,
275
275
  heightRange,
276
276
  pitch = 0.7,
277
- yaw = -1.2,
277
+ yaw = 0,
278
278
  distance = 10,
279
279
  terrainSize = new Vector2(10, 10),
280
280
  terrainResolution = 10,
@@ -287,7 +287,7 @@ export class EngineHarness {
287
287
  cameraController = true,
288
288
  cameraAutoClip = true,
289
289
  shadowmapResolution,
290
- showFps = true
290
+ showFps = false
291
291
  }) {
292
292
 
293
293
  if (showFps) {
@@ -1 +1 @@
1
- {"version":3,"file":"make_cap.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/ecs/path/tube/build/make_cap.js"],"names":[],"mappings":"AAyOA;;;;;;;;;;;;;;GAcG;AACH,4DAbW,MAAM,gBACN,YAAY,GAAC,MAAM,EAAE,cACrB,OAAO,EAAE,gBACT,OAAO,EAAE,eAET,OAAO,EAAE,SACT,MAAM,EAAE,gBACR,MAAM,EAAE,GAAC,YAAY,gBACrB,MAAM,mBACN,MAAM,EAAE,GAAC,YAAY,aACrB,MAAM,QACN,OAAO,QA6BjB;AAED;;;;;;GAMG;AACH,wDALW,MAAM,OACN;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,YAAY,EAAC,MAAM,CAAA;CAAC,mBAC3C,MAAM,QACN,OAAO,QAiBjB;wBA3SuB,OAAO;wBAMP,eAAe"}
1
+ {"version":3,"file":"make_cap.d.ts","sourceRoot":"","sources":["../../../../../../../../src/engine/graphics/ecs/path/tube/build/make_cap.js"],"names":[],"mappings":"AAwQA;;;;;;;;;;;;;;GAcG;AACH,4DAbW,MAAM,gBACN,YAAY,GAAC,MAAM,EAAE,cACrB,OAAO,EAAE,gBACT,OAAO,EAAE,eAET,OAAO,EAAE,SACT,MAAM,EAAE,gBACR,MAAM,EAAE,GAAC,YAAY,gBACrB,MAAM,mBACN,MAAM,EAAE,GAAC,YAAY,aACrB,MAAM,QACN,OAAO,QA6BjB;AAED;;;;;;GAMG;AACH,wDALW,MAAM,OACN;IAAC,aAAa,EAAC,MAAM,CAAC;IAAC,YAAY,EAAC,MAAM,CAAA;CAAC,mBAC3C,MAAM,QACN,OAAO,QAiBjB;wBA1UuB,OAAO;wBAMP,eAAe"}
@@ -79,13 +79,44 @@ function make_cap_round(
79
79
  const B = in_binormals[index];
80
80
  const T = in_tangents[index];
81
81
 
82
- const tangent = new Vector3();
83
- tangent.crossVectors(N, B);
84
- tangent.normalize();
85
- tangent.multiplyScalar(-direction);
82
+ // Outward cap axis. Derive it from the PATH itself (the segment between the
83
+ // cap and its neighbour sample) rather than from N x B: the Frenet frame's
84
+ // tangent can flip sign at curvature features, and an N x B-derived axis
85
+ // would then make the cap bulge inward for a frame ("cap twists off-axis").
86
+ // The path direction is always well-defined and outward. Fall back to the
87
+ // frame only if the neighbour sample is coincident (degenerate segment).
88
+ const point_count = in_positions.length / 3;
89
+ const neighbour = direction > 0
90
+ ? Math.min(index + 1, point_count - 1)
91
+ : Math.max(index - 1, 0);
92
+ const n3 = neighbour * 3;
93
+
94
+ const tangent = new Vector3(
95
+ Px - in_positions[n3],
96
+ Py - in_positions[n3 + 1],
97
+ Pz - in_positions[n3 + 2]
98
+ );
99
+
100
+ if (tangent.lengthSq() > 1e-12) {
101
+ tangent.normalize();
102
+ } else {
103
+ // degenerate: neighbour coincides with the cap point, use the frame
104
+ tangent.crossVectors(N, B);
105
+ tangent.normalize();
106
+ tangent.multiplyScalar(-direction);
107
+ }
86
108
 
87
- const normal = direction > 0 ? N : N.clone().negate();
88
- const binormal = direction > 0 ? B : B.clone().negate();
109
+ // Use the path frame's N/B directly for BOTH ends. The profile orientation
110
+ // must match the tube body it caps, otherwise the cap's cross-section is
111
+ // mis-aligned where it meets the body. Negating N/B for the end cap (as was
112
+ // done previously) point-reflects the profile in the N-B plane (a 180 degree
113
+ // turn about the tangent): invisible for a centrally-symmetric profile (a
114
+ // circle) but it twists the cap for any asymmetric one. The end cap's face
115
+ // winding is already corrected by the `j = cap_segment_count - i` ring-order
116
+ // reversal below (it advances the end-cap rings in the same direction vs `i`
117
+ // as the start cap), so no profile negation is needed.
118
+ const normal = N;
119
+ const binormal = B;
89
120
 
90
121
  const angular_step_i = (Math.PI / 2) / cap_segment_count;
91
122
 
@@ -1 +1 @@
1
- {"version":3,"file":"make_sky_hosek.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/path_tracer/make_sky_hosek.js"],"names":[],"mappings":"AAQA,2LAmCC"}
1
+ {"version":3,"file":"make_sky_hosek.d.ts","sourceRoot":"","sources":["../../../../../../src/engine/graphics/sh3/path_tracer/make_sky_hosek.js"],"names":[],"mappings":"AASA,2LAmCC"}
@@ -1,44 +1,45 @@
1
- import {
2
- sky_hosek_compute_irradiance_by_direction,
3
- sky_hosek_precompute
4
- } from "../sky/hosek/sky_hosek_compute_irradiance_by_direction.js";
5
- import { vec3 } from "gl-matrix";
6
- import { array_copy } from "../../../../core/collection/array/array_copy.js";
7
- import { max2 } from "../../../../core/math/max2.js";
8
-
9
- export function make_sky_hosek(sun = [0, 1, 0], turbidity = 1, overcast = 0, albedo = [0, 0, 0]) {
10
- const coeffs = new Float32Array(27);
11
- const rad = new Float32Array([1, 1, 1]);
12
- const sun_position = new Float32Array([sun[0], sun[2], sun[1]]);
13
-
14
- vec3.normalize(sun_position, sun_position);
15
-
16
- sky_hosek_precompute(coeffs, rad, sun_position, turbidity, albedo, overcast);
17
-
18
- const v3 = [];
19
-
20
- const value_scale = 8e-5;
21
-
22
- return (result, result_offset, direction, direction_offset) => {
23
- const d_x = direction[direction_offset];
24
- const d_z = direction[direction_offset + 2];
25
- const d_y = direction[direction_offset + 1];
26
-
27
- sky_hosek_compute_irradiance_by_direction(
28
- v3, coeffs, rad, sun_position,
29
- d_z ,
30
- d_x ,
31
- d_y ,
32
- );
33
-
34
- vec3.scale(v3, v3, value_scale);
35
-
36
- // clamp sky contribution
37
- v3[0] = max2(0, v3[0]);
38
- v3[1] = max2(0, v3[1]);
39
- v3[2] = max2(0, v3[2]);
40
-
41
-
42
- array_copy(v3, 0, result, result_offset, 3);
43
- }
44
- }
1
+ import {
2
+ sky_hosek_compute_irradiance_by_direction,
3
+ sky_hosek_precompute
4
+ } from "../sky/hosek/sky_hosek_compute_irradiance_by_direction.js";
5
+ import { v3_array_scale } from "../../../../core/geom/vec3/v3_array_scale.js";
6
+ import { v3_array_normalize } from "../../../../core/geom/vec3/v3_array_normalize.js";
7
+ import { array_copy } from "../../../../core/collection/array/array_copy.js";
8
+ import { max2 } from "../../../../core/math/max2.js";
9
+
10
+ export function make_sky_hosek(sun = [0, 1, 0], turbidity = 1, overcast = 0, albedo = [0, 0, 0]) {
11
+ const coeffs = new Float32Array(27);
12
+ const rad = new Float32Array([1, 1, 1]);
13
+ const sun_position = new Float32Array([sun[0], sun[1], sun[2]]);
14
+
15
+ v3_array_normalize(sun_position, 0, sun_position, 0);
16
+
17
+ sky_hosek_precompute(coeffs, rad, sun_position, turbidity, albedo, overcast);
18
+
19
+ const v3 = [];
20
+
21
+ const value_scale = 8e-5;
22
+
23
+ return (result, result_offset, direction, direction_offset) => {
24
+ const d_x = direction[direction_offset];
25
+ const d_y = direction[direction_offset + 1];
26
+ const d_z = direction[direction_offset + 2];
27
+
28
+ sky_hosek_compute_irradiance_by_direction(
29
+ v3, coeffs, rad, sun_position,
30
+ d_x,
31
+ d_y,
32
+ d_z,
33
+ );
34
+
35
+ v3_array_scale(v3, 0, v3, 0, value_scale);
36
+
37
+ // clamp sky contribution
38
+ v3[0] = max2(0, v3[0]);
39
+ v3[1] = max2(0, v3[1]);
40
+ v3[2] = max2(0, v3[2]);
41
+
42
+
43
+ array_copy(v3, 0, result, result_offset, 3);
44
+ }
45
+ }
@@ -1,24 +1,22 @@
1
- /// <reference types="gl-matrix/index.js" />
2
1
  /**
3
2
  * @see https://github.com/andrewwillmott/sun-sky/blob/master/SunSky.cpp
4
3
  * @param {number[]} mCoeffsXYZ output, float[3][9], Hosek 9-term distribution coefficients
5
4
  * @param {number[]} mRadXYZ output, vec3, Overall average radiance
6
- * @param {vec3} sun_direction
5
+ * @param {number[]|Float32Array} sun_direction direction to the sun, engine frame (+Y up)
7
6
  * @param {number} turbidity should be between 1 and 10
8
- * @param {vec3} rgbAlbedo albedo in linear color space (make sure it's not sRGB)
7
+ * @param {number[]|Float32Array} rgbAlbedo albedo in linear color space (make sure it's not sRGB)
9
8
  * @param {number} overcast
10
9
  */
11
- export function sky_hosek_precompute(mCoeffsXYZ: number[], mRadXYZ: number[], sun_direction: vec3, turbidity: number, rgbAlbedo: vec3, overcast: number): void;
10
+ export function sky_hosek_precompute(mCoeffsXYZ: number[], mRadXYZ: number[], sun_direction: number[] | Float32Array, turbidity: number, rgbAlbedo: number[] | Float32Array, overcast: number): void;
12
11
  /**
13
12
  *
14
13
  * @param {number[]|Float32Array} out vec3 in RGB color space
15
14
  * @param {number[]|Float32Array} mCoeffsXYZ float[3][9]
16
15
  * @param {number[]|Float32Array} mRadXYZ vec3
17
- * @param {number[]|Float32Array} mToSun vec3
16
+ * @param {number[]|Float32Array} mToSun vec3, direction to the sun, engine frame (+Y up)
18
17
  * @param {number} direction_x
19
- * @param {number} direction_y
18
+ * @param {number} direction_y up axis (engine +Y up)
20
19
  * @param {number} direction_z
21
20
  */
22
21
  export function sky_hosek_compute_irradiance_by_direction(out: number[] | Float32Array, mCoeffsXYZ: number[] | Float32Array, mRadXYZ: number[] | Float32Array, mToSun: number[] | Float32Array, direction_x: number, direction_y: number, direction_z: number): void;
23
- import { vec3 } from "gl-matrix";
24
22
  //# sourceMappingURL=sky_hosek_compute_irradiance_by_direction.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sky_hosek_compute_irradiance_by_direction.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/graphics/sh3/sky/hosek/sky_hosek_compute_irradiance_by_direction.js"],"names":[],"mappings":";AA+NA;;;;;;;;GAQG;AACH,iDAPW,MAAM,EAAE,WACR,MAAM,EAAE,iBACR,IAAI,aACJ,MAAM,aACN,IAAI,YACJ,MAAM,QA4HhB;AAGD;;;;;;;;;GASG;AACH,+DARW,MAAM,EAAE,GAAC,YAAY,cACrB,MAAM,EAAE,GAAC,YAAY,WACrB,MAAM,EAAE,GAAC,YAAY,UACrB,MAAM,EAAE,GAAC,YAAY,eACrB,MAAM,eACN,MAAM,eACN,MAAM,QA6BhB;qBAxYoB,WAAW"}
1
+ {"version":3,"file":"sky_hosek_compute_irradiance_by_direction.d.ts","sourceRoot":"","sources":["../../../../../../../src/engine/graphics/sh3/sky/hosek/sky_hosek_compute_irradiance_by_direction.js"],"names":[],"mappings":"AAyNA;;;;;;;;GAQG;AACH,iDAPW,MAAM,EAAE,WACR,MAAM,EAAE,iBACR,MAAM,EAAE,GAAC,YAAY,aACrB,MAAM,aACN,MAAM,EAAE,GAAC,YAAY,YACrB,MAAM,QAgHhB;AAGD;;;;;;;;;GASG;AACH,+DARW,MAAM,EAAE,GAAC,YAAY,cACrB,MAAM,EAAE,GAAC,YAAY,WACrB,MAAM,EAAE,GAAC,YAAY,UACrB,MAAM,EAAE,GAAC,YAAY,eACrB,MAAM,eACN,MAAM,eACN,MAAM,QAwChB"}
@@ -1,395 +1,388 @@
1
- // --- Mathematical constants -------------------------------------------------
2
-
3
- import { vec3 } from "gl-matrix";
4
- import { rgb_to_xyz } from "../../../../../core/color/xyz/rgb_to_xyz.js";
5
- import { xyz_to_rgb } from "../../../../../core/color/xyz/xyz_to_rgb.js";
6
- import { v3_dot } from "../../../../../core/geom/vec3/v3_dot.js";
7
- import { clamp } from "../../../../../core/math/clamp.js";
8
- import { clamp01 } from "../../../../../core/math/clamp01.js";
9
- import { lerp } from "../../../../../core/math/lerp.js";
10
- import { kHosekCoeffsX, kHosekCoeffsY, kHosekCoeffsZ, kHosekRadX, kHosekRadY, kHosekRadZ } from "./data.js";
11
-
12
- const vl_pi = (3.14159265358979323846);
13
- const vl_halfPi = (vl_pi / 2.0);
14
- const vl_quarterPi = (vl_pi / 4.0);
15
- const vl_twoPi = (2.0 * vl_pi);
16
-
17
-
18
- /**
19
- *
20
- * @param {number[]|Float32Array} w float[6]
21
- * @param {number[]|Float32Array} data float[6]
22
- * @param {number} data_offset
23
- * @return {number}
24
- */
25
- function EvalQuintic_2(w, data, data_offset) {
26
- return w[0] * data[data_offset + 0]
27
- + w[1] * data[data_offset + 1]
28
- + w[2] * data[data_offset + 2]
29
- + w[3] * data[data_offset + 3]
30
- + w[4] * data[data_offset + 4]
31
- + w[5] * data[data_offset + 5];
32
- }
33
-
34
- /**
35
- *
36
- * @param {number[]|Float32Array} w float[6]
37
- * @param {number[]|Float32Array} data float[6][9] flattened
38
- * @param {number} data_offset
39
- * @param {number[]|Float32Array} coeffs float[9] output is written here
40
- * @param {number} coeffs_offset
41
- */
42
- function EvalQuintic_3(w, data, data_offset, coeffs, coeffs_offset) {
43
- for (let i = 0; i < 9; i++) {
44
- coeffs[coeffs_offset + i] = w[0] * data[data_offset + 0 * 6 + i]
45
- + w[1] * data[data_offset + 1 * 6 + i]
46
- + w[2] * data[data_offset + 2 * 6 + i]
47
- + w[3] * data[data_offset + 3 * 6 + i]
48
- + w[4] * data[data_offset + 4 * 6 + i]
49
- + w[5] * data[data_offset + 5 * 6 + i];
50
- }
51
- }
52
-
53
- /**
54
- *
55
- * @param {number} s
56
- * @param {number[]|ArrayLike<number>|Float32Array} w
57
- */
58
- function FindQuinticWeights(s, w) {
59
- const s1 = s;
60
- const s2 = s1 * s1;
61
- const s3 = s1 * s2;
62
- const s4 = s2 * s2;
63
- const s5 = s2 * s3;
64
-
65
- const is1 = 1.0 - s1;
66
- const is2 = is1 * is1;
67
- const is3 = is1 * is2;
68
- const is4 = is2 * is2;
69
- const is5 = is2 * is3;
70
-
71
- w[0] = is5;
72
- w[1] = is4 * s1 * 5.0;
73
- w[2] = is3 * s2 * 10.0;
74
- w[3] = is2 * s3 * 10.0;
75
- w[4] = is1 * s4 * 5.0;
76
- w[5] = s5;
77
- }
78
-
79
- /**
80
- *
81
- * @param {number[]|Float32Array} dataset9 albedo x 2, turbidity x 10, quintics x 6, weights x 9
82
- * @param {number[]|Float32Array} datasetR albedo x 2, turbidity x 10, quintics x 6
83
- * @param {number} turbidity
84
- * @param {number} albedo
85
- * @param {number} solarElevation
86
- * @param {number[]|Float32Array} coeffs result is written here
87
- * @param {number} coeffs_offset
88
- * @return {number}
89
- * @constructor
90
- */
91
- function FindHosekCoeffs
92
- (
93
- dataset9,
94
- datasetR,
95
- turbidity,
96
- albedo,
97
- solarElevation,
98
- coeffs,
99
- coeffs_offset
100
- ) {
101
- const tbi = clamp(Math.floor(turbidity), 1, 9);
102
-
103
- const tbf = turbidity - tbi;
104
-
105
- const s = Math.pow(solarElevation / vl_halfPi, (1.0 / 3.0));
106
-
107
- const quinticWeights = new Float32Array(6);
108
- FindQuinticWeights(s, quinticWeights);
109
-
110
- const ic = new Float32Array(4 * 9);
111
-
112
- const size_set9_1 = 6 * 9;
113
- const size_set9_0 = 10 * size_set9_1;
114
- EvalQuintic_3(quinticWeights, dataset9, (tbi - 1) * size_set9_1, ic, 0);
115
- EvalQuintic_3(quinticWeights, dataset9, size_set9_0 + (tbi - 1) * size_set9_1, ic, 9);
116
- EvalQuintic_3(quinticWeights, dataset9, tbi * size_set9_1, ic, 18);
117
- EvalQuintic_3(quinticWeights, dataset9, size_set9_0 + tbi * size_set9_1, ic, 27);
118
-
119
- const size_set_R_1 = 6;
120
- const size_set_R_0 = 10 * size_set_R_1;
121
- const ir = [
122
- EvalQuintic_2(quinticWeights, datasetR, (tbi - 1) * size_set_R_1),
123
- EvalQuintic_2(quinticWeights, datasetR, size_set_R_0 + (tbi - 1) * size_set_R_1),
124
- EvalQuintic_2(quinticWeights, datasetR, tbi * size_set_R_1),
125
- EvalQuintic_2(quinticWeights, datasetR, size_set_R_0 + tbi * size_set_R_1),
126
- ];
127
-
128
- const cw = [
129
- (1.0 - albedo) * (1.0 - tbf),
130
- albedo * (1.0 - tbf),
131
- (1.0 - albedo) * tbf,
132
- albedo * tbf,
133
- ];
134
-
135
- for (let i = 0; i < 9; i++) {
136
- coeffs[coeffs_offset + i] = cw[0] * ic[i]
137
- + cw[1] * ic[9 + i]
138
- + cw[2] * ic[2 * 9 + i]
139
- + cw[3] * ic[3 * 9 + i];
140
- }
141
-
142
- return cw[0] * ir[0] + cw[1] * ir[1] + cw[2] * ir[2] + cw[3] * ir[3];
143
- }
144
-
145
- /**
146
- *
147
- * Hosek:
148
- * (1 + A e ^ (B / cos(t))) (1 + C e ^ (D g) + E cos(g) ^ 2 + F mieM(g, G) + H cos(t)^1/2 + (I - 1))
149
- *
150
- * These bits are the same as Preetham, but do different jobs in some cases
151
- * A: sky gradient, carries white -> blue gradient
152
- * B: sky tightness
153
- * C: sun, carries most of sun-centred blue term
154
- * D: sun tightness, higher = tighter
155
- * E: rosy hue around sun
156
- *
157
- * Hosek-specific:
158
- * F: mie term, does most of the heavy lifting for sunset glow
159
- * G: mie tuning
160
- * H: zenith gradient
161
- * I: constant term balanced with H
162
- *
163
- * Notes:
164
- * A/B still carries some of the "blue" base of sky, but much comes from C/D
165
- * C/E minimal effect in sunset situations, carry bulk of sun halo in sun-overhead
166
- * F/G sunset glow, but also takes sun halo from yellowish to white overhead
167
- * @param {number[]|Float32Array} coeffs
168
- * @param {number} coeffs_offset
169
- * @param {number} cosTheta
170
- * @param {number} gamma
171
- * @param {number} cosGamma
172
- * @return {number}
173
- * @constructor
174
- */
175
- function EvalHosekCoeffs(
176
- coeffs,
177
- coeffs_offset,
178
- cosTheta,
179
- gamma,
180
- cosGamma
181
- ) {
182
- const c_0 = coeffs[coeffs_offset + 0];
183
- const c_1 = coeffs[coeffs_offset + 1];
184
- const c_2 = coeffs[coeffs_offset + 2];
185
- const c_3 = coeffs[coeffs_offset + 3];
186
- const c_4 = coeffs[coeffs_offset + 4];
187
- const c_5 = coeffs[coeffs_offset + 5];
188
- const c_6 = coeffs[coeffs_offset + 6];
189
- const c_7 = coeffs[coeffs_offset + 7];
190
- const c_8 = coeffs[coeffs_offset + 8];
191
-
192
- // Current coeffs ordering is AB I CDEF HG
193
- // 01 2 3456 78
194
- const expM = Math.exp(c_4 * gamma); // D g
195
- const rayM = cosGamma * cosGamma; // Rayleigh scattering
196
- const mieM = (1.0 + rayM) / Math.pow((1.0 + c_8 * c_8 - 2.0 * c_8 * cosGamma), 1.5); // G
197
- const zenith = Math.sqrt(cosTheta); // vertical zenith gradient
198
-
199
- return (
200
- 1.0 + c_0 * Math.exp(c_1 / (cosTheta + 0.01)) // A, B
201
- )
202
- * (1.0
203
- + c_3 * expM // C
204
- + c_5 * rayM // E
205
- + c_6 * mieM // F
206
- + c_7 * zenith // H
207
- + (c_2 - 1.0) // I
208
- );
209
- }
210
-
211
- /**
212
- *
213
- * @param {number} thetaS
214
- * @param {number} T
215
- * @return {number}
216
- */
217
- function ZenithLuminance(thetaS, T) {
218
- const chi = (4.0 / 9.0 - T / 120.0) * (vl_pi - 2.0 * thetaS);
219
- let Lz = (4.0453 * T - 4.9710) * Math.tan(chi) - 0.2155 * T + 2.4192;
220
- Lz *= 1000.0; // conversion from kcd/m^2 to cd/m^2
221
- return Lz;
222
- }
223
-
224
- /**
225
- * @see https://github.com/andrewwillmott/sun-sky/blob/master/SunSky.cpp
226
- * @param {number[]} mCoeffsXYZ output, float[3][9], Hosek 9-term distribution coefficients
227
- * @param {number[]} mRadXYZ output, vec3, Overall average radiance
228
- * @param {vec3} sun_direction
229
- * @param {number} turbidity should be between 1 and 10
230
- * @param {vec3} rgbAlbedo albedo in linear color space (make sure it's not sRGB)
231
- * @param {number} overcast
232
- */
233
- export function sky_hosek_precompute(
234
- mCoeffsXYZ,
235
- mRadXYZ,
236
- sun_direction,
237
- turbidity,
238
- rgbAlbedo,
239
- overcast
240
- ) {
241
-
242
- const mToSun = sun_direction;
243
-
244
- const sun_x = mToSun[0];
245
- const sun_y = mToSun[1];
246
- const sun_z = mToSun[2];
247
-
248
- const solarElevation = sun_z > 0 ? Math.asin(sun_z) : 0;
249
-
250
- const albedo = [];
251
- rgb_to_xyz(albedo, rgbAlbedo);
252
-
253
- /**
254
- * Hosek 9-term distribution coefficients
255
- * float[3][9]
256
- * @type {Float32Array}
257
- */
258
- // const mCoeffsXYZ = new Float32Array(3 * 9);
259
-
260
- /**
261
- * Overall average radiance
262
- * Vec3f
263
- * @type {Float32Array}
264
- */
265
- //const mRadXYZ = new Float32Array(3);
266
-
267
- // Note that the hosek coefficients change with time of day, vs. Preetham where the 'upper' coefficients stay the same,
268
- // and only the scaler mPerezInvDen, consisting of time-dependent normalisation and zenith luminnce factors, changes.
269
- mRadXYZ[0] = FindHosekCoeffs(kHosekCoeffsX, kHosekRadX, turbidity, albedo[0], solarElevation, mCoeffsXYZ, 0);
270
- mRadXYZ[1] = FindHosekCoeffs(kHosekCoeffsY, kHosekRadY, turbidity, albedo[1], solarElevation, mCoeffsXYZ, 9);
271
- mRadXYZ[2] = FindHosekCoeffs(kHosekCoeffsZ, kHosekRadZ, turbidity, albedo[2], solarElevation, mCoeffsXYZ, 18);
272
-
273
- vec3.scale(mRadXYZ, mRadXYZ, 683); // convert to luminance in lumens
274
-
275
-
276
- if (sun_z < 0.0) // sun below horizon?
277
- {
278
- const s = clamp01(1.0 + sun_z * 50.0); // goes from 1 to 0 as the sun sets
279
- const is = 1.0 - s;
280
-
281
- // Emulate Preetham's zenith darkening
282
- const darken = ZenithLuminance(Math.acos(sun_z), turbidity) / ZenithLuminance(vl_halfPi, turbidity);
283
-
284
- // Take C/E/F which control sun term to zero
285
- for (let j = 0; j < 3; j++) {
286
- mCoeffsXYZ[j * 9 + 3] *= s;
287
- mCoeffsXYZ[j * 9 + 5] *= s;
288
- mCoeffsXYZ[j * 9 + 6] *= s;
289
-
290
- // Take horizon term H to zero, as it's an orange glow at this point
291
- mCoeffsXYZ[j * 9 + 7] *= s;
292
-
293
- // Take I term back to 1
294
- mCoeffsXYZ[j * 9 + 2] *= s;
295
- mCoeffsXYZ[j * 9 + 2] += is;
296
- }
297
-
298
- vec3.scale(mRadXYZ, mRadXYZ, darken);
299
- }
300
-
301
- if (overcast !== 0.0) // Handle overcast term
302
- {
303
- const is = overcast;
304
- const s = 1.0 - overcast; // goes to 0 as we go to overcast
305
-
306
- // Hosek isn't self-normalising, unlike Preetham/CIE, which divides by PreethamLower().
307
- // Thus when we lerp to the CIE overcast model, we get some non-linearities.
308
- // We deal with this by using ratios of normalisation terms to balance.
309
- // Another difference is that Hosek is relative to the average radiance,
310
- // whereas CIE is the zenith radiance, so rather than taking the zenith
311
- // as normalising as in CIE, we average over the zenith and two horizon
312
- // points.
313
- const cosGammaZ = sun_z;
314
- const gammaZ = Math.acos(cosGammaZ);
315
- const cosGammaH = sun_y;
316
- const gammaHP = Math.acos(sun_y);
317
- const gammaHN = vl_pi - gammaHP;
318
-
319
- const sc0 = EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 1.0, gammaZ, cosGammaZ) * 2.0
320
- + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHP, +cosGammaH)
321
- + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHN, -cosGammaH);
322
-
323
- for (let j = 0; j < 3; j++) {
324
- // sun flare -> 0 strength/base chroma
325
- // Take C/E/F which control sun term to zero
326
- mCoeffsXYZ[j * 9 + 3] *= s;
327
- mCoeffsXYZ[j * 9 + 5] *= s;
328
- mCoeffsXYZ[j * 9 + 6] *= s;
329
-
330
- // Take H back to 0
331
- mCoeffsXYZ[j * 9 + 7] *= s;
332
-
333
- // Take I term back to 1
334
- mCoeffsXYZ[j * 9 + 2] *= s;
335
- mCoeffsXYZ[j * 9 + 2] += is;
336
-
337
- // Take A/B to CIE cloudy sky model: 4, -0.7
338
- mCoeffsXYZ[j * 9 + 0] = lerp(mCoeffsXYZ[j * 9 + 0], 4.0, is);
339
- mCoeffsXYZ[j * 9 + 1] = lerp(mCoeffsXYZ[j * 9 + 1], -0.7, is);
340
- }
341
-
342
- const sc1 = EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 1.0, gammaZ, cosGammaZ) * 2.0
343
- + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHP, +cosGammaH)
344
- + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHN, -cosGammaH);
345
-
346
- const rescale = sc0 / sc1;
347
- vec3.scale(mRadXYZ, mRadXYZ, rescale);
348
-
349
-
350
- // move back to white point
351
- mRadXYZ[0] = lerp(mRadXYZ[0], mRadXYZ[1], is);
352
- mRadXYZ[2] = lerp(mRadXYZ[2], mRadXYZ[1], is);
353
- }
354
-
355
- }
356
-
357
-
358
- /**
359
- *
360
- * @param {number[]|Float32Array} out vec3 in RGB color space
361
- * @param {number[]|Float32Array} mCoeffsXYZ float[3][9]
362
- * @param {number[]|Float32Array} mRadXYZ vec3
363
- * @param {number[]|Float32Array} mToSun vec3
364
- * @param {number} direction_x
365
- * @param {number} direction_y
366
- * @param {number} direction_z
367
- */
368
- export function sky_hosek_compute_irradiance_by_direction(
369
- out,
370
- mCoeffsXYZ,
371
- mRadXYZ,
372
- mToSun,
373
- direction_x,
374
- direction_y,
375
- direction_z,
376
- ) {
377
- let cosTheta = direction_z;
378
- const cosGamma = v3_dot(mToSun[0], mToSun[1], mToSun[2], direction_x, direction_y, direction_z);
379
- const gamma = Math.acos(cosGamma);
380
-
381
- if (cosTheta < 0.0) {
382
- // clamp angle
383
- cosTheta = 0.0;
384
- }
385
-
386
- const x = EvalHosekCoeffs(mCoeffsXYZ, 0, cosTheta, gamma, cosGamma);
387
- const y = EvalHosekCoeffs(mCoeffsXYZ, 9, cosTheta, gamma, cosGamma);
388
- const z = EvalHosekCoeffs(mCoeffsXYZ, 18, cosTheta, gamma, cosGamma);
389
-
390
- out[0] = x * mRadXYZ[0];
391
- out[1] = y * mRadXYZ[1];
392
- out[2] = z * mRadXYZ[2];
393
-
394
- xyz_to_rgb(out, out);
395
- }
1
+ import { rgb_to_xyz } from "../../../../../core/color/xyz/rgb_to_xyz.js";
2
+ import { xyz_to_rgb } from "../../../../../core/color/xyz/xyz_to_rgb.js";
3
+ import { v3_angle_cos_between } from "../../../../../core/geom/vec3/v3_angle_cos_between.js";
4
+ import { v3_array_scale } from "../../../../../core/geom/vec3/v3_array_scale.js";
5
+ import { v3_length } from "../../../../../core/geom/vec3/v3_length.js";
6
+ import { clamp } from "../../../../../core/math/clamp.js";
7
+ import { clamp01 } from "../../../../../core/math/clamp01.js";
8
+ import { lerp } from "../../../../../core/math/lerp.js";
9
+ import { kHosekCoeffsX, kHosekCoeffsY, kHosekCoeffsZ, kHosekRadX, kHosekRadY, kHosekRadZ } from "./data.js";
10
+
11
+ const HALF_PI = Math.PI * 0.5;
12
+
13
+
14
+ /**
15
+ *
16
+ * @param {number[]|Float32Array} w float[6]
17
+ * @param {number[]|Float32Array} data float[6]
18
+ * @param {number} data_offset
19
+ * @return {number}
20
+ */
21
+ function EvalQuintic_2(w, data, data_offset) {
22
+ return w[0] * data[data_offset + 0]
23
+ + w[1] * data[data_offset + 1]
24
+ + w[2] * data[data_offset + 2]
25
+ + w[3] * data[data_offset + 3]
26
+ + w[4] * data[data_offset + 4]
27
+ + w[5] * data[data_offset + 5];
28
+ }
29
+
30
+ /**
31
+ *
32
+ * @param {number[]|Float32Array} w float[6]
33
+ * @param {number[]|Float32Array} data float[6][9] flattened
34
+ * @param {number} data_offset
35
+ * @param {number[]|Float32Array} coeffs float[9] output is written here
36
+ * @param {number} coeffs_offset
37
+ */
38
+ function EvalQuintic_3(w, data, data_offset, coeffs, coeffs_offset) {
39
+ for (let i = 0; i < 9; i++) {
40
+ coeffs[coeffs_offset + i] = w[0] * data[data_offset + 0 * 9 + i]
41
+ + w[1] * data[data_offset + 1 * 9 + i]
42
+ + w[2] * data[data_offset + 2 * 9 + i]
43
+ + w[3] * data[data_offset + 3 * 9 + i]
44
+ + w[4] * data[data_offset + 4 * 9 + i]
45
+ + w[5] * data[data_offset + 5 * 9 + i];
46
+ }
47
+ }
48
+
49
+ /**
50
+ *
51
+ * @param {number} s
52
+ * @param {number[]|ArrayLike<number>|Float32Array} w
53
+ */
54
+ function FindQuinticWeights(s, w) {
55
+ const s1 = s;
56
+ const s2 = s1 * s1;
57
+ const s3 = s1 * s2;
58
+ const s4 = s2 * s2;
59
+ const s5 = s2 * s3;
60
+
61
+ const is1 = 1.0 - s1;
62
+ const is2 = is1 * is1;
63
+ const is3 = is1 * is2;
64
+ const is4 = is2 * is2;
65
+ const is5 = is2 * is3;
66
+
67
+ w[0] = is5;
68
+ w[1] = is4 * s1 * 5.0;
69
+ w[2] = is3 * s2 * 10.0;
70
+ w[3] = is2 * s3 * 10.0;
71
+ w[4] = is1 * s4 * 5.0;
72
+ w[5] = s5;
73
+ }
74
+
75
+ // Reusable scratch buffers for FindHosekCoeffs. Safe to share: evaluation is
76
+ // synchronous and non-reentrant, and each call fully overwrites them.
77
+ const quinticWeights = new Float32Array(6);
78
+ const ic = new Float32Array(4 * 9);
79
+
80
+ /**
81
+ *
82
+ * @param {number[]|Float32Array} dataset9 albedo x 2, turbidity x 10, quintics x 6, weights x 9
83
+ * @param {number[]|Float32Array} datasetR albedo x 2, turbidity x 10, quintics x 6
84
+ * @param {number} turbidity
85
+ * @param {number} albedo
86
+ * @param {number} solarElevation
87
+ * @param {number[]|Float32Array} coeffs result is written here
88
+ * @param {number} coeffs_offset
89
+ * @return {number}
90
+ * @constructor
91
+ */
92
+ function FindHosekCoeffs
93
+ (
94
+ dataset9,
95
+ datasetR,
96
+ turbidity,
97
+ albedo,
98
+ solarElevation,
99
+ coeffs,
100
+ coeffs_offset
101
+ ) {
102
+ const tbi = clamp(Math.floor(turbidity), 1, 9);
103
+
104
+ const tbf = turbidity - tbi;
105
+
106
+ const s = Math.cbrt(solarElevation / HALF_PI);
107
+
108
+ FindQuinticWeights(s, quinticWeights);
109
+
110
+ const size_set9_1 = 6 * 9;
111
+ const size_set9_0 = 10 * size_set9_1;
112
+ EvalQuintic_3(quinticWeights, dataset9, (tbi - 1) * size_set9_1, ic, 0);
113
+ EvalQuintic_3(quinticWeights, dataset9, size_set9_0 + (tbi - 1) * size_set9_1, ic, 9);
114
+ EvalQuintic_3(quinticWeights, dataset9, tbi * size_set9_1, ic, 18);
115
+ EvalQuintic_3(quinticWeights, dataset9, size_set9_0 + tbi * size_set9_1, ic, 27);
116
+
117
+ const size_set_R_1 = 6;
118
+ const size_set_R_0 = 10 * size_set_R_1;
119
+ const ir_0 = EvalQuintic_2(quinticWeights, datasetR, (tbi - 1) * size_set_R_1);
120
+ const ir_1 = EvalQuintic_2(quinticWeights, datasetR, size_set_R_0 + (tbi - 1) * size_set_R_1);
121
+ const ir_2 = EvalQuintic_2(quinticWeights, datasetR, tbi * size_set_R_1);
122
+ const ir_3 = EvalQuintic_2(quinticWeights, datasetR, size_set_R_0 + tbi * size_set_R_1);
123
+
124
+ const cw_0 = (1.0 - albedo) * (1.0 - tbf);
125
+ const cw_1 = albedo * (1.0 - tbf);
126
+ const cw_2 = (1.0 - albedo) * tbf;
127
+ const cw_3 = albedo * tbf;
128
+
129
+ for (let i = 0; i < 9; i++) {
130
+ coeffs[coeffs_offset + i] = cw_0 * ic[i]
131
+ + cw_1 * ic[9 + i]
132
+ + cw_2 * ic[2 * 9 + i]
133
+ + cw_3 * ic[3 * 9 + i];
134
+ }
135
+
136
+ return cw_0 * ir_0 + cw_1 * ir_1 + cw_2 * ir_2 + cw_3 * ir_3;
137
+ }
138
+
139
+ /**
140
+ *
141
+ * Hosek:
142
+ * (1 + A e ^ (B / cos(t))) (1 + C e ^ (D g) + E cos(g) ^ 2 + F mieM(g, G) + H cos(t)^1/2 + (I - 1))
143
+ *
144
+ * These bits are the same as Preetham, but do different jobs in some cases
145
+ * A: sky gradient, carries white -> blue gradient
146
+ * B: sky tightness
147
+ * C: sun, carries most of sun-centred blue term
148
+ * D: sun tightness, higher = tighter
149
+ * E: rosy hue around sun
150
+ *
151
+ * Hosek-specific:
152
+ * F: mie term, does most of the heavy lifting for sunset glow
153
+ * G: mie tuning
154
+ * H: zenith gradient
155
+ * I: constant term balanced with H
156
+ *
157
+ * Notes:
158
+ * A/B still carries some of the "blue" base of sky, but much comes from C/D
159
+ * C/E minimal effect in sunset situations, carry bulk of sun halo in sun-overhead
160
+ * F/G sunset glow, but also takes sun halo from yellowish to white overhead
161
+ * @param {number[]|Float32Array} coeffs
162
+ * @param {number} coeffs_offset
163
+ * @param {number} cosTheta
164
+ * @param {number} gamma
165
+ * @param {number} cosGamma
166
+ * @return {number}
167
+ * @constructor
168
+ */
169
+ function EvalHosekCoeffs(
170
+ coeffs,
171
+ coeffs_offset,
172
+ cosTheta,
173
+ gamma,
174
+ cosGamma
175
+ ) {
176
+ const c_0 = coeffs[coeffs_offset + 0];
177
+ const c_1 = coeffs[coeffs_offset + 1];
178
+ const c_2 = coeffs[coeffs_offset + 2];
179
+ const c_3 = coeffs[coeffs_offset + 3];
180
+ const c_4 = coeffs[coeffs_offset + 4];
181
+ const c_5 = coeffs[coeffs_offset + 5];
182
+ const c_6 = coeffs[coeffs_offset + 6];
183
+ const c_7 = coeffs[coeffs_offset + 7];
184
+ const c_8 = coeffs[coeffs_offset + 8];
185
+
186
+ // Current coeffs ordering is AB I CDEF HG
187
+ // 01 2 3456 78
188
+ const expM = Math.exp(c_4 * gamma); // D g
189
+ const rayM = cosGamma * cosGamma; // Rayleigh scattering
190
+ const mieM = (1.0 + rayM) / Math.pow((1.0 + c_8 * c_8 - 2.0 * c_8 * cosGamma), 1.5); // G
191
+ const zenith = Math.sqrt(cosTheta); // vertical zenith gradient
192
+
193
+ return (
194
+ 1.0 + c_0 * Math.exp(c_1 / (cosTheta + 0.01)) // A, B
195
+ )
196
+ * (1.0
197
+ + c_3 * expM // C
198
+ + c_5 * rayM // E
199
+ + c_6 * mieM // F
200
+ + c_7 * zenith // H
201
+ + (c_2 - 1.0) // I
202
+ );
203
+ }
204
+
205
+ /**
206
+ *
207
+ * @param {number} thetaS
208
+ * @param {number} T
209
+ * @return {number}
210
+ */
211
+ function ZenithLuminance(thetaS, T) {
212
+ const chi = (4.0 / 9.0 - T / 120.0) * (Math.PI - 2.0 * thetaS);
213
+ let Lz = (4.0453 * T - 4.9710) * Math.tan(chi) - 0.2155 * T + 2.4192;
214
+ Lz *= 1000.0; // conversion from kcd/m^2 to cd/m^2
215
+ return Lz;
216
+ }
217
+
218
+ /**
219
+ * @see https://github.com/andrewwillmott/sun-sky/blob/master/SunSky.cpp
220
+ * @param {number[]} mCoeffsXYZ output, float[3][9], Hosek 9-term distribution coefficients
221
+ * @param {number[]} mRadXYZ output, vec3, Overall average radiance
222
+ * @param {number[]|Float32Array} sun_direction direction to the sun, engine frame (+Y up)
223
+ * @param {number} turbidity should be between 1 and 10
224
+ * @param {number[]|Float32Array} rgbAlbedo albedo in linear color space (make sure it's not sRGB)
225
+ * @param {number} overcast
226
+ */
227
+ export function sky_hosek_precompute(
228
+ mCoeffsXYZ,
229
+ mRadXYZ,
230
+ sun_direction,
231
+ turbidity,
232
+ rgbAlbedo,
233
+ overcast
234
+ ) {
235
+
236
+ const mToSun = sun_direction;
237
+
238
+ const sun_x = mToSun[0];
239
+ const sun_y = mToSun[1];
240
+ const sun_z = mToSun[2];
241
+
242
+ // Y is up (engine convention): solar elevation is measured from the Y component.
243
+ const solarElevation = sun_y > 0 ? Math.asin(sun_y) : 0;
244
+
245
+ const albedo = [];
246
+ rgb_to_xyz(albedo, rgbAlbedo);
247
+
248
+
249
+ // Note that the hosek coefficients change with time of day, vs. Preetham where the 'upper' coefficients stay the same,
250
+ // and only the scaler mPerezInvDen, consisting of time-dependent normalisation and zenith luminnce factors, changes.
251
+ mRadXYZ[0] = FindHosekCoeffs(kHosekCoeffsX, kHosekRadX, turbidity, albedo[0], solarElevation, mCoeffsXYZ, 0);
252
+ mRadXYZ[1] = FindHosekCoeffs(kHosekCoeffsY, kHosekRadY, turbidity, albedo[1], solarElevation, mCoeffsXYZ, 9);
253
+ mRadXYZ[2] = FindHosekCoeffs(kHosekCoeffsZ, kHosekRadZ, turbidity, albedo[2], solarElevation, mCoeffsXYZ, 18);
254
+
255
+ v3_array_scale(mRadXYZ, 0, mRadXYZ, 0, 683); // convert to luminance in lumens
256
+
257
+
258
+ if (sun_y < 0.0) // sun below horizon?
259
+ {
260
+ const s = clamp01(1.0 + sun_y * 50.0); // goes from 1 to 0 as the sun sets
261
+ const is = 1.0 - s;
262
+
263
+ // Emulate Preetham's zenith darkening
264
+ const darken = ZenithLuminance(Math.acos(sun_y), turbidity) / ZenithLuminance(HALF_PI, turbidity);
265
+
266
+ // Take C/E/F which control sun term to zero
267
+ for (let j = 0; j < 3; j++) {
268
+ mCoeffsXYZ[j * 9 + 3] *= s;
269
+ mCoeffsXYZ[j * 9 + 5] *= s;
270
+ mCoeffsXYZ[j * 9 + 6] *= s;
271
+
272
+ // Take horizon term H to zero, as it's an orange glow at this point
273
+ mCoeffsXYZ[j * 9 + 7] *= s;
274
+
275
+ // Take I term back to 1
276
+ mCoeffsXYZ[j * 9 + 2] *= s;
277
+ mCoeffsXYZ[j * 9 + 2] += is;
278
+ }
279
+
280
+ v3_array_scale(mRadXYZ, 0, mRadXYZ, 0, darken);
281
+ }
282
+
283
+ if (overcast !== 0.0) // Handle overcast term
284
+ {
285
+ const is = overcast;
286
+ const s = 1.0 - overcast; // goes to 0 as we go to overcast
287
+
288
+ // Hosek isn't self-normalising, unlike Preetham/CIE, which divides by PreethamLower().
289
+ // Thus when we lerp to the CIE overcast model, we get some non-linearities.
290
+ // We deal with this by using ratios of normalisation terms to balance.
291
+ // Another difference is that Hosek is relative to the average radiance,
292
+ // whereas CIE is the zenith radiance, so rather than taking the zenith
293
+ // as normalising as in CIE, we average over the zenith and two horizon
294
+ // points.
295
+ const cosGammaZ = sun_y;
296
+ const gammaZ = Math.acos(cosGammaZ);
297
+ const cosGammaH = sun_z;
298
+ const gammaHP = Math.acos(sun_z);
299
+ const gammaHN = Math.PI - gammaHP;
300
+
301
+ const sc0 = EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 1.0, gammaZ, cosGammaZ) * 2.0
302
+ + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHP, +cosGammaH)
303
+ + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHN, -cosGammaH);
304
+
305
+ for (let j = 0; j < 3; j++) {
306
+ // sun flare -> 0 strength/base chroma
307
+ // Take C/E/F which control sun term to zero
308
+ mCoeffsXYZ[j * 9 + 3] *= s;
309
+ mCoeffsXYZ[j * 9 + 5] *= s;
310
+ mCoeffsXYZ[j * 9 + 6] *= s;
311
+
312
+ // Take H back to 0
313
+ mCoeffsXYZ[j * 9 + 7] *= s;
314
+
315
+ // Take I term back to 1
316
+ mCoeffsXYZ[j * 9 + 2] *= s;
317
+ mCoeffsXYZ[j * 9 + 2] += is;
318
+
319
+ // Take A/B to CIE cloudy sky model: 4, -0.7
320
+ mCoeffsXYZ[j * 9 + 0] = lerp(mCoeffsXYZ[j * 9 + 0], 4.0, is);
321
+ mCoeffsXYZ[j * 9 + 1] = lerp(mCoeffsXYZ[j * 9 + 1], -0.7, is);
322
+ }
323
+
324
+ const sc1 = EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 1.0, gammaZ, cosGammaZ) * 2.0
325
+ + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHP, +cosGammaH)
326
+ + EvalHosekCoeffs(mCoeffsXYZ, 1 * 9, 0.0, gammaHN, -cosGammaH);
327
+
328
+ const rescale = sc0 / sc1;
329
+ v3_array_scale(mRadXYZ, 0, mRadXYZ, 0, rescale);
330
+
331
+
332
+ // move back to white point
333
+ mRadXYZ[0] = lerp(mRadXYZ[0], mRadXYZ[1], is);
334
+ mRadXYZ[2] = lerp(mRadXYZ[2], mRadXYZ[1], is);
335
+ }
336
+
337
+ }
338
+
339
+
340
+ /**
341
+ *
342
+ * @param {number[]|Float32Array} out vec3 in RGB color space
343
+ * @param {number[]|Float32Array} mCoeffsXYZ float[3][9]
344
+ * @param {number[]|Float32Array} mRadXYZ vec3
345
+ * @param {number[]|Float32Array} mToSun vec3, direction to the sun, engine frame (+Y up)
346
+ * @param {number} direction_x
347
+ * @param {number} direction_y up axis (engine +Y up)
348
+ * @param {number} direction_z
349
+ */
350
+ export function sky_hosek_compute_irradiance_by_direction(
351
+ out,
352
+ mCoeffsXYZ,
353
+ mRadXYZ,
354
+ mToSun,
355
+ direction_x,
356
+ direction_y,
357
+ direction_z,
358
+ ) {
359
+ // Normalise the view direction so cosTheta stays a true cosine even when the
360
+ // caller passes a non-unit direction.
361
+ const direction_length = v3_length(direction_x, direction_y, direction_z);
362
+ const inv_direction_length = direction_length !== 0.0 ? 1.0 / direction_length : 0.0;
363
+
364
+ let cosTheta = direction_y * inv_direction_length;
365
+
366
+ // v3_angle_cos_between divides by both magnitudes and clamps to [-1, 1], so it
367
+ // is robust to non-unit inputs and gamma = acos(cosGamma) can never be NaN.
368
+ const cosGamma = v3_angle_cos_between(
369
+ mToSun[0], mToSun[1], mToSun[2],
370
+ direction_x, direction_y, direction_z
371
+ );
372
+ const gamma = Math.acos(cosGamma);
373
+
374
+ if (cosTheta < 0.0) {
375
+ // clamp angle
376
+ cosTheta = 0.0;
377
+ }
378
+
379
+ const x = EvalHosekCoeffs(mCoeffsXYZ, 0, cosTheta, gamma, cosGamma);
380
+ const y = EvalHosekCoeffs(mCoeffsXYZ, 9, cosTheta, gamma, cosGamma);
381
+ const z = EvalHosekCoeffs(mCoeffsXYZ, 18, cosTheta, gamma, cosGamma);
382
+
383
+ out[0] = x * mRadXYZ[0];
384
+ out[1] = y * mRadXYZ[1];
385
+ out[2] = z * mRadXYZ[2];
386
+
387
+ xyz_to_rgb(out, out);
388
+ }