@aethervtt/shard-scatter 0.0.0-stage → 0.2.1
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 +21 -0
- package/README.md +2 -2
- package/dist/components.d.ts +44 -0
- package/dist/components.js +98 -0
- package/dist/foliage.d.ts +43 -0
- package/dist/foliage.js +175 -0
- package/dist/heightfield-surface.d.ts +35 -0
- package/dist/heightfield-surface.js +61 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +120 -0
- package/dist/material.d.ts +20 -0
- package/dist/material.js +149 -0
- package/dist/mesh-surface.d.ts +79 -0
- package/dist/mesh-surface.js +470 -0
- package/dist/methods.d.ts +10 -0
- package/dist/methods.js +341 -0
- package/dist/overlays.d.ts +4 -0
- package/dist/overlays.js +84 -0
- package/dist/planet.d.ts +74 -0
- package/dist/planet.js +675 -0
- package/dist/plugin.d.ts +17 -0
- package/dist/plugin.js +265 -0
- package/dist/preview-render.d.ts +10 -0
- package/dist/preview-render.js +155 -0
- package/dist/preview.d.ts +5 -0
- package/dist/preview.js +11 -0
- package/dist/rules.d.ts +97 -0
- package/dist/rules.js +170 -0
- package/dist/runtime.d.ts +138 -0
- package/dist/runtime.js +702 -0
- package/dist/set.d.ts +45 -0
- package/dist/set.js +131 -0
- package/dist/surface.d.ts +124 -0
- package/dist/surface.js +20 -0
- package/dist/testing.d.ts +236 -0
- package/dist/testing.js +231 -0
- package/dist/viewers.d.ts +9 -0
- package/dist/viewers.js +22 -0
- package/package.json +49 -4
package/dist/planet.js
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { DEG, dcos, sinCos } from "@aethervtt/shard-mesh";
|
|
2
|
+
import { sampleNoiseAsync, sampleOffset } from "@aethervtt/shard-noise";
|
|
3
|
+
import {
|
|
4
|
+
assembleChunk,
|
|
5
|
+
biomeWeights,
|
|
6
|
+
buildChunk,
|
|
7
|
+
chunkCenter,
|
|
8
|
+
chunkLayout,
|
|
9
|
+
chunkSpec,
|
|
10
|
+
dominantBiome,
|
|
11
|
+
faceToDirection,
|
|
12
|
+
heightAt,
|
|
13
|
+
MAX_BIOMES,
|
|
14
|
+
MAX_DEPTH,
|
|
15
|
+
NOISE_OFFSET,
|
|
16
|
+
nodeAt,
|
|
17
|
+
nodeExtent,
|
|
18
|
+
SNAP,
|
|
19
|
+
sampleChunkAsync
|
|
20
|
+
} from "@aethervtt/shard-terrain";
|
|
21
|
+
import { placeInGrid } from "@aethervtt/shard-transform";
|
|
22
|
+
import {
|
|
23
|
+
cellRandom,
|
|
24
|
+
FOLIAGE,
|
|
25
|
+
inRange,
|
|
26
|
+
jitterFor,
|
|
27
|
+
MAX_FOLIAGE_PER_CHUNK,
|
|
28
|
+
pickItem,
|
|
29
|
+
STREAM_ACCEPT,
|
|
30
|
+
STREAM_ITEM,
|
|
31
|
+
STREAM_SCALE,
|
|
32
|
+
STREAM_SHADE,
|
|
33
|
+
STREAM_VARIANT,
|
|
34
|
+
STREAM_X,
|
|
35
|
+
STREAM_Y,
|
|
36
|
+
STREAM_YAW
|
|
37
|
+
} from "./rules.js";
|
|
38
|
+
import {
|
|
39
|
+
P_CELL,
|
|
40
|
+
P_ITEM,
|
|
41
|
+
P_QX,
|
|
42
|
+
P_SCALE,
|
|
43
|
+
P_SHADE,
|
|
44
|
+
P_VARIANT,
|
|
45
|
+
P_X,
|
|
46
|
+
PLACEMENT_STRIDE
|
|
47
|
+
} from "./surface.js";
|
|
48
|
+
import { cameraPositions } from "./viewers.js";
|
|
49
|
+
const TARGET_PROPS = 256;
|
|
50
|
+
const NORMAL_STEP = 0.5;
|
|
51
|
+
const nodeMetres = (radius, depth) => radius * Math.PI / 2 / 2 ** depth;
|
|
52
|
+
function ruleDepth(rule, radius, colliderDepth) {
|
|
53
|
+
if (rule.kind === FOLIAGE) {
|
|
54
|
+
let d2 = colliderDepth;
|
|
55
|
+
while (d2 < MAX_DEPTH && rule.density * nodeMetres(radius, d2) ** 2 > MAX_FOLIAGE_PER_CHUNK) d2++;
|
|
56
|
+
return d2;
|
|
57
|
+
}
|
|
58
|
+
if (!(rule.density > 0)) return Math.min(MAX_DEPTH, colliderDepth);
|
|
59
|
+
const size = Math.sqrt(TARGET_PROPS / rule.density);
|
|
60
|
+
const d = Math.round(Math.log2(nodeMetres(radius, 0) / size));
|
|
61
|
+
return Math.max(0, Math.min(MAX_DEPTH, d));
|
|
62
|
+
}
|
|
63
|
+
function cellsPerChunk(rule, radius, depth) {
|
|
64
|
+
if (!Number.isFinite(rule.cell)) return 1;
|
|
65
|
+
return Math.max(1, Math.round(nodeMetres(radius, depth) / rule.cell));
|
|
66
|
+
}
|
|
67
|
+
const dir = new Float64Array(3);
|
|
68
|
+
const node = new Float64Array(3);
|
|
69
|
+
const corner = new Float64Array(6);
|
|
70
|
+
const sc = new Float64Array(2);
|
|
71
|
+
class PlanetSurface {
|
|
72
|
+
kind = "planet";
|
|
73
|
+
entity;
|
|
74
|
+
rules = [];
|
|
75
|
+
version = 0;
|
|
76
|
+
/** Per rule: chunk depth and cells per chunk side. */
|
|
77
|
+
depths = [];
|
|
78
|
+
cells = [];
|
|
79
|
+
/** Graph values of each rule's noise mask, by rule index (undefined: no mask). */
|
|
80
|
+
masks = [];
|
|
81
|
+
rt;
|
|
82
|
+
cameras;
|
|
83
|
+
constructor(rt, cameras) {
|
|
84
|
+
this.rt = rt;
|
|
85
|
+
this.entity = rt.entity;
|
|
86
|
+
this.cameras = cameras;
|
|
87
|
+
}
|
|
88
|
+
get ready() {
|
|
89
|
+
return this.rt.ready;
|
|
90
|
+
}
|
|
91
|
+
/** Rebuilds per-rule depths after the rules or the planet changed. */
|
|
92
|
+
configure(rules, masks, version) {
|
|
93
|
+
const s = this.rt.settings;
|
|
94
|
+
this.rules = rules;
|
|
95
|
+
this.masks = masks;
|
|
96
|
+
this.version = version;
|
|
97
|
+
this.depths = rules.map((r) => ruleDepth(r, s.radius, this.rt.colliderDepth));
|
|
98
|
+
this.cells = rules.map((r, i) => cellsPerChunk(r, s.radius, this.depths[i]));
|
|
99
|
+
}
|
|
100
|
+
viewers(world, out) {
|
|
101
|
+
const rt = this.rt;
|
|
102
|
+
let n = cameraPositions(world, this.cameras, (x, y, z, i) => {
|
|
103
|
+
if (out.points.length < (i + 1) * 3) out.points = grow(out.points, (i + 1) * 3);
|
|
104
|
+
rt.frame.pointToPlanet(x, y, z, out.points, i * 3);
|
|
105
|
+
});
|
|
106
|
+
out.cameras = n;
|
|
107
|
+
for (let a = 0; a < rt.anchors; a++) {
|
|
108
|
+
if (out.points.length < (n + 1) * 3) out.points = grow(out.points, (n + 1) * 3);
|
|
109
|
+
out.points[n * 3] = rt.anchorPos[a * 3];
|
|
110
|
+
out.points[n * 3 + 1] = rt.anchorPos[a * 3 + 1];
|
|
111
|
+
out.points[n * 3 + 2] = rt.anchorPos[a * 3 + 2];
|
|
112
|
+
n++;
|
|
113
|
+
}
|
|
114
|
+
out.anchors = n - out.cameras;
|
|
115
|
+
}
|
|
116
|
+
chunksNear(rule, viewers, first, count, range, visit) {
|
|
117
|
+
const rt = this.rt;
|
|
118
|
+
const s = rt.settings;
|
|
119
|
+
const depth = this.depths[rule.index];
|
|
120
|
+
const n = 2 ** depth;
|
|
121
|
+
const ext = nodeExtent(depth);
|
|
122
|
+
const size = nodeMetres(s.radius, depth);
|
|
123
|
+
const reach = Math.ceil(range / (size * 0.7)) + 1;
|
|
124
|
+
for (let v = first; v < first + count; v++) {
|
|
125
|
+
const px = viewers[v * 3];
|
|
126
|
+
const py = viewers[v * 3 + 1];
|
|
127
|
+
const pz = viewers[v * 3 + 2];
|
|
128
|
+
const len = Math.sqrt(px * px + py * py + pz * pz);
|
|
129
|
+
if (!(len > 0)) continue;
|
|
130
|
+
const dx = px / len;
|
|
131
|
+
const dy = py / len;
|
|
132
|
+
const dz = pz / len;
|
|
133
|
+
const surface = s.radius * ellipsoidScale(s.shape, dx, dy, dz) + heightAt(rt, dx, dy, dz);
|
|
134
|
+
const altitude = Math.max(0, len - surface);
|
|
135
|
+
if (altitude > range) continue;
|
|
136
|
+
nodeAt(dx, dy, dz, depth, node);
|
|
137
|
+
const face = node[0];
|
|
138
|
+
const cx = node[1];
|
|
139
|
+
const cy = node[2];
|
|
140
|
+
for (let dj = -reach; dj <= reach; dj++) {
|
|
141
|
+
for (let di = -reach; di <= reach; di++) {
|
|
142
|
+
let f = face;
|
|
143
|
+
let x = cx + di;
|
|
144
|
+
let y = cy + dj;
|
|
145
|
+
if (x < 0 || y < 0 || x >= n || y >= n) {
|
|
146
|
+
faceToDirection(face, -1 + (x + 0.5) * ext, -1 + (y + 0.5) * ext, dir);
|
|
147
|
+
nodeAt(dir[0], dir[1], dir[2], depth, node);
|
|
148
|
+
f = node[0];
|
|
149
|
+
x = node[1];
|
|
150
|
+
y = node[2];
|
|
151
|
+
}
|
|
152
|
+
faceToDirection(f, -1 + (x + 0.5) * ext, -1 + (y + 0.5) * ext, dir);
|
|
153
|
+
const hx = dir[0] - dx;
|
|
154
|
+
const hy = dir[1] - dy;
|
|
155
|
+
const hz = dir[2] - dz;
|
|
156
|
+
const horizontal = s.radius * Math.sqrt(hx * hx + hy * hy + hz * hz);
|
|
157
|
+
const extent = size * 0.75;
|
|
158
|
+
const near = Math.max(0, horizontal - extent);
|
|
159
|
+
const distance = Math.sqrt(near * near + altitude * altitude);
|
|
160
|
+
if (distance > range) continue;
|
|
161
|
+
visit(this.chunk(rule, f, depth, x, y), distance);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** A chunk of `rule` (its center and extent filled in). */
|
|
167
|
+
chunk(rule, face, depth, x, y) {
|
|
168
|
+
const s = this.rt.settings;
|
|
169
|
+
const center = chunkCenter(face, depth, x, y, s.radius, s.shape, new Float64Array(3));
|
|
170
|
+
return {
|
|
171
|
+
key: `${face}/${depth}/${x}/${y}`,
|
|
172
|
+
rule: rule.index,
|
|
173
|
+
face,
|
|
174
|
+
depth,
|
|
175
|
+
x,
|
|
176
|
+
y,
|
|
177
|
+
center,
|
|
178
|
+
extent: nodeMetres(s.radius, depth) * 0.75
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
chunkAt(rule, x, y, z) {
|
|
182
|
+
const len = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
183
|
+
const depth = this.depths[rule.index];
|
|
184
|
+
nodeAt(x / len, y / len, z / len, depth, node);
|
|
185
|
+
return this.chunk(rule, node[0], depth, node[1], node[2]);
|
|
186
|
+
}
|
|
187
|
+
upAt(x, y, z, out) {
|
|
188
|
+
const len = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
189
|
+
out[0] = x / len;
|
|
190
|
+
out[1] = y / len;
|
|
191
|
+
out[2] = z / len;
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
placeChunkRoot(world, root, chunk) {
|
|
195
|
+
placeInGrid(world, root, this.entity, chunk.center);
|
|
196
|
+
}
|
|
197
|
+
startPlacement(chunk, due, workers) {
|
|
198
|
+
const rule = this.rules[chunk.rule];
|
|
199
|
+
const state = this.candidates(rule, chunk);
|
|
200
|
+
const job = { chunk, due, ready: false, cancelled: false, state };
|
|
201
|
+
if (state.count === 0) {
|
|
202
|
+
job.ready = true;
|
|
203
|
+
return job;
|
|
204
|
+
}
|
|
205
|
+
if (workers && workers.size > 0) {
|
|
206
|
+
const rt = this.rt;
|
|
207
|
+
const s = rt.settings;
|
|
208
|
+
const tasks = [];
|
|
209
|
+
const h = new Float32Array(state.count * 3);
|
|
210
|
+
const t = new Float32Array(state.count);
|
|
211
|
+
const m = new Float32Array(state.count);
|
|
212
|
+
const k = new Float32Array(state.count);
|
|
213
|
+
const opts = { origin: state.origin };
|
|
214
|
+
if (rt.height) tasks.push(sampleNoiseAsync(workers, rt.height, s.seed, state.points, h, opts));
|
|
215
|
+
if (rt.climate) {
|
|
216
|
+
tasks.push(
|
|
217
|
+
sampleNoiseAsync(workers, rt.climate, s.seed, state.base, t, {
|
|
218
|
+
...opts,
|
|
219
|
+
node: "temperature"
|
|
220
|
+
}),
|
|
221
|
+
sampleNoiseAsync(workers, rt.climate, s.seed, state.base, m, {
|
|
222
|
+
...opts,
|
|
223
|
+
node: "moisture"
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const mask = this.masks[rule.index];
|
|
228
|
+
if (mask) tasks.push(sampleNoiseAsync(workers, mask, rule.seed, state.base, k, opts));
|
|
229
|
+
state.pending = Promise.all(tasks).then(
|
|
230
|
+
() => {
|
|
231
|
+
if (job.cancelled || job.ready) return;
|
|
232
|
+
state.heights = h;
|
|
233
|
+
state.temps = t;
|
|
234
|
+
state.moist = m;
|
|
235
|
+
state.mask = k;
|
|
236
|
+
job.ready = true;
|
|
237
|
+
},
|
|
238
|
+
() => {
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return job;
|
|
243
|
+
}
|
|
244
|
+
finishPlacement(job) {
|
|
245
|
+
const state = job.state;
|
|
246
|
+
const rule = this.rules[job.chunk.rule];
|
|
247
|
+
if (!job.ready) {
|
|
248
|
+
this.sampleInline(rule, state);
|
|
249
|
+
job.ready = true;
|
|
250
|
+
}
|
|
251
|
+
return this.assemble(rule, job.chunk, state);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A chunk's lattice: cells per side, and the chance and jitter of each cell from the chunk's real
|
|
255
|
+
* size (its edges measured across the middle; cells aren't square on a cube-sphere).
|
|
256
|
+
*/
|
|
257
|
+
lattice(rule, chunk) {
|
|
258
|
+
const R = this.rt.settings.radius;
|
|
259
|
+
const k = this.cells[rule.index];
|
|
260
|
+
const ext = nodeExtent(chunk.depth);
|
|
261
|
+
const u0 = -1 + chunk.x * ext;
|
|
262
|
+
const v0 = -1 + chunk.y * ext;
|
|
263
|
+
const edge = (ua, va, ub, vb) => {
|
|
264
|
+
faceToDirection(chunk.face, ua, va, corner, 0);
|
|
265
|
+
faceToDirection(chunk.face, ub, vb, corner, 3);
|
|
266
|
+
const x = corner[0] - corner[3];
|
|
267
|
+
const y = corner[1] - corner[4];
|
|
268
|
+
const z = corner[2] - corner[5];
|
|
269
|
+
return R * Math.sqrt(x * x + y * y + z * z);
|
|
270
|
+
};
|
|
271
|
+
const cellU = edge(u0, v0 + ext / 2, u0 + ext, v0 + ext / 2) / k;
|
|
272
|
+
const cellV = edge(u0 + ext / 2, v0, u0 + ext / 2, v0 + ext) / k;
|
|
273
|
+
return {
|
|
274
|
+
k,
|
|
275
|
+
accept: Math.min(1, rule.density * cellU * cellV),
|
|
276
|
+
jitter: jitterFor(Math.min(cellU, cellV), rule.spacing)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** The chunk's lattice cells that pass the acceptance roll, as directions and sample points. */
|
|
280
|
+
candidates(rule, chunk) {
|
|
281
|
+
const s = this.rt.settings;
|
|
282
|
+
const R = s.radius;
|
|
283
|
+
const ext = nodeExtent(chunk.depth);
|
|
284
|
+
const { k, accept, jitter } = this.lattice(rule, chunk);
|
|
285
|
+
const cellFace = ext / k;
|
|
286
|
+
const u0 = -1 + chunk.x * ext;
|
|
287
|
+
const v0 = -1 + chunk.y * ext;
|
|
288
|
+
const lo = (1 - jitter) / 2;
|
|
289
|
+
const cells = [];
|
|
290
|
+
const dirs = [];
|
|
291
|
+
for (let j = 0; j < k; j++) {
|
|
292
|
+
for (let i = 0; i < k; i++) {
|
|
293
|
+
const gi = chunk.x * k + i;
|
|
294
|
+
const gj = chunk.y * k + j;
|
|
295
|
+
if (cellRandom(rule.seed, gi, gj, chunk.face, STREAM_ACCEPT) >= accept) continue;
|
|
296
|
+
const fx = lo + jitter * cellRandom(rule.seed, gi, gj, chunk.face, STREAM_X);
|
|
297
|
+
const fy = lo + jitter * cellRandom(rule.seed, gi, gj, chunk.face, STREAM_Y);
|
|
298
|
+
faceToDirection(chunk.face, -1 + (gi + fx) * cellFace, -1 + (gj + fy) * cellFace, dir);
|
|
299
|
+
cells.push(i + j * k);
|
|
300
|
+
dirs.push(dir[0], dir[1], dir[2]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const count = cells.length;
|
|
304
|
+
const cdir = new Float64Array(3);
|
|
305
|
+
faceToDirection(chunk.face, u0 + ext / 2, v0 + ext / 2, cdir);
|
|
306
|
+
const origin = new Float64Array(3);
|
|
307
|
+
for (let a = 0; a < 3; a++)
|
|
308
|
+
origin[a] = Math.round((cdir[a] * R + NOISE_OFFSET[a]) / SNAP) * SNAP;
|
|
309
|
+
const points = new Float32Array(count * 9);
|
|
310
|
+
const base = new Float32Array(count * 3);
|
|
311
|
+
const step = NORMAL_STEP / R;
|
|
312
|
+
for (let c = 0; c < count; c++) {
|
|
313
|
+
const x = dirs[c * 3];
|
|
314
|
+
const y = dirs[c * 3 + 1];
|
|
315
|
+
const z = dirs[c * 3 + 2];
|
|
316
|
+
tangents(x, y, z, t1, t2);
|
|
317
|
+
for (let q = 0; q < 3; q++) {
|
|
318
|
+
let px = x;
|
|
319
|
+
let py = y;
|
|
320
|
+
let pz = z;
|
|
321
|
+
if (q > 0) {
|
|
322
|
+
const e = q === 1 ? t1 : t2;
|
|
323
|
+
px += e[0] * step;
|
|
324
|
+
py += e[1] * step;
|
|
325
|
+
pz += e[2] * step;
|
|
326
|
+
const l = Math.sqrt(px * px + py * py + pz * pz);
|
|
327
|
+
px /= l;
|
|
328
|
+
py /= l;
|
|
329
|
+
pz /= l;
|
|
330
|
+
}
|
|
331
|
+
const o = (c * 3 + q) * 3;
|
|
332
|
+
points[o] = px * R + NOISE_OFFSET[0] - origin[0];
|
|
333
|
+
points[o + 1] = py * R + NOISE_OFFSET[1] - origin[1];
|
|
334
|
+
points[o + 2] = pz * R + NOISE_OFFSET[2] - origin[2];
|
|
335
|
+
if (q === 0) {
|
|
336
|
+
base[c * 3] = points[o];
|
|
337
|
+
base[c * 3 + 1] = points[o + 1];
|
|
338
|
+
base[c * 3 + 2] = points[o + 2];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
k,
|
|
344
|
+
count,
|
|
345
|
+
cells: Int32Array.from(cells),
|
|
346
|
+
dirs: Float64Array.from(dirs),
|
|
347
|
+
origin,
|
|
348
|
+
points,
|
|
349
|
+
base,
|
|
350
|
+
heights: new Float32Array(0),
|
|
351
|
+
temps: new Float32Array(0),
|
|
352
|
+
moist: new Float32Array(0),
|
|
353
|
+
mask: new Float32Array(0),
|
|
354
|
+
candidates: k * k,
|
|
355
|
+
pending: void 0
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
startPatch(chunk, workers) {
|
|
359
|
+
const job = { chunk, ready: false, cancelled: false, state: void 0 };
|
|
360
|
+
if (workers && workers.size > 0) {
|
|
361
|
+
const sampling = sampleChunkAsync(this.rt, workers, chunk.face, chunk.depth, chunk.x, chunk.y);
|
|
362
|
+
job.state = sampling;
|
|
363
|
+
sampling.done.then(
|
|
364
|
+
() => {
|
|
365
|
+
if (!job.cancelled) job.ready = true;
|
|
366
|
+
},
|
|
367
|
+
() => {
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return job;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* The chunk's ground as terrain builds it (the collider chunk's own vertices at the collider
|
|
375
|
+
* depth, so grass stands on the drawn ground), with the rule's density at each vertex: its
|
|
376
|
+
* height, slope and noise masks, and its biome.
|
|
377
|
+
*/
|
|
378
|
+
finishPatch(job) {
|
|
379
|
+
const rt = this.rt;
|
|
380
|
+
const s = rt.settings;
|
|
381
|
+
const { chunk } = job;
|
|
382
|
+
const rule = this.rules[chunk.rule];
|
|
383
|
+
const spec = chunkSpec(rt, chunk.face, chunk.depth, chunk.x, chunk.y);
|
|
384
|
+
const sampling = job.state;
|
|
385
|
+
const mesh = job.ready && sampling ? assembleChunk(spec, sampling.pts, sampling.values, sampling.temps, sampling.moist) : buildChunk(spec);
|
|
386
|
+
const n = s.resolution;
|
|
387
|
+
const count = n * n;
|
|
388
|
+
const index = chunkLayout(n).index;
|
|
389
|
+
const positions = new Float32Array(count * 3);
|
|
390
|
+
const normals = new Float32Array(count * 3);
|
|
391
|
+
const density = new Float32Array(count);
|
|
392
|
+
const d = mesh.data;
|
|
393
|
+
const cx = chunk.center[0];
|
|
394
|
+
const cy = chunk.center[1];
|
|
395
|
+
const cz = chunk.center[2];
|
|
396
|
+
const cosLo = dcos(Math.max(0, rule.slopeLo) * DEG);
|
|
397
|
+
const cosHi = dcos(Math.min(90, rule.slopeHi) * DEG);
|
|
398
|
+
const slopeMask = rule.slopeLo < rule.slopeHi;
|
|
399
|
+
const mask = this.masks[rule.index];
|
|
400
|
+
let maskValues;
|
|
401
|
+
if (mask) {
|
|
402
|
+
const origin = new Float64Array(3);
|
|
403
|
+
const R = s.radius;
|
|
404
|
+
const cl = Math.sqrt(cx * cx + cy * cy + cz * cz) || 1;
|
|
405
|
+
for (let a = 0; a < 3; a++)
|
|
406
|
+
origin[a] = Math.round((chunk.center[a] / cl * R + NOISE_OFFSET[a]) / SNAP) * SNAP;
|
|
407
|
+
const pts = new Float32Array(count * 3);
|
|
408
|
+
for (let k2 = 0; k2 < count; k2++) {
|
|
409
|
+
const vi = index[k2];
|
|
410
|
+
const x = cx + d.positions[vi * 3];
|
|
411
|
+
const y = cy + d.positions[vi * 3 + 1];
|
|
412
|
+
const z = cz + d.positions[vi * 3 + 2];
|
|
413
|
+
const l = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
414
|
+
pts[k2 * 3] = x / l * R + NOISE_OFFSET[0] - origin[0];
|
|
415
|
+
pts[k2 * 3 + 1] = y / l * R + NOISE_OFFSET[1] - origin[1];
|
|
416
|
+
pts[k2 * 3 + 2] = z / l * R + NOISE_OFFSET[2] - origin[2];
|
|
417
|
+
}
|
|
418
|
+
maskValues = new Float32Array(count);
|
|
419
|
+
sampleOffset(mask, rule.seed, origin, pts, maskValues);
|
|
420
|
+
}
|
|
421
|
+
for (let k2 = 0; k2 < count; k2++) {
|
|
422
|
+
const vi = index[k2];
|
|
423
|
+
const px = d.positions[vi * 3];
|
|
424
|
+
const py = d.positions[vi * 3 + 1];
|
|
425
|
+
const pz = d.positions[vi * 3 + 2];
|
|
426
|
+
positions[k2 * 3] = px;
|
|
427
|
+
positions[k2 * 3 + 1] = py;
|
|
428
|
+
positions[k2 * 3 + 2] = pz;
|
|
429
|
+
const nx = d.normals[vi * 3];
|
|
430
|
+
const ny = d.normals[vi * 3 + 1];
|
|
431
|
+
const nz = d.normals[vi * 3 + 2];
|
|
432
|
+
normals[k2 * 3] = nx;
|
|
433
|
+
normals[k2 * 3 + 1] = ny;
|
|
434
|
+
normals[k2 * 3 + 2] = nz;
|
|
435
|
+
const h = mesh.heights[k2];
|
|
436
|
+
let w = inRange(h, rule.heightLo, rule.heightHi) ? 1 : 0;
|
|
437
|
+
const x = cx + px;
|
|
438
|
+
const y = cy + py;
|
|
439
|
+
const z = cz + pz;
|
|
440
|
+
const l = Math.sqrt(x * x + y * y + z * z) || 1;
|
|
441
|
+
const cos = Math.abs((nx * x + ny * y + nz * z) / l);
|
|
442
|
+
if (w > 0 && slopeMask && (cos > cosLo + 1e-12 || cos < cosHi - 1e-12)) w = 0;
|
|
443
|
+
if (w > 0 && maskValues && !(maskValues[k2] > rule.noiseAbove)) w = 0;
|
|
444
|
+
if (w > 0 && rule.biome >= 0) {
|
|
445
|
+
biomeWeights(
|
|
446
|
+
rt.table,
|
|
447
|
+
{
|
|
448
|
+
temperature: d.uvs[vi * 2],
|
|
449
|
+
moisture: d.uvs[vi * 2 + 1],
|
|
450
|
+
height: h,
|
|
451
|
+
slope: Math.acos(Math.min(1, cos)) * 180 / Math.PI,
|
|
452
|
+
latitude: y / l
|
|
453
|
+
},
|
|
454
|
+
weights
|
|
455
|
+
);
|
|
456
|
+
if (dominantBiome(weights, rt.table.count) !== rule.biome) w = 0;
|
|
457
|
+
}
|
|
458
|
+
density[k2] = w;
|
|
459
|
+
}
|
|
460
|
+
const { k, accept, jitter } = this.lattice(rule, chunk);
|
|
461
|
+
return {
|
|
462
|
+
grid: n,
|
|
463
|
+
positions,
|
|
464
|
+
normals,
|
|
465
|
+
density,
|
|
466
|
+
cell0: [chunk.x * k, chunk.y * k],
|
|
467
|
+
domain: chunk.face,
|
|
468
|
+
accept,
|
|
469
|
+
jitter,
|
|
470
|
+
radial: [cx, cy, cz]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
chunkTransform(_world, chunk, out) {
|
|
474
|
+
const m = this.rt.frame.toOrigin;
|
|
475
|
+
const x = chunk.center[0];
|
|
476
|
+
const y = chunk.center[1];
|
|
477
|
+
const z = chunk.center[2];
|
|
478
|
+
for (let r = 0; r < 3; r++) {
|
|
479
|
+
out[r * 4] = m[r * 4];
|
|
480
|
+
out[r * 4 + 1] = m[r * 4 + 1];
|
|
481
|
+
out[r * 4 + 2] = m[r * 4 + 2];
|
|
482
|
+
out[r * 4 + 3] = m[r * 4] * x + m[r * 4 + 1] * y + m[r * 4 + 2] * z + m[r * 4 + 3];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
sampleInline(rule, state) {
|
|
486
|
+
const rt = this.rt;
|
|
487
|
+
const s = rt.settings;
|
|
488
|
+
state.heights = new Float32Array(state.count * 3);
|
|
489
|
+
state.temps = new Float32Array(state.count);
|
|
490
|
+
state.moist = new Float32Array(state.count);
|
|
491
|
+
state.mask = new Float32Array(state.count);
|
|
492
|
+
if (state.count === 0) return;
|
|
493
|
+
if (rt.height) sampleOffset(rt.height, s.seed, state.origin, state.points, state.heights);
|
|
494
|
+
if (rt.climate) {
|
|
495
|
+
sampleOffset(rt.climate, s.seed, state.origin, state.base, state.temps, "temperature");
|
|
496
|
+
sampleOffset(rt.climate, s.seed, state.origin, state.base, state.moist, "moisture");
|
|
497
|
+
}
|
|
498
|
+
const mask = this.masks[rule.index];
|
|
499
|
+
if (mask) sampleOffset(mask, rule.seed, state.origin, state.base, state.mask);
|
|
500
|
+
}
|
|
501
|
+
/** Samples into placements: masks, biome, then each item's transform from its cell's hashes. */
|
|
502
|
+
assemble(rule, chunk, state) {
|
|
503
|
+
const rt = this.rt;
|
|
504
|
+
const s = rt.settings;
|
|
505
|
+
const R = s.radius;
|
|
506
|
+
const sx = s.shape[0];
|
|
507
|
+
const sy = s.shape[1];
|
|
508
|
+
const sz = s.shape[2];
|
|
509
|
+
const scale = s.heightScale;
|
|
510
|
+
const out = new Float32Array(state.count * PLACEMENT_STRIDE);
|
|
511
|
+
const cosLo = dcos(Math.max(0, rule.slopeLo) * DEG);
|
|
512
|
+
const cosHi = dcos(Math.min(90, rule.slopeHi) * DEG);
|
|
513
|
+
const slopeMask = rule.slopeLo < rule.slopeHi;
|
|
514
|
+
let n = 0;
|
|
515
|
+
const k = state.k;
|
|
516
|
+
const cx = chunk.center[0];
|
|
517
|
+
const cy = chunk.center[1];
|
|
518
|
+
const cz = chunk.center[2];
|
|
519
|
+
for (let c = 0; c < state.count; c++) {
|
|
520
|
+
const h0 = state.heights[c * 3] * scale;
|
|
521
|
+
if (!inRange(h0, rule.heightLo, rule.heightHi)) continue;
|
|
522
|
+
if (this.masks[rule.index] && !(state.mask[c] > rule.noiseAbove)) continue;
|
|
523
|
+
for (let q = 0; q < 3; q++) {
|
|
524
|
+
let x = state.dirs[c * 3];
|
|
525
|
+
let y = state.dirs[c * 3 + 1];
|
|
526
|
+
let z = state.dirs[c * 3 + 2];
|
|
527
|
+
if (q > 0) {
|
|
528
|
+
tangents(x, y, z, t1, t2);
|
|
529
|
+
const e = q === 1 ? t1 : t2;
|
|
530
|
+
const step = NORMAL_STEP / R;
|
|
531
|
+
x += e[0] * step;
|
|
532
|
+
y += e[1] * step;
|
|
533
|
+
z += e[2] * step;
|
|
534
|
+
const l = Math.sqrt(x * x + y * y + z * z);
|
|
535
|
+
x /= l;
|
|
536
|
+
y /= l;
|
|
537
|
+
z /= l;
|
|
538
|
+
}
|
|
539
|
+
const r = R + state.heights[c * 3 + q] * scale;
|
|
540
|
+
tri[q * 3] = x * sx * r;
|
|
541
|
+
tri[q * 3 + 1] = y * sy * r;
|
|
542
|
+
tri[q * 3 + 2] = z * sz * r;
|
|
543
|
+
}
|
|
544
|
+
const ax = tri[3] - tri[0];
|
|
545
|
+
const ay = tri[4] - tri[1];
|
|
546
|
+
const az = tri[5] - tri[2];
|
|
547
|
+
const bx = tri[6] - tri[0];
|
|
548
|
+
const by = tri[7] - tri[1];
|
|
549
|
+
const bz = tri[8] - tri[2];
|
|
550
|
+
let nx = ay * bz - az * by;
|
|
551
|
+
let ny = az * bx - ax * bz;
|
|
552
|
+
let nz = ax * by - ay * bx;
|
|
553
|
+
const nl = Math.sqrt(nx * nx + ny * ny + nz * nz) || 1;
|
|
554
|
+
nx /= nl;
|
|
555
|
+
ny /= nl;
|
|
556
|
+
nz /= nl;
|
|
557
|
+
const rl = Math.sqrt(tri[0] ** 2 + tri[1] ** 2 + tri[2] ** 2) || 1;
|
|
558
|
+
const ux = tri[0] / rl;
|
|
559
|
+
const uy = tri[1] / rl;
|
|
560
|
+
const uz = tri[2] / rl;
|
|
561
|
+
let cos = nx * ux + ny * uy + nz * uz;
|
|
562
|
+
if (cos < 0) {
|
|
563
|
+
nx = -nx;
|
|
564
|
+
ny = -ny;
|
|
565
|
+
nz = -nz;
|
|
566
|
+
cos = -cos;
|
|
567
|
+
}
|
|
568
|
+
if (slopeMask && (cos > cosLo + 1e-12 || cos < cosHi - 1e-12)) continue;
|
|
569
|
+
if (rule.biome >= 0) {
|
|
570
|
+
const slope = Math.acos(Math.min(1, cos)) * 180 / Math.PI;
|
|
571
|
+
biomeWeights(
|
|
572
|
+
rt.table,
|
|
573
|
+
{
|
|
574
|
+
temperature: rt.climate ? state.temps[c] : 0,
|
|
575
|
+
moisture: rt.climate ? state.moist[c] : 0,
|
|
576
|
+
height: h0,
|
|
577
|
+
slope,
|
|
578
|
+
latitude: state.dirs[c * 3 + 1]
|
|
579
|
+
},
|
|
580
|
+
weights
|
|
581
|
+
);
|
|
582
|
+
if (dominantBiome(weights, rt.table.count) !== rule.biome) continue;
|
|
583
|
+
}
|
|
584
|
+
const cell = state.cells[c];
|
|
585
|
+
const gi = chunk.x * k + cell % k;
|
|
586
|
+
const gj = chunk.y * k + Math.floor(cell / k);
|
|
587
|
+
const o = n * PLACEMENT_STRIDE;
|
|
588
|
+
out[o + P_ITEM] = pickItem(rule, cellRandom(rule.seed, gi, gj, chunk.face, STREAM_ITEM));
|
|
589
|
+
const item = rule.items[out[o + P_ITEM]];
|
|
590
|
+
out[o + P_VARIANT] = Math.min(
|
|
591
|
+
item.variants - 1,
|
|
592
|
+
Math.floor(cellRandom(rule.seed, gi, gj, chunk.face, STREAM_VARIANT) * item.variants)
|
|
593
|
+
);
|
|
594
|
+
out[o + P_X] = tri[0] - cx;
|
|
595
|
+
out[o + P_X + 1] = tri[1] - cy;
|
|
596
|
+
out[o + P_X + 2] = tri[2] - cz;
|
|
597
|
+
let upx = ux + (nx - ux) * rule.align;
|
|
598
|
+
let upy = uy + (ny - uy) * rule.align;
|
|
599
|
+
let upz = uz + (nz - uz) * rule.align;
|
|
600
|
+
const ul = Math.sqrt(upx * upx + upy * upy + upz * upz) || 1;
|
|
601
|
+
upx /= ul;
|
|
602
|
+
upy /= ul;
|
|
603
|
+
upz /= ul;
|
|
604
|
+
writeRotation(
|
|
605
|
+
upx,
|
|
606
|
+
upy,
|
|
607
|
+
upz,
|
|
608
|
+
cellRandom(rule.seed, gi, gj, chunk.face, STREAM_YAW) * Math.PI * 2,
|
|
609
|
+
out,
|
|
610
|
+
o + P_QX
|
|
611
|
+
);
|
|
612
|
+
out[o + P_SCALE] = rule.scaleMin + (rule.scaleMax - rule.scaleMin) * cellRandom(rule.seed, gi, gj, chunk.face, STREAM_SCALE);
|
|
613
|
+
out[o + P_CELL] = cell;
|
|
614
|
+
out[o + P_SHADE] = cellRandom(rule.seed, gi, gj, chunk.face, STREAM_SHADE);
|
|
615
|
+
n++;
|
|
616
|
+
}
|
|
617
|
+
return { chunk, count: n, data: out, candidates: state.candidates };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const weights = new Float32Array(MAX_BIOMES);
|
|
621
|
+
const tri = new Float64Array(9);
|
|
622
|
+
const t1 = new Float64Array(3);
|
|
623
|
+
const t2 = new Float64Array(3);
|
|
624
|
+
function tangents(x, y, z, e1, e2) {
|
|
625
|
+
const ax = Math.abs(y) < 0.9 ? 0 : 1;
|
|
626
|
+
const ay = Math.abs(y) < 0.9 ? 1 : 0;
|
|
627
|
+
let e1x = ay * z;
|
|
628
|
+
let e1y = -ax * z;
|
|
629
|
+
let e1z = ax * y - ay * x;
|
|
630
|
+
const l1 = Math.sqrt(e1x * e1x + e1y * e1y + e1z * e1z) || 1;
|
|
631
|
+
e1x /= l1;
|
|
632
|
+
e1y /= l1;
|
|
633
|
+
e1z /= l1;
|
|
634
|
+
e1[0] = e1x;
|
|
635
|
+
e1[1] = e1y;
|
|
636
|
+
e1[2] = e1z;
|
|
637
|
+
e2[0] = y * e1z - z * e1y;
|
|
638
|
+
e2[1] = z * e1x - x * e1z;
|
|
639
|
+
e2[2] = x * e1y - y * e1x;
|
|
640
|
+
}
|
|
641
|
+
function writeRotation(ux, uy, uz, yaw, out, o) {
|
|
642
|
+
let ax = uz;
|
|
643
|
+
const ay = 0;
|
|
644
|
+
let az = -ux;
|
|
645
|
+
let aw = 1 + uy;
|
|
646
|
+
const len = Math.sqrt(ax * ax + az * az + aw * aw) || 1;
|
|
647
|
+
ax /= len;
|
|
648
|
+
az /= len;
|
|
649
|
+
aw /= len;
|
|
650
|
+
sinCos(yaw / 2, sc);
|
|
651
|
+
const ys = sc[0];
|
|
652
|
+
const yc = sc[1];
|
|
653
|
+
out[o] = ax * yc - az * ys;
|
|
654
|
+
out[o + 1] = aw * ys + ay * yc;
|
|
655
|
+
out[o + 2] = az * yc + ax * ys;
|
|
656
|
+
out[o + 3] = aw * yc - ay * ys;
|
|
657
|
+
}
|
|
658
|
+
function ellipsoidScale(shape, x, y, z) {
|
|
659
|
+
const a = x * shape[0];
|
|
660
|
+
const b = y * shape[1];
|
|
661
|
+
const c = z * shape[2];
|
|
662
|
+
return Math.sqrt(a * a + b * b + c * c);
|
|
663
|
+
}
|
|
664
|
+
function grow(a, n) {
|
|
665
|
+
const out = new Float64Array(Math.max(n, a.length * 2));
|
|
666
|
+
out.set(a);
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
export {
|
|
670
|
+
PlanetSurface,
|
|
671
|
+
cellsPerChunk,
|
|
672
|
+
nodeMetres,
|
|
673
|
+
ruleDepth,
|
|
674
|
+
writeRotation
|
|
675
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Workers } from '@aethervtt/shard-platform';
|
|
2
|
+
import { type Plugin } from '@aethervtt/shard-runtime';
|
|
3
|
+
export interface ScatterPluginOptions {
|
|
4
|
+
/** Where placement samples the ground (default: inline on the main thread). */
|
|
5
|
+
workers?: Workers;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Props on every surface (spec 0045): planets with `Planet.scatter` or biome sets. Resolves each
|
|
9
|
+
* surface's sets and item meshes, then places, spawns and despawns prop chunks around cameras and
|
|
10
|
+
* anchors. Runs with or without a GPU (headless tests place the same props).
|
|
11
|
+
*/
|
|
12
|
+
export declare const updateScatter: import("@aethervtt/shard-core").SystemDef<{
|
|
13
|
+
cameras: import("@aethervtt/shard-core").Query;
|
|
14
|
+
meshes: import("@aethervtt/shard-core").Query;
|
|
15
|
+
}>;
|
|
16
|
+
/** Scatter for an app: props on planets (with the terrain plugin) and mesh surfaces. */
|
|
17
|
+
export declare function scatterPlugin(options?: ScatterPluginOptions): Plugin;
|