scena3d 0.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/dist/index.js ADDED
@@ -0,0 +1,665 @@
1
+ // src/core/random.ts
2
+ var Rng = class _Rng {
3
+ constructor(seed = 1) {
4
+ this.state = seed >>> 0 || 1;
5
+ }
6
+ /** Next float in [0, 1) (mulberry32). */
7
+ next() {
8
+ this.state = this.state + 1831565813 >>> 0;
9
+ let t = this.state;
10
+ t = Math.imul(t ^ t >>> 15, t | 1);
11
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
12
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
13
+ }
14
+ /** Float in [min, max). */
15
+ range(min, max) {
16
+ return min + this.next() * (max - min);
17
+ }
18
+ /** Integer in [min, max] inclusive. */
19
+ int(min, max) {
20
+ return min + Math.floor(this.next() * (max - min + 1));
21
+ }
22
+ /** Random element of a non-empty array. */
23
+ pick(items) {
24
+ return items[Math.floor(this.next() * items.length)];
25
+ }
26
+ /** value ± spread (uniform). */
27
+ jitter(value, spread) {
28
+ return value + (this.next() * 2 - 1) * spread;
29
+ }
30
+ /** A new independent Rng derived from this one. */
31
+ fork() {
32
+ return new _Rng(Math.floor(this.next() * 4294967295) || 1);
33
+ }
34
+ };
35
+ function hash2(ix, iz, seed) {
36
+ let h = ix * 374761393 + iz * 668265263 + seed * 2246822519 >>> 0;
37
+ h = Math.imul(h ^ h >>> 13, 1274126177) >>> 0;
38
+ return ((h ^ h >>> 16) >>> 0) / 4294967296;
39
+ }
40
+ var smooth = (t) => t * t * (3 - 2 * t);
41
+ function valueNoise2(x, z, seed) {
42
+ const ix = Math.floor(x);
43
+ const iz = Math.floor(z);
44
+ const fx = smooth(x - ix);
45
+ const fz = smooth(z - iz);
46
+ const a = hash2(ix, iz, seed);
47
+ const b = hash2(ix + 1, iz, seed);
48
+ const c = hash2(ix, iz + 1, seed);
49
+ const d = hash2(ix + 1, iz + 1, seed);
50
+ return a + (b - a) * fx + (c - a) * fz + (a - b - c + d) * fx * fz;
51
+ }
52
+ function fractalNoise2(x, z, seed, octaves = 4, lacunarity = 2, gain = 0.5) {
53
+ let amplitude = 1;
54
+ let frequency = 1;
55
+ let sum = 0;
56
+ let total = 0;
57
+ for (let i = 0; i < octaves; i++) {
58
+ sum += valueNoise2(x * frequency, z * frequency, seed + i * 101) * amplitude;
59
+ total += amplitude;
60
+ amplitude *= gain;
61
+ frequency *= lacunarity;
62
+ }
63
+ return sum / total;
64
+ }
65
+
66
+ // src/core/palette.ts
67
+ var PALETTES = {
68
+ meadow: {
69
+ foliage: [3120727, 3650154, 2789199, 4569208],
70
+ trunk: 7031347,
71
+ rock: [9080728, 7765126, 10133672],
72
+ wood: 9070146,
73
+ woodDark: 7031347,
74
+ metal: 4015185,
75
+ lampGlow: 16767113,
76
+ grassLow: 4169050,
77
+ grassHigh: 7319142,
78
+ cliff: 8223346,
79
+ peak: 15265007,
80
+ skyTop: 4026552,
81
+ skyBottom: 12573160,
82
+ fog: 12111837
83
+ },
84
+ autumn: {
85
+ foliage: [13202735, 14257722, 11754538, 14722373],
86
+ trunk: 6111280,
87
+ rock: [9274744, 7827299, 10261642],
88
+ wood: 8215098,
89
+ woodDark: 6111280,
90
+ metal: 4603706,
91
+ lampGlow: 16762225,
92
+ grassLow: 10324543,
93
+ grassHigh: 11901770,
94
+ cliff: 8549482,
95
+ peak: 14933716,
96
+ skyTop: 9333928,
97
+ skyBottom: 15255976,
98
+ fog: 14270888
99
+ },
100
+ dusk: {
101
+ foliage: [2055750, 2385999, 1724992, 2978904],
102
+ trunk: 4272455,
103
+ rock: [5658226, 4737121, 6579332],
104
+ wood: 6113891,
105
+ woodDark: 4272455,
106
+ metal: 2829117,
107
+ lampGlow: 16757596,
108
+ grassLow: 2976594,
109
+ grassHigh: 4029022,
110
+ cliff: 5394795,
111
+ peak: 12105945,
112
+ skyTop: 1909061,
113
+ skyBottom: 13199946,
114
+ fog: 6969978
115
+ }
116
+ };
117
+ var DEFAULT_PALETTE = PALETTES.meadow;
118
+
119
+ // src/core/types.ts
120
+ import { Vector3 } from "three";
121
+ function collectObstacles(props) {
122
+ const obstacles = [];
123
+ for (const prop of props) {
124
+ if (prop.obstacleRadius <= 0) continue;
125
+ prop.object.updateWorldMatrix(true, false);
126
+ obstacles.push({
127
+ center: prop.object.getWorldPosition(new Vector3()),
128
+ radius: prop.obstacleRadius * maxScale(prop.object)
129
+ });
130
+ }
131
+ return obstacles;
132
+ }
133
+ function maxScale(object) {
134
+ return Math.max(object.scale.x, object.scale.z);
135
+ }
136
+
137
+ // src/props/tree.ts
138
+ import {
139
+ ConeGeometry,
140
+ CylinderGeometry,
141
+ Group,
142
+ IcosahedronGeometry,
143
+ Mesh,
144
+ MeshStandardMaterial
145
+ } from "three";
146
+ function createTree(options = {}) {
147
+ const rng = new Rng(options.seed ?? 1);
148
+ const palette = options.palette ?? DEFAULT_PALETTE;
149
+ const style = options.style ?? (rng.next() < 0.6 ? "pine" : "oak");
150
+ const height = options.height ?? rng.range(3.2, 5.2);
151
+ const group = new Group();
152
+ group.name = `tree-${style}`;
153
+ const trunkMaterial = new MeshStandardMaterial({ color: palette.trunk, flatShading: true });
154
+ const foliageMaterial = new MeshStandardMaterial({
155
+ color: rng.pick(palette.foliage),
156
+ flatShading: true
157
+ });
158
+ if (style === "pine") {
159
+ const trunkHeight = height * 0.25;
160
+ const trunk = new Mesh(new CylinderGeometry(0.09, 0.14, trunkHeight, 6), trunkMaterial);
161
+ trunk.position.y = trunkHeight / 2;
162
+ group.add(trunk);
163
+ const tiers = rng.int(3, 4);
164
+ let y = trunkHeight;
165
+ let radius = height * rng.range(0.24, 0.3);
166
+ const tierHeight = (height - trunkHeight) / tiers + 0.15;
167
+ for (let i = 0; i < tiers; i++) {
168
+ const cone = new Mesh(new ConeGeometry(radius, tierHeight * 1.35, 7), foliageMaterial);
169
+ cone.position.y = y + tierHeight * 0.55;
170
+ cone.rotation.y = rng.range(0, Math.PI);
171
+ group.add(cone);
172
+ y += tierHeight * 0.8;
173
+ radius *= 0.72;
174
+ }
175
+ } else {
176
+ const trunkHeight = height * 0.45;
177
+ const trunk = new Mesh(new CylinderGeometry(0.12, 0.2, trunkHeight, 6), trunkMaterial);
178
+ trunk.position.y = trunkHeight / 2;
179
+ trunk.rotation.z = rng.range(-0.08, 0.08);
180
+ group.add(trunk);
181
+ const blobs = rng.int(2, 4);
182
+ for (let i = 0; i < blobs; i++) {
183
+ const radius = height * rng.range(0.18, 0.28);
184
+ const blob = new Mesh(new IcosahedronGeometry(radius, 0), foliageMaterial);
185
+ blob.position.set(
186
+ rng.jitter(0, height * 0.16),
187
+ trunkHeight + radius * rng.range(0.5, 0.9) + i * radius * 0.35,
188
+ rng.jitter(0, height * 0.16)
189
+ );
190
+ blob.rotation.set(rng.range(0, Math.PI), rng.range(0, Math.PI), 0);
191
+ group.add(blob);
192
+ }
193
+ }
194
+ return { object: group, obstacleRadius: style === "pine" ? 0.5 : 0.6 };
195
+ }
196
+
197
+ // src/props/rock.ts
198
+ import {
199
+ Group as Group2,
200
+ IcosahedronGeometry as IcosahedronGeometry2,
201
+ Mesh as Mesh2,
202
+ MeshStandardMaterial as MeshStandardMaterial2
203
+ } from "three";
204
+ function createRock(options = {}) {
205
+ const rng = new Rng(options.seed ?? 1);
206
+ const palette = options.palette ?? DEFAULT_PALETTE;
207
+ const size = options.size ?? rng.range(0.4, 1.1);
208
+ const geometry = new IcosahedronGeometry2(size, 0);
209
+ const positions = geometry.getAttribute("position");
210
+ const seen = /* @__PURE__ */ new Map();
211
+ for (let i = 0; i < positions.count; i++) {
212
+ const key = `${positions.getX(i).toFixed(4)},${positions.getY(i).toFixed(4)},${positions.getZ(i).toFixed(4)}`;
213
+ let offset = seen.get(key);
214
+ if (!offset) {
215
+ offset = [rng.jitter(0, size * 0.22), rng.jitter(0, size * 0.16), rng.jitter(0, size * 0.22)];
216
+ seen.set(key, offset);
217
+ }
218
+ positions.setXYZ(
219
+ i,
220
+ positions.getX(i) + offset[0],
221
+ Math.max(positions.getY(i) + offset[1], -size * 0.15),
222
+ // flatten the bottom
223
+ positions.getZ(i) + offset[2]
224
+ );
225
+ }
226
+ geometry.computeVertexNormals();
227
+ const rock = new Mesh2(
228
+ geometry,
229
+ new MeshStandardMaterial2({ color: rng.pick(palette.rock), flatShading: true })
230
+ );
231
+ rock.position.y = size * 0.15;
232
+ rock.scale.y = rng.range(0.6, 0.9);
233
+ const group = new Group2();
234
+ group.name = "rock";
235
+ group.add(rock);
236
+ return { object: group, obstacleRadius: size * 1.05 };
237
+ }
238
+
239
+ // src/props/crate.ts
240
+ import { BoxGeometry, Group as Group3, Mesh as Mesh3, MeshStandardMaterial as MeshStandardMaterial3 } from "three";
241
+ function createCrate(options = {}) {
242
+ const rng = new Rng(options.seed ?? 1);
243
+ const palette = options.palette ?? DEFAULT_PALETTE;
244
+ const size = options.size ?? 1;
245
+ const wear = options.weathering ?? 0.3;
246
+ const group = new Group3();
247
+ group.name = "crate";
248
+ const panelMaterial = new MeshStandardMaterial3({ color: palette.wood, flatShading: true });
249
+ panelMaterial.color.offsetHSL(0, 0, -rng.range(0, wear * 0.18));
250
+ const frameMaterial = new MeshStandardMaterial3({ color: palette.woodDark, flatShading: true });
251
+ const body = new Mesh3(new BoxGeometry(size * 0.92, size * 0.92, size * 0.92), panelMaterial);
252
+ body.position.y = size / 2;
253
+ group.add(body);
254
+ const beam = size * 0.12;
255
+ const long = size * 1;
256
+ for (const y of [beam / 2, size - beam / 2]) {
257
+ for (const [rx, rz, w, d] of [
258
+ [0, size / 2 - beam / 2, long, beam],
259
+ [0, -(size / 2 - beam / 2), long, beam],
260
+ [size / 2 - beam / 2, 0, beam, long],
261
+ [-(size / 2 - beam / 2), 0, beam, long]
262
+ ]) {
263
+ const rail = new Mesh3(new BoxGeometry(w, beam, d), frameMaterial);
264
+ rail.position.set(rx, y, rz);
265
+ group.add(rail);
266
+ }
267
+ }
268
+ for (const x of [-1, 1]) {
269
+ for (const z of [-1, 1]) {
270
+ const post = new Mesh3(new BoxGeometry(beam, size, beam), frameMaterial);
271
+ post.position.set(x * (size / 2 - beam / 2), size / 2, z * (size / 2 - beam / 2));
272
+ group.add(post);
273
+ }
274
+ }
275
+ group.rotation.y = rng.range(0, Math.PI / 2);
276
+ return { object: group, obstacleRadius: size * 0.75 };
277
+ }
278
+
279
+ // src/props/fence.ts
280
+ import { BoxGeometry as BoxGeometry2, CylinderGeometry as CylinderGeometry2, Group as Group4, Mesh as Mesh4, MeshStandardMaterial as MeshStandardMaterial4 } from "three";
281
+ function createFence(options = {}) {
282
+ const rng = new Rng(options.seed ?? 1);
283
+ const palette = options.palette ?? DEFAULT_PALETTE;
284
+ const length = options.length ?? 6;
285
+ const spacing = options.postSpacing ?? 1.5;
286
+ const height = options.height ?? 1.1;
287
+ const group = new Group4();
288
+ group.name = "fence";
289
+ const postMaterial = new MeshStandardMaterial4({ color: palette.woodDark, flatShading: true });
290
+ const railMaterial = new MeshStandardMaterial4({ color: palette.wood, flatShading: true });
291
+ const posts = Math.max(2, Math.round(length / spacing) + 1);
292
+ const step = length / (posts - 1);
293
+ for (let i = 0; i < posts; i++) {
294
+ const post = new Mesh4(new CylinderGeometry2(0.06, 0.075, height, 5), postMaterial);
295
+ post.position.set(-length / 2 + i * step, height / 2, rng.jitter(0, 0.03));
296
+ post.rotation.z = rng.range(-0.04, 0.04);
297
+ group.add(post);
298
+ }
299
+ for (const railY of [height * 0.55, height * 0.85]) {
300
+ const rail = new Mesh4(new BoxGeometry2(length, 0.07, 0.05), railMaterial);
301
+ rail.position.y = rng.jitter(railY, 0.02);
302
+ rail.rotation.x = rng.range(-0.02, 0.02);
303
+ group.add(rail);
304
+ }
305
+ return { object: group, obstacleRadius: length / 2 };
306
+ }
307
+
308
+ // src/props/lamp.ts
309
+ import {
310
+ BoxGeometry as BoxGeometry3,
311
+ CylinderGeometry as CylinderGeometry3,
312
+ Group as Group5,
313
+ Mesh as Mesh5,
314
+ MeshStandardMaterial as MeshStandardMaterial5,
315
+ PointLight,
316
+ SphereGeometry
317
+ } from "three";
318
+ function createLamp(options = {}) {
319
+ const rng = new Rng(options.seed ?? 1);
320
+ const palette = options.palette ?? DEFAULT_PALETTE;
321
+ const height = options.height ?? rng.range(2.6, 3.1);
322
+ const group = new Group5();
323
+ group.name = "lamp";
324
+ const metal = new MeshStandardMaterial5({ color: palette.metal, flatShading: true });
325
+ const post = new Mesh5(new CylinderGeometry3(0.05, 0.08, height, 6), metal);
326
+ post.position.y = height / 2;
327
+ group.add(post);
328
+ const head = new Mesh5(new BoxGeometry3(0.34, 0.26, 0.34), metal);
329
+ head.position.y = height + 0.1;
330
+ group.add(head);
331
+ const bulb = new Mesh5(
332
+ new SphereGeometry(0.11, 8, 6),
333
+ new MeshStandardMaterial5({
334
+ color: palette.lampGlow,
335
+ emissive: palette.lampGlow,
336
+ emissiveIntensity: 1.6
337
+ })
338
+ );
339
+ bulb.position.y = height - 0.03;
340
+ group.add(bulb);
341
+ if (options.light) {
342
+ const light = new PointLight(palette.lampGlow, options.lightIntensity ?? 6, 12, 1.8);
343
+ light.position.y = height - 0.05;
344
+ group.add(light);
345
+ }
346
+ return { object: group, obstacleRadius: 0.25 };
347
+ }
348
+
349
+ // src/environment/terrain.ts
350
+ import {
351
+ BufferAttribute as BufferAttribute2,
352
+ Color,
353
+ Mesh as Mesh6,
354
+ MeshStandardMaterial as MeshStandardMaterial6,
355
+ PlaneGeometry
356
+ } from "three";
357
+ function createTerrain(options = {}) {
358
+ const seed = options.seed ?? 1;
359
+ const size = options.size ?? 80;
360
+ const resolution = options.resolution ?? 96;
361
+ const amplitude = options.amplitude ?? 6;
362
+ const noiseScale = options.noiseScale ?? 28;
363
+ const octaves = options.octaves ?? 4;
364
+ const flatness = options.valleyFlatness ?? 0.55;
365
+ const palette = options.palette ?? DEFAULT_PALETTE;
366
+ const heightAt = (x, z) => {
367
+ const n = fractalNoise2(x / noiseScale, z / noiseScale, seed, octaves);
368
+ const shaped = Math.pow(n, 1 + flatness * 2);
369
+ return shaped * amplitude;
370
+ };
371
+ const geometry = new PlaneGeometry(size, size, resolution - 1, resolution - 1);
372
+ geometry.rotateX(-Math.PI / 2);
373
+ const positions = geometry.getAttribute("position");
374
+ for (let i = 0; i < positions.count; i++) {
375
+ positions.setY(i, heightAt(positions.getX(i), positions.getZ(i)));
376
+ }
377
+ geometry.computeVertexNormals();
378
+ const normals = geometry.getAttribute("normal");
379
+ const colors = new Float32Array(positions.count * 3);
380
+ const grassLow = new Color(palette.grassLow);
381
+ const grassHigh = new Color(palette.grassHigh);
382
+ const cliff = new Color(palette.cliff);
383
+ const peak = new Color(palette.peak);
384
+ const scratch = new Color();
385
+ for (let i = 0; i < positions.count; i++) {
386
+ const h = positions.getY(i) / amplitude;
387
+ const slope = 1 - normals.getY(i);
388
+ scratch.copy(grassLow).lerp(grassHigh, Math.min(1, h * 1.6));
389
+ if (h > 0.75) scratch.lerp(peak, (h - 0.75) * 4);
390
+ if (slope > 0.15) scratch.lerp(cliff, Math.min(1, (slope - 0.15) * 4));
391
+ colors[i * 3] = scratch.r;
392
+ colors[i * 3 + 1] = scratch.g;
393
+ colors[i * 3 + 2] = scratch.b;
394
+ }
395
+ geometry.setAttribute("color", new BufferAttribute2(colors, 3));
396
+ const mesh = new Mesh6(
397
+ geometry,
398
+ new MeshStandardMaterial6({ vertexColors: true, flatShading: true })
399
+ );
400
+ mesh.name = "terrain";
401
+ return { mesh, heightAt, size, seed };
402
+ }
403
+
404
+ // src/environment/sky.ts
405
+ import {
406
+ BackSide,
407
+ Color as Color2,
408
+ Mesh as Mesh7,
409
+ ShaderMaterial,
410
+ SphereGeometry as SphereGeometry2
411
+ } from "three";
412
+ function createSky(options = {}) {
413
+ const palette = options.palette ?? DEFAULT_PALETTE;
414
+ const material = new ShaderMaterial({
415
+ side: BackSide,
416
+ depthWrite: false,
417
+ uniforms: {
418
+ topColor: { value: new Color2(options.topColor ?? palette.skyTop) },
419
+ bottomColor: { value: new Color2(options.bottomColor ?? palette.skyBottom) }
420
+ },
421
+ vertexShader: (
422
+ /* glsl */
423
+ `
424
+ varying vec3 vWorld;
425
+ void main() {
426
+ vWorld = (modelMatrix * vec4(position, 1.0)).xyz;
427
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
428
+ }`
429
+ ),
430
+ fragmentShader: (
431
+ /* glsl */
432
+ `
433
+ uniform vec3 topColor;
434
+ uniform vec3 bottomColor;
435
+ varying vec3 vWorld;
436
+ void main() {
437
+ float t = clamp(normalize(vWorld).y * 0.5 + 0.5, 0.0, 1.0);
438
+ gl_FragColor = vec4(mix(bottomColor, topColor, pow(t, 0.8)), 1.0);
439
+ }`
440
+ )
441
+ });
442
+ const mesh = new Mesh7(new SphereGeometry2(options.radius ?? 400, 16, 12), material);
443
+ mesh.name = "sky";
444
+ return {
445
+ mesh,
446
+ setColors(top, bottom) {
447
+ material.uniforms.topColor.value.setHex(top);
448
+ material.uniforms.bottomColor.value.setHex(bottom);
449
+ }
450
+ };
451
+ }
452
+
453
+ // src/environment/lighting.ts
454
+ import {
455
+ AmbientLight,
456
+ DirectionalLight,
457
+ Fog,
458
+ Group as Group6,
459
+ HemisphereLight
460
+ } from "three";
461
+ var PRESETS = {
462
+ day: {
463
+ sun: 16774368,
464
+ sunIntensity: 1.6,
465
+ sunPos: [30, 45, 20],
466
+ ambient: 14674687,
467
+ ambientIntensity: 0.35,
468
+ skyTint: 12376319,
469
+ groundTint: 6978918
470
+ },
471
+ "golden-hour": {
472
+ sun: 16758881,
473
+ sunIntensity: 1.7,
474
+ sunPos: [40, 14, -12],
475
+ ambient: 16768192,
476
+ ambientIntensity: 0.3,
477
+ skyTint: 16764830,
478
+ groundTint: 8022616
479
+ },
480
+ overcast: {
481
+ sun: 14212840,
482
+ sunIntensity: 0.7,
483
+ sunPos: [12, 40, 8],
484
+ ambient: 13620960,
485
+ ambientIntensity: 0.65,
486
+ skyTint: 13160668,
487
+ groundTint: 7371384
488
+ },
489
+ night: {
490
+ sun: 9348824,
491
+ sunIntensity: 0.35,
492
+ sunPos: [-20, 30, -25],
493
+ ambient: 3818600,
494
+ ambientIntensity: 0.35,
495
+ skyTint: 2897248,
496
+ groundTint: 1975344
497
+ }
498
+ };
499
+ function createLightingRig(preset = "day") {
500
+ const config = PRESETS[preset];
501
+ const group = new Group6();
502
+ group.name = `lighting-${preset}`;
503
+ const sun = new DirectionalLight(config.sun, config.sunIntensity);
504
+ sun.position.set(...config.sunPos);
505
+ const ambient = new AmbientLight(config.ambient, config.ambientIntensity);
506
+ const hemisphere = new HemisphereLight(config.skyTint, config.groundTint, 0.5);
507
+ group.add(sun, ambient, hemisphere);
508
+ return { group, sun, ambient, hemisphere };
509
+ }
510
+ var FOG = {
511
+ haze: { near: 45, far: 160 },
512
+ thick: { near: 12, far: 70 },
513
+ eerie: { near: 6, far: 42 }
514
+ };
515
+ function applyFog(scene, preset, palette = DEFAULT_PALETTE) {
516
+ if (preset === "clear") {
517
+ scene.fog = null;
518
+ return;
519
+ }
520
+ const { near, far } = FOG[preset];
521
+ scene.fog = new Fog(palette.fog, near, far);
522
+ }
523
+
524
+ // src/scatter/scatter.ts
525
+ import {
526
+ DynamicDrawUsage,
527
+ Group as Group7,
528
+ InstancedMesh,
529
+ Matrix4,
530
+ Mesh as Mesh8,
531
+ Quaternion,
532
+ Vector3 as Vector32
533
+ } from "three";
534
+ function scatter(options) {
535
+ const seed = options.seed ?? 1;
536
+ const rng = new Rng(seed);
537
+ const { area } = options;
538
+ const width = area.max.x - area.min.x;
539
+ const depth = area.max.z - area.min.z;
540
+ const surface = options.surface ?? 0;
541
+ const heightAt = typeof surface === "number" ? () => surface : (x, z) => surface(x, z);
542
+ const minSpacing = options.minSpacing ?? 1.2;
543
+ const clumpScale = options.clumpScale ?? 18;
544
+ const attempts = options.count ?? Math.round(width * depth * (options.density ?? 0.03));
545
+ const keepOut = (options.keepOut ?? []).map((k) => ({
546
+ x: k.center.x,
547
+ z: "z" in k.center ? k.center.z : k.center.z,
548
+ radius: k.radius
549
+ }));
550
+ const weights = options.items.map((i) => i.weight ?? 1);
551
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
552
+ const pickItem = () => {
553
+ let r = rng.next() * totalWeight;
554
+ for (let i = 0; i < weights.length; i++) {
555
+ r -= weights[i];
556
+ if (r <= 0) return i;
557
+ }
558
+ return weights.length - 1;
559
+ };
560
+ const cell = Math.max(minSpacing, 1e-3);
561
+ const occupied = /* @__PURE__ */ new Map();
562
+ const tooClose = (x, z) => {
563
+ const cx = Math.floor(x / cell);
564
+ const cz = Math.floor(z / cell);
565
+ for (let dx = -1; dx <= 1; dx++) {
566
+ for (let dz = -1; dz <= 1; dz++) {
567
+ const bucket = occupied.get(`${cx + dx},${cz + dz}`);
568
+ if (!bucket) continue;
569
+ for (const p of bucket) {
570
+ if ((p.x - x) ** 2 + (p.z - z) ** 2 < minSpacing * minSpacing) return true;
571
+ }
572
+ }
573
+ }
574
+ return false;
575
+ };
576
+ const placements = [];
577
+ for (let i = 0; i < attempts; i++) {
578
+ const x = rng.range(area.min.x, area.max.x);
579
+ const z = rng.range(area.min.z, area.max.z);
580
+ if (valueNoise2(x / clumpScale, z / clumpScale, seed + 77) < rng.range(0.15, 0.55)) continue;
581
+ if (keepOut.some((k) => (k.x - x) ** 2 + (k.z - z) ** 2 < k.radius * k.radius)) continue;
582
+ if (tooClose(x, z)) continue;
583
+ const y = heightAt(x, z);
584
+ if (options.mask && !options.mask(x, z, y)) continue;
585
+ const itemIndex = pickItem();
586
+ const [minScale, maxScale2] = options.items[itemIndex].scale ?? [0.8, 1.25];
587
+ placements.push({
588
+ position: new Vector32(x, y, z),
589
+ rotationY: rng.range(0, Math.PI * 2),
590
+ scale: rng.range(minScale, maxScale2),
591
+ itemIndex
592
+ });
593
+ const key = `${Math.floor(x / cell)},${Math.floor(z / cell)}`;
594
+ let bucket = occupied.get(key);
595
+ if (!bucket) occupied.set(key, bucket = []);
596
+ bucket.push({ x, z });
597
+ }
598
+ const group = new Group7();
599
+ group.name = "scatter";
600
+ const obstacles = [];
601
+ const placementMatrix = new Matrix4();
602
+ const composed = new Matrix4();
603
+ const quaternion = new Quaternion();
604
+ const up = new Vector32(0, 1, 0);
605
+ const scaleVector = new Vector32();
606
+ options.items.forEach((item, itemIndex) => {
607
+ const variantCount = item.variants ?? 4;
608
+ const variants = [];
609
+ for (let v = 0; v < variantCount; v++) variants.push(item.create(rng.fork()));
610
+ const byVariant = variants.map(() => []);
611
+ for (const placement of placements) {
612
+ if (placement.itemIndex !== itemIndex) continue;
613
+ const v = Math.abs(Math.floor(placement.position.x * 31 + placement.position.z * 17)) % variantCount;
614
+ byVariant[v].push(placement);
615
+ }
616
+ variants.forEach((variant, v) => {
617
+ const list = byVariant[v];
618
+ if (list.length === 0) return;
619
+ variant.object.updateMatrixWorld(true);
620
+ variant.object.traverse((child) => {
621
+ if (!(child instanceof Mesh8)) return;
622
+ const instanced = new InstancedMesh(child.geometry, child.material, list.length);
623
+ instanced.instanceMatrix.setUsage(DynamicDrawUsage);
624
+ list.forEach((placement, index) => {
625
+ quaternion.setFromAxisAngle(up, placement.rotationY);
626
+ scaleVector.setScalar(placement.scale);
627
+ placementMatrix.compose(placement.position, quaternion, scaleVector);
628
+ composed.multiplyMatrices(placementMatrix, child.matrixWorld);
629
+ instanced.setMatrixAt(index, composed);
630
+ });
631
+ instanced.instanceMatrix.needsUpdate = true;
632
+ group.add(instanced);
633
+ });
634
+ if (variant.obstacleRadius > 0) {
635
+ for (const placement of list) {
636
+ obstacles.push({
637
+ center: placement.position.clone(),
638
+ radius: variant.obstacleRadius * placement.scale
639
+ });
640
+ }
641
+ }
642
+ });
643
+ });
644
+ return { group, placements, obstacles, count: placements.length };
645
+ }
646
+ export {
647
+ DEFAULT_PALETTE,
648
+ PALETTES,
649
+ Rng,
650
+ applyFog,
651
+ collectObstacles,
652
+ createCrate,
653
+ createFence,
654
+ createLamp,
655
+ createLightingRig,
656
+ createRock,
657
+ createSky,
658
+ createTerrain,
659
+ createTree,
660
+ fractalNoise2,
661
+ hash2,
662
+ scatter,
663
+ valueNoise2
664
+ };
665
+ //# sourceMappingURL=index.js.map