scena3d 0.1.0 → 0.3.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.cjs CHANGED
@@ -23,16 +23,28 @@ __export(index_exports, {
23
23
  DEFAULT_PALETTE: () => DEFAULT_PALETTE,
24
24
  PALETTES: () => PALETTES,
25
25
  Rng: () => Rng,
26
+ aboveWater: () => aboveWater,
26
27
  applyFog: () => applyFog,
28
+ applyWind: () => applyWind,
27
29
  collectObstacles: () => collectObstacles,
30
+ createBush: () => createBush,
28
31
  createCrate: () => createCrate,
32
+ createDayCycle: () => createDayCycle,
29
33
  createFence: () => createFence,
34
+ createGrassTuft: () => createGrassTuft,
35
+ createHouse: () => createHouse,
30
36
  createLamp: () => createLamp,
31
37
  createLightingRig: () => createLightingRig,
38
+ createPath: () => createPath,
32
39
  createRock: () => createRock,
40
+ createRuin: () => createRuin,
33
41
  createSky: () => createSky,
34
42
  createTerrain: () => createTerrain,
43
+ createTower: () => createTower,
35
44
  createTree: () => createTree,
45
+ createVillage: () => createVillage,
46
+ createWater: () => createWater,
47
+ createWell: () => createWell,
36
48
  fractalNoise2: () => fractalNoise2,
37
49
  hash2: () => hash2,
38
50
  scatter: () => scatter,
@@ -121,7 +133,12 @@ var PALETTES = {
121
133
  peak: 15265007,
122
134
  skyTop: 4026552,
123
135
  skyBottom: 12573160,
124
- fog: 12111837
136
+ fog: 12111837,
137
+ water: 4161454,
138
+ sand: 13220234,
139
+ path: 10125663,
140
+ wall: 14273712,
141
+ roof: 11032126
125
142
  },
126
143
  autumn: {
127
144
  foliage: [13202735, 14257722, 11754538, 14722373],
@@ -137,7 +154,12 @@ var PALETTES = {
137
154
  peak: 14933716,
138
155
  skyTop: 9333928,
139
156
  skyBottom: 15255976,
140
- fog: 14270888
157
+ fog: 14270888,
158
+ water: 4881042,
159
+ sand: 13348995,
160
+ path: 9270356,
161
+ wall: 13416596,
162
+ roof: 9062960
141
163
  },
142
164
  dusk: {
143
165
  foliage: [2055750, 2385999, 1724992, 2978904],
@@ -153,7 +175,33 @@ var PALETTES = {
153
175
  peak: 12105945,
154
176
  skyTop: 1909061,
155
177
  skyBottom: 13199946,
156
- fog: 6969978
178
+ fog: 6969978,
179
+ water: 2968168,
180
+ sand: 9075306,
181
+ path: 6969938,
182
+ wall: 9274009,
183
+ roof: 4535640
184
+ },
185
+ winter: {
186
+ foliage: [3038280, 3696986, 5405288, 8889750],
187
+ trunk: 4864563,
188
+ rock: [10134701, 8687256, 11581632],
189
+ wood: 7823433,
190
+ woodDark: 5522746,
191
+ metal: 3752013,
192
+ lampGlow: 16767113,
193
+ grassLow: 13621726,
194
+ grassHigh: 15002606,
195
+ cliff: 7764349,
196
+ peak: 16054266,
197
+ skyTop: 5929894,
198
+ skyBottom: 14214380,
199
+ fog: 13424864,
200
+ water: 4878470,
201
+ sand: 12108486,
202
+ path: 9143160,
203
+ wall: 13814203,
204
+ roof: 7030328
157
205
  }
158
206
  };
159
207
  var DEFAULT_PALETTE = PALETTES.meadow;
@@ -368,8 +416,321 @@ function createLamp(options = {}) {
368
416
  return { object: group, obstacleRadius: 0.25 };
369
417
  }
370
418
 
371
- // src/environment/terrain.ts
419
+ // src/props/grass.ts
372
420
  var import_three7 = require("three");
421
+ function createGrassTuft(options = {}) {
422
+ const rng = new Rng(options.seed ?? 1);
423
+ const palette = options.palette ?? DEFAULT_PALETTE;
424
+ const blades = options.blades ?? rng.int(4, 6);
425
+ const group = new import_three7.Group();
426
+ group.name = "grass";
427
+ const material = new import_three7.MeshStandardMaterial({
428
+ color: rng.next() < 0.5 ? palette.grassHigh : rng.pick(palette.foliage),
429
+ flatShading: true
430
+ });
431
+ for (let i = 0; i < blades; i++) {
432
+ const height = rng.range(0.25, 0.5);
433
+ const blade = new import_three7.Mesh(new import_three7.ConeGeometry(0.035, height, 3), material);
434
+ blade.position.set(rng.jitter(0, 0.12), height / 2, rng.jitter(0, 0.12));
435
+ blade.rotation.set(rng.range(-0.25, 0.25), rng.range(0, Math.PI), rng.range(-0.25, 0.25));
436
+ group.add(blade);
437
+ }
438
+ return { object: group, obstacleRadius: 0 };
439
+ }
440
+ function createBush(options = {}) {
441
+ const rng = new Rng(options.seed ?? 1);
442
+ const palette = options.palette ?? DEFAULT_PALETTE;
443
+ const size = options.size ?? rng.range(0.4, 0.7);
444
+ const group = new import_three7.Group();
445
+ group.name = "bush";
446
+ const material = new import_three7.MeshStandardMaterial({ color: rng.pick(palette.foliage), flatShading: true });
447
+ const blobs = rng.int(2, 3);
448
+ for (let i = 0; i < blobs; i++) {
449
+ const radius = size * rng.range(0.55, 0.85);
450
+ const blob = new import_three7.Mesh(new import_three7.IcosahedronGeometry(radius, 0), material);
451
+ blob.position.set(rng.jitter(0, size * 0.4), radius * 0.7, rng.jitter(0, size * 0.4));
452
+ blob.scale.y = rng.range(0.7, 0.85);
453
+ blob.rotation.y = rng.range(0, Math.PI);
454
+ group.add(blob);
455
+ }
456
+ return { object: group, obstacleRadius: size * 0.6 };
457
+ }
458
+
459
+ // src/props/building.ts
460
+ var import_three8 = require("three");
461
+ function prismGeometry(width, height, depth) {
462
+ const w = width / 2;
463
+ const d = depth / 2;
464
+ const positions = new Float32Array([
465
+ // front gable (z = +d)
466
+ -w,
467
+ 0,
468
+ d,
469
+ w,
470
+ 0,
471
+ d,
472
+ 0,
473
+ height,
474
+ d,
475
+ // back gable (z = -d)
476
+ w,
477
+ 0,
478
+ -d,
479
+ -w,
480
+ 0,
481
+ -d,
482
+ 0,
483
+ height,
484
+ -d,
485
+ // left slope
486
+ -w,
487
+ 0,
488
+ -d,
489
+ -w,
490
+ 0,
491
+ d,
492
+ 0,
493
+ height,
494
+ d,
495
+ -w,
496
+ 0,
497
+ -d,
498
+ 0,
499
+ height,
500
+ d,
501
+ 0,
502
+ height,
503
+ -d,
504
+ // right slope
505
+ w,
506
+ 0,
507
+ d,
508
+ w,
509
+ 0,
510
+ -d,
511
+ 0,
512
+ height,
513
+ -d,
514
+ w,
515
+ 0,
516
+ d,
517
+ 0,
518
+ height,
519
+ -d,
520
+ 0,
521
+ height,
522
+ d,
523
+ // bottom
524
+ -w,
525
+ 0,
526
+ -d,
527
+ w,
528
+ 0,
529
+ -d,
530
+ w,
531
+ 0,
532
+ d,
533
+ -w,
534
+ 0,
535
+ -d,
536
+ w,
537
+ 0,
538
+ d,
539
+ -w,
540
+ 0,
541
+ d
542
+ ]);
543
+ const geometry = new import_three8.BufferGeometry();
544
+ geometry.setAttribute("position", new import_three8.BufferAttribute(positions, 3));
545
+ geometry.computeVertexNormals();
546
+ return geometry;
547
+ }
548
+ function createHouse(options = {}) {
549
+ const rng = new Rng(options.seed ?? 1);
550
+ const palette = options.palette ?? DEFAULT_PALETTE;
551
+ const width = options.width ?? rng.range(3.2, 4.2);
552
+ const depth = options.depth ?? width * rng.range(0.75, 0.9);
553
+ const wallHeight = options.wallHeight ?? rng.range(2.1, 2.4);
554
+ const group = new import_three8.Group();
555
+ group.name = "house";
556
+ const wall = new import_three8.MeshStandardMaterial({ color: palette.wall, flatShading: true });
557
+ wall.color.offsetHSL(0, 0, rng.range(-0.03, 0.03));
558
+ const roof = new import_three8.MeshStandardMaterial({ color: palette.roof, flatShading: true });
559
+ roof.color.offsetHSL(0, 0, rng.range(-0.04, 0.04));
560
+ const stone = new import_three8.MeshStandardMaterial({ color: palette.rock[0], flatShading: true });
561
+ const wood = new import_three8.MeshStandardMaterial({ color: palette.woodDark, flatShading: true });
562
+ const foundation = new import_three8.Mesh(new import_three8.BoxGeometry(width + 0.3, 1.6, depth + 0.3), stone);
563
+ foundation.position.y = -0.55;
564
+ group.add(foundation);
565
+ const body = new import_three8.Mesh(new import_three8.BoxGeometry(width, wallHeight, depth), wall);
566
+ body.position.y = wallHeight / 2;
567
+ group.add(body);
568
+ const ridgeHeight = width * rng.range(0.32, 0.4);
569
+ const roofMesh = new import_three8.Mesh(prismGeometry(width + 0.5, ridgeHeight, depth + 0.6), roof);
570
+ roofMesh.position.y = wallHeight - 0.02;
571
+ group.add(roofMesh);
572
+ const chimney = new import_three8.Mesh(new import_three8.BoxGeometry(0.34, ridgeHeight + 0.8, 0.34), stone);
573
+ chimney.position.set(rng.pick([-1, 1]) * width * 0.22, wallHeight + ridgeHeight * 0.45, depth * 0.12);
574
+ group.add(chimney);
575
+ const door = new import_three8.Mesh(new import_three8.BoxGeometry(0.85, 1.5, 0.08), wood);
576
+ door.position.set(rng.range(-0.4, 0.4), 0.75, depth / 2 + 0.02);
577
+ group.add(door);
578
+ const glass = new import_three8.MeshStandardMaterial({
579
+ color: palette.lampGlow,
580
+ emissive: palette.lampGlow,
581
+ emissiveIntensity: 1
582
+ });
583
+ const windowGeometry = new import_three8.BoxGeometry(0.6, 0.62, 0.08);
584
+ const front = new import_three8.Mesh(windowGeometry, glass);
585
+ front.position.set(door.position.x < 0 ? width * 0.28 : -width * 0.28, 1.35, depth / 2 + 0.02);
586
+ group.add(front);
587
+ for (const side of [-1, 1]) {
588
+ if (rng.next() < 0.35) continue;
589
+ const pane = new import_three8.Mesh(windowGeometry, glass);
590
+ pane.position.set(side * (width / 2 + 0.02), 1.35, rng.range(-0.3, 0.3) * depth);
591
+ pane.rotation.y = Math.PI / 2;
592
+ group.add(pane);
593
+ }
594
+ return { object: group, obstacleRadius: Math.hypot(width + 0.5, depth + 0.5) / 2 };
595
+ }
596
+ function createTower(options = {}) {
597
+ const rng = new Rng(options.seed ?? 1);
598
+ const palette = options.palette ?? DEFAULT_PALETTE;
599
+ const height = options.height ?? rng.range(4.2, 5.2);
600
+ const group = new import_three8.Group();
601
+ group.name = "tower";
602
+ const wood = new import_three8.MeshStandardMaterial({ color: palette.wood, flatShading: true });
603
+ const woodDark = new import_three8.MeshStandardMaterial({ color: palette.woodDark, flatShading: true });
604
+ const roof = new import_three8.MeshStandardMaterial({ color: palette.roof, flatShading: true });
605
+ const spread = 1;
606
+ for (const x of [-1, 1]) {
607
+ for (const z of [-1, 1]) {
608
+ const leg = new import_three8.Mesh(new import_three8.CylinderGeometry(0.09, 0.12, height, 6), woodDark);
609
+ leg.position.set(x * spread * 0.75, height / 2 - 0.6, z * spread * 0.75);
610
+ leg.rotation.z = -x * 0.1;
611
+ leg.rotation.x = z * 0.1;
612
+ group.add(leg);
613
+ }
614
+ }
615
+ for (const level of [0.3, 0.62]) {
616
+ const brace = new import_three8.Mesh(new import_three8.BoxGeometry(spread * 2.1, 0.09, 0.07), wood);
617
+ brace.position.set(0, height * level, spread * 0.82 * (1 - level * 0.35));
618
+ brace.rotation.z = rng.pick([-0.5, 0.5]);
619
+ group.add(brace);
620
+ const braceSide = new import_three8.Mesh(new import_three8.BoxGeometry(0.07, 0.09, spread * 2.1), wood);
621
+ braceSide.position.set(spread * 0.82 * (1 - level * 0.35), height * level, 0);
622
+ braceSide.rotation.x = rng.pick([-0.5, 0.5]);
623
+ group.add(braceSide);
624
+ }
625
+ const platform = new import_three8.Mesh(new import_three8.BoxGeometry(spread * 2.2, 0.14, spread * 2.2), wood);
626
+ platform.position.y = height - 0.5;
627
+ group.add(platform);
628
+ for (const x of [-1, 1]) {
629
+ for (const z of [-1, 1]) {
630
+ const post = new import_three8.Mesh(new import_three8.BoxGeometry(0.08, 0.9, 0.08), woodDark);
631
+ post.position.set(x * spread * 1, height - 0.05, z * spread * 1);
632
+ group.add(post);
633
+ }
634
+ }
635
+ for (const [rx, rz, w, d] of [
636
+ [0, 1, spread * 2.1, 0.06],
637
+ [0, -1, spread * 2.1, 0.06],
638
+ [1, 0, 0.06, spread * 2.1],
639
+ [-1, 0, 0.06, spread * 2.1]
640
+ ]) {
641
+ const rail = new import_three8.Mesh(new import_three8.BoxGeometry(w, 0.07, d), wood);
642
+ rail.position.set(rx * spread, height + 0.28, rz * spread);
643
+ group.add(rail);
644
+ }
645
+ const cap = new import_three8.Mesh(new import_three8.CylinderGeometry(0, spread * 1.55, 1, 4), roof);
646
+ cap.position.y = height + 1.15;
647
+ cap.rotation.y = Math.PI / 4;
648
+ group.add(cap);
649
+ return { object: group, obstacleRadius: spread * 1.35 };
650
+ }
651
+ function createWell(options = {}) {
652
+ const rng = new Rng(options.seed ?? 1);
653
+ const palette = options.palette ?? DEFAULT_PALETTE;
654
+ const group = new import_three8.Group();
655
+ group.name = "well";
656
+ const stone = new import_three8.MeshStandardMaterial({ color: rng.pick(palette.rock), flatShading: true });
657
+ const wood = new import_three8.MeshStandardMaterial({ color: palette.woodDark, flatShading: true });
658
+ const roof = new import_three8.MeshStandardMaterial({ color: palette.roof, flatShading: true });
659
+ const ring = new import_three8.Mesh(new import_three8.CylinderGeometry(0.85, 0.95, 0.75, 10), stone);
660
+ ring.position.y = 0.375;
661
+ group.add(ring);
662
+ const inner = new import_three8.Mesh(
663
+ new import_three8.CylinderGeometry(0.62, 0.62, 0.05, 10),
664
+ new import_three8.MeshStandardMaterial({ color: 1450542 })
665
+ );
666
+ inner.position.y = 0.76;
667
+ group.add(inner);
668
+ for (const side of [-1, 1]) {
669
+ const post = new import_three8.Mesh(new import_three8.CylinderGeometry(0.06, 0.075, 1.9, 5), wood);
670
+ post.position.set(side * 0.72, 0.95, 0);
671
+ group.add(post);
672
+ }
673
+ const bar = new import_three8.Mesh(new import_three8.CylinderGeometry(0.045, 0.045, 1.5, 5), wood);
674
+ bar.rotation.z = Math.PI / 2;
675
+ bar.position.y = 1.72;
676
+ group.add(bar);
677
+ const cap = new import_three8.Mesh(prismGeometry(2, 0.55, 0.65), roof);
678
+ cap.position.y = 1.86;
679
+ cap.rotation.y = Math.PI / 2;
680
+ group.add(cap);
681
+ const rope = new import_three8.Mesh(new import_three8.CylinderGeometry(0.012, 0.012, 0.6, 4), wood);
682
+ rope.position.y = 1.42;
683
+ group.add(rope);
684
+ const bucket = new import_three8.Mesh(new import_three8.CylinderGeometry(0.13, 0.1, 0.2, 7), wood);
685
+ bucket.position.y = 1.05;
686
+ group.add(bucket);
687
+ return { object: group, obstacleRadius: 1 };
688
+ }
689
+ function createRuin(options = {}) {
690
+ const rng = new Rng(options.seed ?? 1);
691
+ const palette = options.palette ?? DEFAULT_PALETTE;
692
+ const size = options.size ?? rng.range(3.5, 5);
693
+ const depth = size * rng.range(0.7, 0.9);
694
+ const group = new import_three8.Group();
695
+ group.name = "ruin";
696
+ const stone = new import_three8.MeshStandardMaterial({ color: rng.pick(palette.rock), flatShading: true });
697
+ stone.color.offsetHSL(0, 0, rng.range(-0.04, 0.02));
698
+ const thickness = 0.35;
699
+ const runs = [
700
+ [0, -depth / 2, size, true],
701
+ [-size / 2, 0, depth, false],
702
+ [size / 2, 0, depth, false],
703
+ [size * 0.3, depth / 2, size * 0.35, true]
704
+ ];
705
+ for (const [cx, cz, length, alongX] of runs) {
706
+ const segments = Math.max(2, Math.round(length / 1.1));
707
+ const step = length / segments;
708
+ for (let i = 0; i < segments; i++) {
709
+ if (rng.next() < 0.22) continue;
710
+ const height = rng.range(0.5, 2.2);
711
+ const wall = new import_three8.Mesh(
712
+ new import_three8.BoxGeometry(alongX ? step * 0.96 : thickness, height, alongX ? thickness : step * 0.96),
713
+ stone
714
+ );
715
+ const offset = -length / 2 + step * (i + 0.5);
716
+ wall.position.set(alongX ? cx + offset : cx, height / 2, alongX ? cz : cz + offset);
717
+ wall.rotation.y = rng.range(-0.03, 0.03);
718
+ group.add(wall);
719
+ }
720
+ }
721
+ const blocks = rng.int(4, 7);
722
+ for (let i = 0; i < blocks; i++) {
723
+ const s = rng.range(0.25, 0.55);
724
+ const block = new import_three8.Mesh(new import_three8.BoxGeometry(s, s * rng.range(0.6, 1), s * rng.range(0.7, 1.2)), stone);
725
+ block.position.set(rng.range(-size * 0.6, size * 0.6), s * 0.3, rng.range(-depth * 0.6, depth * 0.7));
726
+ block.rotation.set(rng.range(-0.3, 0.3), rng.range(0, Math.PI), rng.range(-0.3, 0.3));
727
+ group.add(block);
728
+ }
729
+ return { object: group, obstacleRadius: Math.hypot(size, depth) / 2 };
730
+ }
731
+
732
+ // src/environment/terrain.ts
733
+ var import_three9 = require("three");
373
734
  function createTerrain(options = {}) {
374
735
  const seed = options.seed ?? 1;
375
736
  const size = options.size ?? 80;
@@ -384,7 +745,7 @@ function createTerrain(options = {}) {
384
745
  const shaped = Math.pow(n, 1 + flatness * 2);
385
746
  return shaped * amplitude;
386
747
  };
387
- const geometry = new import_three7.PlaneGeometry(size, size, resolution - 1, resolution - 1);
748
+ const geometry = new import_three9.PlaneGeometry(size, size, resolution - 1, resolution - 1);
388
749
  geometry.rotateX(-Math.PI / 2);
389
750
  const positions = geometry.getAttribute("position");
390
751
  for (let i = 0; i < positions.count; i++) {
@@ -393,40 +754,46 @@ function createTerrain(options = {}) {
393
754
  geometry.computeVertexNormals();
394
755
  const normals = geometry.getAttribute("normal");
395
756
  const colors = new Float32Array(positions.count * 3);
396
- const grassLow = new import_three7.Color(palette.grassLow);
397
- const grassHigh = new import_three7.Color(palette.grassHigh);
398
- const cliff = new import_three7.Color(palette.cliff);
399
- const peak = new import_three7.Color(palette.peak);
400
- const scratch = new import_three7.Color();
757
+ const grassLow = new import_three9.Color(palette.grassLow);
758
+ const grassHigh = new import_three9.Color(palette.grassHigh);
759
+ const cliff = new import_three9.Color(palette.cliff);
760
+ const peak = new import_three9.Color(palette.peak);
761
+ const sand = new import_three9.Color(palette.sand);
762
+ const waterLevel = options.waterLevel;
763
+ const scratch = new import_three9.Color();
401
764
  for (let i = 0; i < positions.count; i++) {
402
- const h = positions.getY(i) / amplitude;
765
+ const y = positions.getY(i);
766
+ const h = y / amplitude;
403
767
  const slope = 1 - normals.getY(i);
404
768
  scratch.copy(grassLow).lerp(grassHigh, Math.min(1, h * 1.6));
405
769
  if (h > 0.75) scratch.lerp(peak, (h - 0.75) * 4);
406
770
  if (slope > 0.15) scratch.lerp(cliff, Math.min(1, (slope - 0.15) * 4));
771
+ if (waterLevel !== void 0 && y < waterLevel + 0.5) {
772
+ scratch.lerp(sand, Math.min(1, (waterLevel + 0.5 - y) * 1.6));
773
+ }
407
774
  colors[i * 3] = scratch.r;
408
775
  colors[i * 3 + 1] = scratch.g;
409
776
  colors[i * 3 + 2] = scratch.b;
410
777
  }
411
- geometry.setAttribute("color", new import_three7.BufferAttribute(colors, 3));
412
- const mesh = new import_three7.Mesh(
778
+ geometry.setAttribute("color", new import_three9.BufferAttribute(colors, 3));
779
+ const mesh = new import_three9.Mesh(
413
780
  geometry,
414
- new import_three7.MeshStandardMaterial({ vertexColors: true, flatShading: true })
781
+ new import_three9.MeshStandardMaterial({ vertexColors: true, flatShading: true })
415
782
  );
416
783
  mesh.name = "terrain";
417
784
  return { mesh, heightAt, size, seed };
418
785
  }
419
786
 
420
787
  // src/environment/sky.ts
421
- var import_three8 = require("three");
788
+ var import_three10 = require("three");
422
789
  function createSky(options = {}) {
423
790
  const palette = options.palette ?? DEFAULT_PALETTE;
424
- const material = new import_three8.ShaderMaterial({
425
- side: import_three8.BackSide,
791
+ const material = new import_three10.ShaderMaterial({
792
+ side: import_three10.BackSide,
426
793
  depthWrite: false,
427
794
  uniforms: {
428
- topColor: { value: new import_three8.Color(options.topColor ?? palette.skyTop) },
429
- bottomColor: { value: new import_three8.Color(options.bottomColor ?? palette.skyBottom) }
795
+ topColor: { value: new import_three10.Color(options.topColor ?? palette.skyTop) },
796
+ bottomColor: { value: new import_three10.Color(options.bottomColor ?? palette.skyBottom) }
430
797
  },
431
798
  vertexShader: (
432
799
  /* glsl */
@@ -449,7 +816,7 @@ function createSky(options = {}) {
449
816
  }`
450
817
  )
451
818
  });
452
- const mesh = new import_three8.Mesh(new import_three8.SphereGeometry(options.radius ?? 400, 16, 12), material);
819
+ const mesh = new import_three10.Mesh(new import_three10.SphereGeometry(options.radius ?? 400, 16, 12), material);
453
820
  mesh.name = "sky";
454
821
  return {
455
822
  mesh,
@@ -461,7 +828,7 @@ function createSky(options = {}) {
461
828
  }
462
829
 
463
830
  // src/environment/lighting.ts
464
- var import_three9 = require("three");
831
+ var import_three11 = require("three");
465
832
  var PRESETS = {
466
833
  day: {
467
834
  sun: 16774368,
@@ -502,12 +869,12 @@ var PRESETS = {
502
869
  };
503
870
  function createLightingRig(preset = "day") {
504
871
  const config = PRESETS[preset];
505
- const group = new import_three9.Group();
872
+ const group = new import_three11.Group();
506
873
  group.name = `lighting-${preset}`;
507
- const sun = new import_three9.DirectionalLight(config.sun, config.sunIntensity);
874
+ const sun = new import_three11.DirectionalLight(config.sun, config.sunIntensity);
508
875
  sun.position.set(...config.sunPos);
509
- const ambient = new import_three9.AmbientLight(config.ambient, config.ambientIntensity);
510
- const hemisphere = new import_three9.HemisphereLight(config.skyTint, config.groundTint, 0.5);
876
+ const ambient = new import_three11.AmbientLight(config.ambient, config.ambientIntensity);
877
+ const hemisphere = new import_three11.HemisphereLight(config.skyTint, config.groundTint, 0.5);
511
878
  group.add(sun, ambient, hemisphere);
512
879
  return { group, sun, ambient, hemisphere };
513
880
  }
@@ -522,11 +889,381 @@ function applyFog(scene, preset, palette = DEFAULT_PALETTE) {
522
889
  return;
523
890
  }
524
891
  const { near, far } = FOG[preset];
525
- scene.fog = new import_three9.Fog(palette.fog, near, far);
892
+ scene.fog = new import_three11.Fog(palette.fog, near, far);
893
+ }
894
+
895
+ // src/environment/water.ts
896
+ var import_three12 = require("three");
897
+ function createWater(options = {}) {
898
+ const level = options.level ?? 0.8;
899
+ const size = options.size ?? 200;
900
+ const resolution = options.resolution ?? 40;
901
+ const amplitude = options.amplitude ?? 0.06;
902
+ const speed = options.speed ?? 1;
903
+ const palette = options.palette ?? DEFAULT_PALETTE;
904
+ const geometry = new import_three12.PlaneGeometry(size, size, resolution, resolution);
905
+ geometry.rotateX(-Math.PI / 2);
906
+ const positions = geometry.getAttribute("position");
907
+ const mesh = new import_three12.Mesh(
908
+ geometry,
909
+ new import_three12.MeshStandardMaterial({
910
+ color: palette.water,
911
+ transparent: true,
912
+ opacity: 0.85,
913
+ flatShading: true,
914
+ metalness: 0.35,
915
+ roughness: 0.4
916
+ })
917
+ );
918
+ mesh.name = "water";
919
+ mesh.position.y = level;
920
+ let time = 0;
921
+ const update = (dt) => {
922
+ time += dt * speed;
923
+ for (let i = 0; i < positions.count; i++) {
924
+ const x = positions.getX(i);
925
+ const z = positions.getZ(i);
926
+ positions.setY(
927
+ i,
928
+ Math.sin(x * 0.35 + time) * Math.cos(z * 0.3 + time * 0.8) * amplitude
929
+ );
930
+ }
931
+ positions.needsUpdate = true;
932
+ geometry.computeVertexNormals();
933
+ };
934
+ update(0);
935
+ return {
936
+ mesh,
937
+ level,
938
+ update,
939
+ isUnderwater: (groundHeight) => groundHeight < level
940
+ };
941
+ }
942
+ function aboveWater(terrain, water, margin = 0.25) {
943
+ return (x, z) => terrain.heightAt(x, z) > water.level + margin;
944
+ }
945
+
946
+ // src/environment/dayCycle.ts
947
+ var import_three13 = require("three");
948
+ function createDayCycle(options = {}) {
949
+ const palette = options.palette ?? DEFAULT_PALETTE;
950
+ const dayLength = options.dayLength ?? 60;
951
+ const frames = [
952
+ { t: 0, skyTop: 725030, skyBottom: 1712960, sun: 9348824, sunIntensity: 0.03, ambientIntensity: 0.06, fog: 1317422 },
953
+ { t: 0.23, skyTop: 2569052, skyBottom: 6968432, sun: 13605490, sunIntensity: 0.3, ambientIntensity: 0.12, fog: 4866648 },
954
+ { t: 0.3, skyTop: 4877216, skyBottom: 15247738, sun: 16758881, sunIntensity: 1, ambientIntensity: 0.26, fog: 11569778 },
955
+ { t: 0.5, skyTop: palette.skyTop, skyBottom: palette.skyBottom, sun: 16774368, sunIntensity: 1.9, ambientIntensity: 0.45, fog: palette.fog },
956
+ { t: 0.7, skyTop: 4872852, skyBottom: 14718302, sun: 16752720, sunIntensity: 1, ambientIntensity: 0.26, fog: 10517096 },
957
+ { t: 0.78, skyTop: 2304594, skyBottom: 9065824, sun: 14191194, sunIntensity: 0.25, ambientIntensity: 0.11, fog: 4603476 },
958
+ { t: 1, skyTop: 725030, skyBottom: 1712960, sun: 9348824, sunIntensity: 0.03, ambientIntensity: 0.06, fog: 1317422 }
959
+ ];
960
+ const lampLights = [];
961
+ const lampBulbs = [];
962
+ for (const entry of options.lamps ?? []) {
963
+ const root = entry.isObject3D ? entry : entry.object;
964
+ root.traverse((child) => {
965
+ if (child instanceof import_three13.PointLight) {
966
+ lampLights.push({ light: child, base: child.intensity || 6 });
967
+ }
968
+ const material = child.material;
969
+ if (material?.emissive && material.emissiveIntensity > 0.5) {
970
+ lampBulbs.push({ material, base: material.emissiveIntensity });
971
+ }
972
+ });
973
+ }
974
+ const colorA = new import_three13.Color();
975
+ const colorB = new import_three13.Color();
976
+ let timeOfDay = options.timeOfDay ?? 0.5;
977
+ let sunElevation = 0;
978
+ const sample = (t) => {
979
+ let a = frames[0];
980
+ let b = frames[frames.length - 1];
981
+ for (let i = 0; i < frames.length - 1; i++) {
982
+ if (t >= frames[i].t && t <= frames[i + 1].t) {
983
+ a = frames[i];
984
+ b = frames[i + 1];
985
+ break;
986
+ }
987
+ }
988
+ const f = b.t === a.t ? 0 : (t - a.t) / (b.t - a.t);
989
+ const lerpHex = (ha, hb) => colorA.setHex(ha).lerp(colorB.setHex(hb), f).getHex();
990
+ return {
991
+ t,
992
+ skyTop: lerpHex(a.skyTop, b.skyTop),
993
+ skyBottom: lerpHex(a.skyBottom, b.skyBottom),
994
+ sun: lerpHex(a.sun, b.sun),
995
+ sunIntensity: a.sunIntensity + (b.sunIntensity - a.sunIntensity) * f,
996
+ ambientIntensity: a.ambientIntensity + (b.ambientIntensity - a.ambientIntensity) * f,
997
+ fog: lerpHex(a.fog, b.fog)
998
+ };
999
+ };
1000
+ const apply = () => {
1001
+ const t = timeOfDay;
1002
+ const frame = sample(t);
1003
+ const sunAngle = (t - 0.25) * Math.PI * 2;
1004
+ sunElevation = Math.sin(sunAngle);
1005
+ options.sky?.setColors(frame.skyTop, frame.skyBottom);
1006
+ if (options.rig) {
1007
+ const { sun, ambient, hemisphere } = options.rig;
1008
+ sun.color.setHex(frame.sun);
1009
+ sun.intensity = frame.sunIntensity;
1010
+ sun.position.set(Math.cos(sunAngle) * 40, Math.max(sunElevation, -0.2) * 45 + 6, 16);
1011
+ ambient.intensity = frame.ambientIntensity;
1012
+ hemisphere.intensity = frame.ambientIntensity * 1.4;
1013
+ }
1014
+ if (options.scene?.fog && "color" in options.scene.fog) {
1015
+ options.scene.fog.color.setHex(frame.fog);
1016
+ }
1017
+ const night = Math.min(1, Math.max(0, (0.06 - sunElevation) / 0.16));
1018
+ for (const { light, base } of lampLights) light.intensity = base * night;
1019
+ for (const { material, base } of lampBulbs) {
1020
+ material.emissiveIntensity = 0.15 * base + 0.85 * base * night;
1021
+ }
1022
+ };
1023
+ apply();
1024
+ return {
1025
+ get timeOfDay() {
1026
+ return timeOfDay;
1027
+ },
1028
+ set timeOfDay(t) {
1029
+ timeOfDay = (t % 1 + 1) % 1;
1030
+ apply();
1031
+ },
1032
+ get sunElevation() {
1033
+ return sunElevation;
1034
+ },
1035
+ get isNight() {
1036
+ return sunElevation < 0;
1037
+ },
1038
+ update(dt) {
1039
+ timeOfDay = (timeOfDay + dt / dayLength) % 1;
1040
+ apply();
1041
+ },
1042
+ set(t) {
1043
+ timeOfDay = (t % 1 + 1) % 1;
1044
+ apply();
1045
+ }
1046
+ };
1047
+ }
1048
+
1049
+ // src/environment/wind.ts
1050
+ var import_three14 = require("three");
1051
+ function applyWind(target, options = {}) {
1052
+ const strength = options.strength ?? 0.06;
1053
+ const frequency = options.frequency ?? 1.2;
1054
+ const anchor = options.anchorHeight ?? 0.6;
1055
+ const time = { value: 0 };
1056
+ const patched = /* @__PURE__ */ new Set();
1057
+ target.traverse((child) => {
1058
+ if (!(child instanceof import_three14.Mesh)) return;
1059
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
1060
+ for (const material of materials) {
1061
+ if (patched.has(material)) continue;
1062
+ patched.add(material);
1063
+ material.onBeforeCompile = (shader) => {
1064
+ shader.uniforms.uWindTime = time;
1065
+ shader.vertexShader = shader.vertexShader.replace(
1066
+ "#include <common>",
1067
+ `#include <common>
1068
+ uniform float uWindTime;`
1069
+ ).replace(
1070
+ "#include <begin_vertex>",
1071
+ `#include <begin_vertex>
1072
+ {
1073
+ float sway = max(position.y - ${anchor.toFixed(3)}, 0.0);
1074
+ float phase = 0.0;
1075
+ #ifdef USE_INSTANCING
1076
+ phase = instanceMatrix[3].x * 0.7 + instanceMatrix[3].z * 1.3;
1077
+ #endif
1078
+ float w = sin(uWindTime * ${(frequency * 2).toFixed(3)} + phase + position.y * 0.8);
1079
+ transformed.x += w * sway * ${strength.toFixed(4)} * 4.0;
1080
+ transformed.z += cos(uWindTime * ${(frequency * 1.4).toFixed(3)} + phase) * sway * ${strength.toFixed(4)} * 2.0;
1081
+ }`
1082
+ );
1083
+ };
1084
+ material.needsUpdate = true;
1085
+ }
1086
+ });
1087
+ return {
1088
+ update(dt) {
1089
+ time.value += dt;
1090
+ },
1091
+ materials: [...patched]
1092
+ };
1093
+ }
1094
+
1095
+ // src/environment/path.ts
1096
+ var import_three15 = require("three");
1097
+ function createPath(points, options = {}) {
1098
+ const width = options.width ?? 1.8;
1099
+ const surface = options.surface ?? 0;
1100
+ const heightAt = typeof surface === "number" ? () => surface : (x, z) => surface(x, z);
1101
+ const loop = options.loop ?? false;
1102
+ const palette = options.palette ?? DEFAULT_PALETTE;
1103
+ const controls = points.map((p) => new import_three15.Vector3(p.x, 0, "z" in p ? p.z : 0));
1104
+ const curve = new import_three15.CatmullRomCurve3(controls, loop, "centripetal");
1105
+ const length = curve.getLength();
1106
+ const samples = Math.max(8, Math.ceil(length * (options.samplesPerUnit ?? 1)));
1107
+ const route = [];
1108
+ for (let i = 0; i <= samples; i++) {
1109
+ if (loop && i === samples) break;
1110
+ const p = curve.getPoint(i / samples);
1111
+ route.push(new import_three15.Vector3(p.x, heightAt(p.x, p.z), p.z));
1112
+ }
1113
+ const edgeCount = loop ? route.length + 1 : route.length;
1114
+ const positions = new Float32Array(edgeCount * 2 * 3);
1115
+ const direction = new import_three15.Vector3();
1116
+ const perp = new import_three15.Vector3();
1117
+ for (let i = 0; i < edgeCount; i++) {
1118
+ const current = route[i % route.length];
1119
+ const previous = route[(i - 1 + route.length) % route.length];
1120
+ const next = route[(i + 1) % route.length];
1121
+ if (!loop && i === 0) direction.subVectors(next, current);
1122
+ else if (!loop && i === edgeCount - 1) direction.subVectors(current, previous);
1123
+ else direction.subVectors(next, previous);
1124
+ perp.set(-direction.z, 0, direction.x).normalize().multiplyScalar(width / 2);
1125
+ const left = i * 6;
1126
+ positions[left] = current.x - perp.x;
1127
+ positions[left + 1] = heightAt(current.x - perp.x, current.z - perp.z) + 0.05;
1128
+ positions[left + 2] = current.z - perp.z;
1129
+ positions[left + 3] = current.x + perp.x;
1130
+ positions[left + 4] = heightAt(current.x + perp.x, current.z + perp.z) + 0.05;
1131
+ positions[left + 5] = current.z + perp.z;
1132
+ }
1133
+ const indices = [];
1134
+ for (let i = 0; i < edgeCount - 1; i++) {
1135
+ const a = i * 2;
1136
+ indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);
1137
+ }
1138
+ const geometry = new import_three15.BufferGeometry();
1139
+ geometry.setAttribute("position", new import_three15.BufferAttribute(positions, 3));
1140
+ geometry.setIndex(indices);
1141
+ geometry.computeVertexNormals();
1142
+ const mesh = new import_three15.Mesh(
1143
+ geometry,
1144
+ new import_three15.MeshStandardMaterial({
1145
+ color: palette.path,
1146
+ flatShading: true,
1147
+ polygonOffset: true,
1148
+ polygonOffsetFactor: -1
1149
+ })
1150
+ );
1151
+ mesh.name = "path";
1152
+ const keepOutMargin = options.keepOutMargin ?? 0.6;
1153
+ const keepOut = route.filter((_p, i) => i % 2 === 0 || i === route.length - 1).map((p) => ({ center: { x: p.x, z: p.z }, radius: width / 2 + keepOutMargin }));
1154
+ const half = width / 2;
1155
+ const contains = (x, z) => {
1156
+ const count = loop ? route.length : route.length - 1;
1157
+ for (let i = 0; i < count; i++) {
1158
+ const a = route[i];
1159
+ const b = route[(i + 1) % route.length];
1160
+ const abx = b.x - a.x;
1161
+ const abz = b.z - a.z;
1162
+ const lengthSq = abx * abx + abz * abz || 1e-9;
1163
+ const t = Math.max(0, Math.min(1, ((x - a.x) * abx + (z - a.z) * abz) / lengthSq));
1164
+ const dx = x - (a.x + abx * t);
1165
+ const dz = z - (a.z + abz * t);
1166
+ if (dx * dx + dz * dz <= half * half) return true;
1167
+ }
1168
+ return false;
1169
+ };
1170
+ return { mesh, route, keepOut, contains, loop };
1171
+ }
1172
+
1173
+ // src/generators/village.ts
1174
+ var import_three16 = require("three");
1175
+ function createVillage(options = {}) {
1176
+ const rng = new Rng(options.seed ?? 1);
1177
+ const palette = options.palette ?? DEFAULT_PALETTE;
1178
+ const center = options.center ?? { x: 0, z: 0 };
1179
+ const radius = options.radius ?? 12;
1180
+ const houseCount = options.houses ?? 5;
1181
+ const surface = options.surface ?? 0;
1182
+ const heightAt = typeof surface === "number" ? () => surface : (x, z) => surface(x, z);
1183
+ const group = new import_three16.Group();
1184
+ group.name = "village";
1185
+ const props = [];
1186
+ const lamps = [];
1187
+ const place = (prop, x, z, rotY) => {
1188
+ prop.object.position.set(x, heightAt(x, z), z);
1189
+ prop.object.rotation.y = rotY;
1190
+ group.add(prop.object);
1191
+ props.push(prop);
1192
+ return prop;
1193
+ };
1194
+ const findSpot = (angle, distance, footprint) => {
1195
+ let x = center.x;
1196
+ let z = center.z;
1197
+ for (let attempt = 0; attempt < 10; attempt++) {
1198
+ const a = angle + rng.range(-0.25, 0.25) * (attempt > 0 ? 1 : 0.3);
1199
+ const d = distance * rng.range(attempt > 0 ? 0.8 : 0.95, 1.1);
1200
+ x = center.x + Math.cos(a) * d;
1201
+ z = center.z + Math.sin(a) * d;
1202
+ const y = heightAt(x, z);
1203
+ if (options.mask && !options.mask(x, z, y)) continue;
1204
+ let min = y;
1205
+ let max = y;
1206
+ for (const [dx, dz] of [[footprint, 0], [-footprint, 0], [0, footprint], [0, -footprint]]) {
1207
+ const h = heightAt(x + dx, z + dz);
1208
+ min = Math.min(min, h);
1209
+ max = Math.max(max, h);
1210
+ }
1211
+ if (max - min <= Math.max(0.9, footprint * 0.35)) break;
1212
+ }
1213
+ return { x, z };
1214
+ };
1215
+ const well = createWell({ seed: rng.int(1, 1e9), palette });
1216
+ place(well, center.x, center.z, rng.range(0, Math.PI * 2));
1217
+ let lightBudget = options.lampLights ?? 3;
1218
+ for (let i = 0; i < houseCount; i++) {
1219
+ const angle = i / houseCount * Math.PI * 2 + rng.range(-0.15, 0.15);
1220
+ const house = createHouse({ seed: rng.int(1, 1e9), palette });
1221
+ const spot = findSpot(angle, radius * rng.range(0.7, 0.95), house.obstacleRadius);
1222
+ const facing = Math.atan2(center.x - spot.x, center.z - spot.z);
1223
+ place(house, spot.x, spot.z, facing);
1224
+ lamps.push(house);
1225
+ if (rng.next() < 0.7) {
1226
+ const crateAngle = angle + rng.range(-0.5, 0.5);
1227
+ const crateDistance = Math.hypot(spot.x - center.x, spot.z - center.z) - house.obstacleRadius - 0.7;
1228
+ place(
1229
+ createCrate({ seed: rng.int(1, 1e9), size: rng.range(0.7, 1), palette }),
1230
+ center.x + Math.cos(crateAngle) * crateDistance,
1231
+ center.z + Math.sin(crateAngle) * crateDistance,
1232
+ rng.range(0, Math.PI)
1233
+ );
1234
+ }
1235
+ if (i % 2 === 0) {
1236
+ const lampDistance = Math.hypot(spot.x - center.x, spot.z - center.z) * 0.55;
1237
+ const lamp = createLamp({ seed: rng.int(1, 1e9), light: lightBudget > 0, palette });
1238
+ lightBudget--;
1239
+ place(lamp, center.x + Math.cos(angle) * lampDistance, center.z + Math.sin(angle) * lampDistance, 0);
1240
+ lamps.push(lamp);
1241
+ }
1242
+ }
1243
+ if (options.tower ?? true) {
1244
+ const angle = rng.range(0, Math.PI * 2);
1245
+ const tower = createTower({ seed: rng.int(1, 1e9), palette });
1246
+ const spot = findSpot(angle, radius * 1.05, tower.obstacleRadius);
1247
+ place(tower, spot.x, spot.z, rng.range(0, Math.PI * 2));
1248
+ }
1249
+ if (options.ruin ?? true) {
1250
+ const angle = rng.range(0, Math.PI * 2);
1251
+ const ruin = createRuin({ seed: rng.int(1, 1e9), palette });
1252
+ const spot = findSpot(angle, radius * 1.35, ruin.obstacleRadius);
1253
+ place(ruin, spot.x, spot.z, rng.range(0, Math.PI * 2));
1254
+ }
1255
+ return {
1256
+ group,
1257
+ props,
1258
+ obstacles: collectObstacles(props),
1259
+ lamps,
1260
+ keepOut: [{ center: { ...center }, radius: radius * 1.5 }],
1261
+ center
1262
+ };
526
1263
  }
527
1264
 
528
1265
  // src/scatter/scatter.ts
529
- var import_three10 = require("three");
1266
+ var import_three17 = require("three");
530
1267
  function scatter(options) {
531
1268
  const seed = options.seed ?? 1;
532
1269
  const rng = new Rng(seed);
@@ -581,7 +1318,7 @@ function scatter(options) {
581
1318
  const itemIndex = pickItem();
582
1319
  const [minScale, maxScale2] = options.items[itemIndex].scale ?? [0.8, 1.25];
583
1320
  placements.push({
584
- position: new import_three10.Vector3(x, y, z),
1321
+ position: new import_three17.Vector3(x, y, z),
585
1322
  rotationY: rng.range(0, Math.PI * 2),
586
1323
  scale: rng.range(minScale, maxScale2),
587
1324
  itemIndex
@@ -591,14 +1328,14 @@ function scatter(options) {
591
1328
  if (!bucket) occupied.set(key, bucket = []);
592
1329
  bucket.push({ x, z });
593
1330
  }
594
- const group = new import_three10.Group();
1331
+ const group = new import_three17.Group();
595
1332
  group.name = "scatter";
596
1333
  const obstacles = [];
597
- const placementMatrix = new import_three10.Matrix4();
598
- const composed = new import_three10.Matrix4();
599
- const quaternion = new import_three10.Quaternion();
600
- const up = new import_three10.Vector3(0, 1, 0);
601
- const scaleVector = new import_three10.Vector3();
1334
+ const placementMatrix = new import_three17.Matrix4();
1335
+ const composed = new import_three17.Matrix4();
1336
+ const quaternion = new import_three17.Quaternion();
1337
+ const up = new import_three17.Vector3(0, 1, 0);
1338
+ const scaleVector = new import_three17.Vector3();
602
1339
  options.items.forEach((item, itemIndex) => {
603
1340
  const variantCount = item.variants ?? 4;
604
1341
  const variants = [];
@@ -614,9 +1351,9 @@ function scatter(options) {
614
1351
  if (list.length === 0) return;
615
1352
  variant.object.updateMatrixWorld(true);
616
1353
  variant.object.traverse((child) => {
617
- if (!(child instanceof import_three10.Mesh)) return;
618
- const instanced = new import_three10.InstancedMesh(child.geometry, child.material, list.length);
619
- instanced.instanceMatrix.setUsage(import_three10.DynamicDrawUsage);
1354
+ if (!(child instanceof import_three17.Mesh)) return;
1355
+ const instanced = new import_three17.InstancedMesh(child.geometry, child.material, list.length);
1356
+ instanced.instanceMatrix.setUsage(import_three17.DynamicDrawUsage);
620
1357
  list.forEach((placement, index) => {
621
1358
  quaternion.setFromAxisAngle(up, placement.rotationY);
622
1359
  scaleVector.setScalar(placement.scale);
@@ -644,16 +1381,28 @@ function scatter(options) {
644
1381
  DEFAULT_PALETTE,
645
1382
  PALETTES,
646
1383
  Rng,
1384
+ aboveWater,
647
1385
  applyFog,
1386
+ applyWind,
648
1387
  collectObstacles,
1388
+ createBush,
649
1389
  createCrate,
1390
+ createDayCycle,
650
1391
  createFence,
1392
+ createGrassTuft,
1393
+ createHouse,
651
1394
  createLamp,
652
1395
  createLightingRig,
1396
+ createPath,
653
1397
  createRock,
1398
+ createRuin,
654
1399
  createSky,
655
1400
  createTerrain,
1401
+ createTower,
656
1402
  createTree,
1403
+ createVillage,
1404
+ createWater,
1405
+ createWell,
657
1406
  fractalNoise2,
658
1407
  hash2,
659
1408
  scatter,