scena3d 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -11
- package/dist/index.cjs +1203 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +534 -3
- package/dist/index.d.ts +534 -3
- package/dist/index.js +1204 -41
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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/
|
|
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
|
|
354
|
-
MeshStandardMaterial as
|
|
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
|
|
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
|
|
396
|
-
const mesh = new
|
|
758
|
+
geometry.setAttribute("color", new BufferAttribute3(colors, 3));
|
|
759
|
+
const mesh = new Mesh8(
|
|
397
760
|
geometry,
|
|
398
|
-
new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,534 @@ function applyFog(scene, preset, palette = DEFAULT_PALETTE) {
|
|
|
521
884
|
scene.fog = new Fog(palette.fog, near, far);
|
|
522
885
|
}
|
|
523
886
|
|
|
524
|
-
// src/
|
|
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
|
|
525
1088
|
import {
|
|
526
|
-
|
|
527
|
-
|
|
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
|
+
|
|
1264
|
+
// src/kits/kit.ts
|
|
1265
|
+
import {
|
|
1266
|
+
BoxGeometry as BoxGeometry5,
|
|
1267
|
+
CylinderGeometry as CylinderGeometry5,
|
|
1268
|
+
Group as Group10,
|
|
528
1269
|
InstancedMesh,
|
|
529
1270
|
Matrix4,
|
|
530
|
-
Mesh as
|
|
1271
|
+
Mesh as Mesh13,
|
|
1272
|
+
MeshStandardMaterial as MeshStandardMaterial11,
|
|
1273
|
+
PointLight as PointLight3,
|
|
1274
|
+
SphereGeometry as SphereGeometry3,
|
|
1275
|
+
Vector3 as Vector33
|
|
1276
|
+
} from "three";
|
|
1277
|
+
var KIT_UNIT = 2;
|
|
1278
|
+
function assembleKit(rows, options = {}) {
|
|
1279
|
+
const rng = new Rng(options.seed ?? 1);
|
|
1280
|
+
const palette = options.palette ?? DEFAULT_PALETTE;
|
|
1281
|
+
const unit = options.unit ?? KIT_UNIT;
|
|
1282
|
+
const wallHeight = options.wallHeight ?? 2.6;
|
|
1283
|
+
let torchLights = options.torchLights ?? 4;
|
|
1284
|
+
const height = rows.length;
|
|
1285
|
+
const width = Math.max(...rows.map((r) => r.length), 0);
|
|
1286
|
+
const originX = (-width / 2 + 0.5) * unit;
|
|
1287
|
+
const originZ = (-height / 2 + 0.5) * unit;
|
|
1288
|
+
const worldOf = (col, row) => ({
|
|
1289
|
+
x: originX + col * unit,
|
|
1290
|
+
z: originZ + row * unit
|
|
1291
|
+
});
|
|
1292
|
+
const group = new Group10();
|
|
1293
|
+
group.name = "kit";
|
|
1294
|
+
const obstacles = [];
|
|
1295
|
+
const spawns = [];
|
|
1296
|
+
const torches = [];
|
|
1297
|
+
const floorCells = /* @__PURE__ */ new Set();
|
|
1298
|
+
const wallCells = [];
|
|
1299
|
+
const floorTiles = [];
|
|
1300
|
+
for (let row = 0; row < height; row++) {
|
|
1301
|
+
for (let col = 0; col < (rows[row] ?? "").length; col++) {
|
|
1302
|
+
const cell = rows[row][col];
|
|
1303
|
+
if (cell === " " || cell === void 0) continue;
|
|
1304
|
+
const { x, z } = worldOf(col, row);
|
|
1305
|
+
if (cell === "#") {
|
|
1306
|
+
wallCells.push({ x, z });
|
|
1307
|
+
obstacles.push({ center: new Vector33(x, wallHeight / 2, z), radius: unit * 0.71 });
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
floorTiles.push({ x, z });
|
|
1311
|
+
floorCells.add(`${col},${row}`);
|
|
1312
|
+
if (cell === "D") {
|
|
1313
|
+
const horizontalRun = rows[row][col - 1] === "#" || rows[row][col + 1] === "#";
|
|
1314
|
+
const lintel = new Mesh13(
|
|
1315
|
+
new BoxGeometry5(
|
|
1316
|
+
horizontalRun ? unit : unit * 0.4,
|
|
1317
|
+
wallHeight * 0.25,
|
|
1318
|
+
horizontalRun ? unit * 0.4 : unit
|
|
1319
|
+
),
|
|
1320
|
+
new MeshStandardMaterial11({ color: palette.woodDark, flatShading: true })
|
|
1321
|
+
);
|
|
1322
|
+
lintel.position.set(x, wallHeight * 0.875, z);
|
|
1323
|
+
group.add(lintel);
|
|
1324
|
+
} else if (cell === "T") {
|
|
1325
|
+
group.add(createTorch(x, z, palette, rng, torchLights > 0));
|
|
1326
|
+
torchLights--;
|
|
1327
|
+
torches.push(new Vector33(x, 0, z));
|
|
1328
|
+
obstacles.push({ center: new Vector33(x, 0, z), radius: 0.3 });
|
|
1329
|
+
} else if (cell === "S") {
|
|
1330
|
+
spawns.push(new Vector33(x, 0, z));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const stone = new MeshStandardMaterial11({ color: rng.pick(palette.rock), flatShading: true });
|
|
1335
|
+
const stoneDark = new MeshStandardMaterial11({ color: palette.cliff, flatShading: true });
|
|
1336
|
+
const matrix = new Matrix4();
|
|
1337
|
+
if (wallCells.length > 0) {
|
|
1338
|
+
const walls = new InstancedMesh(
|
|
1339
|
+
new BoxGeometry5(unit, wallHeight, unit),
|
|
1340
|
+
stone,
|
|
1341
|
+
wallCells.length
|
|
1342
|
+
);
|
|
1343
|
+
wallCells.forEach((cell, i) => {
|
|
1344
|
+
walls.setMatrixAt(i, matrix.makeTranslation(cell.x, wallHeight / 2, cell.z));
|
|
1345
|
+
});
|
|
1346
|
+
walls.instanceMatrix.needsUpdate = true;
|
|
1347
|
+
group.add(walls);
|
|
1348
|
+
}
|
|
1349
|
+
if (floorTiles.length > 0) {
|
|
1350
|
+
const floors = new InstancedMesh(
|
|
1351
|
+
new BoxGeometry5(unit, 0.2, unit),
|
|
1352
|
+
stoneDark,
|
|
1353
|
+
floorTiles.length
|
|
1354
|
+
);
|
|
1355
|
+
floorTiles.forEach((cell, i) => {
|
|
1356
|
+
floors.setMatrixAt(i, matrix.makeTranslation(cell.x, -0.1, cell.z));
|
|
1357
|
+
});
|
|
1358
|
+
floors.instanceMatrix.needsUpdate = true;
|
|
1359
|
+
group.add(floors);
|
|
1360
|
+
}
|
|
1361
|
+
return {
|
|
1362
|
+
group,
|
|
1363
|
+
obstacles,
|
|
1364
|
+
spawns,
|
|
1365
|
+
torches,
|
|
1366
|
+
floorAt(x, z) {
|
|
1367
|
+
const col = Math.round((x - originX) / unit);
|
|
1368
|
+
const row = Math.round((z - originZ) / unit);
|
|
1369
|
+
return floorCells.has(`${col},${row}`);
|
|
1370
|
+
},
|
|
1371
|
+
size: { width: width * unit, depth: height * unit }
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
function createTorch(x, z, palette, rng, light) {
|
|
1375
|
+
const torch = new Group10();
|
|
1376
|
+
torch.name = "torch";
|
|
1377
|
+
const pole = new Mesh13(
|
|
1378
|
+
new CylinderGeometry5(0.04, 0.06, 1.5, 5),
|
|
1379
|
+
new MeshStandardMaterial11({ color: palette.woodDark, flatShading: true })
|
|
1380
|
+
);
|
|
1381
|
+
pole.position.y = 0.75;
|
|
1382
|
+
torch.add(pole);
|
|
1383
|
+
const flame = new Mesh13(
|
|
1384
|
+
new SphereGeometry3(0.1, 6, 5),
|
|
1385
|
+
new MeshStandardMaterial11({
|
|
1386
|
+
color: palette.lampGlow,
|
|
1387
|
+
emissive: palette.lampGlow,
|
|
1388
|
+
emissiveIntensity: 1.8
|
|
1389
|
+
})
|
|
1390
|
+
);
|
|
1391
|
+
flame.position.y = 1.58;
|
|
1392
|
+
flame.scale.y = rng.range(1.2, 1.5);
|
|
1393
|
+
torch.add(flame);
|
|
1394
|
+
if (light) {
|
|
1395
|
+
const point = new PointLight3(palette.lampGlow, 5, 9, 1.9);
|
|
1396
|
+
point.position.y = 1.6;
|
|
1397
|
+
torch.add(point);
|
|
1398
|
+
}
|
|
1399
|
+
torch.position.set(x, 0, z);
|
|
1400
|
+
return torch;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/scene/manifest.ts
|
|
1404
|
+
import { CylinderGeometry as CylinderGeometry6, Group as Group12, Mesh as Mesh15, MeshStandardMaterial as MeshStandardMaterial12 } from "three";
|
|
1405
|
+
|
|
1406
|
+
// src/scatter/scatter.ts
|
|
1407
|
+
import {
|
|
1408
|
+
DynamicDrawUsage,
|
|
1409
|
+
Group as Group11,
|
|
1410
|
+
InstancedMesh as InstancedMesh2,
|
|
1411
|
+
Matrix4 as Matrix42,
|
|
1412
|
+
Mesh as Mesh14,
|
|
531
1413
|
Quaternion,
|
|
532
|
-
Vector3 as
|
|
1414
|
+
Vector3 as Vector34
|
|
533
1415
|
} from "three";
|
|
534
1416
|
function scatter(options) {
|
|
535
1417
|
const seed = options.seed ?? 1;
|
|
@@ -585,7 +1467,7 @@ function scatter(options) {
|
|
|
585
1467
|
const itemIndex = pickItem();
|
|
586
1468
|
const [minScale, maxScale2] = options.items[itemIndex].scale ?? [0.8, 1.25];
|
|
587
1469
|
placements.push({
|
|
588
|
-
position: new
|
|
1470
|
+
position: new Vector34(x, y, z),
|
|
589
1471
|
rotationY: rng.range(0, Math.PI * 2),
|
|
590
1472
|
scale: rng.range(minScale, maxScale2),
|
|
591
1473
|
itemIndex
|
|
@@ -595,18 +1477,59 @@ function scatter(options) {
|
|
|
595
1477
|
if (!bucket) occupied.set(key, bucket = []);
|
|
596
1478
|
bucket.push({ x, z });
|
|
597
1479
|
}
|
|
598
|
-
const group = new
|
|
1480
|
+
const group = new Group11();
|
|
599
1481
|
group.name = "scatter";
|
|
600
1482
|
const obstacles = [];
|
|
601
|
-
const placementMatrix = new
|
|
602
|
-
const composed = new
|
|
1483
|
+
const placementMatrix = new Matrix42();
|
|
1484
|
+
const composed = new Matrix42();
|
|
603
1485
|
const quaternion = new Quaternion();
|
|
604
|
-
const up = new
|
|
605
|
-
const scaleVector = new
|
|
1486
|
+
const up = new Vector34(0, 1, 0);
|
|
1487
|
+
const scaleVector = new Vector34();
|
|
1488
|
+
const emit = (variant, list, parent) => {
|
|
1489
|
+
variant.object.updateMatrixWorld(true);
|
|
1490
|
+
variant.object.traverse((child) => {
|
|
1491
|
+
if (!(child instanceof Mesh14)) return;
|
|
1492
|
+
const instanced = new InstancedMesh2(child.geometry, child.material, list.length);
|
|
1493
|
+
instanced.instanceMatrix.setUsage(DynamicDrawUsage);
|
|
1494
|
+
list.forEach((placement, index) => {
|
|
1495
|
+
quaternion.setFromAxisAngle(up, placement.rotationY);
|
|
1496
|
+
scaleVector.setScalar(placement.scale);
|
|
1497
|
+
placementMatrix.compose(placement.position, quaternion, scaleVector);
|
|
1498
|
+
composed.multiplyMatrices(placementMatrix, child.matrixWorld);
|
|
1499
|
+
instanced.setMatrixAt(index, composed);
|
|
1500
|
+
});
|
|
1501
|
+
instanced.instanceMatrix.needsUpdate = true;
|
|
1502
|
+
parent.add(instanced);
|
|
1503
|
+
});
|
|
1504
|
+
};
|
|
1505
|
+
const tileSize = options.lod?.tileSize ?? 16;
|
|
1506
|
+
const tileMap = /* @__PURE__ */ new Map();
|
|
1507
|
+
const tileFor = (x, z) => {
|
|
1508
|
+
const tx = Math.floor(x / tileSize);
|
|
1509
|
+
const tz = Math.floor(z / tileSize);
|
|
1510
|
+
const key = `${tx},${tz}`;
|
|
1511
|
+
let tile = tileMap.get(key);
|
|
1512
|
+
if (!tile) {
|
|
1513
|
+
tile = {
|
|
1514
|
+
center: { x: (tx + 0.5) * tileSize, z: (tz + 0.5) * tileSize },
|
|
1515
|
+
near: new Group11(),
|
|
1516
|
+
far: new Group11()
|
|
1517
|
+
};
|
|
1518
|
+
tile.far.visible = false;
|
|
1519
|
+
group.add(tile.near, tile.far);
|
|
1520
|
+
tileMap.set(key, tile);
|
|
1521
|
+
}
|
|
1522
|
+
return tile;
|
|
1523
|
+
};
|
|
606
1524
|
options.items.forEach((item, itemIndex) => {
|
|
607
1525
|
const variantCount = item.variants ?? 4;
|
|
608
1526
|
const variants = [];
|
|
609
1527
|
for (let v = 0; v < variantCount; v++) variants.push(item.create(rng.fork()));
|
|
1528
|
+
const useLod = options.lod !== void 0 && item.createFar !== void 0;
|
|
1529
|
+
const farVariants = [];
|
|
1530
|
+
if (useLod) {
|
|
1531
|
+
for (let v = 0; v < variantCount; v++) farVariants.push(item.createFar(rng.fork()));
|
|
1532
|
+
}
|
|
610
1533
|
const byVariant = variants.map(() => []);
|
|
611
1534
|
for (const placement of placements) {
|
|
612
1535
|
if (placement.itemIndex !== itemIndex) continue;
|
|
@@ -616,21 +1539,21 @@ function scatter(options) {
|
|
|
616
1539
|
variants.forEach((variant, v) => {
|
|
617
1540
|
const list = byVariant[v];
|
|
618
1541
|
if (list.length === 0) return;
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
group
|
|
633
|
-
}
|
|
1542
|
+
if (useLod) {
|
|
1543
|
+
const byTile = /* @__PURE__ */ new Map();
|
|
1544
|
+
for (const placement of list) {
|
|
1545
|
+
const tile = tileFor(placement.position.x, placement.position.z);
|
|
1546
|
+
let bucket = byTile.get(tile);
|
|
1547
|
+
if (!bucket) byTile.set(tile, bucket = []);
|
|
1548
|
+
bucket.push(placement);
|
|
1549
|
+
}
|
|
1550
|
+
for (const [tile, bucket] of byTile) {
|
|
1551
|
+
emit(variant, bucket, tile.near);
|
|
1552
|
+
emit(farVariants[v], bucket, tile.far);
|
|
1553
|
+
}
|
|
1554
|
+
} else {
|
|
1555
|
+
emit(variant, list, group);
|
|
1556
|
+
}
|
|
634
1557
|
if (variant.obstacleRadius > 0) {
|
|
635
1558
|
for (const placement of list) {
|
|
636
1559
|
obstacles.push({
|
|
@@ -641,22 +1564,262 @@ function scatter(options) {
|
|
|
641
1564
|
}
|
|
642
1565
|
});
|
|
643
1566
|
});
|
|
644
|
-
|
|
1567
|
+
const result = { group, placements, obstacles, count: placements.length };
|
|
1568
|
+
if (options.lod) {
|
|
1569
|
+
const tiles = [...tileMap.values()];
|
|
1570
|
+
const swapOut = options.lod.distance * 1.1;
|
|
1571
|
+
const swapIn = options.lod.distance * 0.9;
|
|
1572
|
+
result.tiles = tiles;
|
|
1573
|
+
result.update = (camera) => {
|
|
1574
|
+
for (const tile of tiles) {
|
|
1575
|
+
const dx = camera.position.x - tile.center.x;
|
|
1576
|
+
const dz = camera.position.z - tile.center.z;
|
|
1577
|
+
const distance = Math.hypot(dx, dz);
|
|
1578
|
+
if (tile.near.visible && distance > swapOut) {
|
|
1579
|
+
tile.near.visible = false;
|
|
1580
|
+
tile.far.visible = true;
|
|
1581
|
+
} else if (!tile.near.visible && distance < swapIn) {
|
|
1582
|
+
tile.near.visible = true;
|
|
1583
|
+
tile.far.visible = false;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
return result;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/scene/manifest.ts
|
|
1592
|
+
var PROP_FACTORIES = {
|
|
1593
|
+
tree: (seed, palette) => createTree({ seed, palette }),
|
|
1594
|
+
rock: (seed, palette) => createRock({ seed, palette }),
|
|
1595
|
+
bush: (seed, palette) => createBush({ seed, palette }),
|
|
1596
|
+
grass: (seed, palette) => createGrassTuft({ seed, palette }),
|
|
1597
|
+
crate: (seed, palette) => createCrate({ seed, palette }),
|
|
1598
|
+
fence: (seed, palette) => createFence({ seed, palette }),
|
|
1599
|
+
lamp: (seed, palette) => createLamp({ seed, palette })
|
|
1600
|
+
};
|
|
1601
|
+
var FAR_FACTORIES = {
|
|
1602
|
+
tree: (seed, palette) => {
|
|
1603
|
+
const rng = new Rng(seed);
|
|
1604
|
+
const group = new Group12();
|
|
1605
|
+
const cone = new Mesh15(
|
|
1606
|
+
new CylinderGeometry6(0, rng.range(1, 1.4), rng.range(2.6, 3.8), 5),
|
|
1607
|
+
new MeshStandardMaterial12({ color: rng.pick(palette.foliage), flatShading: true })
|
|
1608
|
+
);
|
|
1609
|
+
cone.position.y = cone.geometry.parameters.height / 2 + 0.3;
|
|
1610
|
+
group.add(cone);
|
|
1611
|
+
return { object: group, obstacleRadius: 0 };
|
|
1612
|
+
},
|
|
1613
|
+
bush: (seed, palette) => {
|
|
1614
|
+
const rng = new Rng(seed);
|
|
1615
|
+
const group = new Group12();
|
|
1616
|
+
const cone = new Mesh15(
|
|
1617
|
+
new CylinderGeometry6(0.1, rng.range(0.5, 0.7), rng.range(0.5, 0.8), 5),
|
|
1618
|
+
new MeshStandardMaterial12({ color: rng.pick(palette.foliage), flatShading: true })
|
|
1619
|
+
);
|
|
1620
|
+
cone.position.y = 0.3;
|
|
1621
|
+
group.add(cone);
|
|
1622
|
+
return { object: group, obstacleRadius: 0 };
|
|
1623
|
+
},
|
|
1624
|
+
grass: () => ({ object: new Group12(), obstacleRadius: 0 })
|
|
1625
|
+
};
|
|
1626
|
+
function buildScene(manifest, scene) {
|
|
1627
|
+
const seed = manifest.seed ?? 1;
|
|
1628
|
+
const palette = manifest.palette ? PALETTES[manifest.palette] : DEFAULT_PALETTE;
|
|
1629
|
+
const group = new Group12();
|
|
1630
|
+
group.name = "scena-scene";
|
|
1631
|
+
const updates = [];
|
|
1632
|
+
let terrain;
|
|
1633
|
+
if (manifest.terrain) {
|
|
1634
|
+
terrain = createTerrain({
|
|
1635
|
+
...manifest.terrain,
|
|
1636
|
+
seed,
|
|
1637
|
+
waterLevel: manifest.water?.level,
|
|
1638
|
+
palette
|
|
1639
|
+
});
|
|
1640
|
+
group.add(terrain.mesh);
|
|
1641
|
+
}
|
|
1642
|
+
const heightAt = terrain ? terrain.heightAt : () => 0;
|
|
1643
|
+
let water;
|
|
1644
|
+
if (manifest.water) {
|
|
1645
|
+
water = createWater({
|
|
1646
|
+
level: manifest.water.level,
|
|
1647
|
+
size: manifest.water.size ?? (terrain ? terrain.size * 1.4 : 200),
|
|
1648
|
+
palette
|
|
1649
|
+
});
|
|
1650
|
+
group.add(water.mesh);
|
|
1651
|
+
updates.push((dt) => water.update(dt));
|
|
1652
|
+
}
|
|
1653
|
+
const dryLand = terrain && water ? aboveWater(terrain, water, 0.3) : () => true;
|
|
1654
|
+
let sky;
|
|
1655
|
+
if (manifest.sky ?? true) {
|
|
1656
|
+
sky = createSky({ palette });
|
|
1657
|
+
group.add(sky.mesh);
|
|
1658
|
+
}
|
|
1659
|
+
const rig = createLightingRig(manifest.lighting ?? "day");
|
|
1660
|
+
group.add(rig.group);
|
|
1661
|
+
if (scene && manifest.fog !== false) applyFog(scene, manifest.fog ?? "haze", palette);
|
|
1662
|
+
const paths = (manifest.paths ?? []).map((spec) => {
|
|
1663
|
+
const path = createPath(spec.points, {
|
|
1664
|
+
surface: heightAt,
|
|
1665
|
+
width: spec.width,
|
|
1666
|
+
loop: spec.loop,
|
|
1667
|
+
palette
|
|
1668
|
+
});
|
|
1669
|
+
group.add(path.mesh);
|
|
1670
|
+
return path;
|
|
1671
|
+
});
|
|
1672
|
+
const onAnyPath = (x, z) => paths.some((p) => p.contains(x, z));
|
|
1673
|
+
let village;
|
|
1674
|
+
if (manifest.village) {
|
|
1675
|
+
village = createVillage({
|
|
1676
|
+
...manifest.village,
|
|
1677
|
+
seed: seed + 1,
|
|
1678
|
+
surface: heightAt,
|
|
1679
|
+
mask: (x, z) => dryLand(x, z) && !onAnyPath(x, z),
|
|
1680
|
+
palette
|
|
1681
|
+
});
|
|
1682
|
+
group.add(village.group);
|
|
1683
|
+
}
|
|
1684
|
+
const wind = manifest.wind ?? true;
|
|
1685
|
+
const scatters = (manifest.scatters ?? []).map((spec, index) => {
|
|
1686
|
+
const inset = terrain ? terrain.size * 0.45 : 40;
|
|
1687
|
+
const avoidWater = spec.avoidWater ?? true;
|
|
1688
|
+
const avoidPaths = spec.avoidPaths ?? true;
|
|
1689
|
+
const result = scatter({
|
|
1690
|
+
seed: seed + 10 + index,
|
|
1691
|
+
area: spec.area ?? { min: { x: -inset, z: -inset }, max: { x: inset, z: inset } },
|
|
1692
|
+
surface: heightAt,
|
|
1693
|
+
density: spec.density,
|
|
1694
|
+
count: spec.count,
|
|
1695
|
+
minSpacing: spec.minSpacing,
|
|
1696
|
+
clumpScale: spec.clumpScale,
|
|
1697
|
+
lod: spec.lod,
|
|
1698
|
+
items: spec.items.map((item) => {
|
|
1699
|
+
const far = spec.lod ? FAR_FACTORIES[item.type] : void 0;
|
|
1700
|
+
return {
|
|
1701
|
+
create: (rng) => PROP_FACTORIES[item.type](rng.int(1, 1e9), palette),
|
|
1702
|
+
createFar: far && ((rng) => far(rng.int(1, 1e9), palette)),
|
|
1703
|
+
weight: item.weight,
|
|
1704
|
+
variants: item.variants,
|
|
1705
|
+
scale: item.scale
|
|
1706
|
+
};
|
|
1707
|
+
}),
|
|
1708
|
+
mask: (x, z, y) => (spec.maxHeight === void 0 || y < spec.maxHeight) && (!avoidWater || dryLand(x, z)) && (!avoidPaths || !onAnyPath(x, z)),
|
|
1709
|
+
keepOut: [
|
|
1710
|
+
...avoidPaths ? paths.flatMap((p) => p.keepOut) : [],
|
|
1711
|
+
...village ? village.keepOut : []
|
|
1712
|
+
]
|
|
1713
|
+
});
|
|
1714
|
+
group.add(result.group);
|
|
1715
|
+
if (wind !== false) {
|
|
1716
|
+
const sway = applyWind(result.group, {
|
|
1717
|
+
strength: typeof wind === "object" ? wind.strength ?? 0.05 : 0.05
|
|
1718
|
+
});
|
|
1719
|
+
updates.push((dt) => sway.update(dt));
|
|
1720
|
+
}
|
|
1721
|
+
return result;
|
|
1722
|
+
});
|
|
1723
|
+
let cycle;
|
|
1724
|
+
if (manifest.dayCycle) {
|
|
1725
|
+
cycle = createDayCycle({
|
|
1726
|
+
sky,
|
|
1727
|
+
rig,
|
|
1728
|
+
scene,
|
|
1729
|
+
lamps: village?.lamps,
|
|
1730
|
+
palette,
|
|
1731
|
+
dayLength: manifest.dayCycle.dayLength,
|
|
1732
|
+
timeOfDay: manifest.dayCycle.timeOfDay
|
|
1733
|
+
});
|
|
1734
|
+
updates.push((dt) => cycle.update(dt));
|
|
1735
|
+
}
|
|
1736
|
+
scene?.add(group);
|
|
1737
|
+
return {
|
|
1738
|
+
group,
|
|
1739
|
+
palette,
|
|
1740
|
+
terrain,
|
|
1741
|
+
water,
|
|
1742
|
+
sky,
|
|
1743
|
+
rig,
|
|
1744
|
+
cycle,
|
|
1745
|
+
paths,
|
|
1746
|
+
village,
|
|
1747
|
+
scatters,
|
|
1748
|
+
obstacles: [
|
|
1749
|
+
...village ? village.obstacles : [],
|
|
1750
|
+
...scatters.flatMap((s) => s.obstacles)
|
|
1751
|
+
],
|
|
1752
|
+
heightAt,
|
|
1753
|
+
update(dt) {
|
|
1754
|
+
for (const fn of updates) fn(dt);
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/scene/markers.ts
|
|
1760
|
+
import { Vector3 as Vector35 } from "three";
|
|
1761
|
+
function extractMarkers(root) {
|
|
1762
|
+
root.updateWorldMatrix(true, true);
|
|
1763
|
+
const spawns = {};
|
|
1764
|
+
const routePoints = {};
|
|
1765
|
+
const obstacles = [];
|
|
1766
|
+
const keepOut = [];
|
|
1767
|
+
root.traverse((node) => {
|
|
1768
|
+
const name = node.name.replace(/\.\d+$/, "");
|
|
1769
|
+
const match = /^(spawn|route|obstacle|keepout)_(.+)$/.exec(name);
|
|
1770
|
+
if (!match) return;
|
|
1771
|
+
const [, kind, rest] = match;
|
|
1772
|
+
const position = node.getWorldPosition(new Vector35());
|
|
1773
|
+
const radius = Math.max(node.scale.x, node.scale.z);
|
|
1774
|
+
if (kind === "spawn") {
|
|
1775
|
+
spawns[rest] = position;
|
|
1776
|
+
} else if (kind === "route") {
|
|
1777
|
+
const step = /^(.*)_(\d+)$/.exec(rest);
|
|
1778
|
+
const routeName = step ? step[1] : rest;
|
|
1779
|
+
const index = step ? parseInt(step[2], 10) : 0;
|
|
1780
|
+
(routePoints[routeName] ?? (routePoints[routeName] = [])).push({ index, position });
|
|
1781
|
+
} else if (kind === "obstacle") {
|
|
1782
|
+
obstacles.push({ center: position, radius });
|
|
1783
|
+
} else {
|
|
1784
|
+
keepOut.push({ center: { x: position.x, z: position.z }, radius });
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
const routes = {};
|
|
1788
|
+
for (const [name, points] of Object.entries(routePoints)) {
|
|
1789
|
+
routes[name] = points.sort((a, b) => a.index - b.index).map((p) => p.position);
|
|
1790
|
+
}
|
|
1791
|
+
return { spawns, routes, obstacles, keepOut };
|
|
645
1792
|
}
|
|
646
1793
|
export {
|
|
647
1794
|
DEFAULT_PALETTE,
|
|
1795
|
+
KIT_UNIT,
|
|
648
1796
|
PALETTES,
|
|
649
1797
|
Rng,
|
|
1798
|
+
aboveWater,
|
|
650
1799
|
applyFog,
|
|
1800
|
+
applyWind,
|
|
1801
|
+
assembleKit,
|
|
1802
|
+
buildScene,
|
|
651
1803
|
collectObstacles,
|
|
1804
|
+
createBush,
|
|
652
1805
|
createCrate,
|
|
1806
|
+
createDayCycle,
|
|
653
1807
|
createFence,
|
|
1808
|
+
createGrassTuft,
|
|
1809
|
+
createHouse,
|
|
654
1810
|
createLamp,
|
|
655
1811
|
createLightingRig,
|
|
1812
|
+
createPath,
|
|
656
1813
|
createRock,
|
|
1814
|
+
createRuin,
|
|
657
1815
|
createSky,
|
|
658
1816
|
createTerrain,
|
|
1817
|
+
createTower,
|
|
659
1818
|
createTree,
|
|
1819
|
+
createVillage,
|
|
1820
|
+
createWater,
|
|
1821
|
+
createWell,
|
|
1822
|
+
extractMarkers,
|
|
660
1823
|
fractalNoise2,
|
|
661
1824
|
hash2,
|
|
662
1825
|
scatter,
|