genshin-engine 3.9.0 → 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, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, ClampToEdgeWrapping, Color, CylinderGeometry, Data3DTexture, DataTexture, DirectionalLight, DoubleSide, 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, exp, exp2, float, getViewPosition, hash, instanceIndex, instancedBufferAttribute, max, min, 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, saturate, screenUV, sin, smoothstep, step, texture, texture3D, time, uniform, uv, varying, vec2, vec3, vec4, velocity, viewportDepthTexture, viewportSharedTexture } from "three/tsl";
3
- import { MeshBasicNodeMaterial, MeshPhysicalNodeMaterial, 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";
@@ -22,10 +23,7 @@ const DEFAULT_SKY_SHAPE = {
22
23
  const DEFAULT_SAMPLE_COUNT = 33;
23
24
  const DEFAULT_SKY_GRADIENT = {
24
25
  green: Array.from({ length: DEFAULT_SAMPLE_COUNT }, () => 0),
25
- red: Array.from({ length: DEFAULT_SAMPLE_COUNT }, (_, index) => {
26
- const share = index / 32;
27
- return 1 - share * share * (3 - 2 * share);
28
- })
26
+ red: Array.from({ length: DEFAULT_SAMPLE_COUNT }, (_value, index) => 1 - MathUtils.smoothstep(index, 0, 32))
29
27
  };
30
28
  //#endregion
31
29
  //#region src/post/toneMapNeutral.ts
@@ -33,44 +31,88 @@ const TOE_END = .08;
33
31
  const TOE_OFFSET = .04;
34
32
  const COMPRESSION_START = .76;
35
33
  const DESATURATION = .15;
36
- const toneMapNeutral = ([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
+ ]) => {
37
40
  const lowest = Math.min(red, green, blue);
38
41
  const offset = lowest < TOE_END ? lowest - 6.25 * lowest * lowest : TOE_OFFSET;
39
- const offsetColor = [
40
- red - offset,
41
- green - offset,
42
- blue - offset
43
- ];
44
- const peak = Math.max(...offsetColor);
45
- 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
+ }
46
52
  const headroom = .24;
47
53
  const newPeak = 1 - headroom * headroom / (peak + headroom - COMPRESSION_START);
48
54
  const whiteShare = 1 - 1 / (DESATURATION * (peak - newPeak) + 1);
49
- 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;
50
59
  };
51
60
  //#endregion
52
61
  //#region src/post/toSceneColor.ts
53
62
  const INVERSE_STEP_COUNT = 16;
54
63
  const SLOPE_NUDGE = 1e-4;
55
64
  const SCENE_CHANNEL_LIMIT = 1.5;
56
- const toSceneColor = (displayColor) => {
57
- const target = [
58
- displayColor.r,
59
- displayColor.g,
60
- displayColor.b
61
- ];
62
- let input = [...target];
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;
63
99
  for (let step = 0; step < INVERSE_STEP_COUNT; step++) {
64
- const current = input;
65
- const output = toneMapNeutral(current);
66
- input = current.map((channel, index) => {
67
- 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;
68
106
  nudged[index] = channel + SLOPE_NUDGE;
69
- const slope = ((toneMapNeutral(nudged)[index] ?? 0) - (output[index] ?? 0)) / SLOPE_NUDGE;
70
- return Math.min(Math.max(channel + ((target[index] ?? 0) - (output[index] ?? 0)) / slope, 0), SCENE_CHANNEL_LIMIT);
71
- });
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];
72
114
  }
73
- return new Color(...input);
115
+ return sceneColor.setRGB(input[0], input[1], input[2]);
74
116
  };
75
117
  //#endregion
76
118
  //#region src/atmosphere/applySkyState.ts
@@ -88,30 +130,32 @@ const applySkyState = (skyState, { fogUniforms, godraysLight, hemisphere, light,
88
130
  lightUniforms.lightColor.value.copy(lightColor);
89
131
  lightUniforms.sunDirection.value.copy(lightDirection);
90
132
  lightUniforms.rimColor.value.copy(horizonColor);
91
- fogUniforms.color.value.copy(toSceneColor(skyState.fogColor ?? horizonColor));
92
- fogUniforms.scatterColor.value.copy(toSceneColor(lightColor));
133
+ toSceneColor(skyState.fogColor ?? horizonColor, fogUniforms.color.value);
134
+ toSceneColor(lightColor, fogUniforms.scatterColor.value);
93
135
  fogUniforms.scatterDirection.value.copy(lightDirection);
94
136
  postUniforms.godraysColor.value.copy(lightColor);
95
- skyUniforms.cloudLitColor.value.copy(toSceneColor(skyState.cloudLitColor));
96
- skyUniforms.cloudShadeColor.value.copy(toSceneColor(skyState.cloudShadeColor));
97
- skyUniforms.cloudLitBackColor.value.copy(toSceneColor(skyState.cloudLitBackColor ?? skyState.cloudLitColor));
98
- skyUniforms.cloudShadeBackColor.value.copy(toSceneColor(skyState.cloudShadeBackColor ?? skyState.cloudShadeColor));
99
- skyUniforms.horizonColor.value.copy(toSceneColor(horizonColor));
100
- skyUniforms.horizonBackColor.value.copy(toSceneColor(skyState.horizonBackColor ?? horizonColor));
101
- skyUniforms.zenithBackColor.value.copy(toSceneColor(skyState.zenithBackColor ?? skyState.zenithColor));
102
- skyUniforms.haloColor.value.copy(toSceneColor(skyState.haloColor ?? BLACK));
103
- skyUniforms.sunHaloColor.value.copy(toSceneColor(skyState.sunHaloColor ?? BLACK));
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);
104
146
  skyUniforms.lightColor.value.copy(lightColor);
105
147
  skyUniforms.moonDirection.value.copy(skyState.moonDirection);
148
+ toSceneColor(skyState.moonGlowColor ?? BLACK, skyUniforms.moonGlowColor.value);
106
149
  skyUniforms.starIntensity.value = skyState.starIntensity;
107
150
  skyUniforms.sunDirection.value.copy(skyState.sunDirection);
108
- skyUniforms.zenithColor.value.copy(toSceneColor(skyState.zenithColor));
151
+ toSceneColor(skyState.zenithColor, skyUniforms.zenithColor.value);
109
152
  const { frontBackBlend, haloHeight, horizonBand, moonSize, sunHaloSize } = skyState.shape ?? DEFAULT_SKY_SHAPE;
110
153
  skyUniforms.frontBackBlend.value = frontBackBlend;
111
154
  skyUniforms.haloHeight.value = haloHeight;
112
155
  skyUniforms.horizonBand.value = horizonBand;
113
156
  skyUniforms.moonSize.value = moonSize;
114
157
  skyUniforms.sunHaloSize.value = sunHaloSize;
158
+ if (!godraysLight) return;
115
159
  godraysDirection.subVectors(godraysLight.position, godraysLight.target.position).normalize();
116
160
  if (godraysDirection.dot(lightDirection) >= GODRAYS_REDRAW_COSINE) return;
117
161
  godraysLight.position.copy(godraysLight.target.position).addScaledVector(lightDirection, lightDistance);
@@ -129,30 +173,33 @@ const computeSunDirection = (minutes, tilt, sunDirection) => {
129
173
  return sunDirection.set(Math.sin(hourAngle), height * Math.cos(tilt), height * Math.sin(tilt));
130
174
  };
131
175
  //#endregion
176
+ //#region src/atmosphere/getCloudAtlasColumns.ts
177
+ const getCloudAtlasColumns = (spriteCount) => Math.ceil(Math.sqrt(spriteCount));
178
+ //#endregion
132
179
  //#region src/materials/addSmoothLoop.ts
133
180
  const addSmoothLoop = (path, points) => {
134
181
  if (points.length < 3) return;
135
- const readMidpoint = (index) => {
182
+ const getMidpoint = (index) => {
136
183
  const [startX = 0, startY = 0] = points[index % points.length] ?? [];
137
184
  const [endX = 0, endY = 0] = points[(index + 1) % points.length] ?? [];
138
185
  return [(startX + endX) / 2, (startY + endY) / 2];
139
186
  };
140
- path.moveTo(...readMidpoint(points.length - 1));
141
- for (const [index, [x, y]] of points.entries()) path.quadraticCurveTo(x, y, ...readMidpoint(index));
187
+ path.moveTo(...getMidpoint(points.length - 1));
188
+ for (const [index, [x, y]] of points.entries()) path.quadraticCurveTo(x, y, ...getMidpoint(index));
142
189
  path.closePath();
143
190
  };
144
191
  //#endregion
145
192
  //#region src/atmosphere/createCloudAtlasTexture.ts
146
- const EDGE_BLUR_SHARE = .012;
147
- const getCloudAtlasColumns = (spriteCount) => Math.ceil(Math.sqrt(spriteCount));
193
+ const EDGE_BLUR_SHARE = .02;
194
+ const CROWN_BLUR_SHARE = .035;
148
195
  const createCloudAtlasTexture = (sprites, cellSize) => {
149
196
  const columns = getCloudAtlasColumns(sprites.length);
150
197
  const canvas = new OffscreenCanvas(columns * cellSize, columns * cellSize);
151
198
  const context = canvas.getContext("2d");
152
199
  if (context) {
153
200
  context.globalCompositeOperation = "lighter";
154
- context.filter = `blur(${cellSize * EDGE_BLUR_SHARE}px)`;
155
- const fill = (loops, column, row, style) => {
201
+ const fill = (loops, column, row, style, blurShare) => {
202
+ context.filter = `blur(${cellSize * blurShare}px)`;
156
203
  const path = new Path2D();
157
204
  const toCanvas = ([x, y]) => [(column + x) * cellSize, (row + 1 - y) * cellSize];
158
205
  for (const loop of loops) addSmoothLoop(path, loop.map((point) => toCanvas(point)));
@@ -162,8 +209,8 @@ const createCloudAtlasTexture = (sprites, cellSize) => {
162
209
  for (const [index, { lit, outline }] of sprites.entries()) {
163
210
  const column = index % columns;
164
211
  const row = Math.floor(index / columns);
165
- fill(outline, column, row, "#f00");
166
- fill(lit, column, row, "#0f0");
212
+ fill(outline, column, row, "#f00", EDGE_BLUR_SHARE);
213
+ fill(lit, column, row, "#0f0", CROWN_BLUR_SHARE);
167
214
  }
168
215
  }
169
216
  const texture = new CanvasTexture(canvas);
@@ -175,18 +222,38 @@ const createCloudAtlasTexture = (sprites, cellSize) => {
175
222
  };
176
223
  //#endregion
177
224
  //#region src/atmosphere/orderByViewDepth.ts
178
- const orderByViewDepth = (places, { elements }) => {
179
- const depths = places.map(([x = 0, y = 0, z = 0]) => elements[2] * x + elements[6] * y + elements[10] * z + elements[14]);
180
- return depths.map((_, index) => index).toSorted((first, second) => (depths[first] ?? 0) - (depths[second] ?? 0));
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;
181
247
  };
182
248
  //#endregion
183
249
  //#region src/atmosphere/createCloudBandSprite.ts
184
250
  const BELOW_FADE_START = .1;
185
251
  const BELOW_FADE_SCALE = 5;
186
252
  const COVERAGE_LIGHT_SHARE = .4;
187
- const EDGE_FADE_START = .15;
188
- const EDGE_FADE_END = .85;
253
+ const EDGE_FADE_START = 0;
254
+ const EDGE_FADE_END = 1;
189
255
  const DROPPED_COVERAGE = .01;
256
+ const MIN_CROWN_COVER = .05;
190
257
  const createCloudBandSprite = (atlas, clouds, { aspect, spriteCount }, skyUniforms) => {
191
258
  const { cloudCoverage, cloudFrontBackBlend, cloudLitBackColor, cloudLitColor, cloudShadeBackColor, cloudShadeColor, cloudSunBrighten, sunDirection } = skyUniforms;
192
259
  const columns = getCloudAtlasColumns(spriteCount);
@@ -196,6 +263,8 @@ const createCloudBandSprite = (atlas, clouds, { aspect, spriteCount }, skyUnifor
196
263
  const positions = new InstancedBufferAttribute(new Float32Array(clouds.length * 3), 3);
197
264
  const scales = new InstancedBufferAttribute(new Float32Array(clouds.length * 2), 2);
198
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);
199
268
  const mask = texture(atlas, uv().add(instancedBufferAttribute(cells)).div(columns));
200
269
  const place = instancedBufferAttribute(positions);
201
270
  const direction = place.sub(cameraPosition).normalize();
@@ -204,7 +273,8 @@ const createCloudBandSprite = (atlas, clouds, { aspect, spriteCount }, skyUnifor
204
273
  const toward = pow(max(sunCosine.mul(cloudFrontBackBlend).add(float(1).sub(cloudFrontBackBlend)), 0), 3);
205
274
  const lit = mix(cloudLitBackColor, cloudLitColor, toward);
206
275
  const shade = mix(cloudShadeBackColor, cloudShadeColor, toward);
207
- const cloudColor = mix(shade, lit, mask.g).add(lit.mul(cloudCoverage.mul(COVERAGE_LIGHT_SHARE))).mul(float(1).add(cloudSunBrighten.mul(sunCosine.mul(.5).add(.5))));
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))));
208
278
  const material = new SpriteNodeMaterial({
209
279
  depthWrite: false,
210
280
  transparent: true
@@ -212,25 +282,36 @@ const createCloudBandSprite = (atlas, clouds, { aspect, spriteCount }, skyUnifor
212
282
  material.positionNode = place;
213
283
  material.scaleNode = instancedBufferAttribute(scales);
214
284
  material.colorNode = cloudColor;
215
- 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))));
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));
216
286
  material.alphaTest = DROPPED_COVERAGE;
217
287
  const sprite = new Sprite(material);
218
288
  sprite.center.set(.5, 0);
219
289
  sprite.count = clouds.length;
220
290
  sprite.frustumCulled = false;
221
291
  const modelViewMatrix = new Matrix4();
222
- sprite.onBeforeRender = (_renderer, _scene, camera) => {
223
- modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, sprite.matrixWorld);
224
- for (const [instance, cloud] of orderByViewDepth(places, modelViewMatrix).entries()) {
225
- positions.set(places[cloud] ?? [], instance * 3);
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;
226
297
  scales.set(cloudScales[cloud] ?? [], instance * 2);
227
298
  cells.set(cloudCells[cloud] ?? [], instance * 2);
299
+ ranks.setX(instance, (cloud + .5) / clouds.length);
228
300
  }
229
- positions.needsUpdate = true;
230
301
  scales.needsUpdate = true;
231
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();
232
312
  };
233
313
  return {
314
+ cover,
234
315
  dispose: () => {
235
316
  material.dispose();
236
317
  },
@@ -239,15 +320,17 @@ const createCloudBandSprite = (atlas, clouds, { aspect, spriteCount }, skyUnifor
239
320
  };
240
321
  };
241
322
  //#endregion
323
+ //#region src/constants.ts
324
+ const MAX_BYTE = 255;
325
+ //#endregion
242
326
  //#region src/atmosphere/createSkyGradientTexture.ts
243
- const BYTE = 255;
244
327
  const createSkyGradientTexture = ({ green, red }) => {
245
328
  const width = Math.max(red.length, green.length, 1);
246
329
  const data = new Uint8Array(width * 4);
247
330
  for (let index = 0; index < width; index++) {
248
- data[index * 4] = Math.round((red[index] ?? 0) * BYTE);
249
- data[index * 4 + 1] = Math.round((green[index] ?? 0) * BYTE);
250
- data[index * 4 + 3] = BYTE;
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;
251
334
  }
252
335
  const texture = new DataTexture(data, width, 1, RGBAFormat, UnsignedByteType);
253
336
  texture.magFilter = LinearFilter;
@@ -359,7 +442,6 @@ const placeCloudBand = ({ count, distanceRange: [near, far], heightRange: [low,
359
442
  };
360
443
  //#endregion
361
444
  //#region src/atmosphere/sampleSkyState.ts
362
- const lerp = (from, to, amount) => from + (to - from) * amount;
363
445
  const sampleSkyState = (skyKeyframes, minutes, tilt, skyState) => {
364
446
  let nextIndex = 0;
365
447
  while (nextIndex < skyKeyframes.length && (skyKeyframes[nextIndex]?.minutes ?? 0) <= minutes) nextIndex++;
@@ -372,12 +454,12 @@ const sampleSkyState = (skyKeyframes, minutes, tilt, skyState) => {
372
454
  skyState.cloudLitColor.lerpColors(previous.cloudLitColor, next.cloudLitColor, amount);
373
455
  skyState.cloudShadeColor.lerpColors(previous.cloudShadeColor, next.cloudShadeColor, amount);
374
456
  skyState.hemisphereGroundColor.lerpColors(previous.hemisphereGroundColor, next.hemisphereGroundColor, amount);
375
- skyState.hemisphereIntensity = lerp(previous.hemisphereIntensity, next.hemisphereIntensity, amount);
457
+ skyState.hemisphereIntensity = MathUtils.lerp(previous.hemisphereIntensity, next.hemisphereIntensity, amount);
376
458
  skyState.hemisphereSkyColor.lerpColors(previous.hemisphereSkyColor, next.hemisphereSkyColor, amount);
377
459
  skyState.horizonColor.lerpColors(previous.horizonColor, next.horizonColor, amount);
378
460
  skyState.lightColor.lerpColors(previous.lightColor, next.lightColor, amount);
379
- skyState.lightIntensity = lerp(previous.lightIntensity, next.lightIntensity, amount);
380
- 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);
381
463
  skyState.zenithColor.lerpColors(previous.zenithColor, next.zenithColor, amount);
382
464
  computeSunDirection(minutes, tilt, skyState.sunDirection);
383
465
  skyState.moonDirection.copy(skyState.sunDirection).negate();
@@ -385,19 +467,309 @@ const sampleSkyState = (skyKeyframes, minutes, tilt, skyState) => {
385
467
  return skyState;
386
468
  };
387
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
388
755
  //#region src/clock/advanceGameClock.ts
389
756
  const advanceGameClock = (gameClock, deltaSeconds) => {
390
757
  gameClock.minutes = (gameClock.minutes + deltaSeconds * gameClock.minutesPerSecond) % MINUTES_PER_DAY;
391
758
  };
392
759
  //#endregion
393
- //#region src/kits/architecture/createBoxesGeometry.ts
394
- const createBoxesGeometry = (boxes) => {
395
- 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));
760
+ //#region src/kits/mergeGeometryParts.ts
761
+ const mergeGeometryParts = (parts) => {
396
762
  const geometry = mergeGeometries(parts) ?? new BufferGeometry();
397
763
  for (const part of parts) part.dispose();
398
764
  return geometry;
399
765
  };
400
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
401
773
  //#region src/kits/architecture/createLatheStackGeometry.ts
402
774
  const createLatheStackGeometry = ({ isFaceted, radialSegments, sections }) => {
403
775
  let footHeight = 0;
@@ -406,8 +778,7 @@ const createLatheStackGeometry = ({ isFaceted, radialSegments, sections }) => {
406
778
  footHeight += height;
407
779
  return isFaceted ? part.toNonIndexed() : part;
408
780
  });
409
- const stackGeometry = mergeGeometries(parts) ?? new BufferGeometry();
410
- for (const part of parts) part.dispose();
781
+ const stackGeometry = mergeGeometryParts(parts);
411
782
  if (isFaceted) stackGeometry.computeVertexNormals();
412
783
  return stackGeometry;
413
784
  };
@@ -429,24 +800,14 @@ const FIGURE_PROFILE = [
429
800
  new Vector2(0, 6.2)
430
801
  ];
431
802
  const createStatueGeometry = () => {
432
- const lowerStep = new CylinderGeometry(3.2, 3.4, .6, PLINTH_SIDES).translate(0, .3, 0);
433
- const upperStep = new CylinderGeometry(2.6, 2.8, .6, PLINTH_SIDES).translate(0, .9, 0);
434
- const pedestal = new CylinderGeometry(1.8, 2.1, 2.2, PLINTH_SIDES).translate(0, 2.3, 0);
435
- const figure = new LatheGeometry(FIGURE_PROFILE, FIGURE_SEGMENTS).translate(0, 3.4, 0);
436
- const parts = [
437
- lowerStep,
438
- upperStep,
439
- pedestal,
440
- figure
441
- ].map((part) => part.toNonIndexed());
442
- const statueGeometry = mergeGeometries(parts) ?? new BufferGeometry();
443
- for (const part of [
444
- lowerStep,
445
- upperStep,
446
- pedestal,
447
- figure,
448
- ...parts
449
- ]) 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();
450
811
  statueGeometry.computeVertexNormals();
451
812
  return statueGeometry;
452
813
  };
@@ -580,8 +941,7 @@ const createTreeGeometry = (treeOptions) => {
580
941
  segmentGeometry.translate((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2);
581
942
  return segmentGeometry;
582
943
  });
583
- const branchGeometry = mergeGeometries(segmentGeometries) ?? new BufferGeometry();
584
- for (const segmentGeometry of segmentGeometries) segmentGeometry.dispose();
944
+ const branchGeometry = mergeGeometryParts(segmentGeometries);
585
945
  const { indices, normals, positions, uvs } = computeLeafCards(clusterCenters, treeOptions);
586
946
  const leafGeometry = new BufferGeometry();
587
947
  leafGeometry.setAttribute("position", new BufferAttribute(positions, 3));
@@ -596,14 +956,12 @@ const createTreeGeometry = (treeOptions) => {
596
956
  };
597
957
  //#endregion
598
958
  //#region src/materials/computeRampValues.ts
599
- const MAX_BYTE$1 = 255;
600
959
  const computeRampValues = ({ resolution, softness, terminator }) => {
601
960
  const lowerEdge = terminator - softness / 2;
602
961
  const upperEdge = terminator + softness / 2;
603
962
  return Uint8Array.from({ length: resolution }, (_value, index) => {
604
963
  const coordinate = (index + .5) / resolution;
605
- const step = Math.min(Math.max((coordinate - lowerEdge) / (upperEdge - lowerEdge), 0), 1);
606
- return Math.round(step * step * (3 - 2 * step) * MAX_BYTE$1);
964
+ return Math.round(MathUtils.smoothstep(coordinate, lowerEdge, upperEdge) * 255);
607
965
  });
608
966
  };
609
967
  //#endregion
@@ -617,7 +975,58 @@ const createRampTexture = (rampOptions) => {
617
975
  return rampTexture;
618
976
  };
619
977
  //#endregion
620
- //#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
621
1030
  var ToonNodeMaterial = class extends MeshToonNodeMaterial {
622
1031
  emissiveNode = null;
623
1032
  isMeshToonNodeMaterial;
@@ -627,6 +1036,57 @@ var ToonNodeMaterial = class extends MeshToonNodeMaterial {
627
1036
  }
628
1037
  };
629
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
630
1090
  //#region src/nodes/createCausticsNode.ts
631
1091
  const CAUSTIC_SCALE = .35;
632
1092
  const CAUSTIC_SPEED = .6;
@@ -640,21 +1100,6 @@ const createCausticsNode = ({ lightColor }, { causticStrength, deepDepth, level
640
1100
  return lightColor.mul(veins.mul(fade).mul(causticStrength));
641
1101
  };
642
1102
  //#endregion
643
- //#region src/renderer/constants.ts
644
- const GENSHIN_TONE_MAPPING = NeutralToneMapping;
645
- //#endregion
646
- //#region src/nodes/createGenshinRenderer.ts
647
- const createGenshinRenderer = (canvas) => {
648
- const isWindows = window.navigator.userAgent.includes("Windows");
649
- const renderer = new WebGPURenderer({
650
- antialias: false,
651
- canvas,
652
- ...isWindows ? {} : { powerPreference: "high-performance" }
653
- });
654
- renderer.toneMapping = GENSHIN_TONE_MAPPING;
655
- return renderer;
656
- };
657
- //#endregion
658
1103
  //#region src/nodes/createRimNode.ts
659
1104
  const RIM_POWER = 4;
660
1105
  const createRimNode = ({ rimColor, rimStrength, sunDirection }) => {
@@ -715,7 +1160,6 @@ const createLightUniforms = () => ({
715
1160
  });
716
1161
  //#endregion
717
1162
  //#region src/nodes/createSkyColorNode.ts
718
- const LEAST_DIVISOR$1 = 1e-4;
719
1163
  const HALO_TOWARD_START = .3;
720
1164
  const HALO_TOWARD_SPAN = .7;
721
1165
  const HALO_SUN_HEIGHT_START = .2;
@@ -731,8 +1175,8 @@ const createSkyColorNode = ({ frontBackBlend, haloColor, haloHeight, horizonBack
731
1175
  const toward = pow(max(sunCosine.mul(frontBackBlend).add(float(1).sub(frontBackBlend)), 0), 3);
732
1176
  const top = mix(zenithBackColor, zenithColor, toward);
733
1177
  const bottom = mix(horizonBackColor, horizonColor, toward);
734
- const bottomShare = texture(gradient, vec2(elevation.div(max(horizonBand, LEAST_DIVISOR$1)), .5)).r;
735
- const haloShare = texture(gradient, vec2(elevation.div(max(haloHeight, LEAST_DIVISOR$1)), .5)).g;
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;
736
1180
  const sunSide = saturate(sunCosine.mul(.5).add(.5));
737
1181
  const towardSun = smoothstep(0, 1, max(sunSide.sub(HALO_TOWARD_START).div(HALO_TOWARD_SPAN), 0));
738
1182
  const sunUp = smoothstep(0, 1, saturate(abs(sunDirection.y).sub(HALO_SUN_HEIGHT_START).div(HALO_SUN_HEIGHT_SPAN)));
@@ -752,7 +1196,6 @@ const CLOUD_EDGE = .04;
752
1196
  const CLOUD_SHADE_DEPTH = .18;
753
1197
  const STAR_SCALE = 420;
754
1198
  const STAR_THRESHOLD = .9975;
755
- const LEAST_DIVISOR = 1e-4;
756
1199
  const MOON_GLOW_SPREAD = .1;
757
1200
  const MOON_GLOW_POWER = 6;
758
1201
  const createSkyNode = (uniforms, gradient) => {
@@ -779,15 +1222,28 @@ const createSkyNode = (uniforms, gradient) => {
779
1222
  })();
780
1223
  };
781
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
782
1243
  //#region src/nodes/createStoneMaterial.ts
783
- const createStoneMaterial = ({ albedo, rimColor, rimPower, rimStrength, smoothness, specularColor }, glowNode = vec3(0)) => {
784
- const material = new MeshPhysicalNodeMaterial({
785
- color: new Color(albedo),
786
- metalness: 0,
787
- roughness: 1 - smoothness,
788
- specularColor: new Color().fromArray(specularColor),
789
- specularIntensity: 1
790
- });
1244
+ const createStoneMaterial = ({ albedo, rimColor, rimPower, rimStrength }, stoneLight, glowNode = vec3(0)) => {
1245
+ const material = new StoneNodeMaterial(stoneLight);
1246
+ material.colorNode = color(new Color(albedo));
791
1247
  const fresnel = float(1).sub(normalView.dot(positionViewDirection).saturate()).pow(rimPower);
792
1248
  material.emissiveNode = color(new Color().fromArray(rimColor)).mul(fresnel.mul(rimStrength)).add(glowNode);
793
1249
  return material;
@@ -888,14 +1344,13 @@ const createSimplexNoise = (seed) => {
888
1344
  };
889
1345
  //#endregion
890
1346
  //#region src/post/computeGradeLut.ts
891
- const MAX_BYTE = 255;
892
1347
  const RED_LUMINANCE = .2126;
893
1348
  const GREEN_LUMINANCE = .7152;
894
1349
  const BLUE_LUMINANCE = .0722;
895
1350
  const CONTRAST_PIVOT = .5;
896
1351
  const toGradedByte = (channel, luminance, saturation, contrast, tint) => {
897
1352
  const graded = (luminance + (channel - luminance) * saturation - CONTRAST_PIVOT) * contrast + CONTRAST_PIVOT + tint;
898
- return Math.round(Math.min(Math.max(graded, 0), 1) * MAX_BYTE);
1353
+ return Math.round(Math.min(Math.max(graded, 0), 1) * 255);
899
1354
  };
900
1355
  const computeGradeLut = ({ contrast, highlightTint, saturation, shadowTint, size }) => {
901
1356
  const values = new Uint8Array(size * size * size * 4);
@@ -914,7 +1369,7 @@ const computeGradeLut = ({ contrast, highlightTint, saturation, shadowTint, size
914
1369
  values[offset] = toGradedByte(red, luminance, saturation, contrast, redTint);
915
1370
  values[offset + 1] = toGradedByte(green, luminance, saturation, contrast, greenTint);
916
1371
  values[offset + 2] = toGradedByte(blue, luminance, saturation, contrast, blueTint);
917
- values[offset + 3] = MAX_BYTE;
1372
+ values[offset + 3] = 255;
918
1373
  offset += 4;
919
1374
  }
920
1375
  return values;
@@ -926,6 +1381,7 @@ const createFogUniforms = () => ({
926
1381
  color: uniform(new Color()),
927
1382
  density: uniform(0),
928
1383
  heightFalloff: uniform(0),
1384
+ maxOpacity: uniform(1),
929
1385
  scatterColor: uniform(new Color()),
930
1386
  scatterDirection: uniform(new Vector3(0, 1, 0)),
931
1387
  scatterPower: uniform(1),
@@ -961,7 +1417,7 @@ const createGradeLutTexture = (gradeOptions) => {
961
1417
  //#endregion
962
1418
  //#region src/post/createHeightFogNode.ts
963
1419
  const MIN_RAY_CLIMB = 1e-6;
964
- 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) => {
965
1421
  const cameraWorldMatrix = uniform(camera.matrixWorld);
966
1422
  const cameraProjectionMatrixInverse = uniform(camera.projectionMatrixInverse);
967
1423
  return Fn(() => {
@@ -981,59 +1437,94 @@ const createHeightFogNode = (colorNode, depthNode, camera, { baseHeight, color,
981
1437
  If(climb.abs().greaterThan(MIN_RAY_CLIMB), () => {
982
1438
  opticalDepth.assign(densityAtStart.mul(float(1).sub(exp(fogLength.mul(climb).negate()))).div(climb));
983
1439
  });
984
- const opacity = float(1).sub(exp(opticalDepth.negate()));
985
- const scatter = pow(max(ray.normalize().dot(scatterDirection), 0), scatterPower).mul(scatterStrength);
986
- 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));
987
1445
  });
988
1446
  return output;
989
1447
  })();
990
1448
  };
991
1449
  //#endregion
992
- //#region src/renderer/AntialiasingMode.ts
993
- let AntialiasingMode = /* @__PURE__ */ function(AntialiasingMode) {
994
- AntialiasingMode["Smaa"] = "Smaa";
995
- AntialiasingMode["Traa"] = "Traa";
996
- return AntialiasingMode;
997
- }({});
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
+ };
998
1460
  //#endregion
999
1461
  //#region src/post/createPostPipeline.ts
1000
1462
  const BLOOM_STRENGTH = .35;
1001
1463
  const BLOOM_RADIUS = .4;
1002
1464
  const BLOOM_THRESHOLD = .85;
1003
1465
  const MIN_VIEW_DISTANCE = .001;
1004
- 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 }) => {
1005
1467
  const renderPipeline = new RenderPipeline(renderer);
1006
1468
  renderPipeline.outputColorTransform = false;
1007
1469
  const viewDistance = modelViewMatrix.mul(vec4(positionLocal, 1)).z.negate().max(MIN_VIEW_DISTANCE);
1008
1470
  const thicknessNode = outlineThickness.mul(outlineFadeDistance.div(viewDistance).min(1));
1009
1471
  const scenePass = new ToonOutlinePassNode(scene, camera, outlineColor, thicknessNode, float(1));
1010
1472
  const isTraa = antialiasingMode === "Traa";
1011
- if (isTraa) scenePass.setMRT(mrt({
1012
- output,
1013
- velocity
1014
- }));
1473
+ const sceneOutputs = { output };
1474
+ if (isTraa) sceneOutputs.velocity = velocity;
1475
+ if (stoneLight) sceneOutputs[STONE_MASK_OUTPUT] = float(0);
1476
+ scenePass.setMRT(mrt(sceneOutputs));
1015
1477
  const sceneColor = scenePass.getTextureNode("output");
1016
1478
  const sceneDepth = scenePass.getTextureNode("depth");
1017
- 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
+ };
1018
1487
  let litNode = sceneColor;
1019
- 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) {
1020
1494
  const godraysNode = godrays(sceneDepth, camera, godraysLight);
1021
1495
  godraysNode.raymarchSteps.value = godraysStepCount;
1022
1496
  const blurredGodrays = bilateralBlur(godraysNode.getTextureNode());
1023
- litNode = depthAwareBlend(sceneColor, blurredGodrays.getTextureNode(), sceneDepth, camera, { blendColor: godraysColor });
1497
+ passNodes.push(godraysNode, blurredGodrays);
1498
+ litNode = depthAwareBlend(litNode, blurredGodrays.getTextureNode(), sceneDepth, camera, { blendColor: godraysColor });
1024
1499
  postPipeline.godraysNode = godraysNode;
1025
1500
  }
1026
- 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
+ });
1027
1507
  let bloomedNode = foggedNode;
1028
- if (isBloomEnabled) {
1508
+ if (isBloomed && isBloomEnabled) {
1029
1509
  const bloomNode = bloom(foggedNode, BLOOM_STRENGTH, BLOOM_RADIUS, BLOOM_THRESHOLD);
1510
+ passNodes.push(bloomNode);
1030
1511
  bloomedNode = foggedNode.add(bloomNode);
1031
1512
  postPipeline.bloomNode = bloomNode;
1032
1513
  }
1033
- 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
+ }
1034
1520
  const displayNode = renderOutput(resolvedNode);
1035
- const gradedNode = lut3D(displayNode, texture3D(gradeLutTexture), gradeLutTexture.image.width, gradeIntensity);
1036
- 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
+ }
1037
1528
  return postPipeline;
1038
1529
  };
1039
1530
  //#endregion
@@ -1046,14 +1537,6 @@ const createPostUniforms = () => ({
1046
1537
  outlineThickness: uniform(.003)
1047
1538
  });
1048
1539
  //#endregion
1049
- //#region src/renderer/QualityTier.ts
1050
- let QualityTier = /* @__PURE__ */ function(QualityTier) {
1051
- QualityTier["High"] = "High";
1052
- QualityTier["Low"] = "Low";
1053
- QualityTier["Medium"] = "Medium";
1054
- return QualityTier;
1055
- }({});
1056
- //#endregion
1057
1540
  //#region src/renderer/QualityTierSettingsMap.ts
1058
1541
  const QualityTierSettingsMap = {
1059
1542
  ["High"]: {
@@ -1062,6 +1545,7 @@ const QualityTierSettingsMap = {
1062
1545
  godraysStepCount: 60,
1063
1546
  grassDensity: 1,
1064
1547
  isBloomEnabled: true,
1548
+ isOcclusionEnabled: true,
1065
1549
  maxPixelRatio: 2,
1066
1550
  shadowMapSize: 2048
1067
1551
  },
@@ -1071,6 +1555,7 @@ const QualityTierSettingsMap = {
1071
1555
  godraysStepCount: 0,
1072
1556
  grassDensity: .3,
1073
1557
  isBloomEnabled: false,
1558
+ isOcclusionEnabled: false,
1074
1559
  maxPixelRatio: 1,
1075
1560
  shadowMapSize: 1024
1076
1561
  },
@@ -1080,11 +1565,37 @@ const QualityTierSettingsMap = {
1080
1565
  godraysStepCount: 30,
1081
1566
  grassDensity: .6,
1082
1567
  isBloomEnabled: true,
1568
+ isOcclusionEnabled: false,
1083
1569
  maxPixelRatio: 1.5,
1084
1570
  shadowMapSize: 1024
1085
1571
  }
1086
1572
  };
1087
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
1088
1599
  //#region src/renderer/getScreenDirection.ts
1089
1600
  const X_AXIS = new Vector3(1, 0, 0);
1090
1601
  const Y_AXIS = new Vector3(0, 1, 0);
@@ -1143,27 +1654,30 @@ const createTileStreamer = ({ disposeTile, maxCachedCount, maxPendingCount, requ
1143
1654
  });
1144
1655
  evictStale();
1145
1656
  },
1146
- update: ({ count, keys }, drawn) => {
1657
+ update: (wanted, drawn, surrounding) => {
1147
1658
  frame++;
1148
- let coarsestLevel = 0;
1149
- 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++) {
1150
1664
  const key = keys[index] ?? 0;
1151
1665
  const cachedTile = cachedTileMap.get(key);
1152
1666
  if (cachedTile) cachedTile.lastWantedFrame = frame;
1153
- else {
1154
- if (pendingWantedFrameMap.has(key)) pendingWantedFrameMap.set(key, frame);
1155
- coarsestLevel = Math.max(coarsestLevel, getTerrainTileLevel(key));
1156
- }
1157
- }
1158
- for (let index = 0; index < drawn.count; index++) {
1159
- const cachedTile = cachedTileMap.get(drawn.keys[index] ?? 0);
1160
- if (cachedTile) cachedTile.lastWantedFrame = frame;
1667
+ else if (pendingWantedFrameMap.has(key)) pendingWantedFrameMap.set(key, frame);
1161
1668
  }
1162
- for (let level = coarsestLevel; level >= 0 && pendingWantedFrameMap.size < maxPendingCount; level--) for (let index = 0; index < count && pendingWantedFrameMap.size < maxPendingCount; index++) {
1163
- const key = keys[index] ?? 0;
1164
- if (getTerrainTileLevel(key) !== level || cachedTileMap.has(key) || pendingWantedFrameMap.has(key)) continue;
1165
- pendingWantedFrameMap.set(key, frame);
1166
- 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
+ }
1167
1681
  }
1168
1682
  }
1169
1683
  };
@@ -1246,6 +1760,25 @@ const computeTerrainTile = ({ cellsPerSide, finestTileSize, getHeight, key, writ
1246
1760
  };
1247
1761
  };
1248
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
1249
1782
  //#region src/terrain/createTerrainMaterial.ts
1250
1783
  const createTerrainMaterial = ({ finestRange, morphShare }, toonMaterialOptions, waterUniforms) => {
1251
1784
  const terrainMaterial = createToonMaterial({
@@ -1412,7 +1945,7 @@ const createGrassGeometry = (segmentCount, bladeCount) => {
1412
1945
  const { indices, positions } = computeGrassBlade(segmentCount);
1413
1946
  const grassGeometry = new InstancedBufferGeometry();
1414
1947
  grassGeometry.setAttribute("position", new BufferAttribute(positions, 3));
1415
- 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));
1416
1949
  grassGeometry.setIndex(new BufferAttribute(indices, 1));
1417
1950
  grassGeometry.instanceCount = bladeCount;
1418
1951
  return grassGeometry;
@@ -1478,7 +2011,7 @@ const createGroundCapture = (size, resolution) => {
1478
2011
  };
1479
2012
  //#endregion
1480
2013
  //#region src/vegetation/renderGroundCapture.ts
1481
- const clearColor = new Color();
2014
+ const CLEAR_COLOR = new Color();
1482
2015
  const previousClearColor = new Color();
1483
2016
  const renderGroundCapture = (renderer, scene, { camera, center, material, renderTarget }, { maxHeight }, worldCenter, origin) => {
1484
2017
  center.value.set(worldCenter.x, worldCenter.z);
@@ -1493,7 +2026,7 @@ const renderGroundCapture = (renderer, scene, { camera, center, material, render
1493
2026
  const previousBackgroundNode = scene.backgroundNode;
1494
2027
  scene.overrideMaterial = material;
1495
2028
  scene.backgroundNode = null;
1496
- renderer.setClearColor(clearColor, 0);
2029
+ renderer.setClearColor(CLEAR_COLOR, 0);
1497
2030
  renderer.setRenderTarget(renderTarget);
1498
2031
  renderer.clear();
1499
2032
  renderer.render(scene, camera);
@@ -1507,6 +2040,7 @@ const renderGroundCapture = (renderer, scene, { camera, center, material, render
1507
2040
  const createUnderwaterFogState = () => ({
1508
2041
  aboveDensity: 0,
1509
2042
  aboveHeightFalloff: 0,
2043
+ aboveMaxOpacity: 1,
1510
2044
  aboveStartDistance: 0,
1511
2045
  isUnderwater: false
1512
2046
  });
@@ -1525,18 +2059,21 @@ const createWaterUniforms = () => ({
1525
2059
  });
1526
2060
  //#endregion
1527
2061
  //#region src/water/updateUnderwaterFog.ts
1528
- const updateUnderwaterFog = (eyeHeight, { level, underwaterFogColor, underwaterFogDensity }, { color, density, heightFalloff, startDistance }, underwaterFogState) => {
2062
+ const updateUnderwaterFog = (eyeHeight, { level, underwaterFogColor, underwaterFogDensity }, { color, density, heightFalloff, maxOpacity, startDistance }, underwaterFogState) => {
1529
2063
  const isUnderwater = eyeHeight < level.value;
1530
2064
  if (isUnderwater && !underwaterFogState.isUnderwater) {
1531
2065
  underwaterFogState.aboveDensity = density.value;
1532
2066
  underwaterFogState.aboveHeightFalloff = heightFalloff.value;
2067
+ underwaterFogState.aboveMaxOpacity = maxOpacity.value;
1533
2068
  underwaterFogState.aboveStartDistance = startDistance.value;
1534
2069
  density.value = underwaterFogDensity.value;
1535
2070
  heightFalloff.value = 0;
2071
+ maxOpacity.value = 1;
1536
2072
  startDistance.value = 0;
1537
2073
  } else if (!isUnderwater && underwaterFogState.isUnderwater) {
1538
2074
  density.value = underwaterFogState.aboveDensity;
1539
2075
  heightFalloff.value = underwaterFogState.aboveHeightFalloff;
2076
+ maxOpacity.value = underwaterFogState.aboveMaxOpacity;
1540
2077
  startDistance.value = underwaterFogState.aboveStartDistance;
1541
2078
  }
1542
2079
  underwaterFogState.isUnderwater = isUnderwater;
@@ -1579,4 +2116,4 @@ const computeOutlineDistance = (outline, x, z) => {
1579
2116
  return isInside ? 0 : Math.sqrt(nearestSquared);
1580
2117
  };
1581
2118
  //#endregion
1582
- export { AntialiasingMode, DEFAULT_SKY_GRADIENT, DEFAULT_SKY_SHAPE, GAME_MINUTES_PER_SECOND, GENSHIN_TONE_MAPPING, MINUTES_PER_DAY, QualityTier, QualityTierSettingsMap, TERRAIN_LAYER, TILE_INDEX_OFFSET, TILE_INDEX_SPAN, ToonNodeMaterial, addSmoothLoop, advanceGameClock, applySkyState, computeGradeLut, computeGrassBlade, computeLeafCards, computeOriginShift, computeOutlineDistance, computeRampValues, computeSunDirection, computeTerrainIndices, computeTerrainTile, computeTreeSkeleton, createBoxesGeometry, createCausticsNode, createCloudAtlasTexture, createCloudBandSprite, createFogUniforms, createGenshinRenderer, createGodraysLight, createGradeLutTexture, createGrassGeometry, createGrassMaterial, createGroundCapture, createHeightFogNode, createLatheStackGeometry, createLeafMaterial, createLightUniforms, createPostPipeline, createPostUniforms, createRampTexture, createRimNode, createSeededRandom, createSimplexNoise, createSkyColorNode, createSkyGradientTexture, createSkyNode, createSkyState, createSkyUniforms, createStatueGeometry, createStoneMaterial, createSunLight, createTerrainMaterial, createTerrainSelection, createTerrainTileGeometry, createTileStreamer, createToonMaterial, createTreeGeometry, createUnderwaterFogState, createWaterMaterial, createWaterUniforms, createWindNode, createWindUniforms, getCloudAtlasColumns, getParentTerrainTileKey, getScreenDirection, getTerrainTileColumn, getTerrainTileKey, getTerrainTileLevel, getTerrainTileRow, orderByViewDepth, placeCloudBand, renderGroundCapture, resolveTerrainDraws, sampleSkyState, selectTerrainTiles, toSceneColor, toneMapNeutral, 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 };