genshin-engine 3.6.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/LICENSE +201 -0
- package/README.md +83 -0
- package/dist/index.d.ts +515 -0
- package/dist/index.js +1258 -0
- package/package.json +68 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1258 @@
|
|
|
1
|
+
import { Box3, BufferAttribute, BufferGeometry, Color, CylinderGeometry, Data3DTexture, DataTexture, DirectionalLight, DoubleSide, HalfFloatType, InstancedBufferGeometry, LatheGeometry, LinearFilter, OrthographicCamera, Quaternion, RedFormat, 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 { CSMShadowNode } from "three/examples/jsm/csm/CSMShadowNode.js";
|
|
4
|
+
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
|
5
|
+
import { MeshBasicNodeMaterial, MeshToonNodeMaterial, NeutralToneMapping, RenderPipeline, RenderTarget, ToonOutlinePassNode, WebGPURenderer } from "three/webgpu";
|
|
6
|
+
import { voronoi2d } from "three/examples/jsm/tsl/math/voronoiNoise.js";
|
|
7
|
+
import { bilateralBlur } from "three/examples/jsm/tsl/display/BilateralBlurNode.js";
|
|
8
|
+
import { bloom } from "three/examples/jsm/tsl/display/BloomNode.js";
|
|
9
|
+
import { depthAwareBlend } from "three/examples/jsm/tsl/display/depthAwareBlend.js";
|
|
10
|
+
import { godrays } from "three/examples/jsm/tsl/display/GodraysNode.js";
|
|
11
|
+
import { lut3D } from "three/examples/jsm/tsl/display/Lut3DNode.js";
|
|
12
|
+
import { smaa } from "three/examples/jsm/tsl/display/SMAANode.js";
|
|
13
|
+
import { traa } from "three/examples/jsm/tsl/display/TRAANode.js";
|
|
14
|
+
//#region src/atmosphere/applySkyState.ts
|
|
15
|
+
const GODRAYS_REDRAW_COSINE = Math.cos(.6 * Math.PI / 180);
|
|
16
|
+
const godraysDirection = new Vector3();
|
|
17
|
+
const applySkyState = (skyState, { fogUniforms, godraysLight, hemisphere, light, lightDistance, lightUniforms, postUniforms, skyUniforms }) => {
|
|
18
|
+
const { horizonColor, lightColor, lightDirection } = skyState;
|
|
19
|
+
light.position.copy(light.target.position).addScaledVector(lightDirection, lightDistance);
|
|
20
|
+
light.color.copy(lightColor);
|
|
21
|
+
light.intensity = skyState.lightIntensity;
|
|
22
|
+
hemisphere.color.copy(skyState.hemisphereSkyColor);
|
|
23
|
+
hemisphere.groundColor.copy(skyState.hemisphereGroundColor);
|
|
24
|
+
hemisphere.intensity = skyState.hemisphereIntensity;
|
|
25
|
+
lightUniforms.lightColor.value.copy(lightColor);
|
|
26
|
+
lightUniforms.sunDirection.value.copy(lightDirection);
|
|
27
|
+
lightUniforms.rimColor.value.copy(horizonColor);
|
|
28
|
+
fogUniforms.color.value.copy(horizonColor);
|
|
29
|
+
postUniforms.godraysColor.value.copy(lightColor);
|
|
30
|
+
skyUniforms.cloudLitColor.value.copy(skyState.cloudLitColor);
|
|
31
|
+
skyUniforms.cloudShadeColor.value.copy(skyState.cloudShadeColor);
|
|
32
|
+
skyUniforms.horizonColor.value.copy(horizonColor);
|
|
33
|
+
skyUniforms.lightColor.value.copy(lightColor);
|
|
34
|
+
skyUniforms.moonDirection.value.copy(skyState.moonDirection);
|
|
35
|
+
skyUniforms.starIntensity.value = skyState.starIntensity;
|
|
36
|
+
skyUniforms.sunDirection.value.copy(skyState.sunDirection);
|
|
37
|
+
skyUniforms.zenithColor.value.copy(skyState.zenithColor);
|
|
38
|
+
godraysDirection.subVectors(godraysLight.position, godraysLight.target.position).normalize();
|
|
39
|
+
if (godraysDirection.dot(lightDirection) >= GODRAYS_REDRAW_COSINE) return;
|
|
40
|
+
godraysLight.position.copy(godraysLight.target.position).addScaledVector(lightDirection, lightDistance);
|
|
41
|
+
godraysLight.shadow.needsUpdate = true;
|
|
42
|
+
};
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/clock/constants.ts
|
|
45
|
+
const MINUTES_PER_DAY = 1440;
|
|
46
|
+
const GAME_MINUTES_PER_SECOND = 1;
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/atmosphere/computeSunDirection.ts
|
|
49
|
+
const computeSunDirection = (minutes, tilt, sunDirection) => {
|
|
50
|
+
const hourAngle = minutes / MINUTES_PER_DAY * Math.PI * 2;
|
|
51
|
+
const height = -Math.cos(hourAngle);
|
|
52
|
+
return sunDirection.set(Math.sin(hourAngle), height * Math.cos(tilt), height * Math.sin(tilt));
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/atmosphere/createSkyState.ts
|
|
56
|
+
const createSkyState = () => ({
|
|
57
|
+
cloudLitColor: new Color(),
|
|
58
|
+
cloudShadeColor: new Color(),
|
|
59
|
+
hemisphereGroundColor: new Color(),
|
|
60
|
+
hemisphereIntensity: 0,
|
|
61
|
+
hemisphereSkyColor: new Color(),
|
|
62
|
+
horizonColor: new Color(),
|
|
63
|
+
lightColor: new Color(),
|
|
64
|
+
lightDirection: new Vector3(),
|
|
65
|
+
lightIntensity: 0,
|
|
66
|
+
moonDirection: new Vector3(),
|
|
67
|
+
starIntensity: 0,
|
|
68
|
+
sunDirection: new Vector3(),
|
|
69
|
+
zenithColor: new Color()
|
|
70
|
+
});
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/atmosphere/createSkyUniforms.ts
|
|
73
|
+
const createSkyUniforms = () => ({
|
|
74
|
+
cloudCoverage: uniform(0),
|
|
75
|
+
cloudDrift: uniform(new Vector2()),
|
|
76
|
+
cloudLitColor: uniform(new Color()),
|
|
77
|
+
cloudShadeColor: uniform(new Color()),
|
|
78
|
+
horizonColor: uniform(new Color()),
|
|
79
|
+
lightColor: uniform(new Color()),
|
|
80
|
+
moonDirection: uniform(new Vector3(0, -1, 0)),
|
|
81
|
+
starIntensity: uniform(0),
|
|
82
|
+
sunDirection: uniform(new Vector3(0, 1, 0)),
|
|
83
|
+
zenithColor: uniform(new Color())
|
|
84
|
+
});
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/atmosphere/createSunLight.ts
|
|
87
|
+
const LIGHT_MARGIN = 100;
|
|
88
|
+
const SHADOW_BIAS = -5e-4;
|
|
89
|
+
const SHADOW_NORMAL_BIAS = .05;
|
|
90
|
+
const createSunLight = ({ cascadeCount, maxFar, shadowMapSize }) => {
|
|
91
|
+
const light = new DirectionalLight();
|
|
92
|
+
light.castShadow = true;
|
|
93
|
+
light.shadow.mapSize.set(shadowMapSize, shadowMapSize);
|
|
94
|
+
light.shadow.bias = SHADOW_BIAS;
|
|
95
|
+
light.shadow.normalBias = SHADOW_NORMAL_BIAS;
|
|
96
|
+
light.shadow.camera.far = maxFar + 200;
|
|
97
|
+
const cascadedShadowNode = new CSMShadowNode(light, {
|
|
98
|
+
cascades: cascadeCount,
|
|
99
|
+
lightMargin: LIGHT_MARGIN,
|
|
100
|
+
maxFar,
|
|
101
|
+
mode: "practical"
|
|
102
|
+
});
|
|
103
|
+
cascadedShadowNode.fade = true;
|
|
104
|
+
light.shadow.shadowNode = cascadedShadowNode;
|
|
105
|
+
return {
|
|
106
|
+
cascadedShadowNode,
|
|
107
|
+
light
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/atmosphere/sampleSkyState.ts
|
|
112
|
+
const lerp = (from, to, amount) => from + (to - from) * amount;
|
|
113
|
+
const sampleSkyState = (skyKeyframes, minutes, tilt, skyState) => {
|
|
114
|
+
let nextIndex = 0;
|
|
115
|
+
while (nextIndex < skyKeyframes.length && (skyKeyframes[nextIndex]?.minutes ?? 0) <= minutes) nextIndex++;
|
|
116
|
+
if (nextIndex === skyKeyframes.length) nextIndex = 0;
|
|
117
|
+
const previous = skyKeyframes.at(nextIndex - 1) ?? skyKeyframes[nextIndex];
|
|
118
|
+
const next = skyKeyframes[nextIndex];
|
|
119
|
+
if (!previous || !next) return skyState;
|
|
120
|
+
const span = (next.minutes - previous.minutes + 1440) % 1440 || 1440;
|
|
121
|
+
const amount = (minutes - previous.minutes + MINUTES_PER_DAY) % MINUTES_PER_DAY / span;
|
|
122
|
+
skyState.cloudLitColor.lerpColors(previous.cloudLitColor, next.cloudLitColor, amount);
|
|
123
|
+
skyState.cloudShadeColor.lerpColors(previous.cloudShadeColor, next.cloudShadeColor, amount);
|
|
124
|
+
skyState.hemisphereGroundColor.lerpColors(previous.hemisphereGroundColor, next.hemisphereGroundColor, amount);
|
|
125
|
+
skyState.hemisphereIntensity = lerp(previous.hemisphereIntensity, next.hemisphereIntensity, amount);
|
|
126
|
+
skyState.hemisphereSkyColor.lerpColors(previous.hemisphereSkyColor, next.hemisphereSkyColor, amount);
|
|
127
|
+
skyState.horizonColor.lerpColors(previous.horizonColor, next.horizonColor, amount);
|
|
128
|
+
skyState.lightColor.lerpColors(previous.lightColor, next.lightColor, amount);
|
|
129
|
+
skyState.lightIntensity = lerp(previous.lightIntensity, next.lightIntensity, amount);
|
|
130
|
+
skyState.starIntensity = lerp(previous.starIntensity, next.starIntensity, amount);
|
|
131
|
+
skyState.zenithColor.lerpColors(previous.zenithColor, next.zenithColor, amount);
|
|
132
|
+
computeSunDirection(minutes, tilt, skyState.sunDirection);
|
|
133
|
+
skyState.moonDirection.copy(skyState.sunDirection).negate();
|
|
134
|
+
skyState.lightDirection.copy(skyState.sunDirection.y >= 0 ? skyState.sunDirection : skyState.moonDirection);
|
|
135
|
+
return skyState;
|
|
136
|
+
};
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/clock/advanceGameClock.ts
|
|
139
|
+
const advanceGameClock = (gameClock, deltaSeconds) => {
|
|
140
|
+
gameClock.minutes = (gameClock.minutes + deltaSeconds * gameClock.minutesPerSecond) % MINUTES_PER_DAY;
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/kits/statue/createStatueGeometry.ts
|
|
144
|
+
const PLINTH_SIDES = 8;
|
|
145
|
+
const FIGURE_SEGMENTS = 24;
|
|
146
|
+
const FIGURE_PROFILE = [
|
|
147
|
+
new Vector2(0, 0),
|
|
148
|
+
new Vector2(1.5, 0),
|
|
149
|
+
new Vector2(1.25, .8),
|
|
150
|
+
new Vector2(.85, 2.6),
|
|
151
|
+
new Vector2(.7, 3.6),
|
|
152
|
+
new Vector2(.95, 4.3),
|
|
153
|
+
new Vector2(.6, 4.7),
|
|
154
|
+
new Vector2(.45, 5.1),
|
|
155
|
+
new Vector2(.5, 5.5),
|
|
156
|
+
new Vector2(.3, 6),
|
|
157
|
+
new Vector2(0, 6.2)
|
|
158
|
+
];
|
|
159
|
+
const createStatueGeometry = () => {
|
|
160
|
+
const lowerStep = new CylinderGeometry(3.2, 3.4, .6, PLINTH_SIDES).translate(0, .3, 0);
|
|
161
|
+
const upperStep = new CylinderGeometry(2.6, 2.8, .6, PLINTH_SIDES).translate(0, .9, 0);
|
|
162
|
+
const pedestal = new CylinderGeometry(1.8, 2.1, 2.2, PLINTH_SIDES).translate(0, 2.3, 0);
|
|
163
|
+
const figure = new LatheGeometry(FIGURE_PROFILE, FIGURE_SEGMENTS).translate(0, 3.4, 0);
|
|
164
|
+
const parts = [
|
|
165
|
+
lowerStep,
|
|
166
|
+
upperStep,
|
|
167
|
+
pedestal,
|
|
168
|
+
figure
|
|
169
|
+
].map((part) => part.toNonIndexed());
|
|
170
|
+
const statueGeometry = mergeGeometries(parts) ?? new BufferGeometry();
|
|
171
|
+
for (const part of [
|
|
172
|
+
lowerStep,
|
|
173
|
+
upperStep,
|
|
174
|
+
pedestal,
|
|
175
|
+
figure,
|
|
176
|
+
...parts
|
|
177
|
+
]) part.dispose();
|
|
178
|
+
statueGeometry.computeVertexNormals();
|
|
179
|
+
return statueGeometry;
|
|
180
|
+
};
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/random/createSeededRandom.ts
|
|
183
|
+
const createSeededRandom = (seed) => {
|
|
184
|
+
let state = seed;
|
|
185
|
+
return () => {
|
|
186
|
+
state = state + 1831565813 >>> 0;
|
|
187
|
+
let mixed = Math.imul(state ^ state >>> 15, 1 | state);
|
|
188
|
+
mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, 61 | mixed);
|
|
189
|
+
return ((mixed ^ mixed >>> 14) >>> 0) / 2 ** 32;
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/kits/tree/computeLeafCards.ts
|
|
194
|
+
const VERTICES_PER_CARD = 4;
|
|
195
|
+
const INDICES_PER_CARD = 6;
|
|
196
|
+
const CORNER_SIGNS = [
|
|
197
|
+
[-1, -1],
|
|
198
|
+
[1, -1],
|
|
199
|
+
[1, 1],
|
|
200
|
+
[-1, 1]
|
|
201
|
+
];
|
|
202
|
+
const computeLeafCards = (clusterCenters, { cardSize, cardsPerCluster, clusterRadius, seed }) => {
|
|
203
|
+
const random = createSeededRandom(seed + 1);
|
|
204
|
+
const cardCount = clusterCenters.length * cardsPerCluster;
|
|
205
|
+
const positions = new Float32Array(cardCount * VERTICES_PER_CARD * 3);
|
|
206
|
+
const normals = new Float32Array(cardCount * VERTICES_PER_CARD * 3);
|
|
207
|
+
const uvs = new Float32Array(cardCount * VERTICES_PER_CARD * 2);
|
|
208
|
+
const indices = new Uint32Array(cardCount * INDICES_PER_CARD);
|
|
209
|
+
let card = 0;
|
|
210
|
+
for (const { x: centerX, y: centerY, z: centerZ } of clusterCenters) for (let cardIndex = 0; cardIndex < cardsPerCluster; cardIndex++, card++) {
|
|
211
|
+
let offsetX;
|
|
212
|
+
let offsetY;
|
|
213
|
+
let offsetZ;
|
|
214
|
+
do {
|
|
215
|
+
offsetX = random() * 2 - 1;
|
|
216
|
+
offsetY = random() * 2 - 1;
|
|
217
|
+
offsetZ = random() * 2 - 1;
|
|
218
|
+
} while (offsetX * offsetX + offsetY * offsetY + offsetZ * offsetZ > 1);
|
|
219
|
+
const cardX = centerX + offsetX * clusterRadius;
|
|
220
|
+
const cardY = centerY + offsetY * clusterRadius * .7;
|
|
221
|
+
const cardZ = centerZ + offsetZ * clusterRadius;
|
|
222
|
+
const facingAngle = random() * Math.PI * 2;
|
|
223
|
+
const tiltAngle = (random() - .5) * Math.PI;
|
|
224
|
+
const tangentX = Math.cos(facingAngle);
|
|
225
|
+
const tangentZ = Math.sin(facingAngle);
|
|
226
|
+
const bitangentX = -Math.sin(tiltAngle) * tangentZ;
|
|
227
|
+
const bitangentY = Math.cos(tiltAngle);
|
|
228
|
+
const bitangentZ = Math.sin(tiltAngle) * tangentX;
|
|
229
|
+
const vertexStart = card * VERTICES_PER_CARD;
|
|
230
|
+
for (const [cornerIndex, [tangentSign, bitangentSign]] of CORNER_SIGNS.entries()) {
|
|
231
|
+
const vertex = vertexStart + cornerIndex;
|
|
232
|
+
const x = cardX + (tangentX * tangentSign + bitangentX * bitangentSign) * cardSize;
|
|
233
|
+
const y = cardY + bitangentY * bitangentSign * cardSize;
|
|
234
|
+
const z = cardZ + (tangentZ * tangentSign + bitangentZ * bitangentSign) * cardSize;
|
|
235
|
+
const normalX = x - centerX;
|
|
236
|
+
const normalY = y - centerY;
|
|
237
|
+
const normalZ = z - centerZ;
|
|
238
|
+
const length = Math.hypot(normalX, normalY, normalZ) || 1;
|
|
239
|
+
positions[vertex * 3] = x;
|
|
240
|
+
positions[vertex * 3 + 1] = y;
|
|
241
|
+
positions[vertex * 3 + 2] = z;
|
|
242
|
+
normals[vertex * 3] = normalX / length;
|
|
243
|
+
normals[vertex * 3 + 1] = normalY / length;
|
|
244
|
+
normals[vertex * 3 + 2] = normalZ / length;
|
|
245
|
+
uvs[vertex * 2] = (tangentSign + 1) / 2;
|
|
246
|
+
uvs[vertex * 2 + 1] = (bitangentSign + 1) / 2;
|
|
247
|
+
}
|
|
248
|
+
const indexStart = card * INDICES_PER_CARD;
|
|
249
|
+
indices[indexStart] = vertexStart;
|
|
250
|
+
indices[indexStart + 1] = vertexStart + 1;
|
|
251
|
+
indices[indexStart + 2] = vertexStart + 2;
|
|
252
|
+
indices[indexStart + 3] = vertexStart;
|
|
253
|
+
indices[indexStart + 4] = vertexStart + 2;
|
|
254
|
+
indices[indexStart + 5] = vertexStart + 3;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
indices,
|
|
258
|
+
normals,
|
|
259
|
+
positions,
|
|
260
|
+
uvs
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/kits/tree/computeTreeSkeleton.ts
|
|
265
|
+
const TRUNK_LEAN = .08;
|
|
266
|
+
const FORKS_PER_BRANCH = 2;
|
|
267
|
+
const computeTreeSkeleton = ({ branchLength, mainBranchCount, seed, trunkHeight, trunkRadius }) => {
|
|
268
|
+
const random = createSeededRandom(seed);
|
|
269
|
+
const trunkTop = new Vector3((random() - .5) * TRUNK_LEAN * trunkHeight, trunkHeight, (random() - .5) * TRUNK_LEAN * trunkHeight);
|
|
270
|
+
const branchSegments = [{
|
|
271
|
+
end: trunkTop,
|
|
272
|
+
endRadius: trunkRadius * .6,
|
|
273
|
+
start: new Vector3(),
|
|
274
|
+
startRadius: trunkRadius
|
|
275
|
+
}];
|
|
276
|
+
const clusterCenters = [trunkTop.clone().setY(trunkTop.y + branchLength * .6)];
|
|
277
|
+
for (let branchIndex = 0; branchIndex < mainBranchCount; branchIndex++) {
|
|
278
|
+
const angle = branchIndex / mainBranchCount * Math.PI * 2 + (random() - .5);
|
|
279
|
+
const start = new Vector3().lerpVectors(new Vector3(), trunkTop, .75 + random() * .25);
|
|
280
|
+
const direction = new Vector3(Math.cos(angle), .45 + random() * .4, Math.sin(angle)).normalize();
|
|
281
|
+
const end = start.clone().addScaledVector(direction, branchLength * (.75 + random() * .25));
|
|
282
|
+
branchSegments.push({
|
|
283
|
+
end,
|
|
284
|
+
endRadius: trunkRadius * .18,
|
|
285
|
+
start,
|
|
286
|
+
startRadius: trunkRadius * .45
|
|
287
|
+
});
|
|
288
|
+
for (let forkIndex = 0; forkIndex < FORKS_PER_BRANCH; forkIndex++) {
|
|
289
|
+
const forkAngle = angle + (forkIndex - .5) * .9 + (random() - .5) * .3;
|
|
290
|
+
const forkDirection = new Vector3(Math.cos(forkAngle), .6 + random() * .4, Math.sin(forkAngle)).normalize();
|
|
291
|
+
const forkEnd = end.clone().addScaledVector(forkDirection, branchLength * (.45 + random() * .2));
|
|
292
|
+
branchSegments.push({
|
|
293
|
+
end: forkEnd,
|
|
294
|
+
endRadius: trunkRadius * .05,
|
|
295
|
+
start: end,
|
|
296
|
+
startRadius: trunkRadius * .18
|
|
297
|
+
});
|
|
298
|
+
clusterCenters.push(forkEnd);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
branchSegments,
|
|
303
|
+
clusterCenters
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/kits/tree/createTreeGeometry.ts
|
|
308
|
+
const BRANCH_RADIAL_SEGMENTS = 7;
|
|
309
|
+
const UP = new Vector3(0, 1, 0);
|
|
310
|
+
const createTreeGeometry = (treeOptions) => {
|
|
311
|
+
const { branchSegments, clusterCenters } = computeTreeSkeleton(treeOptions);
|
|
312
|
+
const direction = new Vector3();
|
|
313
|
+
const rotation = new Quaternion();
|
|
314
|
+
const segmentGeometries = branchSegments.map(({ end, endRadius, start, startRadius }) => {
|
|
315
|
+
direction.subVectors(end, start);
|
|
316
|
+
const length = direction.length();
|
|
317
|
+
const segmentGeometry = new CylinderGeometry(endRadius, startRadius, length, BRANCH_RADIAL_SEGMENTS, 1, true);
|
|
318
|
+
segmentGeometry.applyQuaternion(rotation.setFromUnitVectors(UP, direction.normalize()));
|
|
319
|
+
segmentGeometry.translate((start.x + end.x) / 2, (start.y + end.y) / 2, (start.z + end.z) / 2);
|
|
320
|
+
return segmentGeometry;
|
|
321
|
+
});
|
|
322
|
+
const branchGeometry = mergeGeometries(segmentGeometries) ?? new BufferGeometry();
|
|
323
|
+
for (const segmentGeometry of segmentGeometries) segmentGeometry.dispose();
|
|
324
|
+
const { indices, normals, positions, uvs } = computeLeafCards(clusterCenters, treeOptions);
|
|
325
|
+
const leafGeometry = new BufferGeometry();
|
|
326
|
+
leafGeometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
327
|
+
leafGeometry.setAttribute("normal", new BufferAttribute(normals, 3));
|
|
328
|
+
leafGeometry.setAttribute("uv", new BufferAttribute(uvs, 2));
|
|
329
|
+
leafGeometry.setIndex(new BufferAttribute(indices, 1));
|
|
330
|
+
leafGeometry.computeBoundingSphere();
|
|
331
|
+
return {
|
|
332
|
+
branchGeometry,
|
|
333
|
+
leafGeometry
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/materials/computeRampValues.ts
|
|
338
|
+
const MAX_BYTE$1 = 255;
|
|
339
|
+
const computeRampValues = ({ resolution, softness, terminator }) => {
|
|
340
|
+
const lowerEdge = terminator - softness / 2;
|
|
341
|
+
const upperEdge = terminator + softness / 2;
|
|
342
|
+
return Uint8Array.from({ length: resolution }, (_value, index) => {
|
|
343
|
+
const coordinate = (index + .5) / resolution;
|
|
344
|
+
const step = Math.min(Math.max((coordinate - lowerEdge) / (upperEdge - lowerEdge), 0), 1);
|
|
345
|
+
return Math.round(step * step * (3 - 2 * step) * MAX_BYTE$1);
|
|
346
|
+
});
|
|
347
|
+
};
|
|
348
|
+
//#endregion
|
|
349
|
+
//#region src/materials/createRampTexture.ts
|
|
350
|
+
const createRampTexture = (rampOptions) => {
|
|
351
|
+
const rampValues = computeRampValues(rampOptions);
|
|
352
|
+
const rampTexture = new DataTexture(rampValues, rampOptions.resolution, 1, RedFormat);
|
|
353
|
+
rampTexture.magFilter = LinearFilter;
|
|
354
|
+
rampTexture.minFilter = LinearFilter;
|
|
355
|
+
rampTexture.needsUpdate = true;
|
|
356
|
+
return rampTexture;
|
|
357
|
+
};
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/nodes/ToonNodeMaterial.ts
|
|
360
|
+
var ToonNodeMaterial = class extends MeshToonNodeMaterial {
|
|
361
|
+
emissiveNode = null;
|
|
362
|
+
isMeshToonNodeMaterial;
|
|
363
|
+
constructor(parameters, isOutlined) {
|
|
364
|
+
super(parameters);
|
|
365
|
+
this.isMeshToonNodeMaterial = isOutlined;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/nodes/createCausticsNode.ts
|
|
370
|
+
const CAUSTIC_SCALE = .35;
|
|
371
|
+
const CAUSTIC_SPEED = .6;
|
|
372
|
+
const createCausticsNode = ({ lightColor }, { causticStrength, deepDepth, level }) => {
|
|
373
|
+
const floorPosition = positionWorld.xz.mul(CAUSTIC_SCALE);
|
|
374
|
+
const drift = time.mul(CAUSTIC_SPEED);
|
|
375
|
+
const cells = voronoi2d(floorPosition, drift).mul(voronoi2d(floorPosition.mul(1.7), drift.mul(1.3)));
|
|
376
|
+
const veins = smoothstep(.35, .45, cells);
|
|
377
|
+
const depthBelow = level.sub(positionWorld.y);
|
|
378
|
+
const fade = smoothstep(0, .3, depthBelow).mul(float(1).sub(smoothstep(0, deepDepth, depthBelow)));
|
|
379
|
+
return lightColor.mul(veins.mul(fade).mul(causticStrength));
|
|
380
|
+
};
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/nodes/createGenshinRenderer.ts
|
|
383
|
+
const createGenshinRenderer = (canvas) => {
|
|
384
|
+
const renderer = new WebGPURenderer({
|
|
385
|
+
antialias: false,
|
|
386
|
+
canvas,
|
|
387
|
+
powerPreference: "high-performance"
|
|
388
|
+
});
|
|
389
|
+
renderer.toneMapping = NeutralToneMapping;
|
|
390
|
+
return renderer;
|
|
391
|
+
};
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/nodes/createRimNode.ts
|
|
394
|
+
const RIM_POWER = 4;
|
|
395
|
+
const createRimNode = ({ rimColor, rimStrength, sunDirection }) => {
|
|
396
|
+
const sunDirectionView = cameraViewMatrix.mul(vec4(sunDirection, 0)).xyz.normalize();
|
|
397
|
+
const fresnel = float(1).sub(normalView.dot(positionViewDirection).saturate()).pow(RIM_POWER);
|
|
398
|
+
const litMask = normalView.dot(sunDirectionView).saturate();
|
|
399
|
+
return rimColor.mul(fresnel.mul(litMask).mul(rimStrength));
|
|
400
|
+
};
|
|
401
|
+
//#endregion
|
|
402
|
+
//#region src/nodes/createToonMaterial.ts
|
|
403
|
+
const createToonMaterial = ({ color, isOutlined = true, isVertexColors = false, lightUniforms, rampTexture }) => {
|
|
404
|
+
const toonMaterial = new ToonNodeMaterial({
|
|
405
|
+
color,
|
|
406
|
+
gradientMap: rampTexture,
|
|
407
|
+
vertexColors: isVertexColors
|
|
408
|
+
}, isOutlined);
|
|
409
|
+
toonMaterial.emissiveNode = createRimNode(lightUniforms);
|
|
410
|
+
return toonMaterial;
|
|
411
|
+
};
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/nodes/createWindNode.ts
|
|
414
|
+
const TURBULENCE_SCALE = .07;
|
|
415
|
+
const TURBULENCE_SPEED = .4;
|
|
416
|
+
const createWindNode = ({ direction, gustSpeed, gustStrength, gustWidth, strength }, groundPosition) => {
|
|
417
|
+
const gustPhase = groundPosition.dot(direction).sub(time.mul(gustSpeed)).div(gustWidth).mul(Math.PI * 2);
|
|
418
|
+
const gust = smoothstep(.2, 1, sin(gustPhase).mul(.5).add(.5)).mul(gustStrength);
|
|
419
|
+
const turbulence = mx_noise_float(vec3(groundPosition.mul(TURBULENCE_SCALE), time.mul(TURBULENCE_SPEED))).mul(.35);
|
|
420
|
+
return direction.mul(strength.add(gust).mul(turbulence.add(1)));
|
|
421
|
+
};
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/nodes/createLeafMaterial.ts
|
|
424
|
+
const LEAF_ALPHA_TEST = .5;
|
|
425
|
+
const SWAY_PER_WIND = .03;
|
|
426
|
+
const FLUTTER = .06;
|
|
427
|
+
const createLeafMaterial = (toonMaterialOptions, windUniforms) => {
|
|
428
|
+
const leafMaterial = createToonMaterial(toonMaterialOptions);
|
|
429
|
+
const centered = uv().sub(.5).mul(2);
|
|
430
|
+
const width = float(1).sub(centered.y.abs().pow(2)).mul(.8);
|
|
431
|
+
leafMaterial.opacityNode = step(0, width.sub(centered.x.abs()));
|
|
432
|
+
leafMaterial.alphaTest = LEAF_ALPHA_TEST;
|
|
433
|
+
leafMaterial.side = DoubleSide;
|
|
434
|
+
if (windUniforms) {
|
|
435
|
+
const worldPosition = modelWorldMatrix.mul(vec4(positionLocal, 1)).xyz;
|
|
436
|
+
const lean = createWindNode(windUniforms, worldPosition.xz).mul(positionLocal.y.mul(SWAY_PER_WIND));
|
|
437
|
+
const phase = hash(worldPosition.x.mul(12.9).add(worldPosition.z.mul(78.2)).floor()).mul(Math.PI * 2);
|
|
438
|
+
const flutter = sin(time.mul(3).add(phase)).mul(FLUTTER).mul(windUniforms.strength.add(windUniforms.gustStrength));
|
|
439
|
+
leafMaterial.positionNode = positionLocal.add(vec3(lean.x, flutter, lean.y));
|
|
440
|
+
}
|
|
441
|
+
return leafMaterial;
|
|
442
|
+
};
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/nodes/createLightUniforms.ts
|
|
445
|
+
const createLightUniforms = () => ({
|
|
446
|
+
lightColor: uniform(new Color()),
|
|
447
|
+
rimColor: uniform(new Color()),
|
|
448
|
+
rimStrength: uniform(0),
|
|
449
|
+
sunDirection: uniform(new Vector3(0, 1, 0))
|
|
450
|
+
});
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/nodes/createSkyNode.ts
|
|
453
|
+
const HORIZON_BAND = .45;
|
|
454
|
+
const SUN_DISC_COSINE = Math.cos(.035);
|
|
455
|
+
const MOON_DISC_COSINE = Math.cos(.05);
|
|
456
|
+
const SUN_GLOW_POWER = 12;
|
|
457
|
+
const HORIZON_GLOW_POWER = 3;
|
|
458
|
+
const SUN_DISC_BRIGHTNESS = 6;
|
|
459
|
+
const CLOUD_SCALE = 1.6;
|
|
460
|
+
const CLOUD_MIN_HEIGHT = .04;
|
|
461
|
+
const CLOUD_EDGE = .04;
|
|
462
|
+
const CLOUD_SHADE_DEPTH = .18;
|
|
463
|
+
const STAR_SCALE = 420;
|
|
464
|
+
const STAR_THRESHOLD = .9975;
|
|
465
|
+
const createSkyNode = ({ cloudCoverage, cloudDrift, cloudLitColor, cloudShadeColor, horizonColor, lightColor, moonDirection, starIntensity, sunDirection, zenithColor }) => Fn(() => {
|
|
466
|
+
const direction = normalWorldGeometry.normalize();
|
|
467
|
+
const height = direction.y;
|
|
468
|
+
const gradient = mix(horizonColor, zenithColor, smoothstep(0, HORIZON_BAND, height));
|
|
469
|
+
const sunCosine = direction.dot(sunDirection);
|
|
470
|
+
const sunVisibility = smoothstep(-.1, .05, sunDirection.y);
|
|
471
|
+
const sunGlow = pow(max(sunCosine, 0), SUN_GLOW_POWER).mul(sunVisibility);
|
|
472
|
+
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);
|
|
473
|
+
const sunDisc = smoothstep(SUN_DISC_COSINE - 5e-4, SUN_DISC_COSINE, sunCosine).mul(sunVisibility);
|
|
474
|
+
const moonDisc = smoothstep(MOON_DISC_COSINE - 5e-4, MOON_DISC_COSINE, direction.dot(moonDirection)).mul(smoothstep(-.05, .05, moonDirection.y));
|
|
475
|
+
const stars = smoothstep(STAR_THRESHOLD, 1, mx_cell_noise_float(direction.mul(STAR_SCALE))).mul(starIntensity).mul(smoothstep(0, .1, height));
|
|
476
|
+
const cloudPosition = direction.xz.div(max(height, CLOUD_MIN_HEIGHT)).mul(CLOUD_SCALE).add(cloudDrift);
|
|
477
|
+
const cloudNoise = mx_fractal_noise_float(vec3(cloudPosition, 0), 4, 2, .5).mul(.5).add(.5);
|
|
478
|
+
const cloudThreshold = float(1).sub(cloudCoverage);
|
|
479
|
+
const cloudDensity = smoothstep(cloudThreshold, cloudThreshold.add(CLOUD_EDGE), cloudNoise).mul(smoothstep(0, .12, height));
|
|
480
|
+
const cloudShade = smoothstep(cloudThreshold.add(CLOUD_SHADE_DEPTH), cloudThreshold.add(.22), cloudNoise);
|
|
481
|
+
const cloudColor = mix(cloudLitColor, cloudShadeColor, cloudShade).add(lightColor.mul(sunGlow.mul(.6)));
|
|
482
|
+
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)));
|
|
483
|
+
return mix(sky, cloudColor, cloudDensity);
|
|
484
|
+
})();
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/nodes/createWaterMaterial.ts
|
|
487
|
+
const RIPPLE_SCALE = .08;
|
|
488
|
+
const RIPPLE_SPEED = .05;
|
|
489
|
+
const REFRACTION_STRENGTH = .015;
|
|
490
|
+
const GLINT_POWER = 180;
|
|
491
|
+
const GLINT_BRIGHTNESS = 5;
|
|
492
|
+
const FOAM_SCALE = .6;
|
|
493
|
+
const REFLECTION_STRENGTH = .55;
|
|
494
|
+
const createWaterMaterial = ({ lightColor, sunDirection }, { horizonColor, zenithColor }, { deepColor, deepDepth, foamColor, foamDepth, shallowColor }) => {
|
|
495
|
+
const waterMaterial = new MeshBasicNodeMaterial({
|
|
496
|
+
side: DoubleSide,
|
|
497
|
+
transparent: true
|
|
498
|
+
});
|
|
499
|
+
const ripplePosition = positionWorld.xz.mul(RIPPLE_SCALE);
|
|
500
|
+
const drift = time.mul(RIPPLE_SPEED);
|
|
501
|
+
const sampleRipples = (offset) => mx_fractal_noise_float(vec3(ripplePosition.add(offset).add(vec2(drift, drift.mul(.6))), 0), 3).add(mx_fractal_noise_float(vec3(ripplePosition.add(offset).mul(1.9).sub(vec2(drift.mul(.8), drift)), 1), 2)).mul(.5);
|
|
502
|
+
const ripple = sampleRipples(vec2(0, 0));
|
|
503
|
+
const slopeX = sampleRipples(vec2(.02, 0)).sub(ripple);
|
|
504
|
+
const slopeZ = sampleRipples(vec2(0, .02)).sub(ripple);
|
|
505
|
+
const normal = vec3(slopeX.mul(-.35 / .02), 1, slopeZ.mul(-.35 / .02)).normalize();
|
|
506
|
+
const refractedUV = screenUV.add(normal.xz.mul(REFRACTION_STRENGTH));
|
|
507
|
+
const floorUV = perspectiveDepthToViewZ(viewportDepthTexture(refractedUV), cameraNear, cameraFar).greaterThan(positionView.z).select(screenUV, refractedUV);
|
|
508
|
+
const depth = positionView.z.sub(perspectiveDepthToViewZ(viewportDepthTexture(floorUV), cameraNear, cameraFar)).max(0);
|
|
509
|
+
const deepness = smoothstep(0, deepDepth, depth);
|
|
510
|
+
const waterColor = mix(shallowColor, deepColor, deepness);
|
|
511
|
+
const floorColor = viewportSharedTexture(floorUV).rgb;
|
|
512
|
+
const clarity = float(1).sub(smoothstep(0, deepDepth.mul(.6), depth));
|
|
513
|
+
const tinted = mix(waterColor, floorColor.mul(shallowColor.mul(1.4)), clarity.mul(.8));
|
|
514
|
+
const eyeDirection = cameraPosition.sub(positionWorld).normalize();
|
|
515
|
+
const fresnel = pow(float(1).sub(normal.dot(eyeDirection).abs().saturate()), 4);
|
|
516
|
+
const skyColor = mix(horizonColor, zenithColor, .35);
|
|
517
|
+
const reflected = mix(tinted, skyColor, fresnel.mul(REFLECTION_STRENGTH));
|
|
518
|
+
const highlight = pow(reflect(sunDirection.negate(), normal).dot(eyeDirection).max(0), GLINT_POWER);
|
|
519
|
+
const glints = lightColor.mul(smoothstep(.45, .5, highlight).mul(GLINT_BRIGHTNESS));
|
|
520
|
+
const shore = float(1).sub(smoothstep(0, foamDepth, depth));
|
|
521
|
+
const foamNoise = mx_fractal_noise_float(vec3(positionWorld.xz.mul(FOAM_SCALE), time.mul(.3)), 2).mul(.5).add(.5);
|
|
522
|
+
const foam = smoothstep(.55, .6, foamNoise.add(shore.mul(.5))).mul(shore);
|
|
523
|
+
waterMaterial.colorNode = mix(reflected, foamColor, foam).add(glints);
|
|
524
|
+
return waterMaterial;
|
|
525
|
+
};
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/noise/createSimplexNoise.ts
|
|
528
|
+
const SKEW = (Math.sqrt(3) - 1) / 2;
|
|
529
|
+
const UNSKEW = (3 - Math.sqrt(3)) / 6;
|
|
530
|
+
const GRADIENTS_X = [
|
|
531
|
+
1,
|
|
532
|
+
-1,
|
|
533
|
+
1,
|
|
534
|
+
-1,
|
|
535
|
+
1,
|
|
536
|
+
-1,
|
|
537
|
+
0,
|
|
538
|
+
0
|
|
539
|
+
];
|
|
540
|
+
const GRADIENTS_Y = [
|
|
541
|
+
1,
|
|
542
|
+
1,
|
|
543
|
+
-1,
|
|
544
|
+
-1,
|
|
545
|
+
0,
|
|
546
|
+
0,
|
|
547
|
+
1,
|
|
548
|
+
-1
|
|
549
|
+
];
|
|
550
|
+
const NOISE_SCALE = 70;
|
|
551
|
+
const getCornerContribution = (gradientIndex, offsetX, offsetY) => {
|
|
552
|
+
const falloff = .5 - offsetX * offsetX - offsetY * offsetY;
|
|
553
|
+
if (falloff < 0) return 0;
|
|
554
|
+
const gradient = gradientIndex & 7;
|
|
555
|
+
return falloff ** 4 * ((GRADIENTS_X[gradient] ?? 0) * offsetX + (GRADIENTS_Y[gradient] ?? 0) * offsetY);
|
|
556
|
+
};
|
|
557
|
+
const createSimplexNoise = (seed) => {
|
|
558
|
+
const random = createSeededRandom(seed);
|
|
559
|
+
const table = Uint8Array.from({ length: 256 }, (_value, index) => index);
|
|
560
|
+
for (let index = table.length - 1; index > 0; index--) {
|
|
561
|
+
const swapIndex = Math.floor(random() * (index + 1));
|
|
562
|
+
[table[index], table[swapIndex]] = [table[swapIndex] ?? 0, table[index] ?? 0];
|
|
563
|
+
}
|
|
564
|
+
const permutation = Uint8Array.from({ length: 512 }, (_value, index) => table[index & 255] ?? 0);
|
|
565
|
+
return (x, y) => {
|
|
566
|
+
const skew = (x + y) * SKEW;
|
|
567
|
+
const cellX = Math.floor(x + skew);
|
|
568
|
+
const cellY = Math.floor(y + skew);
|
|
569
|
+
const unskew = (cellX + cellY) * UNSKEW;
|
|
570
|
+
const offsetX = x - cellX + unskew;
|
|
571
|
+
const offsetY = y - cellY + unskew;
|
|
572
|
+
const middleX = offsetX > offsetY ? 1 : 0;
|
|
573
|
+
const middleY = 1 - middleX;
|
|
574
|
+
const tableX = cellX & 255;
|
|
575
|
+
const tableY = cellY & 255;
|
|
576
|
+
return NOISE_SCALE * (getCornerContribution(permutation[tableX + (permutation[tableY] ?? 0)] ?? 0, offsetX, offsetY) + getCornerContribution(permutation[tableX + middleX + (permutation[tableY + middleY] ?? 0)] ?? 0, offsetX - middleX + UNSKEW, offsetY - middleY + UNSKEW) + getCornerContribution(permutation[tableX + 1 + (permutation[tableY + 1] ?? 0)] ?? 0, offsetX - 1 + 2 * UNSKEW, offsetY - 1 + 2 * UNSKEW));
|
|
577
|
+
};
|
|
578
|
+
};
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/post/computeGradeLut.ts
|
|
581
|
+
const MAX_BYTE = 255;
|
|
582
|
+
const RED_LUMINANCE = .2126;
|
|
583
|
+
const GREEN_LUMINANCE = .7152;
|
|
584
|
+
const BLUE_LUMINANCE = .0722;
|
|
585
|
+
const CONTRAST_PIVOT = .5;
|
|
586
|
+
const toGradedByte = (channel, luminance, saturation, contrast, tint) => {
|
|
587
|
+
const graded = (luminance + (channel - luminance) * saturation - CONTRAST_PIVOT) * contrast + CONTRAST_PIVOT + tint;
|
|
588
|
+
return Math.round(Math.min(Math.max(graded, 0), 1) * MAX_BYTE);
|
|
589
|
+
};
|
|
590
|
+
const computeGradeLut = ({ contrast, highlightTint, saturation, shadowTint, size }) => {
|
|
591
|
+
const values = new Uint8Array(size * size * size * 4);
|
|
592
|
+
const step = 1 / (size - 1);
|
|
593
|
+
let offset = 0;
|
|
594
|
+
for (let blueIndex = 0; blueIndex < size; blueIndex++) for (let greenIndex = 0; greenIndex < size; greenIndex++) for (let redIndex = 0; redIndex < size; redIndex++) {
|
|
595
|
+
const red = redIndex * step;
|
|
596
|
+
const green = greenIndex * step;
|
|
597
|
+
const blue = blueIndex * step;
|
|
598
|
+
const luminance = red * RED_LUMINANCE + green * GREEN_LUMINANCE + blue * BLUE_LUMINANCE;
|
|
599
|
+
const shadowWeight = (1 - luminance) * (1 - luminance);
|
|
600
|
+
const highlightWeight = luminance * luminance;
|
|
601
|
+
const redTint = shadowTint[0] * shadowWeight + highlightTint[0] * highlightWeight;
|
|
602
|
+
const greenTint = shadowTint[1] * shadowWeight + highlightTint[1] * highlightWeight;
|
|
603
|
+
const blueTint = shadowTint[2] * shadowWeight + highlightTint[2] * highlightWeight;
|
|
604
|
+
values[offset] = toGradedByte(red, luminance, saturation, contrast, redTint);
|
|
605
|
+
values[offset + 1] = toGradedByte(green, luminance, saturation, contrast, greenTint);
|
|
606
|
+
values[offset + 2] = toGradedByte(blue, luminance, saturation, contrast, blueTint);
|
|
607
|
+
values[offset + 3] = MAX_BYTE;
|
|
608
|
+
offset += 4;
|
|
609
|
+
}
|
|
610
|
+
return values;
|
|
611
|
+
};
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/post/createFogUniforms.ts
|
|
614
|
+
const createFogUniforms = () => ({
|
|
615
|
+
baseHeight: uniform(0),
|
|
616
|
+
color: uniform(new Color()),
|
|
617
|
+
density: uniform(0),
|
|
618
|
+
heightFalloff: uniform(0),
|
|
619
|
+
startDistance: uniform(0)
|
|
620
|
+
});
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/post/createGodraysLight.ts
|
|
623
|
+
const createGodraysLight = (shadowMapSize, halfExtent) => {
|
|
624
|
+
const godraysLight = new DirectionalLight(16777215, 0);
|
|
625
|
+
godraysLight.castShadow = true;
|
|
626
|
+
godraysLight.shadow.autoUpdate = false;
|
|
627
|
+
godraysLight.shadow.needsUpdate = true;
|
|
628
|
+
godraysLight.shadow.mapSize.set(shadowMapSize, shadowMapSize);
|
|
629
|
+
const { camera } = godraysLight.shadow;
|
|
630
|
+
camera.left = -halfExtent;
|
|
631
|
+
camera.right = halfExtent;
|
|
632
|
+
camera.top = halfExtent;
|
|
633
|
+
camera.bottom = -halfExtent;
|
|
634
|
+
camera.far = halfExtent * 4;
|
|
635
|
+
return godraysLight;
|
|
636
|
+
};
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/post/createGradeLutTexture.ts
|
|
639
|
+
const createGradeLutTexture = (gradeOptions) => {
|
|
640
|
+
const { size } = gradeOptions;
|
|
641
|
+
const gradeLutTexture = new Data3DTexture(computeGradeLut(gradeOptions), size, size, size);
|
|
642
|
+
gradeLutTexture.magFilter = LinearFilter;
|
|
643
|
+
gradeLutTexture.minFilter = LinearFilter;
|
|
644
|
+
gradeLutTexture.needsUpdate = true;
|
|
645
|
+
return gradeLutTexture;
|
|
646
|
+
};
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/post/createHeightFogNode.ts
|
|
649
|
+
const MIN_RAY_CLIMB = 1e-6;
|
|
650
|
+
const createHeightFogNode = (colorNode, depthNode, camera, { baseHeight, color, density, heightFalloff, startDistance }) => {
|
|
651
|
+
const cameraWorldMatrix = uniform(camera.matrixWorld);
|
|
652
|
+
const cameraProjectionMatrixInverse = uniform(camera.projectionMatrixInverse);
|
|
653
|
+
return Fn(() => {
|
|
654
|
+
const depth = depthNode.sample(uv()).r;
|
|
655
|
+
const output = colorNode.toVar();
|
|
656
|
+
If(depth.lessThan(1), () => {
|
|
657
|
+
const viewPosition = getViewPosition(uv(), depth, cameraProjectionMatrixInverse);
|
|
658
|
+
const eye = cameraWorldMatrix.mul(vec4(0, 0, 0, 1)).xyz;
|
|
659
|
+
const ray = cameraWorldMatrix.mul(vec4(viewPosition, 1)).xyz.sub(eye);
|
|
660
|
+
const rayLength = ray.length();
|
|
661
|
+
const fogLength = max(rayLength.sub(startDistance), 0);
|
|
662
|
+
const slope = ray.y.div(rayLength);
|
|
663
|
+
const startHeight = eye.y.add(slope.mul(rayLength.min(startDistance)));
|
|
664
|
+
const densityAtStart = density.mul(exp(startHeight.sub(baseHeight).mul(heightFalloff).negate()));
|
|
665
|
+
const climb = slope.mul(heightFalloff);
|
|
666
|
+
const opticalDepth = densityAtStart.mul(fogLength).toVar();
|
|
667
|
+
If(climb.abs().greaterThan(MIN_RAY_CLIMB), () => {
|
|
668
|
+
opticalDepth.assign(densityAtStart.mul(float(1).sub(exp(fogLength.mul(climb).negate()))).div(climb));
|
|
669
|
+
});
|
|
670
|
+
const opacity = float(1).sub(exp(opticalDepth.negate()));
|
|
671
|
+
output.assign(vec4(mix(output.rgb, color, opacity), output.a));
|
|
672
|
+
});
|
|
673
|
+
return output;
|
|
674
|
+
})();
|
|
675
|
+
};
|
|
676
|
+
//#endregion
|
|
677
|
+
//#region src/renderer/AntialiasingMode.ts
|
|
678
|
+
let AntialiasingMode = /* @__PURE__ */ function(AntialiasingMode) {
|
|
679
|
+
AntialiasingMode["Smaa"] = "Smaa";
|
|
680
|
+
AntialiasingMode["Traa"] = "Traa";
|
|
681
|
+
return AntialiasingMode;
|
|
682
|
+
}({});
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/post/createPostPipeline.ts
|
|
685
|
+
const BLOOM_STRENGTH = .35;
|
|
686
|
+
const BLOOM_RADIUS = .4;
|
|
687
|
+
const BLOOM_THRESHOLD = .85;
|
|
688
|
+
const MIN_VIEW_DISTANCE = .001;
|
|
689
|
+
const createPostPipeline = ({ camera, fogUniforms, godraysLight, gradeLutTexture, postUniforms: { godraysColor, gradeIntensity, outlineColor, outlineFadeDistance, outlineThickness }, qualityTierSettings: { antialiasingMode, godraysStepCount, isBloomEnabled }, renderer, scene }) => {
|
|
690
|
+
const renderPipeline = new RenderPipeline(renderer);
|
|
691
|
+
renderPipeline.outputColorTransform = false;
|
|
692
|
+
const viewDistance = modelViewMatrix.mul(vec4(positionLocal, 1)).z.negate().max(MIN_VIEW_DISTANCE);
|
|
693
|
+
const thicknessNode = outlineThickness.mul(outlineFadeDistance.div(viewDistance).min(1));
|
|
694
|
+
const scenePass = new ToonOutlinePassNode(scene, camera, outlineColor, thicknessNode, float(1));
|
|
695
|
+
const isTraa = antialiasingMode === "Traa";
|
|
696
|
+
if (isTraa) scenePass.setMRT(mrt({
|
|
697
|
+
output,
|
|
698
|
+
velocity
|
|
699
|
+
}));
|
|
700
|
+
const sceneColor = scenePass.getTextureNode("output");
|
|
701
|
+
const sceneDepth = scenePass.getTextureNode("depth");
|
|
702
|
+
const postPipeline = { renderPipeline };
|
|
703
|
+
let litNode = sceneColor;
|
|
704
|
+
if (godraysStepCount > 0) {
|
|
705
|
+
const godraysNode = godrays(sceneDepth, camera, godraysLight);
|
|
706
|
+
godraysNode.raymarchSteps.value = godraysStepCount;
|
|
707
|
+
const blurredGodrays = bilateralBlur(godraysNode.getTextureNode());
|
|
708
|
+
litNode = depthAwareBlend(sceneColor, blurredGodrays.getTextureNode(), sceneDepth, camera, { blendColor: godraysColor });
|
|
709
|
+
postPipeline.godraysNode = godraysNode;
|
|
710
|
+
}
|
|
711
|
+
const foggedNode = createHeightFogNode(litNode, sceneDepth, camera, fogUniforms);
|
|
712
|
+
let bloomedNode = foggedNode;
|
|
713
|
+
if (isBloomEnabled) {
|
|
714
|
+
const bloomNode = bloom(foggedNode, BLOOM_STRENGTH, BLOOM_RADIUS, BLOOM_THRESHOLD);
|
|
715
|
+
bloomedNode = foggedNode.add(bloomNode);
|
|
716
|
+
postPipeline.bloomNode = bloomNode;
|
|
717
|
+
}
|
|
718
|
+
const resolvedNode = isTraa ? traa(bloomedNode, sceneDepth, scenePass.getTextureNode("velocity"), camera) : bloomedNode;
|
|
719
|
+
const displayNode = renderOutput(resolvedNode);
|
|
720
|
+
const gradedNode = lut3D(displayNode, texture3D(gradeLutTexture), gradeLutTexture.image.width, gradeIntensity);
|
|
721
|
+
renderPipeline.outputNode = isTraa ? gradedNode : smaa(gradedNode);
|
|
722
|
+
return postPipeline;
|
|
723
|
+
};
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/post/createPostUniforms.ts
|
|
726
|
+
const createPostUniforms = () => ({
|
|
727
|
+
godraysColor: uniform(new Color(16773590)),
|
|
728
|
+
gradeIntensity: uniform(1),
|
|
729
|
+
outlineColor: uniform(new Color(2761272)),
|
|
730
|
+
outlineFadeDistance: uniform(40),
|
|
731
|
+
outlineThickness: uniform(.003)
|
|
732
|
+
});
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/renderer/QualityTier.ts
|
|
735
|
+
let QualityTier = /* @__PURE__ */ function(QualityTier) {
|
|
736
|
+
QualityTier["High"] = "High";
|
|
737
|
+
QualityTier["Low"] = "Low";
|
|
738
|
+
QualityTier["Medium"] = "Medium";
|
|
739
|
+
return QualityTier;
|
|
740
|
+
}({});
|
|
741
|
+
//#endregion
|
|
742
|
+
//#region src/renderer/QualityTierSettingsMap.ts
|
|
743
|
+
const QualityTierSettingsMap = {
|
|
744
|
+
["High"]: {
|
|
745
|
+
antialiasingMode: "Traa",
|
|
746
|
+
cascadeCount: 4,
|
|
747
|
+
godraysStepCount: 60,
|
|
748
|
+
grassDensity: 1,
|
|
749
|
+
isBloomEnabled: true,
|
|
750
|
+
maxPixelRatio: 2,
|
|
751
|
+
shadowMapSize: 2048
|
|
752
|
+
},
|
|
753
|
+
["Low"]: {
|
|
754
|
+
antialiasingMode: "Smaa",
|
|
755
|
+
cascadeCount: 2,
|
|
756
|
+
godraysStepCount: 0,
|
|
757
|
+
grassDensity: .3,
|
|
758
|
+
isBloomEnabled: false,
|
|
759
|
+
maxPixelRatio: 1,
|
|
760
|
+
shadowMapSize: 1024
|
|
761
|
+
},
|
|
762
|
+
["Medium"]: {
|
|
763
|
+
antialiasingMode: "Smaa",
|
|
764
|
+
cascadeCount: 3,
|
|
765
|
+
godraysStepCount: 30,
|
|
766
|
+
grassDensity: .6,
|
|
767
|
+
isBloomEnabled: true,
|
|
768
|
+
maxPixelRatio: 1.5,
|
|
769
|
+
shadowMapSize: 1024
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/terrain/constants.ts
|
|
774
|
+
const TILE_INDEX_OFFSET = 2 ** 19;
|
|
775
|
+
const TILE_INDEX_SPAN = 2 ** 20;
|
|
776
|
+
const TERRAIN_LAYER = 1;
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/terrain/getTerrainTileLevel.ts
|
|
779
|
+
const getTerrainTileLevel = (key) => Math.floor(key / TILE_INDEX_SPAN / TILE_INDEX_SPAN);
|
|
780
|
+
//#endregion
|
|
781
|
+
//#region src/streaming/createTileStreamer.ts
|
|
782
|
+
const createTileStreamer = ({ disposeTile, maxCachedCount, maxPendingCount, requestTile }) => {
|
|
783
|
+
const cachedTileMap = /* @__PURE__ */ new Map();
|
|
784
|
+
const pendingWantedFrameMap = /* @__PURE__ */ new Map();
|
|
785
|
+
let frame = 0;
|
|
786
|
+
let isDisposed = false;
|
|
787
|
+
const evictStale = () => {
|
|
788
|
+
while (cachedTileMap.size > maxCachedCount) {
|
|
789
|
+
let staleKey;
|
|
790
|
+
let staleFrame = frame;
|
|
791
|
+
for (const [key, { lastWantedFrame }] of cachedTileMap) if (lastWantedFrame < staleFrame) {
|
|
792
|
+
staleKey = key;
|
|
793
|
+
staleFrame = lastWantedFrame;
|
|
794
|
+
}
|
|
795
|
+
if (staleKey === void 0) return;
|
|
796
|
+
const staleTile = cachedTileMap.get(staleKey);
|
|
797
|
+
cachedTileMap.delete(staleKey);
|
|
798
|
+
if (staleTile) disposeTile(staleTile.tile);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
return {
|
|
802
|
+
dispose: () => {
|
|
803
|
+
isDisposed = true;
|
|
804
|
+
for (const { tile } of cachedTileMap.values()) disposeTile(tile);
|
|
805
|
+
cachedTileMap.clear();
|
|
806
|
+
pendingWantedFrameMap.clear();
|
|
807
|
+
},
|
|
808
|
+
get: (key) => cachedTileMap.get(key)?.tile,
|
|
809
|
+
has: (key) => cachedTileMap.has(key),
|
|
810
|
+
receive: (key, tile) => {
|
|
811
|
+
const lastWantedFrame = pendingWantedFrameMap.get(key) ?? frame;
|
|
812
|
+
pendingWantedFrameMap.delete(key);
|
|
813
|
+
if (isDisposed) {
|
|
814
|
+
disposeTile(tile);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
cachedTileMap.set(key, {
|
|
818
|
+
lastWantedFrame,
|
|
819
|
+
tile
|
|
820
|
+
});
|
|
821
|
+
evictStale();
|
|
822
|
+
},
|
|
823
|
+
update: ({ count, keys }, drawn) => {
|
|
824
|
+
frame++;
|
|
825
|
+
let coarsestLevel = 0;
|
|
826
|
+
for (let index = 0; index < count; index++) {
|
|
827
|
+
const key = keys[index] ?? 0;
|
|
828
|
+
const cachedTile = cachedTileMap.get(key);
|
|
829
|
+
if (cachedTile) cachedTile.lastWantedFrame = frame;
|
|
830
|
+
else {
|
|
831
|
+
if (pendingWantedFrameMap.has(key)) pendingWantedFrameMap.set(key, frame);
|
|
832
|
+
coarsestLevel = Math.max(coarsestLevel, getTerrainTileLevel(key));
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
for (let index = 0; index < drawn.count; index++) {
|
|
836
|
+
const cachedTile = cachedTileMap.get(drawn.keys[index] ?? 0);
|
|
837
|
+
if (cachedTile) cachedTile.lastWantedFrame = frame;
|
|
838
|
+
}
|
|
839
|
+
for (let level = coarsestLevel; level >= 0 && pendingWantedFrameMap.size < maxPendingCount; level--) for (let index = 0; index < count && pendingWantedFrameMap.size < maxPendingCount; index++) {
|
|
840
|
+
const key = keys[index] ?? 0;
|
|
841
|
+
if (getTerrainTileLevel(key) !== level || cachedTileMap.has(key) || pendingWantedFrameMap.has(key)) continue;
|
|
842
|
+
pendingWantedFrameMap.set(key, frame);
|
|
843
|
+
requestTile(key);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/terrain/computeTerrainIndices.ts
|
|
850
|
+
const MAX_UINT16_VERTEX_COUNT = 2 ** 16;
|
|
851
|
+
const computeTerrainIndices = (cellsPerSide) => {
|
|
852
|
+
const side = cellsPerSide + 1;
|
|
853
|
+
const indexCount = cellsPerSide * cellsPerSide * 6;
|
|
854
|
+
const indices = side * side > MAX_UINT16_VERTEX_COUNT ? new Uint32Array(indexCount) : new Uint16Array(indexCount);
|
|
855
|
+
let cursor = 0;
|
|
856
|
+
for (let row = 0; row < cellsPerSide; row++) for (let column = 0; column < cellsPerSide; column++) {
|
|
857
|
+
const topLeft = row * side + column;
|
|
858
|
+
const bottomLeft = topLeft + side;
|
|
859
|
+
indices[cursor++] = topLeft;
|
|
860
|
+
indices[cursor++] = bottomLeft;
|
|
861
|
+
indices[cursor++] = topLeft + 1;
|
|
862
|
+
indices[cursor++] = topLeft + 1;
|
|
863
|
+
indices[cursor++] = bottomLeft;
|
|
864
|
+
indices[cursor++] = bottomLeft + 1;
|
|
865
|
+
}
|
|
866
|
+
return indices;
|
|
867
|
+
};
|
|
868
|
+
//#endregion
|
|
869
|
+
//#region src/terrain/getTerrainTileColumn.ts
|
|
870
|
+
const getTerrainTileColumn = (key) => key % TILE_INDEX_SPAN - TILE_INDEX_OFFSET;
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/terrain/getTerrainTileRow.ts
|
|
873
|
+
const getTerrainTileRow = (key) => Math.floor(key / TILE_INDEX_SPAN) % TILE_INDEX_SPAN - TILE_INDEX_OFFSET;
|
|
874
|
+
//#endregion
|
|
875
|
+
//#region src/terrain/computeTerrainTile.ts
|
|
876
|
+
const computeTerrainTile = ({ cellsPerSide, finestTileSize, getHeight, key, writeColor }) => {
|
|
877
|
+
const level = getTerrainTileLevel(key);
|
|
878
|
+
const size = finestTileSize * 2 ** level;
|
|
879
|
+
const step = size / cellsPerSide;
|
|
880
|
+
const left = getTerrainTileColumn(key) * size;
|
|
881
|
+
const back = getTerrainTileRow(key) * size;
|
|
882
|
+
const side = cellsPerSide + 1;
|
|
883
|
+
const paddedSide = side + 2;
|
|
884
|
+
const heights = new Float32Array(paddedSide * paddedSide);
|
|
885
|
+
for (let row = 0; row < paddedSide; row++) for (let column = 0; column < paddedSide; column++) heights[row * paddedSide + column] = getHeight(left + (column - 1) * step, back + (row - 1) * step);
|
|
886
|
+
const vertexCount = side * side;
|
|
887
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
888
|
+
const normals = new Float32Array(vertexCount * 3);
|
|
889
|
+
const colors = new Float32Array(vertexCount * 3);
|
|
890
|
+
const coarsePositions = new Float32Array(vertexCount * 4);
|
|
891
|
+
for (let row = 0; row < side; row++) for (let column = 0; column < side; column++) {
|
|
892
|
+
const paddedIndex = (row + 1) * paddedSide + column + 1;
|
|
893
|
+
const height = heights[paddedIndex] ?? 0;
|
|
894
|
+
const normalX = (heights[paddedIndex - 1] ?? height) - (heights[paddedIndex + 1] ?? height);
|
|
895
|
+
const normalY = 2 * step;
|
|
896
|
+
const normalZ = (heights[paddedIndex - paddedSide] ?? height) - (heights[paddedIndex + paddedSide] ?? height);
|
|
897
|
+
const length = Math.hypot(normalX, normalY, normalZ);
|
|
898
|
+
const index = row * side + column;
|
|
899
|
+
const offset = index * 3;
|
|
900
|
+
const x = column * step;
|
|
901
|
+
const z = row * step;
|
|
902
|
+
positions[offset] = x;
|
|
903
|
+
positions[offset + 1] = height;
|
|
904
|
+
positions[offset + 2] = z;
|
|
905
|
+
normals[offset] = normalX / length;
|
|
906
|
+
normals[offset + 1] = normalY / length;
|
|
907
|
+
normals[offset + 2] = normalZ / length;
|
|
908
|
+
writeColor(colors, offset, height, 1 - normalY / length, left + x, back + z);
|
|
909
|
+
const coarseColumn = column - column % 2;
|
|
910
|
+
const coarseRow = row - row % 2;
|
|
911
|
+
const coarseOffset = index * 4;
|
|
912
|
+
coarsePositions[coarseOffset] = coarseColumn * step;
|
|
913
|
+
coarsePositions[coarseOffset + 1] = heights[(coarseRow + 1) * paddedSide + coarseColumn + 1] ?? height;
|
|
914
|
+
coarsePositions[coarseOffset + 2] = coarseRow * step;
|
|
915
|
+
coarsePositions[coarseOffset + 3] = level;
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
coarsePositions,
|
|
919
|
+
colors,
|
|
920
|
+
key,
|
|
921
|
+
normals,
|
|
922
|
+
positions
|
|
923
|
+
};
|
|
924
|
+
};
|
|
925
|
+
//#endregion
|
|
926
|
+
//#region src/terrain/createTerrainMaterial.ts
|
|
927
|
+
const createTerrainMaterial = ({ finestRange, morphShare }, toonMaterialOptions, waterUniforms) => {
|
|
928
|
+
const terrainMaterial = createToonMaterial({
|
|
929
|
+
...toonMaterialOptions,
|
|
930
|
+
isOutlined: false,
|
|
931
|
+
isVertexColors: true
|
|
932
|
+
});
|
|
933
|
+
const coarsePosition = attribute("coarsePosition", "vec4");
|
|
934
|
+
const morphEnd = exp2(coarsePosition.w).mul(finestRange);
|
|
935
|
+
const morphStart = morphEnd.mul(1 - morphShare);
|
|
936
|
+
const morphAmount = modelWorldMatrix.mul(vec4(positionLocal, 1)).xyz.distance(cameraPosition).sub(morphStart).div(morphEnd.sub(morphStart)).saturate();
|
|
937
|
+
terrainMaterial.positionNode = mix(positionLocal, coarsePosition.xyz, morphAmount);
|
|
938
|
+
if (waterUniforms) {
|
|
939
|
+
const { lightUniforms } = toonMaterialOptions;
|
|
940
|
+
terrainMaterial.emissiveNode = createRimNode(lightUniforms).add(createCausticsNode(lightUniforms, waterUniforms));
|
|
941
|
+
}
|
|
942
|
+
return terrainMaterial;
|
|
943
|
+
};
|
|
944
|
+
//#endregion
|
|
945
|
+
//#region src/terrain/createTerrainSelection.ts
|
|
946
|
+
const createTerrainSelection = (capacity) => ({
|
|
947
|
+
count: 0,
|
|
948
|
+
keys: new Float64Array(capacity)
|
|
949
|
+
});
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region src/terrain/createTerrainTileGeometry.ts
|
|
952
|
+
const createTerrainTileGeometry = ({ coarsePositions, colors, normals, positions }, index) => {
|
|
953
|
+
const geometry = new BufferGeometry();
|
|
954
|
+
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
955
|
+
geometry.setAttribute("normal", new BufferAttribute(normals, 3));
|
|
956
|
+
geometry.setAttribute("color", new BufferAttribute(colors, 3));
|
|
957
|
+
geometry.setAttribute("coarsePosition", new BufferAttribute(coarsePositions, 4));
|
|
958
|
+
geometry.setIndex(index);
|
|
959
|
+
geometry.computeBoundingBox();
|
|
960
|
+
geometry.computeBoundingSphere();
|
|
961
|
+
return geometry;
|
|
962
|
+
};
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region src/terrain/getTerrainTileKey.ts
|
|
965
|
+
const getTerrainTileKey = (level, column, row) => (level * TILE_INDEX_SPAN + row + TILE_INDEX_OFFSET) * TILE_INDEX_SPAN + column + TILE_INDEX_OFFSET;
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/terrain/getParentTerrainTileKey.ts
|
|
968
|
+
const getParentTerrainTileKey = (key) => {
|
|
969
|
+
const level = getTerrainTileLevel(key);
|
|
970
|
+
const column = getTerrainTileColumn(key);
|
|
971
|
+
const row = getTerrainTileRow(key);
|
|
972
|
+
return getTerrainTileKey(level + 1, Math.floor(column / 2), Math.floor(row / 2));
|
|
973
|
+
};
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/terrain/resolveTerrainDraws.ts
|
|
976
|
+
const fallbackKeys = /* @__PURE__ */ new Set();
|
|
977
|
+
const checkFallbackAncestor = (key, levelCount) => {
|
|
978
|
+
if (fallbackKeys.size === 0) return false;
|
|
979
|
+
let ancestorKey = key;
|
|
980
|
+
for (let level = getTerrainTileLevel(key) + 1; level < levelCount; level++) {
|
|
981
|
+
ancestorKey = getParentTerrainTileKey(ancestorKey);
|
|
982
|
+
if (fallbackKeys.has(ancestorKey)) return true;
|
|
983
|
+
}
|
|
984
|
+
return false;
|
|
985
|
+
};
|
|
986
|
+
const resolveTerrainDraws = ({ levelCount }, wanted, checkTileLoaded, draws) => {
|
|
987
|
+
fallbackKeys.clear();
|
|
988
|
+
for (let index = 0; index < wanted.count; index++) {
|
|
989
|
+
let key = wanted.keys[index] ?? 0;
|
|
990
|
+
if (checkTileLoaded(key)) continue;
|
|
991
|
+
for (let level = getTerrainTileLevel(key) + 1; level < levelCount; level++) {
|
|
992
|
+
key = getParentTerrainTileKey(key);
|
|
993
|
+
if (!checkTileLoaded(key)) continue;
|
|
994
|
+
fallbackKeys.add(key);
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
draws.count = 0;
|
|
999
|
+
for (let index = 0; index < wanted.count && draws.count < draws.keys.length; index++) {
|
|
1000
|
+
const key = wanted.keys[index] ?? 0;
|
|
1001
|
+
if (!checkTileLoaded(key) || checkFallbackAncestor(key, levelCount)) continue;
|
|
1002
|
+
draws.keys[draws.count] = key;
|
|
1003
|
+
draws.count++;
|
|
1004
|
+
}
|
|
1005
|
+
for (const key of fallbackKeys) {
|
|
1006
|
+
if (draws.count === draws.keys.length || checkFallbackAncestor(key, levelCount)) continue;
|
|
1007
|
+
draws.keys[draws.count] = key;
|
|
1008
|
+
draws.count++;
|
|
1009
|
+
}
|
|
1010
|
+
return draws;
|
|
1011
|
+
};
|
|
1012
|
+
//#endregion
|
|
1013
|
+
//#region src/terrain/selectTerrainTiles.ts
|
|
1014
|
+
const tileBox = new Box3();
|
|
1015
|
+
const addKey = (selection, key) => {
|
|
1016
|
+
if (selection.count === selection.keys.length) return;
|
|
1017
|
+
selection.keys[selection.count] = key;
|
|
1018
|
+
selection.count++;
|
|
1019
|
+
};
|
|
1020
|
+
const selectTile = (terrainOptions, eye, frustum, selection, level, column, row) => {
|
|
1021
|
+
const { finestRange, finestTileSize, maxHeight, minHeight } = terrainOptions;
|
|
1022
|
+
const size = finestTileSize * 2 ** level;
|
|
1023
|
+
const left = column * size;
|
|
1024
|
+
const back = row * size;
|
|
1025
|
+
const distanceX = Math.max(left - eye.x, 0, eye.x - left - size);
|
|
1026
|
+
const distanceY = Math.max(minHeight - eye.y, 0, eye.y - maxHeight);
|
|
1027
|
+
const distanceZ = Math.max(back - eye.z, 0, eye.z - back - size);
|
|
1028
|
+
const distance = Math.hypot(distanceX, distanceY, distanceZ);
|
|
1029
|
+
const range = finestRange * 2 ** level;
|
|
1030
|
+
if (distance > range) return false;
|
|
1031
|
+
if (frustum) {
|
|
1032
|
+
tileBox.min.set(left, minHeight, back);
|
|
1033
|
+
tileBox.max.set(left + size, maxHeight, back + size);
|
|
1034
|
+
if (!frustum.intersectsBox(tileBox)) return true;
|
|
1035
|
+
}
|
|
1036
|
+
if (level === 0 || distance > range / 2) {
|
|
1037
|
+
addKey(selection, getTerrainTileKey(level, column, row));
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
for (let childRow = row * 2; childRow < row * 2 + 2; childRow++) for (let childColumn = column * 2; childColumn < column * 2 + 2; childColumn++) if (!selectTile(terrainOptions, eye, frustum, selection, level - 1, childColumn, childRow)) addKey(selection, getTerrainTileKey(level - 1, childColumn, childRow));
|
|
1041
|
+
return true;
|
|
1042
|
+
};
|
|
1043
|
+
const selectTerrainTiles = (terrainOptions, eye, frustum, selection) => {
|
|
1044
|
+
const { finestRange, finestTileSize, levelCount } = terrainOptions;
|
|
1045
|
+
const level = levelCount - 1;
|
|
1046
|
+
const size = finestTileSize * 2 ** level;
|
|
1047
|
+
const range = finestRange * 2 ** level;
|
|
1048
|
+
selection.count = 0;
|
|
1049
|
+
for (let row = Math.floor((eye.z - range) / size); row <= Math.floor((eye.z + range) / size); row++) for (let column = Math.floor((eye.x - range) / size); column <= Math.floor((eye.x + range) / size); column++) selectTile(terrainOptions, eye, frustum, selection, level, column, row);
|
|
1050
|
+
return selection;
|
|
1051
|
+
};
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/vegetation/computeGrassBlade.ts
|
|
1054
|
+
const computeGrassBlade = (segmentCount) => {
|
|
1055
|
+
const positions = new Float32Array((segmentCount * 2 + 1) * 3);
|
|
1056
|
+
for (let segment = 0; segment < segmentCount; segment++) {
|
|
1057
|
+
const height = segment / segmentCount;
|
|
1058
|
+
const halfWidth = (1 - height) / 2;
|
|
1059
|
+
const offset = segment * 6;
|
|
1060
|
+
positions[offset] = -halfWidth;
|
|
1061
|
+
positions[offset + 1] = height;
|
|
1062
|
+
positions[offset + 3] = halfWidth;
|
|
1063
|
+
positions[offset + 4] = height;
|
|
1064
|
+
}
|
|
1065
|
+
positions[segmentCount * 6 + 1] = 1;
|
|
1066
|
+
const indices = new Uint16Array((segmentCount - 1) * 6 + 3);
|
|
1067
|
+
let cursor = 0;
|
|
1068
|
+
for (let segment = 0; segment < segmentCount - 1; segment++) {
|
|
1069
|
+
const left = segment * 2;
|
|
1070
|
+
indices[cursor++] = left;
|
|
1071
|
+
indices[cursor++] = left + 1;
|
|
1072
|
+
indices[cursor++] = left + 2;
|
|
1073
|
+
indices[cursor++] = left + 2;
|
|
1074
|
+
indices[cursor++] = left + 1;
|
|
1075
|
+
indices[cursor++] = left + 3;
|
|
1076
|
+
}
|
|
1077
|
+
const lastLeft = (segmentCount - 1) * 2;
|
|
1078
|
+
indices[cursor++] = lastLeft;
|
|
1079
|
+
indices[cursor++] = lastLeft + 1;
|
|
1080
|
+
indices[cursor] = segmentCount * 2;
|
|
1081
|
+
return {
|
|
1082
|
+
indices,
|
|
1083
|
+
positions
|
|
1084
|
+
};
|
|
1085
|
+
};
|
|
1086
|
+
//#endregion
|
|
1087
|
+
//#region src/vegetation/createGrassGeometry.ts
|
|
1088
|
+
const createGrassGeometry = (segmentCount, bladeCount) => {
|
|
1089
|
+
const { indices, positions } = computeGrassBlade(segmentCount);
|
|
1090
|
+
const grassGeometry = new InstancedBufferGeometry();
|
|
1091
|
+
grassGeometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
1092
|
+
grassGeometry.setIndex(new BufferAttribute(indices, 1));
|
|
1093
|
+
grassGeometry.instanceCount = bladeCount;
|
|
1094
|
+
return grassGeometry;
|
|
1095
|
+
};
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/vegetation/createGrassMaterial.ts
|
|
1098
|
+
const MIN_GREENNESS = .05;
|
|
1099
|
+
const BEND_PER_WIND = .9;
|
|
1100
|
+
const createGrassMaterial = ({ bladeHeight, bladeWidth, cameraGround, density, groundCapture, lightUniforms, rampTexture, ring, waterUniforms, windUniforms }) => {
|
|
1101
|
+
const grassMaterial = createToonMaterial({
|
|
1102
|
+
isOutlined: false,
|
|
1103
|
+
lightUniforms,
|
|
1104
|
+
rampTexture
|
|
1105
|
+
});
|
|
1106
|
+
grassMaterial.emissiveNode = null;
|
|
1107
|
+
grassMaterial.side = DoubleSide;
|
|
1108
|
+
const { cellsPerSide, fadeEnd, fadeStart, innerRadius, scale, spacing } = ring;
|
|
1109
|
+
const index = float(instanceIndex);
|
|
1110
|
+
const row = index.div(cellsPerSide).floor();
|
|
1111
|
+
const column = index.sub(row.mul(cellsPerSide));
|
|
1112
|
+
const gridOrigin = cameraGround.div(spacing).floor().mul(spacing);
|
|
1113
|
+
const cell = vec2(column, row).sub(cellsPerSide / 2).mul(spacing).add(gridOrigin);
|
|
1114
|
+
const seed = cell.x.mul(127.1).add(cell.y.mul(311.7));
|
|
1115
|
+
const ground = cell.add(vec2(hash(seed), hash(seed.add(1))).sub(.5).mul(spacing));
|
|
1116
|
+
const captureUV = vec2(ground.x.sub(groundCapture.center.x).div(groundCapture.size).add(.5), float(.5).sub(ground.y.sub(groundCapture.center.y).div(groundCapture.size)));
|
|
1117
|
+
const captured = texture(groundCapture.renderTarget.texture, captureUV).level(float(0));
|
|
1118
|
+
const isInsideCapture = step(0, captureUV.x).mul(step(captureUV.x, 1)).mul(step(0, captureUV.y)).mul(step(captureUV.y, 1));
|
|
1119
|
+
const greenness = captured.g.sub(captured.r.max(captured.b));
|
|
1120
|
+
const isGrown = isInsideCapture.mul(step(MIN_GREENNESS, greenness)).mul(step(waterUniforms.level, captured.a)).mul(step(hash(seed.add(2)), density));
|
|
1121
|
+
const distance = ground.distance(cameraGround);
|
|
1122
|
+
const fade = smoothstep(innerRadius, innerRadius + spacing * 4, distance).mul(float(1).sub(smoothstep(fadeStart, fadeEnd, distance)));
|
|
1123
|
+
const size = hash(seed.add(3)).mul(.5).add(.75).mul(fade).mul(isGrown).mul(scale);
|
|
1124
|
+
const angle = hash(seed.add(4)).mul(Math.PI * 2);
|
|
1125
|
+
const across = positionGeometry.x.mul(bladeWidth).mul(size);
|
|
1126
|
+
const up = positionGeometry.y;
|
|
1127
|
+
const wind = createWindNode(windUniforms, ground).mul(BEND_PER_WIND).mul(up.mul(up));
|
|
1128
|
+
const bladeHeightNode = up.mul(bladeHeight).mul(size).sub(wind.length().mul(bladeHeight).mul(size).mul(.3));
|
|
1129
|
+
grassMaterial.positionNode = vec3(ground.x.add(across.mul(cos(angle))).add(wind.x.mul(bladeHeight).mul(size)), captured.a.add(bladeHeightNode), ground.y.add(across.mul(sin(angle))).add(wind.y.mul(bladeHeight).mul(size)));
|
|
1130
|
+
grassMaterial.normalNode = cameraViewMatrix.mul(vec4(0, 1, 0, 0)).xyz;
|
|
1131
|
+
const groundColor = varying(captured.rgb);
|
|
1132
|
+
grassMaterial.colorNode = mix(groundColor.mul(.72), groundColor.mul(1.12).add(vec3(.03, .03, 0)), up);
|
|
1133
|
+
return grassMaterial;
|
|
1134
|
+
};
|
|
1135
|
+
//#endregion
|
|
1136
|
+
//#region src/vegetation/createGroundCapture.ts
|
|
1137
|
+
const createGroundCapture = (size, resolution) => {
|
|
1138
|
+
const renderTarget = new RenderTarget(resolution, resolution, { type: HalfFloatType });
|
|
1139
|
+
renderTarget.texture.magFilter = LinearFilter;
|
|
1140
|
+
renderTarget.texture.minFilter = LinearFilter;
|
|
1141
|
+
const halfSize = size / 2;
|
|
1142
|
+
const camera = new OrthographicCamera(-halfSize, halfSize, halfSize, -halfSize);
|
|
1143
|
+
camera.rotation.x = -Math.PI / 2;
|
|
1144
|
+
camera.layers.set(1);
|
|
1145
|
+
const material = new MeshBasicNodeMaterial();
|
|
1146
|
+
material.outputNode = vec4(attribute("color", "vec3"), positionWorld.y);
|
|
1147
|
+
return {
|
|
1148
|
+
camera,
|
|
1149
|
+
center: uniform(new Vector2()),
|
|
1150
|
+
material,
|
|
1151
|
+
renderTarget,
|
|
1152
|
+
size
|
|
1153
|
+
};
|
|
1154
|
+
};
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/vegetation/renderGroundCapture.ts
|
|
1157
|
+
const clearColor = new Color();
|
|
1158
|
+
const previousClearColor = new Color();
|
|
1159
|
+
const renderGroundCapture = (renderer, scene, { camera, center, material, renderTarget }, { maxHeight }, worldCenter, origin) => {
|
|
1160
|
+
center.value.set(worldCenter.x, worldCenter.z);
|
|
1161
|
+
camera.position.set(worldCenter.x - origin.x, maxHeight + 1, worldCenter.z - origin.z);
|
|
1162
|
+
camera.far = maxHeight * 4;
|
|
1163
|
+
camera.updateProjectionMatrix();
|
|
1164
|
+
camera.updateMatrixWorld();
|
|
1165
|
+
const previousRenderTarget = renderer.getRenderTarget();
|
|
1166
|
+
const previousClearAlpha = renderer.getClearAlpha();
|
|
1167
|
+
renderer.getClearColor(previousClearColor);
|
|
1168
|
+
const previousOverrideMaterial = scene.overrideMaterial;
|
|
1169
|
+
const previousBackgroundNode = scene.backgroundNode;
|
|
1170
|
+
scene.overrideMaterial = material;
|
|
1171
|
+
scene.backgroundNode = null;
|
|
1172
|
+
renderer.setClearColor(clearColor, 0);
|
|
1173
|
+
renderer.setRenderTarget(renderTarget);
|
|
1174
|
+
renderer.clear();
|
|
1175
|
+
renderer.render(scene, camera);
|
|
1176
|
+
renderer.setRenderTarget(previousRenderTarget);
|
|
1177
|
+
renderer.setClearColor(previousClearColor, previousClearAlpha);
|
|
1178
|
+
scene.overrideMaterial = previousOverrideMaterial;
|
|
1179
|
+
scene.backgroundNode = previousBackgroundNode;
|
|
1180
|
+
};
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/water/createUnderwaterFogState.ts
|
|
1183
|
+
const createUnderwaterFogState = () => ({
|
|
1184
|
+
aboveDensity: 0,
|
|
1185
|
+
aboveHeightFalloff: 0,
|
|
1186
|
+
aboveStartDistance: 0,
|
|
1187
|
+
isUnderwater: false
|
|
1188
|
+
});
|
|
1189
|
+
//#endregion
|
|
1190
|
+
//#region src/water/createWaterUniforms.ts
|
|
1191
|
+
const createWaterUniforms = () => ({
|
|
1192
|
+
causticStrength: uniform(0),
|
|
1193
|
+
deepColor: uniform(new Color()),
|
|
1194
|
+
deepDepth: uniform(1),
|
|
1195
|
+
foamColor: uniform(new Color(16777215)),
|
|
1196
|
+
foamDepth: uniform(0),
|
|
1197
|
+
level: uniform(0),
|
|
1198
|
+
shallowColor: uniform(new Color()),
|
|
1199
|
+
underwaterFogColor: uniform(new Color()),
|
|
1200
|
+
underwaterFogDensity: uniform(0)
|
|
1201
|
+
});
|
|
1202
|
+
//#endregion
|
|
1203
|
+
//#region src/water/updateUnderwaterFog.ts
|
|
1204
|
+
const updateUnderwaterFog = (eyeHeight, { level, underwaterFogColor, underwaterFogDensity }, { color, density, heightFalloff, startDistance }, underwaterFogState) => {
|
|
1205
|
+
const isUnderwater = eyeHeight < level.value;
|
|
1206
|
+
if (isUnderwater && !underwaterFogState.isUnderwater) {
|
|
1207
|
+
underwaterFogState.aboveDensity = density.value;
|
|
1208
|
+
underwaterFogState.aboveHeightFalloff = heightFalloff.value;
|
|
1209
|
+
underwaterFogState.aboveStartDistance = startDistance.value;
|
|
1210
|
+
density.value = underwaterFogDensity.value;
|
|
1211
|
+
heightFalloff.value = 0;
|
|
1212
|
+
startDistance.value = 0;
|
|
1213
|
+
} else if (!isUnderwater && underwaterFogState.isUnderwater) {
|
|
1214
|
+
density.value = underwaterFogState.aboveDensity;
|
|
1215
|
+
heightFalloff.value = underwaterFogState.aboveHeightFalloff;
|
|
1216
|
+
startDistance.value = underwaterFogState.aboveStartDistance;
|
|
1217
|
+
}
|
|
1218
|
+
underwaterFogState.isUnderwater = isUnderwater;
|
|
1219
|
+
if (isUnderwater) color.value.copy(underwaterFogColor.value);
|
|
1220
|
+
};
|
|
1221
|
+
//#endregion
|
|
1222
|
+
//#region src/wind/createWindUniforms.ts
|
|
1223
|
+
const createWindUniforms = () => ({
|
|
1224
|
+
direction: uniform(new Vector2(1, 0)),
|
|
1225
|
+
gustSpeed: uniform(0),
|
|
1226
|
+
gustStrength: uniform(0),
|
|
1227
|
+
gustWidth: uniform(1),
|
|
1228
|
+
strength: uniform(0)
|
|
1229
|
+
});
|
|
1230
|
+
//#endregion
|
|
1231
|
+
//#region src/world/computeOriginShift.ts
|
|
1232
|
+
const computeOriginShift = (cameraPosition, threshold, step, shift) => {
|
|
1233
|
+
if (Math.hypot(cameraPosition.x, cameraPosition.z) <= threshold) return false;
|
|
1234
|
+
shift.set(Math.round(cameraPosition.x / step) * step, 0, Math.round(cameraPosition.z / step) * step);
|
|
1235
|
+
return shift.x !== 0 || shift.z !== 0;
|
|
1236
|
+
};
|
|
1237
|
+
//#endregion
|
|
1238
|
+
//#region src/world/computeOutlineDistance.ts
|
|
1239
|
+
const computeOutlineDistance = (outline, x, z) => {
|
|
1240
|
+
let isInside = false;
|
|
1241
|
+
let nearestSquared = Infinity;
|
|
1242
|
+
for (let index = 0, previousIndex = outline.length - 1; index < outline.length; previousIndex = index++) {
|
|
1243
|
+
const start = outline[previousIndex];
|
|
1244
|
+
const end = outline[index];
|
|
1245
|
+
if (!start || !end) continue;
|
|
1246
|
+
if (end.z > z !== start.z > z && x < (start.x - end.x) * (z - end.z) / (start.z - end.z) + end.x) isInside = !isInside;
|
|
1247
|
+
const edgeX = start.x - end.x;
|
|
1248
|
+
const edgeZ = start.z - end.z;
|
|
1249
|
+
const lengthSquared = edgeX * edgeX + edgeZ * edgeZ;
|
|
1250
|
+
const along = lengthSquared === 0 ? 0 : Math.min(Math.max(((x - end.x) * edgeX + (z - end.z) * edgeZ) / lengthSquared, 0), 1);
|
|
1251
|
+
const offsetX = x - end.x - along * edgeX;
|
|
1252
|
+
const offsetZ = z - end.z - along * edgeZ;
|
|
1253
|
+
nearestSquared = Math.min(nearestSquared, offsetX * offsetX + offsetZ * offsetZ);
|
|
1254
|
+
}
|
|
1255
|
+
return isInside ? 0 : Math.sqrt(nearestSquared);
|
|
1256
|
+
};
|
|
1257
|
+
//#endregion
|
|
1258
|
+
export { AntialiasingMode, GAME_MINUTES_PER_SECOND, 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, createFogUniforms, createGenshinRenderer, createGodraysLight, createGradeLutTexture, createGrassGeometry, createGrassMaterial, createGroundCapture, createHeightFogNode, createLeafMaterial, createLightUniforms, createPostPipeline, createPostUniforms, createRampTexture, createRimNode, createSeededRandom, createSimplexNoise, createSkyNode, createSkyState, createSkyUniforms, createStatueGeometry, createSunLight, createTerrainMaterial, createTerrainSelection, createTerrainTileGeometry, createTileStreamer, createToonMaterial, createTreeGeometry, createUnderwaterFogState, createWaterMaterial, createWaterUniforms, createWindNode, createWindUniforms, getParentTerrainTileKey, getTerrainTileColumn, getTerrainTileKey, getTerrainTileLevel, getTerrainTileRow, renderGroundCapture, resolveTerrainDraws, sampleSkyState, selectTerrainTiles, updateUnderwaterFog };
|