genshin-engine 3.9.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/index.d.ts +2294 -412
- package/dist/index.js +5307 -572
- package/dist/terrainWorker-CLOYYKT1.d.ts +207 -0
- package/dist/terrainWorker-DZ04cA3m.js +520 -0
- package/dist/terrainWorker.d.ts +2 -0
- package/dist/terrainWorker.js +1 -0
- package/package.json +13 -8
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import { Color, MathUtils } from "three";
|
|
2
|
+
//#region src/random/createSeededRandom.ts
|
|
3
|
+
const createSeededRandom = (seed) => {
|
|
4
|
+
let state = seed;
|
|
5
|
+
return () => {
|
|
6
|
+
state = state + 1831565813 >>> 0;
|
|
7
|
+
let mixed = Math.imul(state ^ state >>> 15, 1 | state);
|
|
8
|
+
mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, 61 | mixed);
|
|
9
|
+
return ((mixed ^ mixed >>> 14) >>> 0) / 2 ** 32;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/models/terrain/GroundLayer.ts
|
|
14
|
+
let GroundLayer = /* @__PURE__ */ function(GroundLayer) {
|
|
15
|
+
GroundLayer["Earth"] = "Earth";
|
|
16
|
+
GroundLayer["Grass"] = "Grass";
|
|
17
|
+
GroundLayer["Path"] = "Path";
|
|
18
|
+
GroundLayer["Rock"] = "Rock";
|
|
19
|
+
GroundLayer["Sand"] = "Sand";
|
|
20
|
+
GroundLayer["Snow"] = "Snow";
|
|
21
|
+
return GroundLayer;
|
|
22
|
+
}({});
|
|
23
|
+
const GroundLayers = Object.values(GroundLayer);
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/models/terrain/TerrainFeatureKind.ts
|
|
26
|
+
let TerrainFeatureKind = /* @__PURE__ */ function(TerrainFeatureKind) {
|
|
27
|
+
TerrainFeatureKind["Cliff"] = "cliff";
|
|
28
|
+
TerrainFeatureKind["Plateau"] = "plateau";
|
|
29
|
+
TerrainFeatureKind["Ridge"] = "ridge";
|
|
30
|
+
return TerrainFeatureKind;
|
|
31
|
+
}({});
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/noise/createSimplexNoise.ts
|
|
34
|
+
const SKEW = (Math.sqrt(3) - 1) / 2;
|
|
35
|
+
const UNSKEW = (3 - Math.sqrt(3)) / 6;
|
|
36
|
+
const GRADIENTS_X = [
|
|
37
|
+
1,
|
|
38
|
+
-1,
|
|
39
|
+
1,
|
|
40
|
+
-1,
|
|
41
|
+
1,
|
|
42
|
+
-1,
|
|
43
|
+
0,
|
|
44
|
+
0
|
|
45
|
+
];
|
|
46
|
+
const GRADIENTS_Y = [
|
|
47
|
+
1,
|
|
48
|
+
1,
|
|
49
|
+
-1,
|
|
50
|
+
-1,
|
|
51
|
+
0,
|
|
52
|
+
0,
|
|
53
|
+
1,
|
|
54
|
+
-1
|
|
55
|
+
];
|
|
56
|
+
const NOISE_SCALE = 70;
|
|
57
|
+
const getCornerContribution = (gradientIndex, offsetX, offsetY) => {
|
|
58
|
+
const falloff = .5 - offsetX * offsetX - offsetY * offsetY;
|
|
59
|
+
if (falloff < 0) return 0;
|
|
60
|
+
const gradient = gradientIndex & 7;
|
|
61
|
+
return falloff ** 4 * ((GRADIENTS_X[gradient] ?? 0) * offsetX + (GRADIENTS_Y[gradient] ?? 0) * offsetY);
|
|
62
|
+
};
|
|
63
|
+
const createSimplexNoise = (seed) => {
|
|
64
|
+
const random = createSeededRandom(seed);
|
|
65
|
+
const table = Uint8Array.from({ length: 256 }, (_value, index) => index);
|
|
66
|
+
for (let index = table.length - 1; index > 0; index--) {
|
|
67
|
+
const swapIndex = Math.floor(random() * (index + 1));
|
|
68
|
+
[table[index], table[swapIndex]] = [table[swapIndex] ?? 0, table[index] ?? 0];
|
|
69
|
+
}
|
|
70
|
+
const permutation = Uint8Array.from({ length: 512 }, (_value, index) => table[index & 255] ?? 0);
|
|
71
|
+
return (x, y) => {
|
|
72
|
+
const skew = (x + y) * SKEW;
|
|
73
|
+
const cellX = Math.floor(x + skew);
|
|
74
|
+
const cellY = Math.floor(y + skew);
|
|
75
|
+
const unskew = (cellX + cellY) * UNSKEW;
|
|
76
|
+
const offsetX = x - cellX + unskew;
|
|
77
|
+
const offsetY = y - cellY + unskew;
|
|
78
|
+
const middleX = offsetX > offsetY ? 1 : 0;
|
|
79
|
+
const middleY = 1 - middleX;
|
|
80
|
+
const tableX = cellX & 255;
|
|
81
|
+
const tableY = cellY & 255;
|
|
82
|
+
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));
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/terrain/constants.ts
|
|
87
|
+
const TILE_INDEX_OFFSET = 2 ** 19;
|
|
88
|
+
const TILE_INDEX_SPAN = 2 ** 20;
|
|
89
|
+
const TERRAIN_LAYER = 1;
|
|
90
|
+
const TERRAIN_RING_INITIAL_TILE_COUNT = 64;
|
|
91
|
+
const TERRAIN_CELL_SIZE = 64;
|
|
92
|
+
const GAUSSIAN_REACH_WIDTHS = 3;
|
|
93
|
+
const TERRAIN_HEIGHT_PADDING = 2;
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/terrain/getTerrainTileLevel.ts
|
|
96
|
+
const getTerrainTileLevel = (key) => Math.floor(key / TILE_INDEX_SPAN / TILE_INDEX_SPAN);
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/terrain/measureSegment.ts
|
|
99
|
+
const measurement = /* @__PURE__ */ new Float64Array(2);
|
|
100
|
+
const measureSegment = ({ endX, endZ, startX, startZ }, x, z) => {
|
|
101
|
+
const directionX = endX - startX;
|
|
102
|
+
const directionZ = endZ - startZ;
|
|
103
|
+
const length = Math.hypot(directionX, directionZ);
|
|
104
|
+
const offsetX = x - startX;
|
|
105
|
+
const offsetZ = z - startZ;
|
|
106
|
+
const along = (offsetX * directionX + offsetZ * directionZ) / length;
|
|
107
|
+
measurement[0] = (directionX * offsetZ - directionZ * offsetX) / length;
|
|
108
|
+
measurement[1] = Math.max(0, -along, along - length);
|
|
109
|
+
return measurement;
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/terrain/getCliffBlend.ts
|
|
113
|
+
const getCliffBlend = (feature, x, z) => {
|
|
114
|
+
const measurement = measureSegment(feature, x, z);
|
|
115
|
+
const across = measurement[0] ?? 0;
|
|
116
|
+
const beyond = measurement[1] ?? 0;
|
|
117
|
+
const { falloff, width } = feature;
|
|
118
|
+
return MathUtils.smoothstep(across, -falloff, falloff) * (1 - MathUtils.smoothstep(across, width, width + falloff)) * (1 - MathUtils.smoothstep(beyond, 0, falloff));
|
|
119
|
+
};
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/terrain/getPlateauBlend.ts
|
|
122
|
+
const getPlateauBlend = (feature, x, z) => {
|
|
123
|
+
const { falloff, radius, x: centreX, z: centreZ } = feature;
|
|
124
|
+
return 1 - MathUtils.smoothstep(Math.hypot(x - centreX, z - centreZ), radius, radius + falloff);
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/terrain/getRidgeBlend.ts
|
|
128
|
+
const getRidgeBlend = (feature, x, z) => {
|
|
129
|
+
const measurement = measureSegment(feature, x, z);
|
|
130
|
+
const across = measurement[0] ?? 0;
|
|
131
|
+
const beyond = measurement[1] ?? 0;
|
|
132
|
+
const { width } = feature;
|
|
133
|
+
return Math.exp(-(across ** 2 + beyond ** 2) / (2 * width * width));
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/terrain/TerrainFeatureKindBlendMap.ts
|
|
137
|
+
const TerrainFeatureKindBlendMap = {
|
|
138
|
+
["cliff"]: getCliffBlend,
|
|
139
|
+
["plateau"]: getPlateauBlend,
|
|
140
|
+
["ridge"]: getRidgeBlend
|
|
141
|
+
};
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/terrain/blendGridNodes.ts
|
|
144
|
+
const blendGridNodes = (values, sizeX, sizeZ, column, row) => {
|
|
145
|
+
const left = Math.floor(column);
|
|
146
|
+
const near = Math.floor(row);
|
|
147
|
+
const right = Math.min(left + 1, sizeX - 1);
|
|
148
|
+
const far = Math.min(near + 1, sizeZ - 1);
|
|
149
|
+
const across = column - left;
|
|
150
|
+
const along = row - near;
|
|
151
|
+
return ((values[near * sizeX + left] ?? 0) * (1 - across) + (values[near * sizeX + right] ?? 0) * across) * (1 - along) + ((values[far * sizeX + left] ?? 0) * (1 - across) + (values[far * sizeX + right] ?? 0) * across) * along;
|
|
152
|
+
};
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/terrain/getTerrainTileColumn.ts
|
|
155
|
+
const getTerrainTileColumn = (key) => key % TILE_INDEX_SPAN - TILE_INDEX_OFFSET;
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/terrain/getTerrainTileRow.ts
|
|
158
|
+
const getTerrainTileRow = (key) => Math.floor(key / TILE_INDEX_SPAN) % TILE_INDEX_SPAN - TILE_INDEX_OFFSET;
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/terrain/computeTerrainTile.ts
|
|
161
|
+
const writeNormal = (normals, offset, heights, paddedIndex, paddedSide, reach, step) => {
|
|
162
|
+
const height = heights[paddedIndex] ?? 0;
|
|
163
|
+
const normalX = (heights[paddedIndex - reach] ?? height) - (heights[paddedIndex + reach] ?? height);
|
|
164
|
+
const normalY = 2 * reach * step;
|
|
165
|
+
const normalZ = (heights[paddedIndex - reach * paddedSide] ?? height) - (heights[paddedIndex + reach * paddedSide] ?? height);
|
|
166
|
+
const length = Math.hypot(normalX, normalY, normalZ);
|
|
167
|
+
normals[offset] = normalX / length;
|
|
168
|
+
normals[offset + 1] = normalY / length;
|
|
169
|
+
normals[offset + 2] = normalZ / length;
|
|
170
|
+
return normalY / length;
|
|
171
|
+
};
|
|
172
|
+
const computeTerrainTile = ({ cellsPerSide, finestTileSize, getHeight, key, writeColor }) => {
|
|
173
|
+
const level = getTerrainTileLevel(key);
|
|
174
|
+
const size = finestTileSize * 2 ** level;
|
|
175
|
+
const step = size / cellsPerSide;
|
|
176
|
+
const left = getTerrainTileColumn(key) * size;
|
|
177
|
+
const back = getTerrainTileRow(key) * size;
|
|
178
|
+
const side = cellsPerSide + 1;
|
|
179
|
+
const paddedSide = side + 4;
|
|
180
|
+
const heights = new Float32Array(paddedSide * paddedSide);
|
|
181
|
+
for (let row = 0; row < paddedSide; row++) for (let column = 0; column < paddedSide; column++) heights[row * paddedSide + column] = getHeight(left + (column - 2) * step, back + (row - 2) * step);
|
|
182
|
+
const vertexCount = side * side;
|
|
183
|
+
const positions = new Float32Array(vertexCount * 3);
|
|
184
|
+
const normals = new Float32Array(vertexCount * 3);
|
|
185
|
+
const colors = new Float32Array(vertexCount * 3);
|
|
186
|
+
const coarsePositions = new Float32Array(vertexCount * 4);
|
|
187
|
+
const coarseNormals = new Float32Array(vertexCount * 3);
|
|
188
|
+
const coarseColors = new Float32Array(vertexCount * 3);
|
|
189
|
+
for (let row = 0; row < side; row++) for (let column = 0; column < side; column++) {
|
|
190
|
+
const paddedIndex = (row + 2) * paddedSide + column + 2;
|
|
191
|
+
const height = heights[paddedIndex] ?? 0;
|
|
192
|
+
const index = row * side + column;
|
|
193
|
+
const offset = index * 3;
|
|
194
|
+
const x = column * step;
|
|
195
|
+
const z = row * step;
|
|
196
|
+
positions[offset] = x;
|
|
197
|
+
positions[offset + 1] = height;
|
|
198
|
+
positions[offset + 2] = z;
|
|
199
|
+
writeColor(colors, offset, height, 1 - writeNormal(normals, offset, heights, paddedIndex, paddedSide, 1, step), left + x, back + z);
|
|
200
|
+
const coarseColumn = column - column % 2;
|
|
201
|
+
const coarseRow = row - row % 2;
|
|
202
|
+
const coarseOffset = index * 4;
|
|
203
|
+
coarsePositions[coarseOffset] = coarseColumn * step;
|
|
204
|
+
coarsePositions[coarseOffset + 1] = heights[(coarseRow + 2) * paddedSide + coarseColumn + 2] ?? height;
|
|
205
|
+
coarsePositions[coarseOffset + 2] = coarseRow * step;
|
|
206
|
+
coarsePositions[coarseOffset + 3] = level;
|
|
207
|
+
if (coarseColumn !== column || coarseRow !== row) {
|
|
208
|
+
const evenOffset = (coarseRow * side + coarseColumn) * 3;
|
|
209
|
+
for (let component = 0; component < 3; component++) {
|
|
210
|
+
coarseNormals[offset + component] = coarseNormals[evenOffset + component] ?? 0;
|
|
211
|
+
coarseColors[offset + component] = coarseColors[evenOffset + component] ?? 0;
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
writeColor(coarseColors, offset, height, 1 - writeNormal(coarseNormals, offset, heights, paddedIndex, paddedSide, 2, step), left + x, back + z);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
coarseColors,
|
|
219
|
+
coarseNormals,
|
|
220
|
+
coarsePositions,
|
|
221
|
+
colors,
|
|
222
|
+
key,
|
|
223
|
+
normals,
|
|
224
|
+
positions
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/terrain/fileByCell.ts
|
|
229
|
+
const NO_ITEMS = [];
|
|
230
|
+
const toCellKey = (column, row) => `${column},${row}`;
|
|
231
|
+
const fileByCell = (items, getBounds) => {
|
|
232
|
+
const cellItemsMap = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const item of items) {
|
|
234
|
+
const { maxX, maxZ, minX, minZ } = getBounds(item);
|
|
235
|
+
for (let column = Math.floor(minX / 64); column <= Math.floor(maxX / 64); column++) for (let row = Math.floor(minZ / 64); row <= Math.floor(maxZ / 64); row++) {
|
|
236
|
+
const key = toCellKey(column, row);
|
|
237
|
+
const cellItems = cellItemsMap.get(key);
|
|
238
|
+
if (cellItems) cellItems.push(item);
|
|
239
|
+
else cellItemsMap.set(key, [item]);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return (x, z) => cellItemsMap.get(toCellKey(Math.floor(x / 64), Math.floor(z / 64))) ?? NO_ITEMS;
|
|
243
|
+
};
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/terrain/getGaussianHillBlend.ts
|
|
246
|
+
const getGaussianHillBlend = ({ width, x: hillX, z: hillZ }, x, z) => {
|
|
247
|
+
const reach = 3 * width;
|
|
248
|
+
const acrossX = x - hillX;
|
|
249
|
+
const acrossZ = z - hillZ;
|
|
250
|
+
if (Math.abs(acrossX) > reach || Math.abs(acrossZ) > reach) return 0;
|
|
251
|
+
return Math.exp(-(acrossX ** 2 + acrossZ ** 2) / (2 * width * width));
|
|
252
|
+
};
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/terrain/createGaussianHillsHeight.ts
|
|
255
|
+
const createGaussianHillsHeight = ({ base, hills }) => {
|
|
256
|
+
const getHills = fileByCell(hills, ({ width, x, z }) => {
|
|
257
|
+
const reach = 3 * width;
|
|
258
|
+
return {
|
|
259
|
+
maxX: x + reach,
|
|
260
|
+
maxZ: z + reach,
|
|
261
|
+
minX: x - reach,
|
|
262
|
+
minZ: z - reach
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
return (x, z) => {
|
|
266
|
+
let total = base;
|
|
267
|
+
for (const hill of getHills(x, z)) total += hill.height * getGaussianHillBlend(hill, x, z);
|
|
268
|
+
return total;
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/terrain/getSegmentBounds.ts
|
|
273
|
+
const getSegmentBounds = (startX, startZ, endX, endZ, reach) => ({
|
|
274
|
+
maxX: Math.max(startX, endX) + reach,
|
|
275
|
+
maxZ: Math.max(startZ, endZ) + reach,
|
|
276
|
+
minX: Math.min(startX, endX) - reach,
|
|
277
|
+
minZ: Math.min(startZ, endZ) - reach
|
|
278
|
+
});
|
|
279
|
+
//#endregion
|
|
280
|
+
//#region src/terrain/sampleGroundLayerField.ts
|
|
281
|
+
const sampleGroundLayerField = ({ cellSize, layers, origin, size }, x, z, weights) => {
|
|
282
|
+
const sizeX = size[0] ?? 1;
|
|
283
|
+
const sizeZ = size[1] ?? 1;
|
|
284
|
+
const column = Math.min(Math.max((x - (origin[0] ?? 0)) / cellSize, 0), sizeX - 1);
|
|
285
|
+
const row = Math.min(Math.max((z - (origin[1] ?? 0)) / cellSize, 0), sizeZ - 1);
|
|
286
|
+
let total = 0;
|
|
287
|
+
for (const layer of GroundLayers) {
|
|
288
|
+
const shares = layers[layer];
|
|
289
|
+
const share = shares === void 0 ? 0 : blendGridNodes(shares, sizeX, sizeZ, column, row);
|
|
290
|
+
weights[layer] = share;
|
|
291
|
+
total += share;
|
|
292
|
+
}
|
|
293
|
+
if (total === 0) {
|
|
294
|
+
weights["Grass"] = 1;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
for (const layer of GroundLayers) weights[layer] /= total;
|
|
298
|
+
};
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/terrain/createGroundLayerWeights.ts
|
|
301
|
+
const getBandAmount = ({ end, start }, value) => end >= start ? MathUtils.smoothstep(value, start, end) : 1 - MathUtils.smoothstep(value, end, start);
|
|
302
|
+
const layOver = (weights, layer, amount) => {
|
|
303
|
+
if (amount === 0) return;
|
|
304
|
+
for (const groundLayer of GroundLayers) weights[groundLayer] *= 1 - amount;
|
|
305
|
+
weights[layer] += amount;
|
|
306
|
+
};
|
|
307
|
+
const createGroundLayerWeights = ({ earthSlope, layerField, pathFalloff, paths, rockSlope, sandHeight, snowHeight }) => {
|
|
308
|
+
const weights = {
|
|
309
|
+
["Earth"]: 0,
|
|
310
|
+
["Grass"]: 0,
|
|
311
|
+
["Path"]: 0,
|
|
312
|
+
["Rock"]: 0,
|
|
313
|
+
["Sand"]: 0,
|
|
314
|
+
["Snow"]: 0
|
|
315
|
+
};
|
|
316
|
+
const getPaths = fileByCell(paths, ({ endX, endZ, startX, startZ, width }) => getSegmentBounds(startX, startZ, endX, endZ, width / 2 + pathFalloff));
|
|
317
|
+
return (height, slope, x, z) => {
|
|
318
|
+
if (layerField) sampleGroundLayerField(layerField, x, z, weights);
|
|
319
|
+
else {
|
|
320
|
+
for (const groundLayer of GroundLayers) weights[groundLayer] = 0;
|
|
321
|
+
weights["Grass"] = 1;
|
|
322
|
+
}
|
|
323
|
+
if (earthSlope) layOver(weights, "Earth", getBandAmount(earthSlope, slope));
|
|
324
|
+
if (sandHeight) layOver(weights, "Sand", getBandAmount(sandHeight, height));
|
|
325
|
+
if (snowHeight) layOver(weights, "Snow", getBandAmount(snowHeight, height));
|
|
326
|
+
let pathAmount = 0;
|
|
327
|
+
for (const path of getPaths(x, z)) {
|
|
328
|
+
const measurement = measureSegment(path, x, z);
|
|
329
|
+
const halfWidth = path.width / 2;
|
|
330
|
+
const distance = Math.hypot(measurement[0] ?? 0, measurement[1] ?? 0);
|
|
331
|
+
pathAmount = Math.max(pathAmount, 1 - MathUtils.smoothstep(distance, halfWidth, halfWidth + pathFalloff));
|
|
332
|
+
}
|
|
333
|
+
layOver(weights, "Path", pathAmount);
|
|
334
|
+
if (rockSlope) layOver(weights, "Rock", getBandAmount(rockSlope, slope));
|
|
335
|
+
return weights;
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/terrain/createGroundPaintColor.ts
|
|
340
|
+
const createGroundPaintColor = (groundPaint, seed) => {
|
|
341
|
+
const { layerColors, patchScale } = groundPaint;
|
|
342
|
+
const getWeights = createGroundLayerWeights(groundPaint);
|
|
343
|
+
const noise = createSimplexNoise(seed);
|
|
344
|
+
const linearLayerColors = GroundLayers.map((layer) => ({
|
|
345
|
+
color: new Color(layerColors[layer].color),
|
|
346
|
+
layer,
|
|
347
|
+
patchColor: new Color(layerColors[layer].patchColor)
|
|
348
|
+
}));
|
|
349
|
+
const layerColor = new Color();
|
|
350
|
+
return (colors, offset, height, slope, x, z) => {
|
|
351
|
+
const weights = getWeights(height, slope, x, z);
|
|
352
|
+
const patch = (noise(x / patchScale, z / patchScale) + 1) / 2;
|
|
353
|
+
let red = 0;
|
|
354
|
+
let green = 0;
|
|
355
|
+
let blue = 0;
|
|
356
|
+
for (const { color, layer, patchColor } of linearLayerColors) {
|
|
357
|
+
const weight = weights[layer];
|
|
358
|
+
if (weight === 0) continue;
|
|
359
|
+
layerColor.lerpColors(color, patchColor, patch);
|
|
360
|
+
red += layerColor.r * weight;
|
|
361
|
+
green += layerColor.g * weight;
|
|
362
|
+
blue += layerColor.b * weight;
|
|
363
|
+
}
|
|
364
|
+
colors[offset] = red;
|
|
365
|
+
colors[offset + 1] = green;
|
|
366
|
+
colors[offset + 2] = blue;
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/terrain/sampleTerrainResidualFade.ts
|
|
371
|
+
const sampleTerrainResidualFade = ({ cellSize, clearings, origin, size, weights }, x, z) => {
|
|
372
|
+
const sizeX = size[0] ?? 1;
|
|
373
|
+
const sizeZ = size[1] ?? 1;
|
|
374
|
+
const column = (x - (origin[0] ?? 0)) / cellSize;
|
|
375
|
+
const row = (z - (origin[1] ?? 0)) / cellSize;
|
|
376
|
+
if (column < 0 || row < 0 || column > sizeX - 1 || row > sizeZ - 1) return 0;
|
|
377
|
+
let weight = blendGridNodes(weights, sizeX, sizeZ, column, row);
|
|
378
|
+
for (const { falloff, radius, x: centreX, z: centreZ } of clearings) weight *= MathUtils.smoothstep(Math.hypot(x - centreX, z - centreZ), radius, radius + falloff);
|
|
379
|
+
return weight;
|
|
380
|
+
};
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/terrain/createResidualHeight.ts
|
|
383
|
+
const createResidualHeight = ({ amplitude, fade, octaves, scale, seed }) => {
|
|
384
|
+
const noise = createSimplexNoise(seed);
|
|
385
|
+
return (x, z) => {
|
|
386
|
+
const weight = fade ? sampleTerrainResidualFade(fade, x, z) : 1;
|
|
387
|
+
if (weight === 0) return 0;
|
|
388
|
+
let total = 0;
|
|
389
|
+
let octaveAmplitude = amplitude;
|
|
390
|
+
let frequency = 1 / scale;
|
|
391
|
+
for (let octave = 0; octave < octaves; octave++) {
|
|
392
|
+
total += octaveAmplitude * noise(x * frequency, z * frequency);
|
|
393
|
+
octaveAmplitude /= 2;
|
|
394
|
+
frequency *= 2;
|
|
395
|
+
}
|
|
396
|
+
return weight * total;
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/terrain/getTerrainFeatureBounds.ts
|
|
401
|
+
const TerrainFeatureKindBoundsMap = {
|
|
402
|
+
["cliff"]: (feature) => {
|
|
403
|
+
const { endX, endZ, falloff, startX, startZ, width } = feature;
|
|
404
|
+
return getSegmentBounds(startX, startZ, endX, endZ, width + falloff);
|
|
405
|
+
},
|
|
406
|
+
["plateau"]: (feature) => {
|
|
407
|
+
const { falloff, radius, x, z } = feature;
|
|
408
|
+
const reach = radius + falloff;
|
|
409
|
+
return {
|
|
410
|
+
maxX: x + reach,
|
|
411
|
+
maxZ: z + reach,
|
|
412
|
+
minX: x - reach,
|
|
413
|
+
minZ: z - reach
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
["ridge"]: (feature) => {
|
|
417
|
+
const { endX, endZ, startX, startZ, width } = feature;
|
|
418
|
+
return getSegmentBounds(startX, startZ, endX, endZ, 3 * width);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const getTerrainFeatureBounds = (feature) => TerrainFeatureKindBoundsMap[feature.kind](feature);
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/terrain/createTerrainFeaturesBlend.ts
|
|
424
|
+
const createTerrainFeaturesBlend = (features) => {
|
|
425
|
+
const getFeatures = fileByCell(features, getTerrainFeatureBounds);
|
|
426
|
+
return (x, z) => {
|
|
427
|
+
let blend = 0;
|
|
428
|
+
for (const feature of getFeatures(x, z)) blend = Math.max(blend, TerrainFeatureKindBlendMap[feature.kind](feature, x, z));
|
|
429
|
+
return blend;
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/terrain/createTerrainFeaturesHeight.ts
|
|
434
|
+
const createTerrainFeaturesHeight = (features) => {
|
|
435
|
+
const getFeatures = fileByCell(features, getTerrainFeatureBounds);
|
|
436
|
+
return (x, z) => {
|
|
437
|
+
let total = 0;
|
|
438
|
+
for (const feature of getFeatures(x, z)) total += feature.height * TerrainFeatureKindBlendMap[feature.kind](feature, x, z);
|
|
439
|
+
return total;
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/terrain/getTerrainShapeLevel.ts
|
|
444
|
+
const getTerrainShapeLevel = ({ base, hills }, baseUnder) => {
|
|
445
|
+
if (hills.length === 0) return void 0;
|
|
446
|
+
let minX = Infinity;
|
|
447
|
+
let maxX = -Infinity;
|
|
448
|
+
let minZ = Infinity;
|
|
449
|
+
let maxZ = -Infinity;
|
|
450
|
+
let maxWidth = 0;
|
|
451
|
+
for (const { width, x, z } of hills) {
|
|
452
|
+
minX = Math.min(minX, x);
|
|
453
|
+
maxX = Math.max(maxX, x);
|
|
454
|
+
minZ = Math.min(minZ, z);
|
|
455
|
+
maxZ = Math.max(maxZ, z);
|
|
456
|
+
maxWidth = Math.max(maxWidth, width);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
falloff: 3 * maxWidth,
|
|
460
|
+
height: base - baseUnder,
|
|
461
|
+
kind: "plateau",
|
|
462
|
+
radius: Math.hypot(maxX - minX, maxZ - minZ) / 2,
|
|
463
|
+
x: (minX + maxX) / 2,
|
|
464
|
+
z: (minZ + maxZ) / 2
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/terrain/createTerrainShapeHeight.ts
|
|
469
|
+
const createTerrainShapeHeight = (shape, ...overlays) => {
|
|
470
|
+
const shapes = [shape, ...overlays];
|
|
471
|
+
const features = shapes.flatMap((layer) => layer.features ?? []);
|
|
472
|
+
const levels = overlays.flatMap((overlay) => getTerrainShapeLevel(overlay, shape.base) ?? []);
|
|
473
|
+
const getHillsHeight = createGaussianHillsHeight({
|
|
474
|
+
base: shape.base,
|
|
475
|
+
hills: shapes.flatMap(({ hills }) => hills)
|
|
476
|
+
});
|
|
477
|
+
const getFeaturesHeight = createTerrainFeaturesHeight([...features, ...levels]);
|
|
478
|
+
const residualHeights = shapes.flatMap(({ residual }) => residual ? [createResidualHeight(residual)] : []);
|
|
479
|
+
if (residualHeights.length === 0) return (x, z) => getHillsHeight(x, z) + getFeaturesHeight(x, z);
|
|
480
|
+
const getFeaturesBlend = createTerrainFeaturesBlend(features);
|
|
481
|
+
return (x, z) => {
|
|
482
|
+
const residualShare = 1 - getFeaturesBlend(x, z);
|
|
483
|
+
let residualHeight = 0;
|
|
484
|
+
if (residualShare > 0) for (const getResidualHeight of residualHeights) residualHeight += residualShare * getResidualHeight(x, z);
|
|
485
|
+
return getHillsHeight(x, z) + getFeaturesHeight(x, z) + residualHeight;
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/vegetation/computeScatterPoints.ts
|
|
490
|
+
const CELL_SIZE_PER_SPACING = 1 / Math.SQRT2;
|
|
491
|
+
const CELL_REACH = Math.ceil(Math.SQRT2);
|
|
492
|
+
const checkIsCrowded = (cells, cellsPerSide, points, spacing, column, row, x, z) => {
|
|
493
|
+
for (let neighbourRow = Math.max(0, row - CELL_REACH); neighbourRow <= Math.min(cellsPerSide - 1, row + CELL_REACH); neighbourRow++) for (let neighbourColumn = Math.max(0, column - CELL_REACH); neighbourColumn <= Math.min(cellsPerSide - 1, column + CELL_REACH); neighbourColumn++) {
|
|
494
|
+
const pointIndex = cells[neighbourRow * cellsPerSide + neighbourColumn] ?? -1;
|
|
495
|
+
if (pointIndex === -1) continue;
|
|
496
|
+
const distanceX = (points[pointIndex * 2] ?? 0) - x;
|
|
497
|
+
const distanceZ = (points[pointIndex * 2 + 1] ?? 0) - z;
|
|
498
|
+
if (distanceX * distanceX + distanceZ * distanceZ < spacing * spacing) return true;
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
};
|
|
502
|
+
const computeScatterPoints = ({ candidateCount, checkIsAccepted, seed, size, spacing }) => {
|
|
503
|
+
const random = createSeededRandom(seed);
|
|
504
|
+
const cellSize = spacing * CELL_SIZE_PER_SPACING;
|
|
505
|
+
const cellsPerSide = Math.ceil(size / cellSize);
|
|
506
|
+
const cells = new Int32Array(cellsPerSide * cellsPerSide).fill(-1);
|
|
507
|
+
const points = [];
|
|
508
|
+
for (let candidate = 0; candidate < candidateCount; candidate++) {
|
|
509
|
+
const x = random() * size;
|
|
510
|
+
const z = random() * size;
|
|
511
|
+
const column = Math.floor(x / cellSize);
|
|
512
|
+
const row = Math.floor(z / cellSize);
|
|
513
|
+
if (!checkIsAccepted(x, z) || checkIsCrowded(cells, cellsPerSide, points, spacing, column, row, x, z)) continue;
|
|
514
|
+
cells[row * cellsPerSide + column] = points.length / 2;
|
|
515
|
+
points.push(x, z);
|
|
516
|
+
}
|
|
517
|
+
return Float32Array.from(points);
|
|
518
|
+
};
|
|
519
|
+
//#endregion
|
|
520
|
+
export { TERRAIN_RING_INITIAL_TILE_COUNT as A, getCliffBlend as C, TERRAIN_CELL_SIZE as D, GAUSSIAN_REACH_WIDTHS as E, GroundLayer as F, GroundLayers as I, createSeededRandom as L, TILE_INDEX_SPAN as M, createSimplexNoise as N, TERRAIN_HEIGHT_PADDING as O, TerrainFeatureKind as P, getPlateauBlend as S, getTerrainTileLevel as T, getTerrainTileRow as _, createTerrainFeaturesBlend as a, TerrainFeatureKindBlendMap as b, sampleTerrainResidualFade as c, sampleGroundLayerField as d, getSegmentBounds as f, computeTerrainTile as g, fileByCell as h, createTerrainFeaturesHeight as i, TILE_INDEX_OFFSET as j, TERRAIN_LAYER as k, createGroundPaintColor as l, getGaussianHillBlend as m, createTerrainShapeHeight as n, getTerrainFeatureBounds as o, createGaussianHillsHeight as p, getTerrainShapeLevel as r, createResidualHeight as s, computeScatterPoints as t, createGroundLayerWeights as u, getTerrainTileColumn as v, measureSegment as w, getRidgeBlend as x, blendGridNodes as y };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { E as GroundLayers, T as GroundLayer, a as createTerrainShapeHeight, c as computeTerrainTile, i as getTerrainTileColumn, l as createSeededRandom, n as getTerrainTileRow, o as createGroundPaintColor, r as getTerrainTileLevel, s as createGroundLayerWeights, t as computeScatterPoints } from "./terrainWorker-CLOYYKT1.js";
|
|
2
|
+
export { GroundLayer, GroundLayers, computeScatterPoints, computeTerrainTile, createGroundLayerWeights, createGroundPaintColor, createSeededRandom, createTerrainShapeHeight, getTerrainTileColumn, getTerrainTileLevel, getTerrainTileRow };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { F as GroundLayer, I as GroundLayers, t as computeScatterPoints, g as computeTerrainTile, u as createGroundLayerWeights, l as createGroundPaintColor, L as createSeededRandom, n as createTerrainShapeHeight, v as getTerrainTileColumn, T as getTerrainTileLevel, _ as getTerrainTileRow } from "./terrainWorker-DZ04cA3m.js";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genshin-engine",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "An anime-style open-world engine for the browser on three.js WebGPU — toon materials, outlines, post-processing, streamed terrain
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "An anime-style open-world engine for the browser on three.js WebGPU — toon materials, outlines, post-processing, streamed terrain, parametric world kits and a Web Audio synthesizer, as plain TypeScript and TSL modules.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"anime",
|
|
7
7
|
"cel-shading",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"exports": {
|
|
38
38
|
".": "./dist/index.js",
|
|
39
|
+
"./terrainWorker": "./dist/terrainWorker.js",
|
|
39
40
|
"./package.json": "./package.json"
|
|
40
41
|
},
|
|
41
42
|
"publishConfig": {
|
|
@@ -51,18 +52,22 @@
|
|
|
51
52
|
"test": "vitest",
|
|
52
53
|
"typecheck": "tsc"
|
|
53
54
|
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"three-mesh-bvh": "^0.9.16"
|
|
57
|
+
},
|
|
54
58
|
"devDependencies": {
|
|
55
59
|
"@arethetypeswrong/core": "^0.18.5",
|
|
56
|
-
"@esposter/configuration": "
|
|
57
|
-
"@esposter/shared-node": "
|
|
58
|
-
"@types/node": "^26.6.
|
|
60
|
+
"@esposter/configuration": "4.1.0",
|
|
61
|
+
"@esposter/shared-node": "4.1.0",
|
|
62
|
+
"@types/node": "^26.6.5",
|
|
59
63
|
"@types/three": "^0.186.0",
|
|
60
64
|
"publint": "^0.3.25",
|
|
61
|
-
"tsdown": "^0.23.
|
|
62
|
-
"
|
|
65
|
+
"tsdown": "^0.23.1",
|
|
66
|
+
"vite": "^8.3.4",
|
|
67
|
+
"vitest": "^5.0.4"
|
|
63
68
|
},
|
|
64
69
|
"peerDependencies": {
|
|
65
70
|
"three": "^0.186.1"
|
|
66
71
|
},
|
|
67
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "68de4c22fa7591bf6f6cc0fab2c9a906d8c4d71a"
|
|
68
73
|
}
|