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.js CHANGED
@@ -79,7 +79,12 @@ var PALETTES = {
79
79
  peak: 15265007,
80
80
  skyTop: 4026552,
81
81
  skyBottom: 12573160,
82
- fog: 12111837
82
+ fog: 12111837,
83
+ water: 4161454,
84
+ sand: 13220234,
85
+ path: 10125663,
86
+ wall: 14273712,
87
+ roof: 11032126
83
88
  },
84
89
  autumn: {
85
90
  foliage: [13202735, 14257722, 11754538, 14722373],
@@ -95,7 +100,12 @@ var PALETTES = {
95
100
  peak: 14933716,
96
101
  skyTop: 9333928,
97
102
  skyBottom: 15255976,
98
- fog: 14270888
103
+ fog: 14270888,
104
+ water: 4881042,
105
+ sand: 13348995,
106
+ path: 9270356,
107
+ wall: 13416596,
108
+ roof: 9062960
99
109
  },
100
110
  dusk: {
101
111
  foliage: [2055750, 2385999, 1724992, 2978904],
@@ -111,7 +121,33 @@ var PALETTES = {
111
121
  peak: 12105945,
112
122
  skyTop: 1909061,
113
123
  skyBottom: 13199946,
114
- fog: 6969978
124
+ fog: 6969978,
125
+ water: 2968168,
126
+ sand: 9075306,
127
+ path: 6969938,
128
+ wall: 9274009,
129
+ roof: 4535640
130
+ },
131
+ winter: {
132
+ foliage: [3038280, 3696986, 5405288, 8889750],
133
+ trunk: 4864563,
134
+ rock: [10134701, 8687256, 11581632],
135
+ wood: 7823433,
136
+ woodDark: 5522746,
137
+ metal: 3752013,
138
+ lampGlow: 16767113,
139
+ grassLow: 13621726,
140
+ grassHigh: 15002606,
141
+ cliff: 7764349,
142
+ peak: 16054266,
143
+ skyTop: 5929894,
144
+ skyBottom: 14214380,
145
+ fog: 13424864,
146
+ water: 4878470,
147
+ sand: 12108486,
148
+ path: 9143160,
149
+ wall: 13814203,
150
+ roof: 7030328
115
151
  }
116
152
  };
117
153
  var DEFAULT_PALETTE = PALETTES.meadow;
@@ -346,12 +382,333 @@ function createLamp(options = {}) {
346
382
  return { object: group, obstacleRadius: 0.25 };
347
383
  }
348
384
 
349
- // src/environment/terrain.ts
385
+ // src/props/grass.ts
386
+ import { ConeGeometry as ConeGeometry2, Group as Group6, IcosahedronGeometry as IcosahedronGeometry3, Mesh as Mesh6, MeshStandardMaterial as MeshStandardMaterial6 } from "three";
387
+ function createGrassTuft(options = {}) {
388
+ const rng = new Rng(options.seed ?? 1);
389
+ const palette = options.palette ?? DEFAULT_PALETTE;
390
+ const blades = options.blades ?? rng.int(4, 6);
391
+ const group = new Group6();
392
+ group.name = "grass";
393
+ const material = new MeshStandardMaterial6({
394
+ color: rng.next() < 0.5 ? palette.grassHigh : rng.pick(palette.foliage),
395
+ flatShading: true
396
+ });
397
+ for (let i = 0; i < blades; i++) {
398
+ const height = rng.range(0.25, 0.5);
399
+ const blade = new Mesh6(new ConeGeometry2(0.035, height, 3), material);
400
+ blade.position.set(rng.jitter(0, 0.12), height / 2, rng.jitter(0, 0.12));
401
+ blade.rotation.set(rng.range(-0.25, 0.25), rng.range(0, Math.PI), rng.range(-0.25, 0.25));
402
+ group.add(blade);
403
+ }
404
+ return { object: group, obstacleRadius: 0 };
405
+ }
406
+ function createBush(options = {}) {
407
+ const rng = new Rng(options.seed ?? 1);
408
+ const palette = options.palette ?? DEFAULT_PALETTE;
409
+ const size = options.size ?? rng.range(0.4, 0.7);
410
+ const group = new Group6();
411
+ group.name = "bush";
412
+ const material = new MeshStandardMaterial6({ color: rng.pick(palette.foliage), flatShading: true });
413
+ const blobs = rng.int(2, 3);
414
+ for (let i = 0; i < blobs; i++) {
415
+ const radius = size * rng.range(0.55, 0.85);
416
+ const blob = new Mesh6(new IcosahedronGeometry3(radius, 0), material);
417
+ blob.position.set(rng.jitter(0, size * 0.4), radius * 0.7, rng.jitter(0, size * 0.4));
418
+ blob.scale.y = rng.range(0.7, 0.85);
419
+ blob.rotation.y = rng.range(0, Math.PI);
420
+ group.add(blob);
421
+ }
422
+ return { object: group, obstacleRadius: size * 0.6 };
423
+ }
424
+
425
+ // src/props/building.ts
350
426
  import {
427
+ BoxGeometry as BoxGeometry4,
351
428
  BufferAttribute as BufferAttribute2,
429
+ BufferGeometry,
430
+ CylinderGeometry as CylinderGeometry4,
431
+ Group as Group7,
432
+ Mesh as Mesh7,
433
+ MeshStandardMaterial as MeshStandardMaterial7
434
+ } from "three";
435
+ function prismGeometry(width, height, depth) {
436
+ const w = width / 2;
437
+ const d = depth / 2;
438
+ const positions = new Float32Array([
439
+ // front gable (z = +d)
440
+ -w,
441
+ 0,
442
+ d,
443
+ w,
444
+ 0,
445
+ d,
446
+ 0,
447
+ height,
448
+ d,
449
+ // back gable (z = -d)
450
+ w,
451
+ 0,
452
+ -d,
453
+ -w,
454
+ 0,
455
+ -d,
456
+ 0,
457
+ height,
458
+ -d,
459
+ // left slope
460
+ -w,
461
+ 0,
462
+ -d,
463
+ -w,
464
+ 0,
465
+ d,
466
+ 0,
467
+ height,
468
+ d,
469
+ -w,
470
+ 0,
471
+ -d,
472
+ 0,
473
+ height,
474
+ d,
475
+ 0,
476
+ height,
477
+ -d,
478
+ // right slope
479
+ w,
480
+ 0,
481
+ d,
482
+ w,
483
+ 0,
484
+ -d,
485
+ 0,
486
+ height,
487
+ -d,
488
+ w,
489
+ 0,
490
+ d,
491
+ 0,
492
+ height,
493
+ -d,
494
+ 0,
495
+ height,
496
+ d,
497
+ // bottom
498
+ -w,
499
+ 0,
500
+ -d,
501
+ w,
502
+ 0,
503
+ -d,
504
+ w,
505
+ 0,
506
+ d,
507
+ -w,
508
+ 0,
509
+ -d,
510
+ w,
511
+ 0,
512
+ d,
513
+ -w,
514
+ 0,
515
+ d
516
+ ]);
517
+ const geometry = new BufferGeometry();
518
+ geometry.setAttribute("position", new BufferAttribute2(positions, 3));
519
+ geometry.computeVertexNormals();
520
+ return geometry;
521
+ }
522
+ function createHouse(options = {}) {
523
+ const rng = new Rng(options.seed ?? 1);
524
+ const palette = options.palette ?? DEFAULT_PALETTE;
525
+ const width = options.width ?? rng.range(3.2, 4.2);
526
+ const depth = options.depth ?? width * rng.range(0.75, 0.9);
527
+ const wallHeight = options.wallHeight ?? rng.range(2.1, 2.4);
528
+ const group = new Group7();
529
+ group.name = "house";
530
+ const wall = new MeshStandardMaterial7({ color: palette.wall, flatShading: true });
531
+ wall.color.offsetHSL(0, 0, rng.range(-0.03, 0.03));
532
+ const roof = new MeshStandardMaterial7({ color: palette.roof, flatShading: true });
533
+ roof.color.offsetHSL(0, 0, rng.range(-0.04, 0.04));
534
+ const stone = new MeshStandardMaterial7({ color: palette.rock[0], flatShading: true });
535
+ const wood = new MeshStandardMaterial7({ color: palette.woodDark, flatShading: true });
536
+ const foundation = new Mesh7(new BoxGeometry4(width + 0.3, 1.6, depth + 0.3), stone);
537
+ foundation.position.y = -0.55;
538
+ group.add(foundation);
539
+ const body = new Mesh7(new BoxGeometry4(width, wallHeight, depth), wall);
540
+ body.position.y = wallHeight / 2;
541
+ group.add(body);
542
+ const ridgeHeight = width * rng.range(0.32, 0.4);
543
+ const roofMesh = new Mesh7(prismGeometry(width + 0.5, ridgeHeight, depth + 0.6), roof);
544
+ roofMesh.position.y = wallHeight - 0.02;
545
+ group.add(roofMesh);
546
+ const chimney = new Mesh7(new BoxGeometry4(0.34, ridgeHeight + 0.8, 0.34), stone);
547
+ chimney.position.set(rng.pick([-1, 1]) * width * 0.22, wallHeight + ridgeHeight * 0.45, depth * 0.12);
548
+ group.add(chimney);
549
+ const door = new Mesh7(new BoxGeometry4(0.85, 1.5, 0.08), wood);
550
+ door.position.set(rng.range(-0.4, 0.4), 0.75, depth / 2 + 0.02);
551
+ group.add(door);
552
+ const glass = new MeshStandardMaterial7({
553
+ color: palette.lampGlow,
554
+ emissive: palette.lampGlow,
555
+ emissiveIntensity: 1
556
+ });
557
+ const windowGeometry = new BoxGeometry4(0.6, 0.62, 0.08);
558
+ const front = new Mesh7(windowGeometry, glass);
559
+ front.position.set(door.position.x < 0 ? width * 0.28 : -width * 0.28, 1.35, depth / 2 + 0.02);
560
+ group.add(front);
561
+ for (const side of [-1, 1]) {
562
+ if (rng.next() < 0.35) continue;
563
+ const pane = new Mesh7(windowGeometry, glass);
564
+ pane.position.set(side * (width / 2 + 0.02), 1.35, rng.range(-0.3, 0.3) * depth);
565
+ pane.rotation.y = Math.PI / 2;
566
+ group.add(pane);
567
+ }
568
+ return { object: group, obstacleRadius: Math.hypot(width + 0.5, depth + 0.5) / 2 };
569
+ }
570
+ function createTower(options = {}) {
571
+ const rng = new Rng(options.seed ?? 1);
572
+ const palette = options.palette ?? DEFAULT_PALETTE;
573
+ const height = options.height ?? rng.range(4.2, 5.2);
574
+ const group = new Group7();
575
+ group.name = "tower";
576
+ const wood = new MeshStandardMaterial7({ color: palette.wood, flatShading: true });
577
+ const woodDark = new MeshStandardMaterial7({ color: palette.woodDark, flatShading: true });
578
+ const roof = new MeshStandardMaterial7({ color: palette.roof, flatShading: true });
579
+ const spread = 1;
580
+ for (const x of [-1, 1]) {
581
+ for (const z of [-1, 1]) {
582
+ const leg = new Mesh7(new CylinderGeometry4(0.09, 0.12, height, 6), woodDark);
583
+ leg.position.set(x * spread * 0.75, height / 2 - 0.6, z * spread * 0.75);
584
+ leg.rotation.z = -x * 0.1;
585
+ leg.rotation.x = z * 0.1;
586
+ group.add(leg);
587
+ }
588
+ }
589
+ for (const level of [0.3, 0.62]) {
590
+ const brace = new Mesh7(new BoxGeometry4(spread * 2.1, 0.09, 0.07), wood);
591
+ brace.position.set(0, height * level, spread * 0.82 * (1 - level * 0.35));
592
+ brace.rotation.z = rng.pick([-0.5, 0.5]);
593
+ group.add(brace);
594
+ const braceSide = new Mesh7(new BoxGeometry4(0.07, 0.09, spread * 2.1), wood);
595
+ braceSide.position.set(spread * 0.82 * (1 - level * 0.35), height * level, 0);
596
+ braceSide.rotation.x = rng.pick([-0.5, 0.5]);
597
+ group.add(braceSide);
598
+ }
599
+ const platform = new Mesh7(new BoxGeometry4(spread * 2.2, 0.14, spread * 2.2), wood);
600
+ platform.position.y = height - 0.5;
601
+ group.add(platform);
602
+ for (const x of [-1, 1]) {
603
+ for (const z of [-1, 1]) {
604
+ const post = new Mesh7(new BoxGeometry4(0.08, 0.9, 0.08), woodDark);
605
+ post.position.set(x * spread * 1, height - 0.05, z * spread * 1);
606
+ group.add(post);
607
+ }
608
+ }
609
+ for (const [rx, rz, w, d] of [
610
+ [0, 1, spread * 2.1, 0.06],
611
+ [0, -1, spread * 2.1, 0.06],
612
+ [1, 0, 0.06, spread * 2.1],
613
+ [-1, 0, 0.06, spread * 2.1]
614
+ ]) {
615
+ const rail = new Mesh7(new BoxGeometry4(w, 0.07, d), wood);
616
+ rail.position.set(rx * spread, height + 0.28, rz * spread);
617
+ group.add(rail);
618
+ }
619
+ const cap = new Mesh7(new CylinderGeometry4(0, spread * 1.55, 1, 4), roof);
620
+ cap.position.y = height + 1.15;
621
+ cap.rotation.y = Math.PI / 4;
622
+ group.add(cap);
623
+ return { object: group, obstacleRadius: spread * 1.35 };
624
+ }
625
+ function createWell(options = {}) {
626
+ const rng = new Rng(options.seed ?? 1);
627
+ const palette = options.palette ?? DEFAULT_PALETTE;
628
+ const group = new Group7();
629
+ group.name = "well";
630
+ const stone = new MeshStandardMaterial7({ color: rng.pick(palette.rock), flatShading: true });
631
+ const wood = new MeshStandardMaterial7({ color: palette.woodDark, flatShading: true });
632
+ const roof = new MeshStandardMaterial7({ color: palette.roof, flatShading: true });
633
+ const ring = new Mesh7(new CylinderGeometry4(0.85, 0.95, 0.75, 10), stone);
634
+ ring.position.y = 0.375;
635
+ group.add(ring);
636
+ const inner = new Mesh7(
637
+ new CylinderGeometry4(0.62, 0.62, 0.05, 10),
638
+ new MeshStandardMaterial7({ color: 1450542 })
639
+ );
640
+ inner.position.y = 0.76;
641
+ group.add(inner);
642
+ for (const side of [-1, 1]) {
643
+ const post = new Mesh7(new CylinderGeometry4(0.06, 0.075, 1.9, 5), wood);
644
+ post.position.set(side * 0.72, 0.95, 0);
645
+ group.add(post);
646
+ }
647
+ const bar = new Mesh7(new CylinderGeometry4(0.045, 0.045, 1.5, 5), wood);
648
+ bar.rotation.z = Math.PI / 2;
649
+ bar.position.y = 1.72;
650
+ group.add(bar);
651
+ const cap = new Mesh7(prismGeometry(2, 0.55, 0.65), roof);
652
+ cap.position.y = 1.86;
653
+ cap.rotation.y = Math.PI / 2;
654
+ group.add(cap);
655
+ const rope = new Mesh7(new CylinderGeometry4(0.012, 0.012, 0.6, 4), wood);
656
+ rope.position.y = 1.42;
657
+ group.add(rope);
658
+ const bucket = new Mesh7(new CylinderGeometry4(0.13, 0.1, 0.2, 7), wood);
659
+ bucket.position.y = 1.05;
660
+ group.add(bucket);
661
+ return { object: group, obstacleRadius: 1 };
662
+ }
663
+ function createRuin(options = {}) {
664
+ const rng = new Rng(options.seed ?? 1);
665
+ const palette = options.palette ?? DEFAULT_PALETTE;
666
+ const size = options.size ?? rng.range(3.5, 5);
667
+ const depth = size * rng.range(0.7, 0.9);
668
+ const group = new Group7();
669
+ group.name = "ruin";
670
+ const stone = new MeshStandardMaterial7({ color: rng.pick(palette.rock), flatShading: true });
671
+ stone.color.offsetHSL(0, 0, rng.range(-0.04, 0.02));
672
+ const thickness = 0.35;
673
+ const runs = [
674
+ [0, -depth / 2, size, true],
675
+ [-size / 2, 0, depth, false],
676
+ [size / 2, 0, depth, false],
677
+ [size * 0.3, depth / 2, size * 0.35, true]
678
+ ];
679
+ for (const [cx, cz, length, alongX] of runs) {
680
+ const segments = Math.max(2, Math.round(length / 1.1));
681
+ const step = length / segments;
682
+ for (let i = 0; i < segments; i++) {
683
+ if (rng.next() < 0.22) continue;
684
+ const height = rng.range(0.5, 2.2);
685
+ const wall = new Mesh7(
686
+ new BoxGeometry4(alongX ? step * 0.96 : thickness, height, alongX ? thickness : step * 0.96),
687
+ stone
688
+ );
689
+ const offset = -length / 2 + step * (i + 0.5);
690
+ wall.position.set(alongX ? cx + offset : cx, height / 2, alongX ? cz : cz + offset);
691
+ wall.rotation.y = rng.range(-0.03, 0.03);
692
+ group.add(wall);
693
+ }
694
+ }
695
+ const blocks = rng.int(4, 7);
696
+ for (let i = 0; i < blocks; i++) {
697
+ const s = rng.range(0.25, 0.55);
698
+ const block = new Mesh7(new BoxGeometry4(s, s * rng.range(0.6, 1), s * rng.range(0.7, 1.2)), stone);
699
+ block.position.set(rng.range(-size * 0.6, size * 0.6), s * 0.3, rng.range(-depth * 0.6, depth * 0.7));
700
+ block.rotation.set(rng.range(-0.3, 0.3), rng.range(0, Math.PI), rng.range(-0.3, 0.3));
701
+ group.add(block);
702
+ }
703
+ return { object: group, obstacleRadius: Math.hypot(size, depth) / 2 };
704
+ }
705
+
706
+ // src/environment/terrain.ts
707
+ import {
708
+ BufferAttribute as BufferAttribute3,
352
709
  Color,
353
- Mesh as Mesh6,
354
- MeshStandardMaterial as MeshStandardMaterial6,
710
+ Mesh as Mesh8,
711
+ MeshStandardMaterial as MeshStandardMaterial8,
355
712
  PlaneGeometry
356
713
  } from "three";
357
714
  function createTerrain(options = {}) {
@@ -381,21 +738,27 @@ function createTerrain(options = {}) {
381
738
  const grassHigh = new Color(palette.grassHigh);
382
739
  const cliff = new Color(palette.cliff);
383
740
  const peak = new Color(palette.peak);
741
+ const sand = new Color(palette.sand);
742
+ const waterLevel = options.waterLevel;
384
743
  const scratch = new Color();
385
744
  for (let i = 0; i < positions.count; i++) {
386
- const h = positions.getY(i) / amplitude;
745
+ const y = positions.getY(i);
746
+ const h = y / amplitude;
387
747
  const slope = 1 - normals.getY(i);
388
748
  scratch.copy(grassLow).lerp(grassHigh, Math.min(1, h * 1.6));
389
749
  if (h > 0.75) scratch.lerp(peak, (h - 0.75) * 4);
390
750
  if (slope > 0.15) scratch.lerp(cliff, Math.min(1, (slope - 0.15) * 4));
751
+ if (waterLevel !== void 0 && y < waterLevel + 0.5) {
752
+ scratch.lerp(sand, Math.min(1, (waterLevel + 0.5 - y) * 1.6));
753
+ }
391
754
  colors[i * 3] = scratch.r;
392
755
  colors[i * 3 + 1] = scratch.g;
393
756
  colors[i * 3 + 2] = scratch.b;
394
757
  }
395
- geometry.setAttribute("color", new BufferAttribute2(colors, 3));
396
- const mesh = new Mesh6(
758
+ geometry.setAttribute("color", new BufferAttribute3(colors, 3));
759
+ const mesh = new Mesh8(
397
760
  geometry,
398
- new MeshStandardMaterial6({ vertexColors: true, flatShading: true })
761
+ new MeshStandardMaterial8({ vertexColors: true, flatShading: true })
399
762
  );
400
763
  mesh.name = "terrain";
401
764
  return { mesh, heightAt, size, seed };
@@ -405,7 +768,7 @@ function createTerrain(options = {}) {
405
768
  import {
406
769
  BackSide,
407
770
  Color as Color2,
408
- Mesh as Mesh7,
771
+ Mesh as Mesh9,
409
772
  ShaderMaterial,
410
773
  SphereGeometry as SphereGeometry2
411
774
  } from "three";
@@ -439,7 +802,7 @@ function createSky(options = {}) {
439
802
  }`
440
803
  )
441
804
  });
442
- const mesh = new Mesh7(new SphereGeometry2(options.radius ?? 400, 16, 12), material);
805
+ const mesh = new Mesh9(new SphereGeometry2(options.radius ?? 400, 16, 12), material);
443
806
  mesh.name = "sky";
444
807
  return {
445
808
  mesh,
@@ -455,7 +818,7 @@ import {
455
818
  AmbientLight,
456
819
  DirectionalLight,
457
820
  Fog,
458
- Group as Group6,
821
+ Group as Group8,
459
822
  HemisphereLight
460
823
  } from "three";
461
824
  var PRESETS = {
@@ -498,7 +861,7 @@ var PRESETS = {
498
861
  };
499
862
  function createLightingRig(preset = "day") {
500
863
  const config = PRESETS[preset];
501
- const group = new Group6();
864
+ const group = new Group8();
502
865
  group.name = `lighting-${preset}`;
503
866
  const sun = new DirectionalLight(config.sun, config.sunIntensity);
504
867
  sun.position.set(...config.sunPos);
@@ -521,15 +884,392 @@ function applyFog(scene, preset, palette = DEFAULT_PALETTE) {
521
884
  scene.fog = new Fog(palette.fog, near, far);
522
885
  }
523
886
 
887
+ // src/environment/water.ts
888
+ import { Mesh as Mesh10, MeshStandardMaterial as MeshStandardMaterial9, PlaneGeometry as PlaneGeometry2 } from "three";
889
+ function createWater(options = {}) {
890
+ const level = options.level ?? 0.8;
891
+ const size = options.size ?? 200;
892
+ const resolution = options.resolution ?? 40;
893
+ const amplitude = options.amplitude ?? 0.06;
894
+ const speed = options.speed ?? 1;
895
+ const palette = options.palette ?? DEFAULT_PALETTE;
896
+ const geometry = new PlaneGeometry2(size, size, resolution, resolution);
897
+ geometry.rotateX(-Math.PI / 2);
898
+ const positions = geometry.getAttribute("position");
899
+ const mesh = new Mesh10(
900
+ geometry,
901
+ new MeshStandardMaterial9({
902
+ color: palette.water,
903
+ transparent: true,
904
+ opacity: 0.85,
905
+ flatShading: true,
906
+ metalness: 0.35,
907
+ roughness: 0.4
908
+ })
909
+ );
910
+ mesh.name = "water";
911
+ mesh.position.y = level;
912
+ let time = 0;
913
+ const update = (dt) => {
914
+ time += dt * speed;
915
+ for (let i = 0; i < positions.count; i++) {
916
+ const x = positions.getX(i);
917
+ const z = positions.getZ(i);
918
+ positions.setY(
919
+ i,
920
+ Math.sin(x * 0.35 + time) * Math.cos(z * 0.3 + time * 0.8) * amplitude
921
+ );
922
+ }
923
+ positions.needsUpdate = true;
924
+ geometry.computeVertexNormals();
925
+ };
926
+ update(0);
927
+ return {
928
+ mesh,
929
+ level,
930
+ update,
931
+ isUnderwater: (groundHeight) => groundHeight < level
932
+ };
933
+ }
934
+ function aboveWater(terrain, water, margin = 0.25) {
935
+ return (x, z) => terrain.heightAt(x, z) > water.level + margin;
936
+ }
937
+
938
+ // src/environment/dayCycle.ts
939
+ import { Color as Color3, PointLight as PointLight2 } from "three";
940
+ function createDayCycle(options = {}) {
941
+ const palette = options.palette ?? DEFAULT_PALETTE;
942
+ const dayLength = options.dayLength ?? 60;
943
+ const frames = [
944
+ { t: 0, skyTop: 725030, skyBottom: 1712960, sun: 9348824, sunIntensity: 0.03, ambientIntensity: 0.06, fog: 1317422 },
945
+ { t: 0.23, skyTop: 2569052, skyBottom: 6968432, sun: 13605490, sunIntensity: 0.3, ambientIntensity: 0.12, fog: 4866648 },
946
+ { t: 0.3, skyTop: 4877216, skyBottom: 15247738, sun: 16758881, sunIntensity: 1, ambientIntensity: 0.26, fog: 11569778 },
947
+ { t: 0.5, skyTop: palette.skyTop, skyBottom: palette.skyBottom, sun: 16774368, sunIntensity: 1.9, ambientIntensity: 0.45, fog: palette.fog },
948
+ { t: 0.7, skyTop: 4872852, skyBottom: 14718302, sun: 16752720, sunIntensity: 1, ambientIntensity: 0.26, fog: 10517096 },
949
+ { t: 0.78, skyTop: 2304594, skyBottom: 9065824, sun: 14191194, sunIntensity: 0.25, ambientIntensity: 0.11, fog: 4603476 },
950
+ { t: 1, skyTop: 725030, skyBottom: 1712960, sun: 9348824, sunIntensity: 0.03, ambientIntensity: 0.06, fog: 1317422 }
951
+ ];
952
+ const lampLights = [];
953
+ const lampBulbs = [];
954
+ for (const entry of options.lamps ?? []) {
955
+ const root = entry.isObject3D ? entry : entry.object;
956
+ root.traverse((child) => {
957
+ if (child instanceof PointLight2) {
958
+ lampLights.push({ light: child, base: child.intensity || 6 });
959
+ }
960
+ const material = child.material;
961
+ if (material?.emissive && material.emissiveIntensity > 0.5) {
962
+ lampBulbs.push({ material, base: material.emissiveIntensity });
963
+ }
964
+ });
965
+ }
966
+ const colorA = new Color3();
967
+ const colorB = new Color3();
968
+ let timeOfDay = options.timeOfDay ?? 0.5;
969
+ let sunElevation = 0;
970
+ const sample = (t) => {
971
+ let a = frames[0];
972
+ let b = frames[frames.length - 1];
973
+ for (let i = 0; i < frames.length - 1; i++) {
974
+ if (t >= frames[i].t && t <= frames[i + 1].t) {
975
+ a = frames[i];
976
+ b = frames[i + 1];
977
+ break;
978
+ }
979
+ }
980
+ const f = b.t === a.t ? 0 : (t - a.t) / (b.t - a.t);
981
+ const lerpHex = (ha, hb) => colorA.setHex(ha).lerp(colorB.setHex(hb), f).getHex();
982
+ return {
983
+ t,
984
+ skyTop: lerpHex(a.skyTop, b.skyTop),
985
+ skyBottom: lerpHex(a.skyBottom, b.skyBottom),
986
+ sun: lerpHex(a.sun, b.sun),
987
+ sunIntensity: a.sunIntensity + (b.sunIntensity - a.sunIntensity) * f,
988
+ ambientIntensity: a.ambientIntensity + (b.ambientIntensity - a.ambientIntensity) * f,
989
+ fog: lerpHex(a.fog, b.fog)
990
+ };
991
+ };
992
+ const apply = () => {
993
+ const t = timeOfDay;
994
+ const frame = sample(t);
995
+ const sunAngle = (t - 0.25) * Math.PI * 2;
996
+ sunElevation = Math.sin(sunAngle);
997
+ options.sky?.setColors(frame.skyTop, frame.skyBottom);
998
+ if (options.rig) {
999
+ const { sun, ambient, hemisphere } = options.rig;
1000
+ sun.color.setHex(frame.sun);
1001
+ sun.intensity = frame.sunIntensity;
1002
+ sun.position.set(Math.cos(sunAngle) * 40, Math.max(sunElevation, -0.2) * 45 + 6, 16);
1003
+ ambient.intensity = frame.ambientIntensity;
1004
+ hemisphere.intensity = frame.ambientIntensity * 1.4;
1005
+ }
1006
+ if (options.scene?.fog && "color" in options.scene.fog) {
1007
+ options.scene.fog.color.setHex(frame.fog);
1008
+ }
1009
+ const night = Math.min(1, Math.max(0, (0.06 - sunElevation) / 0.16));
1010
+ for (const { light, base } of lampLights) light.intensity = base * night;
1011
+ for (const { material, base } of lampBulbs) {
1012
+ material.emissiveIntensity = 0.15 * base + 0.85 * base * night;
1013
+ }
1014
+ };
1015
+ apply();
1016
+ return {
1017
+ get timeOfDay() {
1018
+ return timeOfDay;
1019
+ },
1020
+ set timeOfDay(t) {
1021
+ timeOfDay = (t % 1 + 1) % 1;
1022
+ apply();
1023
+ },
1024
+ get sunElevation() {
1025
+ return sunElevation;
1026
+ },
1027
+ get isNight() {
1028
+ return sunElevation < 0;
1029
+ },
1030
+ update(dt) {
1031
+ timeOfDay = (timeOfDay + dt / dayLength) % 1;
1032
+ apply();
1033
+ },
1034
+ set(t) {
1035
+ timeOfDay = (t % 1 + 1) % 1;
1036
+ apply();
1037
+ }
1038
+ };
1039
+ }
1040
+
1041
+ // src/environment/wind.ts
1042
+ import { Mesh as Mesh11 } from "three";
1043
+ function applyWind(target, options = {}) {
1044
+ const strength = options.strength ?? 0.06;
1045
+ const frequency = options.frequency ?? 1.2;
1046
+ const anchor = options.anchorHeight ?? 0.6;
1047
+ const time = { value: 0 };
1048
+ const patched = /* @__PURE__ */ new Set();
1049
+ target.traverse((child) => {
1050
+ if (!(child instanceof Mesh11)) return;
1051
+ const materials = Array.isArray(child.material) ? child.material : [child.material];
1052
+ for (const material of materials) {
1053
+ if (patched.has(material)) continue;
1054
+ patched.add(material);
1055
+ material.onBeforeCompile = (shader) => {
1056
+ shader.uniforms.uWindTime = time;
1057
+ shader.vertexShader = shader.vertexShader.replace(
1058
+ "#include <common>",
1059
+ `#include <common>
1060
+ uniform float uWindTime;`
1061
+ ).replace(
1062
+ "#include <begin_vertex>",
1063
+ `#include <begin_vertex>
1064
+ {
1065
+ float sway = max(position.y - ${anchor.toFixed(3)}, 0.0);
1066
+ float phase = 0.0;
1067
+ #ifdef USE_INSTANCING
1068
+ phase = instanceMatrix[3].x * 0.7 + instanceMatrix[3].z * 1.3;
1069
+ #endif
1070
+ float w = sin(uWindTime * ${(frequency * 2).toFixed(3)} + phase + position.y * 0.8);
1071
+ transformed.x += w * sway * ${strength.toFixed(4)} * 4.0;
1072
+ transformed.z += cos(uWindTime * ${(frequency * 1.4).toFixed(3)} + phase) * sway * ${strength.toFixed(4)} * 2.0;
1073
+ }`
1074
+ );
1075
+ };
1076
+ material.needsUpdate = true;
1077
+ }
1078
+ });
1079
+ return {
1080
+ update(dt) {
1081
+ time.value += dt;
1082
+ },
1083
+ materials: [...patched]
1084
+ };
1085
+ }
1086
+
1087
+ // src/environment/path.ts
1088
+ import {
1089
+ BufferAttribute as BufferAttribute5,
1090
+ BufferGeometry as BufferGeometry2,
1091
+ CatmullRomCurve3,
1092
+ Mesh as Mesh12,
1093
+ MeshStandardMaterial as MeshStandardMaterial10,
1094
+ Vector3 as Vector32
1095
+ } from "three";
1096
+ function createPath(points, options = {}) {
1097
+ const width = options.width ?? 1.8;
1098
+ const surface = options.surface ?? 0;
1099
+ const heightAt = typeof surface === "number" ? () => surface : (x, z) => surface(x, z);
1100
+ const loop = options.loop ?? false;
1101
+ const palette = options.palette ?? DEFAULT_PALETTE;
1102
+ const controls = points.map((p) => new Vector32(p.x, 0, "z" in p ? p.z : 0));
1103
+ const curve = new CatmullRomCurve3(controls, loop, "centripetal");
1104
+ const length = curve.getLength();
1105
+ const samples = Math.max(8, Math.ceil(length * (options.samplesPerUnit ?? 1)));
1106
+ const route = [];
1107
+ for (let i = 0; i <= samples; i++) {
1108
+ if (loop && i === samples) break;
1109
+ const p = curve.getPoint(i / samples);
1110
+ route.push(new Vector32(p.x, heightAt(p.x, p.z), p.z));
1111
+ }
1112
+ const edgeCount = loop ? route.length + 1 : route.length;
1113
+ const positions = new Float32Array(edgeCount * 2 * 3);
1114
+ const direction = new Vector32();
1115
+ const perp = new Vector32();
1116
+ for (let i = 0; i < edgeCount; i++) {
1117
+ const current = route[i % route.length];
1118
+ const previous = route[(i - 1 + route.length) % route.length];
1119
+ const next = route[(i + 1) % route.length];
1120
+ if (!loop && i === 0) direction.subVectors(next, current);
1121
+ else if (!loop && i === edgeCount - 1) direction.subVectors(current, previous);
1122
+ else direction.subVectors(next, previous);
1123
+ perp.set(-direction.z, 0, direction.x).normalize().multiplyScalar(width / 2);
1124
+ const left = i * 6;
1125
+ positions[left] = current.x - perp.x;
1126
+ positions[left + 1] = heightAt(current.x - perp.x, current.z - perp.z) + 0.05;
1127
+ positions[left + 2] = current.z - perp.z;
1128
+ positions[left + 3] = current.x + perp.x;
1129
+ positions[left + 4] = heightAt(current.x + perp.x, current.z + perp.z) + 0.05;
1130
+ positions[left + 5] = current.z + perp.z;
1131
+ }
1132
+ const indices = [];
1133
+ for (let i = 0; i < edgeCount - 1; i++) {
1134
+ const a = i * 2;
1135
+ indices.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);
1136
+ }
1137
+ const geometry = new BufferGeometry2();
1138
+ geometry.setAttribute("position", new BufferAttribute5(positions, 3));
1139
+ geometry.setIndex(indices);
1140
+ geometry.computeVertexNormals();
1141
+ const mesh = new Mesh12(
1142
+ geometry,
1143
+ new MeshStandardMaterial10({
1144
+ color: palette.path,
1145
+ flatShading: true,
1146
+ polygonOffset: true,
1147
+ polygonOffsetFactor: -1
1148
+ })
1149
+ );
1150
+ mesh.name = "path";
1151
+ const keepOutMargin = options.keepOutMargin ?? 0.6;
1152
+ 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 }));
1153
+ const half = width / 2;
1154
+ const contains = (x, z) => {
1155
+ const count = loop ? route.length : route.length - 1;
1156
+ for (let i = 0; i < count; i++) {
1157
+ const a = route[i];
1158
+ const b = route[(i + 1) % route.length];
1159
+ const abx = b.x - a.x;
1160
+ const abz = b.z - a.z;
1161
+ const lengthSq = abx * abx + abz * abz || 1e-9;
1162
+ const t = Math.max(0, Math.min(1, ((x - a.x) * abx + (z - a.z) * abz) / lengthSq));
1163
+ const dx = x - (a.x + abx * t);
1164
+ const dz = z - (a.z + abz * t);
1165
+ if (dx * dx + dz * dz <= half * half) return true;
1166
+ }
1167
+ return false;
1168
+ };
1169
+ return { mesh, route, keepOut, contains, loop };
1170
+ }
1171
+
1172
+ // src/generators/village.ts
1173
+ import { Group as Group9 } from "three";
1174
+ function createVillage(options = {}) {
1175
+ const rng = new Rng(options.seed ?? 1);
1176
+ const palette = options.palette ?? DEFAULT_PALETTE;
1177
+ const center = options.center ?? { x: 0, z: 0 };
1178
+ const radius = options.radius ?? 12;
1179
+ const houseCount = options.houses ?? 5;
1180
+ const surface = options.surface ?? 0;
1181
+ const heightAt = typeof surface === "number" ? () => surface : (x, z) => surface(x, z);
1182
+ const group = new Group9();
1183
+ group.name = "village";
1184
+ const props = [];
1185
+ const lamps = [];
1186
+ const place = (prop, x, z, rotY) => {
1187
+ prop.object.position.set(x, heightAt(x, z), z);
1188
+ prop.object.rotation.y = rotY;
1189
+ group.add(prop.object);
1190
+ props.push(prop);
1191
+ return prop;
1192
+ };
1193
+ const findSpot = (angle, distance, footprint) => {
1194
+ let x = center.x;
1195
+ let z = center.z;
1196
+ for (let attempt = 0; attempt < 10; attempt++) {
1197
+ const a = angle + rng.range(-0.25, 0.25) * (attempt > 0 ? 1 : 0.3);
1198
+ const d = distance * rng.range(attempt > 0 ? 0.8 : 0.95, 1.1);
1199
+ x = center.x + Math.cos(a) * d;
1200
+ z = center.z + Math.sin(a) * d;
1201
+ const y = heightAt(x, z);
1202
+ if (options.mask && !options.mask(x, z, y)) continue;
1203
+ let min = y;
1204
+ let max = y;
1205
+ for (const [dx, dz] of [[footprint, 0], [-footprint, 0], [0, footprint], [0, -footprint]]) {
1206
+ const h = heightAt(x + dx, z + dz);
1207
+ min = Math.min(min, h);
1208
+ max = Math.max(max, h);
1209
+ }
1210
+ if (max - min <= Math.max(0.9, footprint * 0.35)) break;
1211
+ }
1212
+ return { x, z };
1213
+ };
1214
+ const well = createWell({ seed: rng.int(1, 1e9), palette });
1215
+ place(well, center.x, center.z, rng.range(0, Math.PI * 2));
1216
+ let lightBudget = options.lampLights ?? 3;
1217
+ for (let i = 0; i < houseCount; i++) {
1218
+ const angle = i / houseCount * Math.PI * 2 + rng.range(-0.15, 0.15);
1219
+ const house = createHouse({ seed: rng.int(1, 1e9), palette });
1220
+ const spot = findSpot(angle, radius * rng.range(0.7, 0.95), house.obstacleRadius);
1221
+ const facing = Math.atan2(center.x - spot.x, center.z - spot.z);
1222
+ place(house, spot.x, spot.z, facing);
1223
+ lamps.push(house);
1224
+ if (rng.next() < 0.7) {
1225
+ const crateAngle = angle + rng.range(-0.5, 0.5);
1226
+ const crateDistance = Math.hypot(spot.x - center.x, spot.z - center.z) - house.obstacleRadius - 0.7;
1227
+ place(
1228
+ createCrate({ seed: rng.int(1, 1e9), size: rng.range(0.7, 1), palette }),
1229
+ center.x + Math.cos(crateAngle) * crateDistance,
1230
+ center.z + Math.sin(crateAngle) * crateDistance,
1231
+ rng.range(0, Math.PI)
1232
+ );
1233
+ }
1234
+ if (i % 2 === 0) {
1235
+ const lampDistance = Math.hypot(spot.x - center.x, spot.z - center.z) * 0.55;
1236
+ const lamp = createLamp({ seed: rng.int(1, 1e9), light: lightBudget > 0, palette });
1237
+ lightBudget--;
1238
+ place(lamp, center.x + Math.cos(angle) * lampDistance, center.z + Math.sin(angle) * lampDistance, 0);
1239
+ lamps.push(lamp);
1240
+ }
1241
+ }
1242
+ if (options.tower ?? true) {
1243
+ const angle = rng.range(0, Math.PI * 2);
1244
+ const tower = createTower({ seed: rng.int(1, 1e9), palette });
1245
+ const spot = findSpot(angle, radius * 1.05, tower.obstacleRadius);
1246
+ place(tower, spot.x, spot.z, rng.range(0, Math.PI * 2));
1247
+ }
1248
+ if (options.ruin ?? true) {
1249
+ const angle = rng.range(0, Math.PI * 2);
1250
+ const ruin = createRuin({ seed: rng.int(1, 1e9), palette });
1251
+ const spot = findSpot(angle, radius * 1.35, ruin.obstacleRadius);
1252
+ place(ruin, spot.x, spot.z, rng.range(0, Math.PI * 2));
1253
+ }
1254
+ return {
1255
+ group,
1256
+ props,
1257
+ obstacles: collectObstacles(props),
1258
+ lamps,
1259
+ keepOut: [{ center: { ...center }, radius: radius * 1.5 }],
1260
+ center
1261
+ };
1262
+ }
1263
+
524
1264
  // src/scatter/scatter.ts
525
1265
  import {
526
1266
  DynamicDrawUsage,
527
- Group as Group7,
1267
+ Group as Group10,
528
1268
  InstancedMesh,
529
1269
  Matrix4,
530
- Mesh as Mesh8,
1270
+ Mesh as Mesh13,
531
1271
  Quaternion,
532
- Vector3 as Vector32
1272
+ Vector3 as Vector33
533
1273
  } from "three";
534
1274
  function scatter(options) {
535
1275
  const seed = options.seed ?? 1;
@@ -585,7 +1325,7 @@ function scatter(options) {
585
1325
  const itemIndex = pickItem();
586
1326
  const [minScale, maxScale2] = options.items[itemIndex].scale ?? [0.8, 1.25];
587
1327
  placements.push({
588
- position: new Vector32(x, y, z),
1328
+ position: new Vector33(x, y, z),
589
1329
  rotationY: rng.range(0, Math.PI * 2),
590
1330
  scale: rng.range(minScale, maxScale2),
591
1331
  itemIndex
@@ -595,14 +1335,14 @@ function scatter(options) {
595
1335
  if (!bucket) occupied.set(key, bucket = []);
596
1336
  bucket.push({ x, z });
597
1337
  }
598
- const group = new Group7();
1338
+ const group = new Group10();
599
1339
  group.name = "scatter";
600
1340
  const obstacles = [];
601
1341
  const placementMatrix = new Matrix4();
602
1342
  const composed = new Matrix4();
603
1343
  const quaternion = new Quaternion();
604
- const up = new Vector32(0, 1, 0);
605
- const scaleVector = new Vector32();
1344
+ const up = new Vector33(0, 1, 0);
1345
+ const scaleVector = new Vector33();
606
1346
  options.items.forEach((item, itemIndex) => {
607
1347
  const variantCount = item.variants ?? 4;
608
1348
  const variants = [];
@@ -618,7 +1358,7 @@ function scatter(options) {
618
1358
  if (list.length === 0) return;
619
1359
  variant.object.updateMatrixWorld(true);
620
1360
  variant.object.traverse((child) => {
621
- if (!(child instanceof Mesh8)) return;
1361
+ if (!(child instanceof Mesh13)) return;
622
1362
  const instanced = new InstancedMesh(child.geometry, child.material, list.length);
623
1363
  instanced.instanceMatrix.setUsage(DynamicDrawUsage);
624
1364
  list.forEach((placement, index) => {
@@ -647,16 +1387,28 @@ export {
647
1387
  DEFAULT_PALETTE,
648
1388
  PALETTES,
649
1389
  Rng,
1390
+ aboveWater,
650
1391
  applyFog,
1392
+ applyWind,
651
1393
  collectObstacles,
1394
+ createBush,
652
1395
  createCrate,
1396
+ createDayCycle,
653
1397
  createFence,
1398
+ createGrassTuft,
1399
+ createHouse,
654
1400
  createLamp,
655
1401
  createLightingRig,
1402
+ createPath,
656
1403
  createRock,
1404
+ createRuin,
657
1405
  createSky,
658
1406
  createTerrain,
1407
+ createTower,
659
1408
  createTree,
1409
+ createVillage,
1410
+ createWater,
1411
+ createWell,
660
1412
  fractalNoise2,
661
1413
  hash2,
662
1414
  scatter,