genshin-engine 3.8.1 → 4.0.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/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { Box3, BufferAttribute, BufferGeometry, CanvasTexture, Color, CylinderGeometry, Data3DTexture, DataTexture, DirectionalLight, DoubleSide, ExtrudeGeometry, HalfFloatType, InstancedBufferGeometry, LatheGeometry, LinearFilter, MathUtils, NeutralToneMapping, NoColorSpace, OrthographicCamera, Quaternion, RedFormat, ShapePath, Vector2, Vector3 } from "three";
2
- import { Fn, If, attribute, cameraFar, cameraNear, cameraPosition, cameraViewMatrix, cos, exp, exp2, float, getViewPosition, hash, instanceIndex, max, mix, modelViewMatrix, modelWorldMatrix, mrt, mx_cell_noise_float, mx_fractal_noise_float, mx_noise_float, normalView, normalWorldGeometry, output, perspectiveDepthToViewZ, positionGeometry, positionLocal, positionView, positionViewDirection, positionWorld, pow, reflect, renderOutput, screenUV, sin, smoothstep, step, texture, texture3D, time, uniform, uv, varying, vec2, vec3, vec4, velocity, viewportDepthTexture, viewportSharedTexture } from "three/tsl";
3
- import { MeshBasicNodeMaterial, MeshToonNodeMaterial, RenderPipeline, RenderTarget, SpriteNodeMaterial, ToonOutlinePassNode, WebGPURenderer } from "three/webgpu";
1
+ import { Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, ClampToEdgeWrapping, Color, CylinderGeometry, Data3DTexture, DataTexture, DirectionalLight, DoubleSide, FloatType, HalfFloatType, InstancedBufferAttribute, InstancedBufferGeometry, LatheGeometry, LinearFilter, MathUtils, Matrix4, NeutralToneMapping, NoColorSpace, OrthographicCamera, Quaternion, RGBAFormat, RedFormat, Sprite, UnsignedByteType, Vector2, Vector3 } from "three";
2
+ import { Fn, If, abs, asin, attribute, cameraFar, cameraNear, cameraPosition, cameraViewMatrix, clamp, color, cos, diffuseColor, exp, exp2, float, getNormalFromDepth, getViewPosition, hash, instanceIndex, instancedBufferAttribute, luminance, max, min, mix, modelViewMatrix, modelWorldMatrix, mrt, mx_cell_noise_float, mx_fractal_noise_float, mx_noise_float, normalView, normalWorld, normalWorldGeometry, output, perspectiveDepthToViewZ, positionGeometry, positionLocal, positionView, positionViewDirection, positionWorld, pow, reflect, renderOutput, sample, saturate, screenUV, sin, smoothstep, step, texture, texture3D, time, uniform, uniformArray, uv, varying, vec2, vec3, vec4, velocity, viewportDepthTexture, viewportSharedTexture } from "three/tsl";
3
+ import { LightingModel, MeshBasicNodeMaterial, MeshToonNodeMaterial, NodeMaterial, RenderPipeline, RenderTarget, SpriteNodeMaterial, ToonOutlinePassNode, WebGPURenderer } from "three/webgpu";
4
4
  import { CSMShadowNode } from "three/examples/jsm/csm/CSMShadowNode.js";
5
5
  import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
6
6
  import { voronoi2d } from "three/examples/jsm/tsl/math/voronoiNoise.js";
7
+ import GTAONode from "three/examples/jsm/tsl/display/GTAONode.js";
7
8
  import { bilateralBlur } from "three/examples/jsm/tsl/display/BilateralBlurNode.js";
8
9
  import { bloom } from "three/examples/jsm/tsl/display/BloomNode.js";
9
10
  import { depthAwareBlend } from "three/examples/jsm/tsl/display/depthAwareBlend.js";
@@ -11,52 +12,113 @@ import { godrays } from "three/examples/jsm/tsl/display/GodraysNode.js";
11
12
  import { lut3D } from "three/examples/jsm/tsl/display/Lut3DNode.js";
12
13
  import { smaa } from "three/examples/jsm/tsl/display/SMAANode.js";
13
14
  import { traa } from "three/examples/jsm/tsl/display/TRAANode.js";
14
- //#region src/post/toSceneColor.ts
15
+ //#region src/atmosphere/constants.ts
16
+ const DEFAULT_SKY_SHAPE = {
17
+ frontBackBlend: 1,
18
+ haloHeight: 1,
19
+ horizonBand: .45,
20
+ moonSize: 1,
21
+ sunHaloSize: 1
22
+ };
23
+ const DEFAULT_SAMPLE_COUNT = 33;
24
+ const DEFAULT_SKY_GRADIENT = {
25
+ green: Array.from({ length: DEFAULT_SAMPLE_COUNT }, () => 0),
26
+ red: Array.from({ length: DEFAULT_SAMPLE_COUNT }, (_value, index) => 1 - MathUtils.smoothstep(index, 0, 32))
27
+ };
28
+ //#endregion
29
+ //#region src/post/toneMapNeutral.ts
15
30
  const TOE_END = .08;
16
31
  const TOE_OFFSET = .04;
17
32
  const COMPRESSION_START = .76;
18
33
  const DESATURATION = .15;
19
- const INVERSE_STEP_COUNT = 16;
20
- const SLOPE_NUDGE = 1e-4;
21
- const SCENE_CHANNEL_LIMIT = 1.5;
22
- const toneMap = ([red, green, blue]) => {
34
+ const compressChannel = (channel, peak, newPeak, whiteShare) => channel * newPeak / peak + (newPeak - channel * newPeak / peak) * whiteShare;
35
+ const toneMapNeutral = ([red, green, blue], toneMapped = [
36
+ 0,
37
+ 0,
38
+ 0
39
+ ]) => {
23
40
  const lowest = Math.min(red, green, blue);
24
41
  const offset = lowest < TOE_END ? lowest - 6.25 * lowest * lowest : TOE_OFFSET;
25
- const offsetColor = [
26
- red - offset,
27
- green - offset,
28
- blue - offset
29
- ];
30
- const peak = Math.max(...offsetColor);
31
- if (peak < COMPRESSION_START) return offsetColor;
42
+ const offsetRed = red - offset;
43
+ const offsetGreen = green - offset;
44
+ const offsetBlue = blue - offset;
45
+ const peak = Math.max(offsetRed, offsetGreen, offsetBlue);
46
+ if (peak < COMPRESSION_START) {
47
+ toneMapped[0] = offsetRed;
48
+ toneMapped[1] = offsetGreen;
49
+ toneMapped[2] = offsetBlue;
50
+ return toneMapped;
51
+ }
32
52
  const headroom = .24;
33
53
  const newPeak = 1 - headroom * headroom / (peak + headroom - COMPRESSION_START);
34
54
  const whiteShare = 1 - 1 / (DESATURATION * (peak - newPeak) + 1);
35
- return offsetColor.map((channel) => channel * newPeak / peak + (newPeak - channel * newPeak / peak) * whiteShare);
55
+ toneMapped[0] = compressChannel(offsetRed, peak, newPeak, whiteShare);
56
+ toneMapped[1] = compressChannel(offsetGreen, peak, newPeak, whiteShare);
57
+ toneMapped[2] = compressChannel(offsetBlue, peak, newPeak, whiteShare);
58
+ return toneMapped;
36
59
  };
37
- const toSceneColor = (displayColor) => {
38
- const target = [
39
- displayColor.r,
40
- displayColor.g,
41
- displayColor.b
42
- ];
43
- let input = [...target];
60
+ //#endregion
61
+ //#region src/post/toSceneColor.ts
62
+ const INVERSE_STEP_COUNT = 16;
63
+ const SLOPE_NUDGE = 1e-4;
64
+ const SCENE_CHANNEL_LIMIT = 1.5;
65
+ const target = [
66
+ 0,
67
+ 0,
68
+ 0
69
+ ];
70
+ const input = [
71
+ 0,
72
+ 0,
73
+ 0
74
+ ];
75
+ const output$1 = [
76
+ 0,
77
+ 0,
78
+ 0
79
+ ];
80
+ const nudged = [
81
+ 0,
82
+ 0,
83
+ 0
84
+ ];
85
+ const nudgedOutput = [
86
+ 0,
87
+ 0,
88
+ 0
89
+ ];
90
+ const stepped = [
91
+ 0,
92
+ 0,
93
+ 0
94
+ ];
95
+ const toSceneColor = (displayColor, sceneColor = new Color()) => {
96
+ target[0] = input[0] = displayColor.r;
97
+ target[1] = input[1] = displayColor.g;
98
+ target[2] = input[2] = displayColor.b;
44
99
  for (let step = 0; step < INVERSE_STEP_COUNT; step++) {
45
- const current = input;
46
- const output = toneMap(current);
47
- input = current.map((channel, index) => {
48
- const nudged = [...current];
100
+ toneMapNeutral(input, output$1);
101
+ for (let index = 0; index < 3; index++) {
102
+ nudged[0] = input[0];
103
+ nudged[1] = input[1];
104
+ nudged[2] = input[2];
105
+ const channel = input[index] ?? 0;
49
106
  nudged[index] = channel + SLOPE_NUDGE;
50
- const slope = ((toneMap(nudged)[index] ?? 0) - (output[index] ?? 0)) / SLOPE_NUDGE;
51
- return Math.min(Math.max(channel + ((target[index] ?? 0) - (output[index] ?? 0)) / slope, 0), SCENE_CHANNEL_LIMIT);
52
- });
107
+ toneMapNeutral(nudged, nudgedOutput);
108
+ const slope = ((nudgedOutput[index] ?? 0) - (output$1[index] ?? 0)) / SLOPE_NUDGE;
109
+ stepped[index] = Math.min(Math.max(channel + ((target[index] ?? 0) - (output$1[index] ?? 0)) / slope, 0), SCENE_CHANNEL_LIMIT);
110
+ }
111
+ input[0] = stepped[0];
112
+ input[1] = stepped[1];
113
+ input[2] = stepped[2];
53
114
  }
54
- return new Color(...input);
115
+ return sceneColor.setRGB(input[0], input[1], input[2]);
55
116
  };
56
117
  //#endregion
57
118
  //#region src/atmosphere/applySkyState.ts
58
119
  const GODRAYS_REDRAW_COSINE = Math.cos(.6 * Math.PI / 180);
59
120
  const godraysDirection = new Vector3();
121
+ const BLACK = new Color(0, 0, 0);
60
122
  const applySkyState = (skyState, { fogUniforms, godraysLight, hemisphere, light, lightDistance, lightUniforms, postUniforms, skyUniforms }) => {
61
123
  const { horizonColor, lightColor, lightDirection } = skyState;
62
124
  light.position.copy(light.target.position).addScaledVector(lightDirection, lightDistance);
@@ -68,18 +130,32 @@ const applySkyState = (skyState, { fogUniforms, godraysLight, hemisphere, light,
68
130
  lightUniforms.lightColor.value.copy(lightColor);
69
131
  lightUniforms.sunDirection.value.copy(lightDirection);
70
132
  lightUniforms.rimColor.value.copy(horizonColor);
71
- fogUniforms.color.value.copy(toSceneColor(skyState.fogColor ?? horizonColor));
72
- fogUniforms.scatterColor.value.copy(toSceneColor(lightColor));
133
+ toSceneColor(skyState.fogColor ?? horizonColor, fogUniforms.color.value);
134
+ toSceneColor(lightColor, fogUniforms.scatterColor.value);
73
135
  fogUniforms.scatterDirection.value.copy(lightDirection);
74
136
  postUniforms.godraysColor.value.copy(lightColor);
75
- skyUniforms.cloudLitColor.value.copy(toSceneColor(skyState.cloudLitColor));
76
- skyUniforms.cloudShadeColor.value.copy(toSceneColor(skyState.cloudShadeColor));
77
- skyUniforms.horizonColor.value.copy(toSceneColor(horizonColor));
137
+ toSceneColor(skyState.cloudLitColor, skyUniforms.cloudLitColor.value);
138
+ toSceneColor(skyState.cloudShadeColor, skyUniforms.cloudShadeColor.value);
139
+ toSceneColor(skyState.cloudLitBackColor ?? skyState.cloudLitColor, skyUniforms.cloudLitBackColor.value);
140
+ toSceneColor(skyState.cloudShadeBackColor ?? skyState.cloudShadeColor, skyUniforms.cloudShadeBackColor.value);
141
+ toSceneColor(horizonColor, skyUniforms.horizonColor.value);
142
+ toSceneColor(skyState.horizonBackColor ?? horizonColor, skyUniforms.horizonBackColor.value);
143
+ toSceneColor(skyState.zenithBackColor ?? skyState.zenithColor, skyUniforms.zenithBackColor.value);
144
+ toSceneColor(skyState.haloColor ?? BLACK, skyUniforms.haloColor.value);
145
+ toSceneColor(skyState.sunHaloColor ?? BLACK, skyUniforms.sunHaloColor.value);
78
146
  skyUniforms.lightColor.value.copy(lightColor);
79
147
  skyUniforms.moonDirection.value.copy(skyState.moonDirection);
148
+ toSceneColor(skyState.moonGlowColor ?? BLACK, skyUniforms.moonGlowColor.value);
80
149
  skyUniforms.starIntensity.value = skyState.starIntensity;
81
150
  skyUniforms.sunDirection.value.copy(skyState.sunDirection);
82
- skyUniforms.zenithColor.value.copy(toSceneColor(skyState.zenithColor));
151
+ toSceneColor(skyState.zenithColor, skyUniforms.zenithColor.value);
152
+ const { frontBackBlend, haloHeight, horizonBand, moonSize, sunHaloSize } = skyState.shape ?? DEFAULT_SKY_SHAPE;
153
+ skyUniforms.frontBackBlend.value = frontBackBlend;
154
+ skyUniforms.haloHeight.value = haloHeight;
155
+ skyUniforms.horizonBand.value = horizonBand;
156
+ skyUniforms.moonSize.value = moonSize;
157
+ skyUniforms.sunHaloSize.value = sunHaloSize;
158
+ if (!godraysLight) return;
83
159
  godraysDirection.subVectors(godraysLight.position, godraysLight.target.position).normalize();
84
160
  if (godraysDirection.dot(lightDirection) >= GODRAYS_REDRAW_COSINE) return;
85
161
  godraysLight.position.copy(godraysLight.target.position).addScaledVector(lightDirection, lightDistance);
@@ -97,31 +173,44 @@ const computeSunDirection = (minutes, tilt, sunDirection) => {
97
173
  return sunDirection.set(Math.sin(hourAngle), height * Math.cos(tilt), height * Math.sin(tilt));
98
174
  };
99
175
  //#endregion
100
- //#region src/atmosphere/createCloudAtlasTexture.ts
176
+ //#region src/atmosphere/getCloudAtlasColumns.ts
101
177
  const getCloudAtlasColumns = (spriteCount) => Math.ceil(Math.sqrt(spriteCount));
178
+ //#endregion
179
+ //#region src/materials/addSmoothLoop.ts
180
+ const addSmoothLoop = (path, points) => {
181
+ if (points.length < 3) return;
182
+ const getMidpoint = (index) => {
183
+ const [startX = 0, startY = 0] = points[index % points.length] ?? [];
184
+ const [endX = 0, endY = 0] = points[(index + 1) % points.length] ?? [];
185
+ return [(startX + endX) / 2, (startY + endY) / 2];
186
+ };
187
+ path.moveTo(...getMidpoint(points.length - 1));
188
+ for (const [index, [x, y]] of points.entries()) path.quadraticCurveTo(x, y, ...getMidpoint(index));
189
+ path.closePath();
190
+ };
191
+ //#endregion
192
+ //#region src/atmosphere/createCloudAtlasTexture.ts
193
+ const EDGE_BLUR_SHARE = .02;
194
+ const CROWN_BLUR_SHARE = .035;
102
195
  const createCloudAtlasTexture = (sprites, cellSize) => {
103
196
  const columns = getCloudAtlasColumns(sprites.length);
104
197
  const canvas = new OffscreenCanvas(columns * cellSize, columns * cellSize);
105
198
  const context = canvas.getContext("2d");
106
199
  if (context) {
107
200
  context.globalCompositeOperation = "lighter";
108
- const fill = (loops, column, row, style) => {
201
+ const fill = (loops, column, row, style, blurShare) => {
202
+ context.filter = `blur(${cellSize * blurShare}px)`;
109
203
  const path = new Path2D();
110
- for (const [first, ...rest] of loops) {
111
- if (!first) continue;
112
- const toCanvas = ([x, y]) => [(column + x) * cellSize, (row + 1 - y) * cellSize];
113
- path.moveTo(...toCanvas(first));
114
- for (const point of rest) path.lineTo(...toCanvas(point));
115
- path.closePath();
116
- }
204
+ const toCanvas = ([x, y]) => [(column + x) * cellSize, (row + 1 - y) * cellSize];
205
+ for (const loop of loops) addSmoothLoop(path, loop.map((point) => toCanvas(point)));
117
206
  context.fillStyle = style;
118
207
  context.fill(path, "evenodd");
119
208
  };
120
209
  for (const [index, { lit, outline }] of sprites.entries()) {
121
210
  const column = index % columns;
122
211
  const row = Math.floor(index / columns);
123
- fill(outline, column, row, "#f00");
124
- fill(lit, column, row, "#0f0");
212
+ fill(outline, column, row, "#f00", EDGE_BLUR_SHARE);
213
+ fill(lit, column, row, "#0f0", CROWN_BLUR_SHARE);
125
214
  }
126
215
  }
127
216
  const texture = new CanvasTexture(canvas);
@@ -132,21 +221,124 @@ const createCloudAtlasTexture = (sprites, cellSize) => {
132
221
  return texture;
133
222
  };
134
223
  //#endregion
135
- //#region src/atmosphere/createCloudSpriteMaterial.ts
136
- const createCloudSpriteMaterial = (atlas, { index, spriteCount }, { cloudLitColor, cloudShadeColor }) => {
224
+ //#region src/atmosphere/orderByViewDepth.ts
225
+ const orderByViewDepth = (places, { elements }, depths, order) => {
226
+ for (let index = 0; index < places.length; index++) {
227
+ const place = places[index];
228
+ depths[index] = elements[2] * (place?.[0] ?? 0) + elements[6] * (place?.[1] ?? 0) + elements[10] * (place?.[2] ?? 0) + elements[14];
229
+ }
230
+ let isReordered = false;
231
+ for (let index = 1; index < order.length; index++) {
232
+ const place = order[index] ?? 0;
233
+ const depth = depths[place] ?? 0;
234
+ let slot = index;
235
+ while (slot > 0) {
236
+ const nearer = order[slot - 1] ?? 0;
237
+ const nearerDepth = depths[nearer] ?? 0;
238
+ if (nearerDepth < depth || nearerDepth === depth && nearer < place) break;
239
+ order[slot] = nearer;
240
+ slot--;
241
+ }
242
+ if (slot === index) continue;
243
+ order[slot] = place;
244
+ isReordered = true;
245
+ }
246
+ return isReordered;
247
+ };
248
+ //#endregion
249
+ //#region src/atmosphere/createCloudBandSprite.ts
250
+ const BELOW_FADE_START = .1;
251
+ const BELOW_FADE_SCALE = 5;
252
+ const COVERAGE_LIGHT_SHARE = .4;
253
+ const EDGE_FADE_START = 0;
254
+ const EDGE_FADE_END = 1;
255
+ const DROPPED_COVERAGE = .01;
256
+ const MIN_CROWN_COVER = .05;
257
+ const createCloudBandSprite = (atlas, clouds, { aspect, spriteCount }, skyUniforms) => {
258
+ const { cloudCoverage, cloudFrontBackBlend, cloudLitBackColor, cloudLitColor, cloudShadeBackColor, cloudShadeColor, cloudSunBrighten, sunDirection } = skyUniforms;
137
259
  const columns = getCloudAtlasColumns(spriteCount);
138
- const column = index % columns;
139
- const row = Math.floor(index / columns);
140
- const cellUv = uv().add(vec2(column, columns - 1 - row)).div(columns);
141
- const mask = texture(atlas, cellUv);
260
+ const places = clouds.map(({ position }) => [...position]);
261
+ const cloudScales = clouds.map(({ width }) => [width, width / aspect]);
262
+ const cloudCells = clouds.map(({ spriteIndex }) => [spriteIndex % columns, columns - 1 - Math.floor(spriteIndex / columns)]);
263
+ const positions = new InstancedBufferAttribute(new Float32Array(clouds.length * 3), 3);
264
+ const scales = new InstancedBufferAttribute(new Float32Array(clouds.length * 2), 2);
265
+ const cells = new InstancedBufferAttribute(new Float32Array(clouds.length * 2), 2);
266
+ const ranks = new InstancedBufferAttribute(new Float32Array(clouds.length), 1);
267
+ const cover = uniform(1);
268
+ const mask = texture(atlas, uv().add(instancedBufferAttribute(cells)).div(columns));
269
+ const place = instancedBufferAttribute(positions);
270
+ const direction = place.sub(cameraPosition).normalize();
271
+ const elevation = asin(clamp(direction.y, -1, 1)).mul(2 / Math.PI);
272
+ const sunCosine = direction.dot(sunDirection);
273
+ const toward = pow(max(sunCosine.mul(cloudFrontBackBlend).add(float(1).sub(cloudFrontBackBlend)), 0), 3);
274
+ const lit = mix(cloudLitBackColor, cloudLitColor, toward);
275
+ const shade = mix(cloudShadeBackColor, cloudShadeColor, toward);
276
+ const crown = saturate(mask.g.div(max(mask.r, MIN_CROWN_COVER)));
277
+ const cloudColor = mix(shade, lit, crown).add(lit.mul(cloudCoverage.mul(COVERAGE_LIGHT_SHARE))).mul(float(1).add(cloudSunBrighten.mul(sunCosine.mul(.5).add(.5))));
142
278
  const material = new SpriteNodeMaterial({
143
279
  depthWrite: false,
144
280
  transparent: true
145
281
  });
146
- material.colorNode = mix(cloudShadeColor, cloudLitColor, mask.g);
147
- material.opacityNode = mask.r;
148
- material.alphaTest = .5;
149
- return material;
282
+ material.positionNode = place;
283
+ material.scaleNode = instancedBufferAttribute(scales);
284
+ material.colorNode = cloudColor;
285
+ material.opacityNode = smoothstep(EDGE_FADE_START, EDGE_FADE_END, mask.r).mul(smoothstep(0, 1, saturate(elevation.add(BELOW_FADE_START).mul(BELOW_FADE_SCALE)))).mul(step(instancedBufferAttribute(ranks), cover));
286
+ material.alphaTest = DROPPED_COVERAGE;
287
+ const sprite = new Sprite(material);
288
+ sprite.center.set(.5, 0);
289
+ sprite.count = clouds.length;
290
+ sprite.frustumCulled = false;
291
+ const modelViewMatrix = new Matrix4();
292
+ const depths = new Float64Array(clouds.length);
293
+ const order = Uint32Array.from(clouds, (_cloud, index) => index);
294
+ const layOut = () => {
295
+ for (let instance = 0; instance < order.length; instance++) {
296
+ const cloud = order[instance] ?? 0;
297
+ scales.set(cloudScales[cloud] ?? [], instance * 2);
298
+ cells.set(cloudCells[cloud] ?? [], instance * 2);
299
+ ranks.setX(instance, (cloud + .5) / clouds.length);
300
+ }
301
+ scales.needsUpdate = true;
302
+ cells.needsUpdate = true;
303
+ ranks.needsUpdate = true;
304
+ };
305
+ layOut();
306
+ sprite.onBeforeRender = (_renderer, _scene, camera) => {
307
+ modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, sprite.matrixWorld);
308
+ const isReordered = orderByViewDepth(places, modelViewMatrix, depths, order);
309
+ for (let instance = 0; instance < order.length; instance++) positions.set(places[order[instance] ?? 0] ?? [], instance * 3);
310
+ positions.needsUpdate = true;
311
+ if (isReordered) layOut();
312
+ };
313
+ return {
314
+ cover,
315
+ dispose: () => {
316
+ material.dispose();
317
+ },
318
+ places,
319
+ sprite
320
+ };
321
+ };
322
+ //#endregion
323
+ //#region src/constants.ts
324
+ const MAX_BYTE = 255;
325
+ //#endregion
326
+ //#region src/atmosphere/createSkyGradientTexture.ts
327
+ const createSkyGradientTexture = ({ green, red }) => {
328
+ const width = Math.max(red.length, green.length, 1);
329
+ const data = new Uint8Array(width * 4);
330
+ for (let index = 0; index < width; index++) {
331
+ data[index * 4] = Math.round((red[index] ?? 0) * 255);
332
+ data[index * 4 + 1] = Math.round((green[index] ?? 0) * 255);
333
+ data[index * 4 + 3] = 255;
334
+ }
335
+ const texture = new DataTexture(data, width, 1, RGBAFormat, UnsignedByteType);
336
+ texture.magFilter = LinearFilter;
337
+ texture.minFilter = LinearFilter;
338
+ texture.wrapS = ClampToEdgeWrapping;
339
+ texture.wrapT = ClampToEdgeWrapping;
340
+ texture.needsUpdate = true;
341
+ return texture;
150
342
  };
151
343
  //#endregion
152
344
  //#region src/atmosphere/createSkyState.ts
@@ -170,14 +362,27 @@ const createSkyState = () => ({
170
362
  const createSkyUniforms = () => ({
171
363
  cloudCoverage: uniform(0),
172
364
  cloudDrift: uniform(new Vector2()),
365
+ cloudFrontBackBlend: uniform(1),
366
+ cloudLitBackColor: uniform(new Color()),
173
367
  cloudLitColor: uniform(new Color()),
368
+ cloudShadeBackColor: uniform(new Color()),
174
369
  cloudShadeColor: uniform(new Color()),
175
- horizonBand: uniform(.45),
370
+ cloudSunBrighten: uniform(0),
371
+ frontBackBlend: uniform(DEFAULT_SKY_SHAPE.frontBackBlend),
372
+ haloColor: uniform(new Color(0, 0, 0)),
373
+ haloHeight: uniform(DEFAULT_SKY_SHAPE.haloHeight),
374
+ horizonBackColor: uniform(new Color()),
375
+ horizonBand: uniform(DEFAULT_SKY_SHAPE.horizonBand),
176
376
  horizonColor: uniform(new Color()),
177
377
  lightColor: uniform(new Color()),
178
378
  moonDirection: uniform(new Vector3(0, -1, 0)),
379
+ moonGlowColor: uniform(new Color(0, 0, 0)),
380
+ moonSize: uniform(DEFAULT_SKY_SHAPE.moonSize),
179
381
  starIntensity: uniform(0),
180
382
  sunDirection: uniform(new Vector3(0, 1, 0)),
383
+ sunHaloColor: uniform(new Color(0, 0, 0)),
384
+ sunHaloSize: uniform(DEFAULT_SKY_SHAPE.sunHaloSize),
385
+ zenithBackColor: uniform(new Color()),
181
386
  zenithColor: uniform(new Color())
182
387
  });
183
388
  //#endregion
@@ -237,7 +442,6 @@ const placeCloudBand = ({ count, distanceRange: [near, far], heightRange: [low,
237
442
  };
238
443
  //#endregion
239
444
  //#region src/atmosphere/sampleSkyState.ts
240
- const lerp = (from, to, amount) => from + (to - from) * amount;
241
445
  const sampleSkyState = (skyKeyframes, minutes, tilt, skyState) => {
242
446
  let nextIndex = 0;
243
447
  while (nextIndex < skyKeyframes.length && (skyKeyframes[nextIndex]?.minutes ?? 0) <= minutes) nextIndex++;
@@ -250,12 +454,12 @@ const sampleSkyState = (skyKeyframes, minutes, tilt, skyState) => {
250
454
  skyState.cloudLitColor.lerpColors(previous.cloudLitColor, next.cloudLitColor, amount);
251
455
  skyState.cloudShadeColor.lerpColors(previous.cloudShadeColor, next.cloudShadeColor, amount);
252
456
  skyState.hemisphereGroundColor.lerpColors(previous.hemisphereGroundColor, next.hemisphereGroundColor, amount);
253
- skyState.hemisphereIntensity = lerp(previous.hemisphereIntensity, next.hemisphereIntensity, amount);
457
+ skyState.hemisphereIntensity = MathUtils.lerp(previous.hemisphereIntensity, next.hemisphereIntensity, amount);
254
458
  skyState.hemisphereSkyColor.lerpColors(previous.hemisphereSkyColor, next.hemisphereSkyColor, amount);
255
459
  skyState.horizonColor.lerpColors(previous.horizonColor, next.horizonColor, amount);
256
460
  skyState.lightColor.lerpColors(previous.lightColor, next.lightColor, amount);
257
- skyState.lightIntensity = lerp(previous.lightIntensity, next.lightIntensity, amount);
258
- skyState.starIntensity = lerp(previous.starIntensity, next.starIntensity, amount);
461
+ skyState.lightIntensity = MathUtils.lerp(previous.lightIntensity, next.lightIntensity, amount);
462
+ skyState.starIntensity = MathUtils.lerp(previous.starIntensity, next.starIntensity, amount);
259
463
  skyState.zenithColor.lerpColors(previous.zenithColor, next.zenithColor, amount);
260
464
  computeSunDirection(minutes, tilt, skyState.sunDirection);
261
465
  skyState.moonDirection.copy(skyState.sunDirection).negate();
@@ -263,11 +467,309 @@ const sampleSkyState = (skyKeyframes, minutes, tilt, skyState) => {
263
467
  return skyState;
264
468
  };
265
469
  //#endregion
470
+ //#region src/audio/collectMusicNotes.ts
471
+ const collectMusicNotes = ({ segment, start }, from, to) => segment.voices.flatMap((voice) => voice.notes.flatMap((note) => {
472
+ const time = start + note.start;
473
+ return time >= from && time < to ? [{
474
+ note,
475
+ time,
476
+ voice
477
+ }] : [];
478
+ }));
479
+ //#endregion
480
+ //#region src/audio/collectMusicSegments.ts
481
+ const collectMusicSegments = ({ isLooping, order, segments }, from, to) => {
482
+ const passDuration = order.reduce((sum, index) => sum + (segments[index]?.duration ?? 0), 0);
483
+ if (passDuration <= 0) return [];
484
+ const scheduledSegments = [];
485
+ const lastPass = isLooping ? Math.ceil(to / passDuration) : 1;
486
+ for (let pass = Math.max(Math.floor(from / passDuration), 0); pass < lastPass; pass++) {
487
+ let start = pass * passDuration;
488
+ for (const index of order) {
489
+ const segment = segments[index];
490
+ if (!segment) continue;
491
+ else if (start >= to) break;
492
+ else if (start + segment.duration > from) scheduledSegments.push({
493
+ segment,
494
+ start
495
+ });
496
+ start += segment.duration;
497
+ }
498
+ }
499
+ return scheduledSegments;
500
+ };
501
+ //#endregion
502
+ //#region src/audio/constants.ts
503
+ const MUSIC_LOOKAHEAD_SECONDS = 2;
504
+ const MUSIC_SCHEDULE_INTERVAL_MS = 500;
505
+ const RELEASE_TIME_CONSTANTS = 7;
506
+ const MUSIC_NOISE_LENGTH = 2 ** 16;
507
+ const MUSIC_NOISE_BAND_CENTRES = [
508
+ 63,
509
+ 125,
510
+ 250,
511
+ 500,
512
+ 1e3,
513
+ 2e3,
514
+ 4e3,
515
+ 8e3
516
+ ];
517
+ const MUSIC_EXPRESSION_WINDOW_SECONDS = 8;
518
+ const A4_PITCH = 69;
519
+ const A4_FREQUENCY = 440;
520
+ const MIDI_VELOCITY_MAX = 127;
521
+ //#endregion
522
+ //#region src/audio/transformFourier.ts
523
+ const transformFourier = (real, imaginary) => {
524
+ const { length } = real;
525
+ for (let index = 1, reversed = 0; index < length; index++) {
526
+ let bit = length >> 1;
527
+ for (; reversed & bit; bit >>= 1) reversed ^= bit;
528
+ reversed ^= bit;
529
+ if (index < reversed) {
530
+ [real[index], real[reversed]] = [real[reversed] ?? 0, real[index] ?? 0];
531
+ [imaginary[index], imaginary[reversed]] = [imaginary[reversed] ?? 0, imaginary[index] ?? 0];
532
+ }
533
+ }
534
+ for (let size = 2; size <= length; size <<= 1) {
535
+ const half = size >> 1;
536
+ const angle = -2 * Math.PI / size;
537
+ for (let offset = 0; offset < half; offset++) {
538
+ const twiddleReal = Math.cos(angle * offset);
539
+ const twiddleImaginary = Math.sin(angle * offset);
540
+ for (let even = offset; even < length; even += size) {
541
+ const odd = even + half;
542
+ const oddReal = (real[odd] ?? 0) * twiddleReal - (imaginary[odd] ?? 0) * twiddleImaginary;
543
+ const oddImaginary = (real[odd] ?? 0) * twiddleImaginary + (imaginary[odd] ?? 0) * twiddleReal;
544
+ real[odd] = (real[even] ?? 0) - oddReal;
545
+ imaginary[odd] = (imaginary[even] ?? 0) - oddImaginary;
546
+ real[even] = (real[even] ?? 0) + oddReal;
547
+ imaginary[even] = (imaginary[even] ?? 0) + oddImaginary;
548
+ }
549
+ }
550
+ }
551
+ };
552
+ //#endregion
553
+ //#region src/audio/computeNoiseSamples.ts
554
+ const computeNoiseSamples = (noiseBands, sampleRate) => {
555
+ const real = new Float64Array(MUSIC_NOISE_LENGTH);
556
+ const imaginary = new Float64Array(MUSIC_NOISE_LENGTH);
557
+ for (let band = 0; band < noiseBands.length; band++) {
558
+ const level = noiseBands[band] ?? 0;
559
+ const centre = MUSIC_NOISE_BAND_CENTRES[band] ?? sampleRate;
560
+ const lowBin = Math.max(Math.ceil(centre / Math.SQRT2 * MUSIC_NOISE_LENGTH / sampleRate), 1);
561
+ const highBin = Math.min(Math.floor(centre * Math.SQRT2 * MUSIC_NOISE_LENGTH / sampleRate), MUSIC_NOISE_LENGTH / 2 - 1);
562
+ if (level <= 0 || lowBin > highBin) continue;
563
+ const amplitude = level / Math.sqrt(2 * (highBin - lowBin + 1));
564
+ for (let bin = lowBin; bin <= highBin; bin++) {
565
+ const hash = Math.sin(bin * 12.9898 + band * 78.233) * 43758.5453;
566
+ const phase = 2 * Math.PI * (hash - Math.floor(hash));
567
+ const binReal = amplitude * Math.cos(phase);
568
+ const binImaginary = amplitude * Math.sin(phase);
569
+ real[bin] = (real[bin] ?? 0) + binReal;
570
+ real[MUSIC_NOISE_LENGTH - bin] = (real[MUSIC_NOISE_LENGTH - bin] ?? 0) + binReal;
571
+ imaginary[bin] = (imaginary[bin] ?? 0) - binImaginary;
572
+ imaginary[MUSIC_NOISE_LENGTH - bin] = (imaginary[MUSIC_NOISE_LENGTH - bin] ?? 0) + binImaginary;
573
+ }
574
+ }
575
+ transformFourier(real, imaginary);
576
+ return Float32Array.from(real);
577
+ };
578
+ //#endregion
579
+ //#region src/audio/computeSoundEffectSamples.ts
580
+ const computeSoundEffectSamples = ({ frameSeconds, levels }, sampleRate) => {
581
+ const samples = new Float32Array(Math.ceil(levels.length * frameSeconds * sampleRate));
582
+ for (const band of MUSIC_NOISE_BAND_CENTRES.keys()) {
583
+ if (levels.every((frameLevels) => (frameLevels[band] ?? 0) <= 0)) continue;
584
+ const noise = computeNoiseSamples(MUSIC_NOISE_BAND_CENTRES.map((_centre, index) => index === band ? 1 : 0), sampleRate);
585
+ for (let sample = 0; sample < samples.length; sample++) {
586
+ const position = sample / sampleRate / frameSeconds;
587
+ const frame = Math.floor(position);
588
+ const share = position - frame;
589
+ const level = (levels[frame]?.[band] ?? 0) * (1 - share) + (levels[Math.min(frame + 1, levels.length - 1)]?.[band] ?? 0) * share;
590
+ samples[sample] = (samples[sample] ?? 0) + level * (noise[sample % MUSIC_NOISE_LENGTH] ?? 0);
591
+ }
592
+ }
593
+ return samples;
594
+ };
595
+ //#endregion
596
+ //#region src/audio/createInstrumentWave.ts
597
+ const createInstrumentWave = (context, harmonics) => context.createPeriodicWave(new Float32Array(harmonics.length + 1), Float32Array.of(0, ...harmonics), { disableNormalization: true });
598
+ //#endregion
599
+ //#region src/audio/createNoiseBuffer.ts
600
+ const createNoiseBuffer = (context, noiseBands) => {
601
+ if (noiseBands.every((level) => level <= 0)) return void 0;
602
+ const { sampleRate } = context;
603
+ const buffer = new AudioBuffer({
604
+ length: MUSIC_NOISE_LENGTH,
605
+ sampleRate
606
+ });
607
+ buffer.getChannelData(0).set(computeNoiseSamples(noiseBands, sampleRate));
608
+ return buffer;
609
+ };
610
+ //#endregion
611
+ //#region src/audio/scheduleMusicExpression.ts
612
+ const toAmplitude = (gain) => 10 ** (gain / 20);
613
+ const scheduleMusicExpression = (gain, expression, start) => {
614
+ const [first = 0, ...rest] = expression;
615
+ gain.setValueAtTime(toAmplitude(first), start);
616
+ if (rest.length === 0) return;
617
+ gain.setValueAtTime(toAmplitude(first), start + 4);
618
+ for (const [index, windowGain] of rest.entries()) gain.exponentialRampToValueAtTime(toAmplitude(windowGain), start + (index + 1.5) * 8);
619
+ };
620
+ //#endregion
621
+ //#region src/audio/scheduleMusicNote.ts
622
+ const scheduleMusicNote = (context, destination, wave, noiseBuffer, { attack, decay, level, release, sustain, tuning }, { duration, pitch, velocity }, time) => {
623
+ const peak = level * velocity;
624
+ const end = time + duration;
625
+ const oscillator = new OscillatorNode(context, {
626
+ frequency: 440 * 2 ** ((pitch + tuning - 69) / 12),
627
+ periodicWave: wave
628
+ });
629
+ const gain = new GainNode(context, { gain: 0 });
630
+ gain.gain.setValueAtTime(0, time);
631
+ if (duration <= attack) gain.gain.linearRampToValueAtTime(peak * duration / attack, end);
632
+ else {
633
+ gain.gain.linearRampToValueAtTime(peak, time + attack);
634
+ gain.gain.setTargetAtTime(peak * sustain, time + attack, decay);
635
+ gain.gain.setValueAtTime(peak * (sustain + (1 - sustain) * Math.exp(-(duration - attack) / decay)), end);
636
+ }
637
+ gain.gain.setTargetAtTime(0, end, release);
638
+ oscillator.connect(gain).connect(destination);
639
+ oscillator.addEventListener("ended", () => {
640
+ gain.disconnect();
641
+ });
642
+ oscillator.start(time);
643
+ oscillator.stop(end + release * 7);
644
+ if (!noiseBuffer) return;
645
+ const noiseSource = new AudioBufferSourceNode(context, {
646
+ buffer: noiseBuffer,
647
+ loop: true
648
+ });
649
+ noiseSource.connect(gain);
650
+ noiseSource.start(time, time % noiseBuffer.duration);
651
+ noiseSource.stop(end + release * 7);
652
+ };
653
+ //#endregion
654
+ //#region src/audio/createMusicPlayer.ts
655
+ const createMusicPlayer = (context, music, destination = context.destination) => {
656
+ const output = new GainNode(context);
657
+ const instrumentSoundMap = /* @__PURE__ */ new Map();
658
+ let origin = 0;
659
+ let scheduledUntil = 0;
660
+ let startSegmentOutputMap = /* @__PURE__ */ new Map();
661
+ let timer;
662
+ const schedule = () => {
663
+ const until = context.currentTime - origin + 2;
664
+ const nextStartSegmentOutputMap = /* @__PURE__ */ new Map();
665
+ for (const scheduledSegment of collectMusicSegments(music, scheduledUntil, until)) {
666
+ let segmentOutput = startSegmentOutputMap.get(scheduledSegment.start);
667
+ if (!segmentOutput) {
668
+ segmentOutput = new GainNode(context);
669
+ scheduleMusicExpression(segmentOutput.gain, scheduledSegment.segment.expression, origin + scheduledSegment.start);
670
+ segmentOutput.connect(output);
671
+ }
672
+ nextStartSegmentOutputMap.set(scheduledSegment.start, segmentOutput);
673
+ for (const { note, time, voice } of collectMusicNotes(scheduledSegment, scheduledUntil, until)) {
674
+ let sound = instrumentSoundMap.get(voice.instrument);
675
+ if (!sound) {
676
+ sound = {
677
+ noiseBuffer: createNoiseBuffer(context, voice.instrument.noiseBands),
678
+ wave: createInstrumentWave(context, voice.instrument.harmonics)
679
+ };
680
+ instrumentSoundMap.set(voice.instrument, sound);
681
+ }
682
+ scheduleMusicNote(context, segmentOutput, sound.wave, sound.noiseBuffer, voice.instrument, note, origin + time);
683
+ }
684
+ }
685
+ startSegmentOutputMap = nextStartSegmentOutputMap;
686
+ scheduledUntil = until;
687
+ };
688
+ return {
689
+ start: () => {
690
+ output.connect(destination);
691
+ origin = context.currentTime;
692
+ startSegmentOutputMap = /* @__PURE__ */ new Map();
693
+ scheduledUntil = 0;
694
+ schedule();
695
+ timer = setInterval(() => {
696
+ schedule();
697
+ }, 500);
698
+ },
699
+ stop: () => {
700
+ clearInterval(timer);
701
+ output.disconnect();
702
+ }
703
+ };
704
+ };
705
+ //#endregion
706
+ //#region src/audio/createSoundEffectBuffer.ts
707
+ const createSoundEffectBuffer = (context, soundEffect) => {
708
+ const { sampleRate } = context;
709
+ const samples = computeSoundEffectSamples(soundEffect, sampleRate);
710
+ const buffer = new AudioBuffer({
711
+ length: Math.max(samples.length, 1),
712
+ sampleRate
713
+ });
714
+ buffer.getChannelData(0).set(samples);
715
+ return buffer;
716
+ };
717
+ //#endregion
718
+ //#region src/audio/renderMusicSegment.ts
719
+ const renderMusicSegment = (music, segmentIndex, sampleRate) => {
720
+ const segment = music.segments[segmentIndex];
721
+ const context = new OfflineAudioContext({
722
+ length: Math.max(Math.ceil((segment?.duration ?? 0) * sampleRate), 1),
723
+ numberOfChannels: 1,
724
+ sampleRate
725
+ });
726
+ const output = new GainNode(context);
727
+ scheduleMusicExpression(output.gain, segment?.expression ?? [], 0);
728
+ output.connect(context.destination);
729
+ for (const { instrument, notes } of segment?.voices ?? []) {
730
+ const wave = createInstrumentWave(context, instrument.harmonics);
731
+ const noiseBuffer = createNoiseBuffer(context, instrument.noiseBands);
732
+ for (const note of notes) scheduleMusicNote(context, output, wave, noiseBuffer, instrument, note, note.start);
733
+ }
734
+ return context.startRendering();
735
+ };
736
+ //#endregion
737
+ //#region src/audio/selectMusicSample.ts
738
+ const getOutsideDistance = (value, low, high) => value < low ? low - value : Math.max(value - high, 0);
739
+ const selectMusicSample = (samples, pitch, velocity) => {
740
+ const midiVelocity = Math.min(Math.max(Math.round(velocity * 127), 1), 127);
741
+ let selected;
742
+ let selectedKeyDistance = Infinity;
743
+ let selectedVelocityDistance = Infinity;
744
+ for (const sample of samples) {
745
+ const keyDistance = getOutsideDistance(pitch, sample.lowKey, sample.highKey);
746
+ const velocityDistance = getOutsideDistance(midiVelocity, sample.lowVelocity, sample.highVelocity);
747
+ if (keyDistance > selectedKeyDistance || keyDistance === selectedKeyDistance && velocityDistance >= selectedVelocityDistance) continue;
748
+ selected = sample;
749
+ selectedKeyDistance = keyDistance;
750
+ selectedVelocityDistance = velocityDistance;
751
+ }
752
+ return selected;
753
+ };
754
+ //#endregion
266
755
  //#region src/clock/advanceGameClock.ts
267
756
  const advanceGameClock = (gameClock, deltaSeconds) => {
268
757
  gameClock.minutes = (gameClock.minutes + deltaSeconds * gameClock.minutesPerSecond) % MINUTES_PER_DAY;
269
758
  };
270
759
  //#endregion
760
+ //#region src/kits/mergeGeometryParts.ts
761
+ const mergeGeometryParts = (parts) => {
762
+ const geometry = mergeGeometries(parts) ?? new BufferGeometry();
763
+ for (const part of parts) part.dispose();
764
+ return geometry;
765
+ };
766
+ //#endregion
767
+ //#region src/kits/architecture/createBoxesGeometry.ts
768
+ const createBoxesGeometry = (boxes) => {
769
+ const parts = boxes.map(([minX = 0, minY = 0, minZ = 0, maxX = 0, maxY = 0, maxZ = 0]) => new BoxGeometry(maxX - minX, maxY - minY, maxZ - minZ).translate((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2));
770
+ return mergeGeometryParts(parts);
771
+ };
772
+ //#endregion
271
773
  //#region src/kits/architecture/createLatheStackGeometry.ts
272
774
  const createLatheStackGeometry = ({ isFaceted, radialSegments, sections }) => {
273
775
  let footHeight = 0;
@@ -276,26 +778,11 @@ const createLatheStackGeometry = ({ isFaceted, radialSegments, sections }) => {
276
778
  footHeight += height;
277
779
  return isFaceted ? part.toNonIndexed() : part;
278
780
  });
279
- const stackGeometry = mergeGeometries(parts) ?? new BufferGeometry();
280
- for (const part of parts) part.dispose();
781
+ const stackGeometry = mergeGeometryParts(parts);
281
782
  if (isFaceted) stackGeometry.computeVertexNormals();
282
783
  return stackGeometry;
283
784
  };
284
785
  //#endregion
285
- //#region src/kits/architecture/createSilhouetteGeometry.ts
286
- const createSilhouetteGeometry = ({ contours, depth: [front, back] }) => {
287
- const shapePath = new ShapePath();
288
- for (const [first, ...rest] of contours) {
289
- if (!first) continue;
290
- shapePath.moveTo(...first);
291
- for (const point of rest) shapePath.lineTo(...point);
292
- }
293
- return new ExtrudeGeometry(shapePath.toShapes(), {
294
- bevelEnabled: false,
295
- depth: back - front
296
- }).translate(0, 0, front);
297
- };
298
- //#endregion
299
786
  //#region src/kits/statue/createStatueGeometry.ts
300
787
  const PLINTH_SIDES = 8;
301
788
  const FIGURE_SEGMENTS = 24;
@@ -313,24 +800,14 @@ const FIGURE_PROFILE = [
313
800
  new Vector2(0, 6.2)
314
801
  ];
315
802
  const createStatueGeometry = () => {
316
- const lowerStep = new CylinderGeometry(3.2, 3.4, .6, PLINTH_SIDES).translate(0, .3, 0);
317
- const upperStep = new CylinderGeometry(2.6, 2.8, .6, PLINTH_SIDES).translate(0, .9, 0);
318
- const pedestal = new CylinderGeometry(1.8, 2.1, 2.2, PLINTH_SIDES).translate(0, 2.3, 0);
319
- const figure = new LatheGeometry(FIGURE_PROFILE, FIGURE_SEGMENTS).translate(0, 3.4, 0);
320
- const parts = [
321
- lowerStep,
322
- upperStep,
323
- pedestal,
324
- figure
325
- ].map((part) => part.toNonIndexed());
326
- const statueGeometry = mergeGeometries(parts) ?? new BufferGeometry();
327
- for (const part of [
328
- lowerStep,
329
- upperStep,
330
- pedestal,
331
- figure,
332
- ...parts
333
- ]) part.dispose();
803
+ const indexedParts = [
804
+ new CylinderGeometry(3.2, 3.4, .6, PLINTH_SIDES).translate(0, .3, 0),
805
+ new CylinderGeometry(2.6, 2.8, .6, PLINTH_SIDES).translate(0, .9, 0),
806
+ new CylinderGeometry(1.8, 2.1, 2.2, PLINTH_SIDES).translate(0, 2.3, 0),
807
+ new LatheGeometry(FIGURE_PROFILE, FIGURE_SEGMENTS).translate(0, 3.4, 0)
808
+ ];
809
+ const statueGeometry = mergeGeometryParts(indexedParts.map((part) => part.toNonIndexed()));
810
+ for (const part of indexedParts) part.dispose();
334
811
  statueGeometry.computeVertexNormals();
335
812
  return statueGeometry;
336
813
  };
@@ -464,8 +941,7 @@ const createTreeGeometry = (treeOptions) => {
464
941
  segmentGeometry.translate((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2);
465
942
  return segmentGeometry;
466
943
  });
467
- const branchGeometry = mergeGeometries(segmentGeometries) ?? new BufferGeometry();
468
- for (const segmentGeometry of segmentGeometries) segmentGeometry.dispose();
944
+ const branchGeometry = mergeGeometryParts(segmentGeometries);
469
945
  const { indices, normals, positions, uvs } = computeLeafCards(clusterCenters, treeOptions);
470
946
  const leafGeometry = new BufferGeometry();
471
947
  leafGeometry.setAttribute("position", new BufferAttribute(positions, 3));
@@ -480,14 +956,12 @@ const createTreeGeometry = (treeOptions) => {
480
956
  };
481
957
  //#endregion
482
958
  //#region src/materials/computeRampValues.ts
483
- const MAX_BYTE$1 = 255;
484
959
  const computeRampValues = ({ resolution, softness, terminator }) => {
485
960
  const lowerEdge = terminator - softness / 2;
486
961
  const upperEdge = terminator + softness / 2;
487
962
  return Uint8Array.from({ length: resolution }, (_value, index) => {
488
963
  const coordinate = (index + .5) / resolution;
489
- const step = Math.min(Math.max((coordinate - lowerEdge) / (upperEdge - lowerEdge), 0), 1);
490
- return Math.round(step * step * (3 - 2 * step) * MAX_BYTE$1);
964
+ return Math.round(MathUtils.smoothstep(coordinate, lowerEdge, upperEdge) * 255);
491
965
  });
492
966
  };
493
967
  //#endregion
@@ -501,7 +975,58 @@ const createRampTexture = (rampOptions) => {
501
975
  return rampTexture;
502
976
  };
503
977
  //#endregion
504
- //#region src/nodes/ToonNodeMaterial.ts
978
+ //#region src/nodes/constants.ts
979
+ const LEAST_DIVISOR = 1e-4;
980
+ const STONE_SHADOW_EXPONENT = .2;
981
+ const STONE_RAMP_KNOT_COUNT = 16;
982
+ const STONE_HARMONIC_COUNT = 9;
983
+ const STONE_MASK_OUTPUT = "stoneMask";
984
+ const STONE_HEIGHT_FALLOFF = .05;
985
+ //#endregion
986
+ //#region src/nodes/createSunVisibilityNode.ts
987
+ const createSunVisibilityNode = (lightColor, sunRadiance) => luminance(lightColor).div(luminance(sunRadiance).max(LEAST_DIVISOR)).saturate();
988
+ //#endregion
989
+ //#region src/models/nodes/StoneLightingModel.ts
990
+ var StoneLightingModel = class extends LightingModel {
991
+ stoneLight;
992
+ constructor(stoneLight) {
993
+ super();
994
+ this.stoneLight = stoneLight;
995
+ }
996
+ direct({ lightColor, lightDirection, reflectedLight }) {
997
+ const { ramp, sunRadiance } = this.stoneLight;
998
+ const visibility = createSunVisibilityNode(lightColor, sunRadiance);
999
+ const rampU = normalView.dot(lightDirection).mul(visibility.pow(STONE_SHADOW_EXPONENT)).mul(.5).add(.5).mul(15).add(.5).div(16);
1000
+ reflectedLight.directDiffuse.addAssign(texture(ramp, vec2(rampU, .5)).rgb.mul(diffuseColor.rgb));
1001
+ }
1002
+ indirect(builder) {
1003
+ const { harmonics, heightFade } = this.stoneLight;
1004
+ const { x, y, z } = normalWorld;
1005
+ const ambient = harmonics.element(0).add(harmonics.element(1).mul(x)).add(harmonics.element(2).mul(y)).add(harmonics.element(3).mul(z)).add(harmonics.element(4).mul(x.mul(y))).add(harmonics.element(5).mul(y.mul(z))).add(harmonics.element(6).mul(x.mul(z))).add(harmonics.element(7).mul(x.mul(x).sub(z.mul(z)))).add(harmonics.element(8).mul(y.mul(y).mul(3).sub(1))).add(heightFade.mul(positionWorld.y.mul(-.05).exp()));
1006
+ const { reflectedLight } = builder.context;
1007
+ reflectedLight.indirectDiffuse.addAssign(ambient.mul(diffuseColor.rgb));
1008
+ }
1009
+ };
1010
+ //#endregion
1011
+ //#region src/models/nodes/StoneNodeMaterial.ts
1012
+ var StoneNodeMaterial = class extends NodeMaterial {
1013
+ emissiveNode = null;
1014
+ stoneLight;
1015
+ constructor(stoneLight) {
1016
+ super();
1017
+ this.lights = true;
1018
+ this.stoneLight = stoneLight;
1019
+ this.mrtNode = mrt({
1020
+ output,
1021
+ [STONE_MASK_OUTPUT]: float(1)
1022
+ });
1023
+ }
1024
+ setupLightingModel() {
1025
+ return new StoneLightingModel(this.stoneLight);
1026
+ }
1027
+ };
1028
+ //#endregion
1029
+ //#region src/models/nodes/ToonNodeMaterial.ts
505
1030
  var ToonNodeMaterial = class extends MeshToonNodeMaterial {
506
1031
  emissiveNode = null;
507
1032
  isMeshToonNodeMaterial;
@@ -511,6 +1036,57 @@ var ToonNodeMaterial = class extends MeshToonNodeMaterial {
511
1036
  }
512
1037
  };
513
1038
  //#endregion
1039
+ //#region src/models/renderer/AntialiasingMode.ts
1040
+ let AntialiasingMode = /* @__PURE__ */ function(AntialiasingMode) {
1041
+ AntialiasingMode["Smaa"] = "Smaa";
1042
+ AntialiasingMode["Traa"] = "Traa";
1043
+ return AntialiasingMode;
1044
+ }({});
1045
+ //#endregion
1046
+ //#region src/models/renderer/QualityTier.ts
1047
+ let QualityTier = /* @__PURE__ */ function(QualityTier) {
1048
+ QualityTier["High"] = "High";
1049
+ QualityTier["Low"] = "Low";
1050
+ QualityTier["Medium"] = "Medium";
1051
+ return QualityTier;
1052
+ }({});
1053
+ //#endregion
1054
+ //#region src/nodes/applyStoneLight.ts
1055
+ const applyStoneLight = ({ harmonics, hazeColor, hazeScatterColor, heightFade, ramp }, { harmonics: harmonicsUniform, hazeColor: hazeColorUniform, hazeScatterColor: hazeScatterColorUniform, heightFade: heightFadeUniform, ramp: rampTexture, sunRadiance }, light) => {
1056
+ const { data } = rampTexture.image;
1057
+ if (data) for (const [knot, [red = 0, green = 0, blue = 0]] of ramp.entries()) {
1058
+ data[knot * 4] = red;
1059
+ data[knot * 4 + 1] = green;
1060
+ data[knot * 4 + 2] = blue;
1061
+ data[knot * 4 + 3] = 1;
1062
+ }
1063
+ rampTexture.needsUpdate = true;
1064
+ for (const [term, [red = 0, green = 0, blue = 0]] of harmonics.entries()) {
1065
+ const value = harmonicsUniform.array[term];
1066
+ if (value instanceof Vector3) value.set(red, green, blue);
1067
+ }
1068
+ heightFadeUniform.value.fromArray(heightFade);
1069
+ hazeColorUniform.value.fromArray(hazeColor);
1070
+ hazeScatterColorUniform.value.fromArray(hazeScatterColor);
1071
+ sunRadiance.value.copy(light.color).multiplyScalar(light.intensity);
1072
+ };
1073
+ //#endregion
1074
+ //#region src/nodes/computeStoneHarmonics.ts
1075
+ const computeStoneHarmonics = ([x = 0, y = 0, z = 0], harmonics) => {
1076
+ harmonics[0] = 1;
1077
+ harmonics[1] = x;
1078
+ harmonics[2] = y;
1079
+ harmonics[3] = z;
1080
+ harmonics[4] = x * y;
1081
+ harmonics[5] = y * z;
1082
+ harmonics[6] = x * z;
1083
+ harmonics[7] = x * x - z * z;
1084
+ harmonics[8] = 3 * y * y - 1;
1085
+ };
1086
+ //#endregion
1087
+ //#region src/nodes/computeStoneRampCoordinate.ts
1088
+ const computeStoneRampCoordinate = (facing, visibility) => .5 + .5 * facing * Math.max(visibility, 0) ** STONE_SHADOW_EXPONENT;
1089
+ //#endregion
514
1090
  //#region src/nodes/createCausticsNode.ts
515
1091
  const CAUSTIC_SCALE = .35;
516
1092
  const CAUSTIC_SPEED = .6;
@@ -524,21 +1100,6 @@ const createCausticsNode = ({ lightColor }, { causticStrength, deepDepth, level
524
1100
  return lightColor.mul(veins.mul(fade).mul(causticStrength));
525
1101
  };
526
1102
  //#endregion
527
- //#region src/renderer/constants.ts
528
- const GENSHIN_TONE_MAPPING = NeutralToneMapping;
529
- //#endregion
530
- //#region src/nodes/createGenshinRenderer.ts
531
- const createGenshinRenderer = (canvas) => {
532
- const isWindows = window.navigator.userAgent.includes("Windows");
533
- const renderer = new WebGPURenderer({
534
- antialias: false,
535
- canvas,
536
- ...isWindows ? {} : { powerPreference: "high-performance" }
537
- });
538
- renderer.toneMapping = GENSHIN_TONE_MAPPING;
539
- return renderer;
540
- };
541
- //#endregion
542
1103
  //#region src/nodes/createRimNode.ts
543
1104
  const RIM_POWER = 4;
544
1105
  const createRimNode = ({ rimColor, rimStrength, sunDirection }) => {
@@ -598,11 +1159,36 @@ const createLightUniforms = () => ({
598
1159
  sunDirection: uniform(new Vector3(0, 1, 0))
599
1160
  });
600
1161
  //#endregion
1162
+ //#region src/nodes/createSkyColorNode.ts
1163
+ const HALO_TOWARD_START = .3;
1164
+ const HALO_TOWARD_SPAN = .7;
1165
+ const HALO_SUN_HEIGHT_START = .2;
1166
+ const HALO_SUN_HEIGHT_SPAN = .3;
1167
+ const SUN_HALO_MIDDLE_SPREAD = .1;
1168
+ const SUN_HALO_MIDDLE_WEIGHT = .12;
1169
+ const SUN_HALO_OUTER_SPREAD = .01;
1170
+ const SUN_HALO_OUTER_WEIGHT = .03;
1171
+ const createSkyColorNode = ({ frontBackBlend, haloColor, haloHeight, horizonBackColor, horizonBand, horizonColor, sunDirection, sunHaloColor, sunHaloSize, zenithBackColor, zenithColor }, gradient, direction) => {
1172
+ const height = direction.y;
1173
+ const elevation = abs(asin(clamp(height, -1, 1)).mul(2 / Math.PI));
1174
+ const sunCosine = direction.dot(sunDirection);
1175
+ const toward = pow(max(sunCosine.mul(frontBackBlend).add(float(1).sub(frontBackBlend)), 0), 3);
1176
+ const top = mix(zenithBackColor, zenithColor, toward);
1177
+ const bottom = mix(horizonBackColor, horizonColor, toward);
1178
+ const bottomShare = texture(gradient, vec2(elevation.div(max(horizonBand, LEAST_DIVISOR)), .5)).r;
1179
+ const haloShare = texture(gradient, vec2(elevation.div(max(haloHeight, LEAST_DIVISOR)), .5)).g;
1180
+ const sunSide = saturate(sunCosine.mul(.5).add(.5));
1181
+ const towardSun = smoothstep(0, 1, max(sunSide.sub(HALO_TOWARD_START).div(HALO_TOWARD_SPAN), 0));
1182
+ const sunUp = smoothstep(0, 1, saturate(abs(sunDirection.y).sub(HALO_SUN_HEIGHT_START).div(HALO_SUN_HEIGHT_SPAN)));
1183
+ const halo = haloColor.mul(haloShare).mul(sunUp.mul(float(1).sub(towardSun)).add(towardSun));
1184
+ const spread = sunHaloSize.mul(abs(height));
1185
+ const sunHalo = min(pow(sunSide, spread), 1).add(min(pow(sunSide, spread.mul(SUN_HALO_MIDDLE_SPREAD)), 1).mul(SUN_HALO_MIDDLE_WEIGHT)).add(min(pow(sunSide, spread.mul(SUN_HALO_OUTER_SPREAD)), 1).mul(SUN_HALO_OUTER_WEIGHT)).mul(smoothstep(0, 1, max(sunSide.sub(.5).mul(2), 0)));
1186
+ return mix(top, bottom, bottomShare).add(halo).add(sunHaloColor.mul(sunHalo));
1187
+ };
1188
+ //#endregion
601
1189
  //#region src/nodes/createSkyNode.ts
602
1190
  const SUN_DISC_COSINE = Math.cos(.035);
603
1191
  const MOON_DISC_COSINE = Math.cos(.05);
604
- const SUN_GLOW_POWER = 12;
605
- const HORIZON_GLOW_POWER = 3;
606
1192
  const SUN_DISC_BRIGHTNESS = 6;
607
1193
  const CLOUD_SCALE = 1.6;
608
1194
  const CLOUD_MIN_HEIGHT = .04;
@@ -610,26 +1196,58 @@ const CLOUD_EDGE = .04;
610
1196
  const CLOUD_SHADE_DEPTH = .18;
611
1197
  const STAR_SCALE = 420;
612
1198
  const STAR_THRESHOLD = .9975;
613
- const createSkyNode = ({ cloudCoverage, cloudDrift, cloudLitColor, cloudShadeColor, horizonBand, horizonColor, lightColor, moonDirection, starIntensity, sunDirection, zenithColor }) => Fn(() => {
614
- const direction = normalWorldGeometry.normalize();
615
- const height = direction.y;
616
- const gradient = mix(horizonColor, zenithColor, smoothstep(0, horizonBand, height));
617
- const sunCosine = direction.dot(sunDirection);
618
- const sunVisibility = smoothstep(-.1, .05, sunDirection.y);
619
- const sunGlow = pow(max(sunCosine, 0), SUN_GLOW_POWER).mul(sunVisibility);
620
- const horizonGlow = pow(max(sunCosine, 0), HORIZON_GLOW_POWER).mul(float(1).sub(smoothstep(0, .3, height.abs()))).mul(float(1).sub(smoothstep(0, .5, sunDirection.y))).mul(sunVisibility);
621
- const sunDisc = smoothstep(SUN_DISC_COSINE - 5e-4, SUN_DISC_COSINE, sunCosine).mul(sunVisibility);
622
- const moonDisc = smoothstep(MOON_DISC_COSINE - 5e-4, MOON_DISC_COSINE, direction.dot(moonDirection)).mul(smoothstep(-.05, .05, moonDirection.y));
623
- const stars = smoothstep(STAR_THRESHOLD, 1, mx_cell_noise_float(direction.mul(STAR_SCALE))).mul(starIntensity).mul(smoothstep(0, .1, height));
624
- const cloudPosition = direction.xz.div(max(height, CLOUD_MIN_HEIGHT)).mul(CLOUD_SCALE).add(cloudDrift);
625
- const cloudNoise = mx_fractal_noise_float(vec3(cloudPosition, 0), 4, 2, .5).mul(.5).add(.5);
626
- const cloudThreshold = float(1).sub(cloudCoverage);
627
- const cloudDensity = smoothstep(cloudThreshold, cloudThreshold.add(CLOUD_EDGE), cloudNoise).mul(smoothstep(0, .12, height));
628
- const cloudShade = smoothstep(cloudThreshold.add(CLOUD_SHADE_DEPTH), cloudThreshold.add(.22), cloudNoise);
629
- const cloudColor = mix(cloudLitColor, cloudShadeColor, cloudShade).add(lightColor.mul(sunGlow.mul(.6)));
630
- const sky = gradient.add(lightColor.mul(sunGlow.mul(.35).add(horizonGlow.mul(.45)))).add(vec3(stars)).add(vec3(.85, .88, 1).mul(moonDisc)).add(lightColor.mul(sunDisc.mul(SUN_DISC_BRIGHTNESS)));
631
- return mix(sky, cloudColor, cloudDensity);
632
- })();
1199
+ const MOON_GLOW_SPREAD = .1;
1200
+ const MOON_GLOW_POWER = 6;
1201
+ const createSkyNode = (uniforms, gradient) => {
1202
+ const { cloudCoverage, cloudDrift, cloudLitColor, cloudShadeColor, lightColor, moonDirection, moonGlowColor, moonSize, starIntensity, sunDirection } = uniforms;
1203
+ return Fn(() => {
1204
+ const direction = normalWorldGeometry.normalize();
1205
+ const height = direction.y;
1206
+ const sunCosine = direction.dot(sunDirection);
1207
+ const gradientColor = createSkyColorNode(uniforms, gradient, direction);
1208
+ const moonCosine = saturate(direction.dot(moonDirection));
1209
+ const moonGlow = pow(max(moonCosine.sub(1).div(max(moonSize.mul(MOON_GLOW_SPREAD), LEAST_DIVISOR)).add(1), 0), MOON_GLOW_POWER);
1210
+ const sunVisibility = smoothstep(-.1, .05, sunDirection.y);
1211
+ const sunDisc = smoothstep(SUN_DISC_COSINE - 5e-4, SUN_DISC_COSINE, sunCosine).mul(sunVisibility);
1212
+ const moonDisc = smoothstep(MOON_DISC_COSINE - 5e-4, MOON_DISC_COSINE, direction.dot(moonDirection)).mul(smoothstep(-.05, .05, moonDirection.y));
1213
+ const stars = smoothstep(STAR_THRESHOLD, 1, mx_cell_noise_float(direction.mul(STAR_SCALE))).mul(starIntensity).mul(smoothstep(0, .1, height));
1214
+ const cloudPosition = direction.xz.div(max(height, CLOUD_MIN_HEIGHT)).mul(CLOUD_SCALE).add(cloudDrift);
1215
+ const cloudNoise = mx_fractal_noise_float(vec3(cloudPosition, 0), 4, 2, .5).mul(.5).add(.5);
1216
+ const cloudThreshold = float(1).sub(cloudCoverage);
1217
+ const cloudDensity = smoothstep(cloudThreshold, cloudThreshold.add(CLOUD_EDGE), cloudNoise).mul(smoothstep(0, .12, height));
1218
+ const cloudShade = smoothstep(cloudThreshold.add(CLOUD_SHADE_DEPTH), cloudThreshold.add(.22), cloudNoise);
1219
+ const cloudColor = mix(cloudLitColor, cloudShadeColor, cloudShade);
1220
+ const sky = gradientColor.add(moonGlowColor.mul(moonGlow)).add(vec3(stars)).add(vec3(.85, .88, 1).mul(moonDisc)).add(lightColor.mul(sunDisc.mul(SUN_DISC_BRIGHTNESS)));
1221
+ return mix(sky, cloudColor, cloudDensity);
1222
+ })();
1223
+ };
1224
+ //#endregion
1225
+ //#region src/nodes/createStoneLightUniforms.ts
1226
+ const createStoneLightUniforms = () => {
1227
+ const ramp = new DataTexture(/* @__PURE__ */ new Float32Array(64), 16, 1, RGBAFormat, FloatType);
1228
+ ramp.magFilter = LinearFilter;
1229
+ ramp.minFilter = LinearFilter;
1230
+ ramp.wrapS = ClampToEdgeWrapping;
1231
+ ramp.wrapT = ClampToEdgeWrapping;
1232
+ ramp.needsUpdate = true;
1233
+ return {
1234
+ harmonics: uniformArray(Array.from({ length: 9 }, () => new Vector3()), "vec3"),
1235
+ hazeColor: uniform(new Color()),
1236
+ hazeScatterColor: uniform(new Color()),
1237
+ heightFade: uniform(new Color()),
1238
+ ramp,
1239
+ sunRadiance: uniform(new Color())
1240
+ };
1241
+ };
1242
+ //#endregion
1243
+ //#region src/nodes/createStoneMaterial.ts
1244
+ const createStoneMaterial = ({ albedo, rimColor, rimPower, rimStrength }, stoneLight, glowNode = vec3(0)) => {
1245
+ const material = new StoneNodeMaterial(stoneLight);
1246
+ material.colorNode = color(new Color(albedo));
1247
+ const fresnel = float(1).sub(normalView.dot(positionViewDirection).saturate()).pow(rimPower);
1248
+ material.emissiveNode = color(new Color().fromArray(rimColor)).mul(fresnel.mul(rimStrength)).add(glowNode);
1249
+ return material;
1250
+ };
633
1251
  //#endregion
634
1252
  //#region src/nodes/createWaterMaterial.ts
635
1253
  const RIPPLE_SCALE = .08;
@@ -726,14 +1344,13 @@ const createSimplexNoise = (seed) => {
726
1344
  };
727
1345
  //#endregion
728
1346
  //#region src/post/computeGradeLut.ts
729
- const MAX_BYTE = 255;
730
1347
  const RED_LUMINANCE = .2126;
731
1348
  const GREEN_LUMINANCE = .7152;
732
1349
  const BLUE_LUMINANCE = .0722;
733
1350
  const CONTRAST_PIVOT = .5;
734
1351
  const toGradedByte = (channel, luminance, saturation, contrast, tint) => {
735
1352
  const graded = (luminance + (channel - luminance) * saturation - CONTRAST_PIVOT) * contrast + CONTRAST_PIVOT + tint;
736
- return Math.round(Math.min(Math.max(graded, 0), 1) * MAX_BYTE);
1353
+ return Math.round(Math.min(Math.max(graded, 0), 1) * 255);
737
1354
  };
738
1355
  const computeGradeLut = ({ contrast, highlightTint, saturation, shadowTint, size }) => {
739
1356
  const values = new Uint8Array(size * size * size * 4);
@@ -752,7 +1369,7 @@ const computeGradeLut = ({ contrast, highlightTint, saturation, shadowTint, size
752
1369
  values[offset] = toGradedByte(red, luminance, saturation, contrast, redTint);
753
1370
  values[offset + 1] = toGradedByte(green, luminance, saturation, contrast, greenTint);
754
1371
  values[offset + 2] = toGradedByte(blue, luminance, saturation, contrast, blueTint);
755
- values[offset + 3] = MAX_BYTE;
1372
+ values[offset + 3] = 255;
756
1373
  offset += 4;
757
1374
  }
758
1375
  return values;
@@ -764,6 +1381,7 @@ const createFogUniforms = () => ({
764
1381
  color: uniform(new Color()),
765
1382
  density: uniform(0),
766
1383
  heightFalloff: uniform(0),
1384
+ maxOpacity: uniform(1),
767
1385
  scatterColor: uniform(new Color()),
768
1386
  scatterDirection: uniform(new Vector3(0, 1, 0)),
769
1387
  scatterPower: uniform(1),
@@ -799,7 +1417,7 @@ const createGradeLutTexture = (gradeOptions) => {
799
1417
  //#endregion
800
1418
  //#region src/post/createHeightFogNode.ts
801
1419
  const MIN_RAY_CLIMB = 1e-6;
802
- const createHeightFogNode = (colorNode, depthNode, camera, { baseHeight, color, density, heightFalloff, scatterColor, scatterDirection, scatterPower, scatterStrength, startDistance }) => {
1420
+ const createHeightFogNode = (colorNode, depthNode, camera, { baseHeight, color, density, heightFalloff, maxOpacity, scatterColor, scatterDirection, scatterPower, scatterStrength, startDistance }, stoneHaze) => {
803
1421
  const cameraWorldMatrix = uniform(camera.matrixWorld);
804
1422
  const cameraProjectionMatrixInverse = uniform(camera.projectionMatrixInverse);
805
1423
  return Fn(() => {
@@ -819,59 +1437,94 @@ const createHeightFogNode = (colorNode, depthNode, camera, { baseHeight, color,
819
1437
  If(climb.abs().greaterThan(MIN_RAY_CLIMB), () => {
820
1438
  opticalDepth.assign(densityAtStart.mul(float(1).sub(exp(fogLength.mul(climb).negate()))).div(climb));
821
1439
  });
822
- const opacity = float(1).sub(exp(opticalDepth.negate()));
823
- const scatter = pow(max(ray.normalize().dot(scatterDirection), 0), scatterPower).mul(scatterStrength);
824
- output.assign(vec4(mix(output.rgb, mix(color, scatterColor, scatter.min(1)), opacity), output.a));
1440
+ const opacity = float(1).sub(exp(opticalDepth.negate())).mul(maxOpacity);
1441
+ const scatter = pow(max(ray.normalize().dot(scatterDirection), 0), scatterPower).mul(scatterStrength).min(1);
1442
+ const hazeColor = mix(color, scatterColor, scatter);
1443
+ const shownColor = stoneHaze ? mix(hazeColor, mix(stoneHaze.hazeColor, stoneHaze.hazeScatterColor, scatter), stoneHaze.maskNode.sample(uv()).r) : hazeColor;
1444
+ output.assign(vec4(mix(output.rgb, shownColor, opacity), output.a));
825
1445
  });
826
1446
  return output;
827
1447
  })();
828
1448
  };
829
1449
  //#endregion
830
- //#region src/renderer/AntialiasingMode.ts
831
- let AntialiasingMode = /* @__PURE__ */ function(AntialiasingMode) {
832
- AntialiasingMode["Smaa"] = "Smaa";
833
- AntialiasingMode["Traa"] = "Traa";
834
- return AntialiasingMode;
835
- }({});
1450
+ //#region src/post/createOcclusionNode.ts
1451
+ const OCCLUSION_THICKNESS_SHARE = 4;
1452
+ const createOcclusionNode = (depthNode, camera, radius) => {
1453
+ const cameraProjectionMatrixInverse = uniform(camera.projectionMatrixInverse);
1454
+ const normalNode = sample((uv) => getNormalFromDepth(uv, depthNode, cameraProjectionMatrixInverse));
1455
+ const occlusionNode = new GTAONode(depthNode, normalNode, camera);
1456
+ occlusionNode.radius.value = radius;
1457
+ occlusionNode.thickness.value = radius * OCCLUSION_THICKNESS_SHARE;
1458
+ return occlusionNode;
1459
+ };
836
1460
  //#endregion
837
1461
  //#region src/post/createPostPipeline.ts
838
1462
  const BLOOM_STRENGTH = .35;
839
1463
  const BLOOM_RADIUS = .4;
840
1464
  const BLOOM_THRESHOLD = .85;
841
1465
  const MIN_VIEW_DISTANCE = .001;
842
- const createPostPipeline = ({ camera, fogUniforms, godraysLight, gradeLutTexture, postUniforms: { godraysColor, gradeIntensity, outlineColor, outlineFadeDistance, outlineThickness }, qualityTierSettings: { antialiasingMode, godraysStepCount, isBloomEnabled }, renderer, scene }) => {
1466
+ const createPostPipeline = ({ camera, fogUniforms, godraysLight, gradeLutTexture, isBloomed = true, occlusionRadius, postUniforms: { godraysColor, gradeIntensity, outlineColor, outlineFadeDistance, outlineThickness }, qualityTierSettings: { antialiasingMode, godraysStepCount, isBloomEnabled, isOcclusionEnabled }, renderer, scene, stoneLight }) => {
843
1467
  const renderPipeline = new RenderPipeline(renderer);
844
1468
  renderPipeline.outputColorTransform = false;
845
1469
  const viewDistance = modelViewMatrix.mul(vec4(positionLocal, 1)).z.negate().max(MIN_VIEW_DISTANCE);
846
1470
  const thicknessNode = outlineThickness.mul(outlineFadeDistance.div(viewDistance).min(1));
847
1471
  const scenePass = new ToonOutlinePassNode(scene, camera, outlineColor, thicknessNode, float(1));
848
1472
  const isTraa = antialiasingMode === "Traa";
849
- if (isTraa) scenePass.setMRT(mrt({
850
- output,
851
- velocity
852
- }));
1473
+ const sceneOutputs = { output };
1474
+ if (isTraa) sceneOutputs.velocity = velocity;
1475
+ if (stoneLight) sceneOutputs[STONE_MASK_OUTPUT] = float(0);
1476
+ scenePass.setMRT(mrt(sceneOutputs));
853
1477
  const sceneColor = scenePass.getTextureNode("output");
854
1478
  const sceneDepth = scenePass.getTextureNode("depth");
855
- const postPipeline = { renderPipeline };
1479
+ const passNodes = [scenePass];
1480
+ const postPipeline = {
1481
+ dispose: () => {
1482
+ renderPipeline.dispose();
1483
+ for (const passNode of passNodes) passNode.dispose();
1484
+ },
1485
+ renderPipeline
1486
+ };
856
1487
  let litNode = sceneColor;
857
- if (godraysStepCount > 0) {
1488
+ if (occlusionRadius && isOcclusionEnabled) {
1489
+ const occlusionNode = createOcclusionNode(sceneDepth, camera, occlusionRadius);
1490
+ passNodes.push(occlusionNode);
1491
+ litNode = vec4(sceneColor.rgb.mul(occlusionNode.getTextureNode().r), sceneColor.a);
1492
+ }
1493
+ if (godraysLight && godraysStepCount > 0) {
858
1494
  const godraysNode = godrays(sceneDepth, camera, godraysLight);
859
1495
  godraysNode.raymarchSteps.value = godraysStepCount;
860
1496
  const blurredGodrays = bilateralBlur(godraysNode.getTextureNode());
861
- litNode = depthAwareBlend(sceneColor, blurredGodrays.getTextureNode(), sceneDepth, camera, { blendColor: godraysColor });
1497
+ passNodes.push(godraysNode, blurredGodrays);
1498
+ litNode = depthAwareBlend(litNode, blurredGodrays.getTextureNode(), sceneDepth, camera, { blendColor: godraysColor });
862
1499
  postPipeline.godraysNode = godraysNode;
863
1500
  }
864
- const foggedNode = createHeightFogNode(litNode, sceneDepth, camera, fogUniforms);
1501
+ if (stoneLight) scenePass.getTexture(STONE_MASK_OUTPUT).type = UnsignedByteType;
1502
+ const foggedNode = createHeightFogNode(litNode, sceneDepth, camera, fogUniforms, stoneLight && {
1503
+ hazeColor: stoneLight.hazeColor,
1504
+ hazeScatterColor: stoneLight.hazeScatterColor,
1505
+ maskNode: scenePass.getTextureNode("stoneMask")
1506
+ });
865
1507
  let bloomedNode = foggedNode;
866
- if (isBloomEnabled) {
1508
+ if (isBloomed && isBloomEnabled) {
867
1509
  const bloomNode = bloom(foggedNode, BLOOM_STRENGTH, BLOOM_RADIUS, BLOOM_THRESHOLD);
1510
+ passNodes.push(bloomNode);
868
1511
  bloomedNode = foggedNode.add(bloomNode);
869
1512
  postPipeline.bloomNode = bloomNode;
870
1513
  }
871
- const resolvedNode = isTraa ? traa(bloomedNode, sceneDepth, scenePass.getTextureNode("velocity"), camera) : bloomedNode;
1514
+ let resolvedNode = bloomedNode;
1515
+ if (isTraa) {
1516
+ const traaNode = traa(bloomedNode, sceneDepth, scenePass.getTextureNode("velocity"), camera);
1517
+ passNodes.push(traaNode);
1518
+ resolvedNode = traaNode;
1519
+ }
872
1520
  const displayNode = renderOutput(resolvedNode);
873
- const gradedNode = lut3D(displayNode, texture3D(gradeLutTexture), gradeLutTexture.image.width, gradeIntensity);
874
- renderPipeline.outputNode = isTraa ? gradedNode : smaa(gradedNode);
1521
+ const gradedNode = gradeLutTexture ? lut3D(displayNode, texture3D(gradeLutTexture), gradeLutTexture.image.width, gradeIntensity) : displayNode;
1522
+ if (isTraa) renderPipeline.outputNode = gradedNode;
1523
+ else {
1524
+ const smaaNode = smaa(gradedNode);
1525
+ passNodes.push(smaaNode);
1526
+ renderPipeline.outputNode = smaaNode;
1527
+ }
875
1528
  return postPipeline;
876
1529
  };
877
1530
  //#endregion
@@ -884,14 +1537,6 @@ const createPostUniforms = () => ({
884
1537
  outlineThickness: uniform(.003)
885
1538
  });
886
1539
  //#endregion
887
- //#region src/renderer/QualityTier.ts
888
- let QualityTier = /* @__PURE__ */ function(QualityTier) {
889
- QualityTier["High"] = "High";
890
- QualityTier["Low"] = "Low";
891
- QualityTier["Medium"] = "Medium";
892
- return QualityTier;
893
- }({});
894
- //#endregion
895
1540
  //#region src/renderer/QualityTierSettingsMap.ts
896
1541
  const QualityTierSettingsMap = {
897
1542
  ["High"]: {
@@ -900,6 +1545,7 @@ const QualityTierSettingsMap = {
900
1545
  godraysStepCount: 60,
901
1546
  grassDensity: 1,
902
1547
  isBloomEnabled: true,
1548
+ isOcclusionEnabled: true,
903
1549
  maxPixelRatio: 2,
904
1550
  shadowMapSize: 2048
905
1551
  },
@@ -909,6 +1555,7 @@ const QualityTierSettingsMap = {
909
1555
  godraysStepCount: 0,
910
1556
  grassDensity: .3,
911
1557
  isBloomEnabled: false,
1558
+ isOcclusionEnabled: false,
912
1559
  maxPixelRatio: 1,
913
1560
  shadowMapSize: 1024
914
1561
  },
@@ -918,11 +1565,37 @@ const QualityTierSettingsMap = {
918
1565
  godraysStepCount: 30,
919
1566
  grassDensity: .6,
920
1567
  isBloomEnabled: true,
1568
+ isOcclusionEnabled: false,
921
1569
  maxPixelRatio: 1.5,
922
1570
  shadowMapSize: 1024
923
1571
  }
924
1572
  };
925
1573
  //#endregion
1574
+ //#region src/renderer/attachInspector.ts
1575
+ const attachInspector = async (renderer) => {
1576
+ const { Inspector } = await import("three/examples/jsm/inspector/Inspector.js");
1577
+ if (renderer.inspector instanceof Inspector) return renderer.inspector;
1578
+ const inspector = new Inspector();
1579
+ renderer.inspector = inspector;
1580
+ return inspector;
1581
+ };
1582
+ //#endregion
1583
+ //#region src/renderer/constants.ts
1584
+ const GENSHIN_TONE_MAPPING = NeutralToneMapping;
1585
+ //#endregion
1586
+ //#region src/renderer/createGenshinRenderer.ts
1587
+ const createGenshinRenderer = (canvas) => {
1588
+ const isWindows = window.navigator.userAgent.includes("Windows");
1589
+ const renderer = new WebGPURenderer({
1590
+ antialias: false,
1591
+ canvas,
1592
+ ...isWindows ? {} : { powerPreference: "high-performance" }
1593
+ });
1594
+ renderer.toneMapping = GENSHIN_TONE_MAPPING;
1595
+ if (import.meta.env?.DEV && !window.navigator.webdriver) attachInspector(renderer);
1596
+ return renderer;
1597
+ };
1598
+ //#endregion
926
1599
  //#region src/renderer/getScreenDirection.ts
927
1600
  const X_AXIS = new Vector3(1, 0, 0);
928
1601
  const Y_AXIS = new Vector3(0, 1, 0);
@@ -981,27 +1654,30 @@ const createTileStreamer = ({ disposeTile, maxCachedCount, maxPendingCount, requ
981
1654
  });
982
1655
  evictStale();
983
1656
  },
984
- update: ({ count, keys }, drawn) => {
1657
+ update: (wanted, drawn, surrounding) => {
985
1658
  frame++;
986
- let coarsestLevel = 0;
987
- for (let index = 0; index < count; index++) {
1659
+ for (const { count, keys } of [
1660
+ wanted,
1661
+ drawn,
1662
+ surrounding
1663
+ ]) for (let index = 0; index < count; index++) {
988
1664
  const key = keys[index] ?? 0;
989
1665
  const cachedTile = cachedTileMap.get(key);
990
1666
  if (cachedTile) cachedTile.lastWantedFrame = frame;
991
- else {
992
- if (pendingWantedFrameMap.has(key)) pendingWantedFrameMap.set(key, frame);
993
- coarsestLevel = Math.max(coarsestLevel, getTerrainTileLevel(key));
994
- }
995
- }
996
- for (let index = 0; index < drawn.count; index++) {
997
- const cachedTile = cachedTileMap.get(drawn.keys[index] ?? 0);
998
- if (cachedTile) cachedTile.lastWantedFrame = frame;
1667
+ else if (pendingWantedFrameMap.has(key)) pendingWantedFrameMap.set(key, frame);
999
1668
  }
1000
- for (let level = coarsestLevel; level >= 0 && pendingWantedFrameMap.size < maxPendingCount; level--) for (let index = 0; index < count && pendingWantedFrameMap.size < maxPendingCount; index++) {
1001
- const key = keys[index] ?? 0;
1002
- if (getTerrainTileLevel(key) !== level || cachedTileMap.has(key) || pendingWantedFrameMap.has(key)) continue;
1003
- pendingWantedFrameMap.set(key, frame);
1004
- requestTile(key);
1669
+ for (const { count, keys } of [wanted, surrounding]) {
1670
+ let coarsestLevel = 0;
1671
+ for (let index = 0; index < count; index++) {
1672
+ const key = keys[index] ?? 0;
1673
+ if (!cachedTileMap.has(key)) coarsestLevel = Math.max(coarsestLevel, getTerrainTileLevel(key));
1674
+ }
1675
+ for (let level = coarsestLevel; level >= 0 && pendingWantedFrameMap.size < maxPendingCount; level--) for (let index = 0; index < count && pendingWantedFrameMap.size < maxPendingCount; index++) {
1676
+ const key = keys[index] ?? 0;
1677
+ if (getTerrainTileLevel(key) !== level || cachedTileMap.has(key) || pendingWantedFrameMap.has(key)) continue;
1678
+ pendingWantedFrameMap.set(key, frame);
1679
+ requestTile(key);
1680
+ }
1005
1681
  }
1006
1682
  }
1007
1683
  };
@@ -1084,6 +1760,25 @@ const computeTerrainTile = ({ cellsPerSide, finestTileSize, getHeight, key, writ
1084
1760
  };
1085
1761
  };
1086
1762
  //#endregion
1763
+ //#region src/terrain/createGaussianHillsHeight.ts
1764
+ const REACH_WIDTHS = 3;
1765
+ const CELL_SIZE = 64;
1766
+ const toCellKey = (column, row) => `${column},${row}`;
1767
+ const createGaussianHillsHeight = ({ base, hills }) => {
1768
+ const filed = hills.flatMap((hill) => {
1769
+ const reach = REACH_WIDTHS * hill.width;
1770
+ const cells = [];
1771
+ for (let column = Math.floor((hill.x - reach) / CELL_SIZE); column <= Math.floor((hill.x + reach) / CELL_SIZE); column++) for (let row = Math.floor((hill.z - reach) / CELL_SIZE); row <= Math.floor((hill.z + reach) / CELL_SIZE); row++) cells.push([toCellKey(column, row), hill]);
1772
+ return cells;
1773
+ });
1774
+ const cellHillsMap = new Map(Array.from(Map.groupBy(filed, ([key]) => key), ([key, entries]) => [key, entries.map(([, hill]) => hill)]));
1775
+ return (x, z) => {
1776
+ let total = base;
1777
+ for (const { height, width, x: hillX, z: hillZ } of cellHillsMap.get(toCellKey(Math.floor(x / CELL_SIZE), Math.floor(z / CELL_SIZE))) ?? []) total += height * Math.exp(-((x - hillX) ** 2 + (z - hillZ) ** 2) / (2 * width * width));
1778
+ return total;
1779
+ };
1780
+ };
1781
+ //#endregion
1087
1782
  //#region src/terrain/createTerrainMaterial.ts
1088
1783
  const createTerrainMaterial = ({ finestRange, morphShare }, toonMaterialOptions, waterUniforms) => {
1089
1784
  const terrainMaterial = createToonMaterial({
@@ -1250,7 +1945,7 @@ const createGrassGeometry = (segmentCount, bladeCount) => {
1250
1945
  const { indices, positions } = computeGrassBlade(segmentCount);
1251
1946
  const grassGeometry = new InstancedBufferGeometry();
1252
1947
  grassGeometry.setAttribute("position", new BufferAttribute(positions, 3));
1253
- grassGeometry.setAttribute("normal", new BufferAttribute(Float32Array.from({ length: positions.length }, (_, index) => index % 3 === 1 ? 1 : 0), 3));
1948
+ grassGeometry.setAttribute("normal", new BufferAttribute(Float32Array.from({ length: positions.length }, (_value, index) => index % 3 === 1 ? 1 : 0), 3));
1254
1949
  grassGeometry.setIndex(new BufferAttribute(indices, 1));
1255
1950
  grassGeometry.instanceCount = bladeCount;
1256
1951
  return grassGeometry;
@@ -1316,7 +2011,7 @@ const createGroundCapture = (size, resolution) => {
1316
2011
  };
1317
2012
  //#endregion
1318
2013
  //#region src/vegetation/renderGroundCapture.ts
1319
- const clearColor = new Color();
2014
+ const CLEAR_COLOR = new Color();
1320
2015
  const previousClearColor = new Color();
1321
2016
  const renderGroundCapture = (renderer, scene, { camera, center, material, renderTarget }, { maxHeight }, worldCenter, origin) => {
1322
2017
  center.value.set(worldCenter.x, worldCenter.z);
@@ -1331,7 +2026,7 @@ const renderGroundCapture = (renderer, scene, { camera, center, material, render
1331
2026
  const previousBackgroundNode = scene.backgroundNode;
1332
2027
  scene.overrideMaterial = material;
1333
2028
  scene.backgroundNode = null;
1334
- renderer.setClearColor(clearColor, 0);
2029
+ renderer.setClearColor(CLEAR_COLOR, 0);
1335
2030
  renderer.setRenderTarget(renderTarget);
1336
2031
  renderer.clear();
1337
2032
  renderer.render(scene, camera);
@@ -1345,6 +2040,7 @@ const renderGroundCapture = (renderer, scene, { camera, center, material, render
1345
2040
  const createUnderwaterFogState = () => ({
1346
2041
  aboveDensity: 0,
1347
2042
  aboveHeightFalloff: 0,
2043
+ aboveMaxOpacity: 1,
1348
2044
  aboveStartDistance: 0,
1349
2045
  isUnderwater: false
1350
2046
  });
@@ -1363,18 +2059,21 @@ const createWaterUniforms = () => ({
1363
2059
  });
1364
2060
  //#endregion
1365
2061
  //#region src/water/updateUnderwaterFog.ts
1366
- const updateUnderwaterFog = (eyeHeight, { level, underwaterFogColor, underwaterFogDensity }, { color, density, heightFalloff, startDistance }, underwaterFogState) => {
2062
+ const updateUnderwaterFog = (eyeHeight, { level, underwaterFogColor, underwaterFogDensity }, { color, density, heightFalloff, maxOpacity, startDistance }, underwaterFogState) => {
1367
2063
  const isUnderwater = eyeHeight < level.value;
1368
2064
  if (isUnderwater && !underwaterFogState.isUnderwater) {
1369
2065
  underwaterFogState.aboveDensity = density.value;
1370
2066
  underwaterFogState.aboveHeightFalloff = heightFalloff.value;
2067
+ underwaterFogState.aboveMaxOpacity = maxOpacity.value;
1371
2068
  underwaterFogState.aboveStartDistance = startDistance.value;
1372
2069
  density.value = underwaterFogDensity.value;
1373
2070
  heightFalloff.value = 0;
2071
+ maxOpacity.value = 1;
1374
2072
  startDistance.value = 0;
1375
2073
  } else if (!isUnderwater && underwaterFogState.isUnderwater) {
1376
2074
  density.value = underwaterFogState.aboveDensity;
1377
2075
  heightFalloff.value = underwaterFogState.aboveHeightFalloff;
2076
+ maxOpacity.value = underwaterFogState.aboveMaxOpacity;
1378
2077
  startDistance.value = underwaterFogState.aboveStartDistance;
1379
2078
  }
1380
2079
  underwaterFogState.isUnderwater = isUnderwater;
@@ -1417,4 +2116,4 @@ const computeOutlineDistance = (outline, x, z) => {
1417
2116
  return isInside ? 0 : Math.sqrt(nearestSquared);
1418
2117
  };
1419
2118
  //#endregion
1420
- export { AntialiasingMode, GAME_MINUTES_PER_SECOND, GENSHIN_TONE_MAPPING, MINUTES_PER_DAY, QualityTier, QualityTierSettingsMap, TERRAIN_LAYER, TILE_INDEX_OFFSET, TILE_INDEX_SPAN, ToonNodeMaterial, advanceGameClock, applySkyState, computeGradeLut, computeGrassBlade, computeLeafCards, computeOriginShift, computeOutlineDistance, computeRampValues, computeSunDirection, computeTerrainIndices, computeTerrainTile, computeTreeSkeleton, createCausticsNode, createCloudAtlasTexture, createCloudSpriteMaterial, createFogUniforms, createGenshinRenderer, createGodraysLight, createGradeLutTexture, createGrassGeometry, createGrassMaterial, createGroundCapture, createHeightFogNode, createLatheStackGeometry, createLeafMaterial, createLightUniforms, createPostPipeline, createPostUniforms, createRampTexture, createRimNode, createSeededRandom, createSilhouetteGeometry, createSimplexNoise, createSkyNode, createSkyState, createSkyUniforms, createStatueGeometry, createSunLight, createTerrainMaterial, createTerrainSelection, createTerrainTileGeometry, createTileStreamer, createToonMaterial, createTreeGeometry, createUnderwaterFogState, createWaterMaterial, createWaterUniforms, createWindNode, createWindUniforms, getCloudAtlasColumns, getParentTerrainTileKey, getScreenDirection, getTerrainTileColumn, getTerrainTileKey, getTerrainTileLevel, getTerrainTileRow, placeCloudBand, renderGroundCapture, resolveTerrainDraws, sampleSkyState, selectTerrainTiles, toSceneColor, updateUnderwaterFog };
2119
+ export { A4_FREQUENCY, A4_PITCH, AntialiasingMode, DEFAULT_SKY_GRADIENT, DEFAULT_SKY_SHAPE, GAME_MINUTES_PER_SECOND, GENSHIN_TONE_MAPPING, LEAST_DIVISOR, MAX_BYTE, MIDI_VELOCITY_MAX, MINUTES_PER_DAY, MUSIC_EXPRESSION_WINDOW_SECONDS, MUSIC_LOOKAHEAD_SECONDS, MUSIC_NOISE_BAND_CENTRES, MUSIC_NOISE_LENGTH, MUSIC_SCHEDULE_INTERVAL_MS, QualityTier, QualityTierSettingsMap, RELEASE_TIME_CONSTANTS, STONE_HARMONIC_COUNT, STONE_HEIGHT_FALLOFF, STONE_MASK_OUTPUT, STONE_RAMP_KNOT_COUNT, STONE_SHADOW_EXPONENT, StoneLightingModel, StoneNodeMaterial, TERRAIN_LAYER, TILE_INDEX_OFFSET, TILE_INDEX_SPAN, ToonNodeMaterial, addSmoothLoop, advanceGameClock, applySkyState, applyStoneLight, attachInspector, collectMusicNotes, collectMusicSegments, computeGradeLut, computeGrassBlade, computeLeafCards, computeNoiseSamples, computeOriginShift, computeOutlineDistance, computeRampValues, computeSoundEffectSamples, computeStoneHarmonics, computeStoneRampCoordinate, computeSunDirection, computeTerrainIndices, computeTerrainTile, computeTreeSkeleton, createBoxesGeometry, createCausticsNode, createCloudAtlasTexture, createCloudBandSprite, createFogUniforms, createGaussianHillsHeight, createGenshinRenderer, createGodraysLight, createGradeLutTexture, createGrassGeometry, createGrassMaterial, createGroundCapture, createHeightFogNode, createInstrumentWave, createLatheStackGeometry, createLeafMaterial, createLightUniforms, createMusicPlayer, createNoiseBuffer, createOcclusionNode, createPostPipeline, createPostUniforms, createRampTexture, createRimNode, createSeededRandom, createSimplexNoise, createSkyColorNode, createSkyGradientTexture, createSkyNode, createSkyState, createSkyUniforms, createSoundEffectBuffer, createStatueGeometry, createStoneLightUniforms, createStoneMaterial, createSunLight, createSunVisibilityNode, createTerrainMaterial, createTerrainSelection, createTerrainTileGeometry, createTileStreamer, createToonMaterial, createTreeGeometry, createUnderwaterFogState, createWaterMaterial, createWaterUniforms, createWindNode, createWindUniforms, getCloudAtlasColumns, getParentTerrainTileKey, getScreenDirection, getTerrainTileColumn, getTerrainTileKey, getTerrainTileLevel, getTerrainTileRow, mergeGeometryParts, orderByViewDepth, placeCloudBand, renderGroundCapture, renderMusicSegment, resolveTerrainDraws, sampleSkyState, scheduleMusicExpression, scheduleMusicNote, selectMusicSample, selectTerrainTiles, toSceneColor, toneMapNeutral, transformFourier, updateUnderwaterFog };