scena3d 0.3.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/dist/index.js CHANGED
@@ -1261,16 +1261,158 @@ function createVillage(options = {}) {
1261
1261
  };
1262
1262
  }
1263
1263
 
1264
- // src/scatter/scatter.ts
1264
+ // src/kits/kit.ts
1265
1265
  import {
1266
- DynamicDrawUsage,
1266
+ BoxGeometry as BoxGeometry5,
1267
+ CylinderGeometry as CylinderGeometry5,
1267
1268
  Group as Group10,
1268
1269
  InstancedMesh,
1269
1270
  Matrix4,
1270
1271
  Mesh as Mesh13,
1271
- Quaternion,
1272
+ MeshStandardMaterial as MeshStandardMaterial11,
1273
+ PointLight as PointLight3,
1274
+ SphereGeometry as SphereGeometry3,
1272
1275
  Vector3 as Vector33
1273
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,
1413
+ Quaternion,
1414
+ Vector3 as Vector34
1415
+ } from "three";
1274
1416
  function scatter(options) {
1275
1417
  const seed = options.seed ?? 1;
1276
1418
  const rng = new Rng(seed);
@@ -1325,7 +1467,7 @@ function scatter(options) {
1325
1467
  const itemIndex = pickItem();
1326
1468
  const [minScale, maxScale2] = options.items[itemIndex].scale ?? [0.8, 1.25];
1327
1469
  placements.push({
1328
- position: new Vector33(x, y, z),
1470
+ position: new Vector34(x, y, z),
1329
1471
  rotationY: rng.range(0, Math.PI * 2),
1330
1472
  scale: rng.range(minScale, maxScale2),
1331
1473
  itemIndex
@@ -1335,18 +1477,59 @@ function scatter(options) {
1335
1477
  if (!bucket) occupied.set(key, bucket = []);
1336
1478
  bucket.push({ x, z });
1337
1479
  }
1338
- const group = new Group10();
1480
+ const group = new Group11();
1339
1481
  group.name = "scatter";
1340
1482
  const obstacles = [];
1341
- const placementMatrix = new Matrix4();
1342
- const composed = new Matrix4();
1483
+ const placementMatrix = new Matrix42();
1484
+ const composed = new Matrix42();
1343
1485
  const quaternion = new Quaternion();
1344
- const up = new Vector33(0, 1, 0);
1345
- const scaleVector = new Vector33();
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
+ };
1346
1524
  options.items.forEach((item, itemIndex) => {
1347
1525
  const variantCount = item.variants ?? 4;
1348
1526
  const variants = [];
1349
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
+ }
1350
1533
  const byVariant = variants.map(() => []);
1351
1534
  for (const placement of placements) {
1352
1535
  if (placement.itemIndex !== itemIndex) continue;
@@ -1356,21 +1539,21 @@ function scatter(options) {
1356
1539
  variants.forEach((variant, v) => {
1357
1540
  const list = byVariant[v];
1358
1541
  if (list.length === 0) return;
1359
- variant.object.updateMatrixWorld(true);
1360
- variant.object.traverse((child) => {
1361
- if (!(child instanceof Mesh13)) return;
1362
- const instanced = new InstancedMesh(child.geometry, child.material, list.length);
1363
- instanced.instanceMatrix.setUsage(DynamicDrawUsage);
1364
- list.forEach((placement, index) => {
1365
- quaternion.setFromAxisAngle(up, placement.rotationY);
1366
- scaleVector.setScalar(placement.scale);
1367
- placementMatrix.compose(placement.position, quaternion, scaleVector);
1368
- composed.multiplyMatrices(placementMatrix, child.matrixWorld);
1369
- instanced.setMatrixAt(index, composed);
1370
- });
1371
- instanced.instanceMatrix.needsUpdate = true;
1372
- group.add(instanced);
1373
- });
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
+ }
1374
1557
  if (variant.obstacleRadius > 0) {
1375
1558
  for (const placement of list) {
1376
1559
  obstacles.push({
@@ -1381,15 +1564,242 @@ function scatter(options) {
1381
1564
  }
1382
1565
  });
1383
1566
  });
1384
- return { group, placements, obstacles, count: placements.length };
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 };
1385
1792
  }
1386
1793
  export {
1387
1794
  DEFAULT_PALETTE,
1795
+ KIT_UNIT,
1388
1796
  PALETTES,
1389
1797
  Rng,
1390
1798
  aboveWater,
1391
1799
  applyFog,
1392
1800
  applyWind,
1801
+ assembleKit,
1802
+ buildScene,
1393
1803
  collectObstacles,
1394
1804
  createBush,
1395
1805
  createCrate,
@@ -1409,6 +1819,7 @@ export {
1409
1819
  createVillage,
1410
1820
  createWater,
1411
1821
  createWell,
1822
+ extractMarkers,
1412
1823
  fractalNoise2,
1413
1824
  hash2,
1414
1825
  scatter,