From b12debac8c54a7ae4ab0b8b11977a9b85e07641b Mon Sep 17 00:00:00 2001 From: Thomas Burgess Date: Fri, 1 Mar 2024 04:12:05 -0600 Subject: [PATCH] Cleanup game.ts and dynamic terrain initialization (#9) * in progress * update readme * cleaned up game.ts --- README.md | 16 +- build/index.js | 528 ++++++++++-------- index.ts | 4 +- package.json | 1 + src/components/dynamicTerrain.component.ts | 7 + src/components/map.component.ts | 12 + src/components/track.component.ts | 7 +- src/components/tree.component.ts | 2 + .../babylon.dynamicTerrain_modular.ts | 97 ++-- src/game.ts | 146 +---- src/systems/index.ts | 1 + src/systems/mapInit.system.ts | 92 +++ .../terrain/dynamicTerrainInit.system.ts | 153 ++--- src/systems/terrain/tree/treeInit.system.ts | 1 + src/systems/terrain/tree/treePlacer.system.ts | 10 +- src/systems/track/trackInit.system.ts | 91 +++ 16 files changed, 683 insertions(+), 485 deletions(-) create mode 100644 src/components/map.component.ts create mode 100644 src/systems/mapInit.system.ts diff --git a/README.md b/README.md index 449828b..ca43579 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,14 @@ You can always try the latest build here: https://thomasburgess2000.github.io/bo ## TODO: - [ ] Make perimetric LOD - [ ] Create new map data when the camera moves outside the current map -- [x] Fix tree spawning -- [ ] Add trees to the dynamic terrain -- [ ] Add walkable player -- [x] Make larger map -- [ ] Clean up dynamic terrain system (move params to component) +- [ ] Fix tree spawning in the middle of tracks +- [ ] make the trees generate/dispose as you move +- [ ] Add walkable player using Havok physics - [ ] Dispose far away track - [ ] Add wheels to train -- [ ] Be able to lower maxSubZ without terrain clipping through track (fix flattening falloff probably?) -- [ ] Make fog work with dynamic terrain shader \ No newline at end of file +- [ ] Be able to lower maxSubZ without terrain clipping through track and trees at wrong height (aspirational) +- [ ] Make fog work with dynamic terrain shader +- [x] Clean up game.ts +- [x] Make larger map +- [x] Clean up dynamic terrain system (move params to component) +- [x] Add trees to the dynamic terrain \ No newline at end of file diff --git a/build/index.js b/build/index.js index 48dbeb2..32e8760 100644 --- a/build/index.js +++ b/build/index.js @@ -275469,9 +275469,9 @@ var require_babylon_guiEditor_max = __commonJS((exports, module) => { } return true; } - function attemptReplayContinuousQueuedEventInMap(queuedEvent, key, map) { + function attemptReplayContinuousQueuedEventInMap(queuedEvent, key, map2) { if (attemptReplayContinuousQueuedEvent(queuedEvent)) { - map.delete(key); + map2.delete(key); } } function replayUnblockedEvents() { @@ -288697,14 +288697,14 @@ var require_babylon_guiEditor_max = __commonJS((exports, module) => { var findHostInstancesForRefresh = function(root2, families) { { var hostInstances = new Set; - var types14 = new Set(families.map(function(family) { + var types16 = new Set(families.map(function(family) { return family.current; })); - findHostInstancesForMatchingFibersRecursively(root2.current, types14, hostInstances); + findHostInstancesForMatchingFibersRecursively(root2.current, types16, hostInstances); return hostInstances; } }; - function findHostInstancesForMatchingFibersRecursively(fiber, types14, hostInstances) { + function findHostInstancesForMatchingFibersRecursively(fiber, types16, hostInstances) { { var { child, sibling, tag, type } = fiber; var candidateType = null; @@ -288720,7 +288720,7 @@ var require_babylon_guiEditor_max = __commonJS((exports, module) => { } var didMatch = false; if (candidateType !== null) { - if (types14.has(candidateType)) { + if (types16.has(candidateType)) { didMatch = true; } } @@ -288728,11 +288728,11 @@ var require_babylon_guiEditor_max = __commonJS((exports, module) => { findHostInstancesForFiberShallowly(fiber, hostInstances); } else { if (child !== null) { - findHostInstancesForMatchingFibersRecursively(child, types14, hostInstances); + findHostInstancesForMatchingFibersRecursively(child, types16, hostInstances); } } if (sibling !== null) { - findHostInstancesForMatchingFibersRecursively(sibling, types14, hostInstances); + findHostInstancesForMatchingFibersRecursively(sibling, types16, hostInstances); } } } @@ -333803,9 +333803,9 @@ var require_babylon_inspector_bundle_max = __commonJS((exports, module) => { } return true; } - function attemptReplayContinuousQueuedEventInMap(queuedEvent, key, map) { + function attemptReplayContinuousQueuedEventInMap(queuedEvent, key, map2) { if (attemptReplayContinuousQueuedEvent(queuedEvent)) { - map.delete(key); + map2.delete(key); } } function replayUnblockedEvents() { @@ -347031,14 +347031,14 @@ var require_babylon_inspector_bundle_max = __commonJS((exports, module) => { var findHostInstancesForRefresh = function(root2, families) { { var hostInstances = new Set; - var types14 = new Set(families.map(function(family) { + var types16 = new Set(families.map(function(family) { return family.current; })); - findHostInstancesForMatchingFibersRecursively(root2.current, types14, hostInstances); + findHostInstancesForMatchingFibersRecursively(root2.current, types16, hostInstances); return hostInstances; } }; - function findHostInstancesForMatchingFibersRecursively(fiber, types14, hostInstances) { + function findHostInstancesForMatchingFibersRecursively(fiber, types16, hostInstances) { { var { child, sibling, tag, type } = fiber; var candidateType = null; @@ -347054,7 +347054,7 @@ var require_babylon_inspector_bundle_max = __commonJS((exports, module) => { } var didMatch = false; if (candidateType !== null) { - if (types14.has(candidateType)) { + if (types16.has(candidateType)) { didMatch = true; } } @@ -347062,11 +347062,11 @@ var require_babylon_inspector_bundle_max = __commonJS((exports, module) => { findHostInstancesForFiberShallowly(fiber, hostInstances); } else { if (child !== null) { - findHostInstancesForMatchingFibersRecursively(child, types14, hostInstances); + findHostInstancesForMatchingFibersRecursively(child, types16, hostInstances); } } if (sibling !== null) { - findHostInstancesForMatchingFibersRecursively(sibling, types14, hostInstances); + findHostInstancesForMatchingFibersRecursively(sibling, types16, hostInstances); } } } @@ -356803,7 +356803,7 @@ var require_babylon_inspector_bundle_max = __commonJS((exports, module) => { return value.getClassName(); } render() { - const types14 = ["Float", "Vector2", "Vector3", "Quaternion", "Color3", "Color4"]; + const types16 = ["Float", "Vector2", "Vector3", "Quaternion", "Color3", "Color4"]; const loopModes = ["Cycle", "Relative", "Relative from current", "Constant"]; const modes = ["Custom", "List"]; const properties = []; @@ -356852,7 +356852,7 @@ var require_babylon_inspector_bundle_max = __commonJS((exports, module) => { this.setState({ customPropertyMode: evt.currentTarget.value === "Custom" }); }, children: modes.map((mode, i) => { return (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("option", { value: mode, title: mode, children: mode }, mode + i); - }) }), customPropertyMode && (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment, { children: [(0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("input", { type: "text", id: "add-animation-property", ref: this._property, className: "input-text", defaultValue: "" }), (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("select", { id: "add-animation-type", className: "option", ref: this._typeElement, children: types14.map((type, i) => { + }) }), customPropertyMode && (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment, { children: [(0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("input", { type: "text", id: "add-animation-property", ref: this._property, className: "input-text", defaultValue: "" }), (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("select", { id: "add-animation-type", className: "option", ref: this._typeElement, children: types16.map((type, i) => { return (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("option", { value: type, title: type, children: type }, type + i); }) })] }), !customPropertyMode && (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment, { children: [(0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("select", { id: "add-animation-property", className: "option", ref: this._propertylement, onClick: () => { this.forceUpdate(); @@ -387209,13 +387209,13 @@ var InitializationStatus; // src/components/track.component.ts class TrackComponent { - points; - sections; + trackDefinition; initializationStatus = InitializationStatus.NotInitialized; rotations = []; - constructor(points, sections) { - this.points = points; - this.sections = sections; + points = []; + sections = []; + constructor(trackDefinition) { + this.trackDefinition = trackDefinition; } } @@ -387240,6 +387240,8 @@ class Section { // src/systems/track/trackInit.system.ts init_core(); +var TRACK_HEIGHT = 0.5; + class TrackInitSystem extends import_tick_knock2.IterativeSystem { constructor() { super((entity) => entity.hasComponent(TrackComponent)); @@ -387250,6 +387252,10 @@ class TrackInitSystem extends import_tick_knock2.IterativeSystem { return; } trackComponent.initializationStatus = InitializationStatus.Initializing; + const trackComponentDefinition = trackComponent.trackDefinition; + const { points, sections } = this.createPointsAndSections(trackComponentDefinition); + trackComponent.points = points; + trackComponent.sections = sections; const track2 = this.createTrack(trackComponent.points, trackComponent.sections); this.createMeshes(track2, trackComponent.points); trackComponent.rotations = track2.rotations.map(Quaternion.FromRotationMatrix); @@ -387262,6 +387268,68 @@ class TrackInitSystem extends import_tick_knock2.IterativeSystem { const node30 = new TransformNode("trackParent"); return node30; } + createPointsAndSections(trackDefinition) { + const points = []; + const n = 100; + let sections = []; + for (let trackSection of trackDefinition) { + const section = new Section(points.length); + if (trackSection === "straight" || trackSection === "w" || trackSection === "s") { + const newPoints = this.addStraightSection(points, n); + points.push(...newPoints); + } else if (trackSection === "left" || trackSection === "a" || trackSection === "l") { + const newPoints = this.addCurveSection(points, "left", 90); + points.push(...newPoints); + } else if (trackSection === "right" || trackSection === "d" || trackSection === "r") { + const newPoints = this.addCurveSection(points, "right", 90); + points.push(...newPoints); + } else { + console.error("Invalid track section"); + } + sections.push(section); + } + return { points, sections }; + } + addCurveSection(points, turnDirection, turnAngle) { + const currentPoint = points[points.length - 1]; + let secondLastPoint = points[points.length - 2]; + let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); + const initialAngle = Math.atan2(lastDirection.z, lastDirection.x); + const turnAngleRadians = turnAngle * Math.PI / 180; + const ratio = 1.1666666666666667; + const r = 30; + const curvePoints = Math.round(ratio * Math.abs(turnAngle)); + let newPoints = []; + for (let i = 1;i < curvePoints; i++) { + let angle = turnAngleRadians * (i / curvePoints); + let dx = r * Math.sin(angle); + let dz = r * (1 - Math.cos(angle)); + if (turnDirection === "left") { + dz = -dz; + } + let rotatedDx = dx * Math.cos(initialAngle) - dz * Math.sin(initialAngle); + let rotatedDz = dx * Math.sin(initialAngle) + dz * Math.cos(initialAngle); + let newPoint = currentPoint.add(new Vector3(rotatedDx, 0, rotatedDz)); + newPoints.push(newPoint); + } + return newPoints; + } + addStraightSection(points, n) { + let newPoints = []; + if (points.length === 0) { + for (let i = 0;i < n; i++) { + newPoints.push(new Vector3(i * 0.5, TRACK_HEIGHT, 0)); + } + } else { + const currentPoint = points[points.length - 1]; + let secondLastPoint = points[points.length - 2]; + let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); + for (let i = 0;i < n; i++) { + newPoints.push(currentPoint.add(lastDirection.scale((i + 1) * 0.5))); + } + } + return newPoints; + } createTrack(points, sections) { const trackData = { directions: [], @@ -408407,6 +408475,13 @@ class DynamicTerrainComponent { flatPoints; dynamicTerrain = null; initializationStatus = InitializationStatus.NotInitialized; + mapSubX = 1000; + mapSubZ = 1000; + seed = 0.3; + noiseScale = 0.005; + elevationScale = 2; + flatRadius = 20; + noiseResolution = 1; constructor(flatPoints = []) { this.flatPoints = flatPoints; } @@ -409393,18 +409468,18 @@ class DynamicTerrain { return DynamicTerrain._GetHeightFromMap(x, z, mapData, mapSubX, mapSubZ, mapSizeX, mapSizeZ, options, inverted); } static _GetHeightFromMap(x, z, mapData, mapSubX, mapSubZ, mapSizeX, mapSizeZ, options, inverted) { - const x0 = mapData[0]; - const z0 = mapData[2]; + let x0 = mapData[0]; + let z0 = mapData[2]; x = x - Math.floor((x - x0) / mapSizeX) * mapSizeX; z = z - Math.floor((z - z0) / mapSizeZ) * mapSizeZ; - const col1 = Math.floor((x - x0) * mapSubX / mapSizeX); - const row1 = Math.floor((z - z0) * mapSubZ / mapSizeZ); - const col2 = (col1 + 1) % mapSubX; - const row2 = (row1 + 1) % mapSubZ; - const idx1 = 3 * (row1 * mapSubX + col1); - const idx2 = 3 * (row1 * mapSubX + col2); - const idx3 = 3 * (row2 * mapSubX + col1); - const idx4 = 3 * (row2 * mapSubX + col2); + let col1 = Math.floor((x - x0) * mapSubX / mapSizeX); + let row1 = Math.floor((z - z0) * mapSubZ / mapSizeZ); + let col2 = (col1 + 1) % mapSubX; + let row2 = (row1 + 1) % mapSubZ; + let idx1 = 3 * (row1 * mapSubX + col1); + let idx2 = 3 * (row1 * mapSubX + col2); + let idx3 = 3 * (row2 * mapSubX + col1); + let idx4 = 3 * (row2 * mapSubX + col2); const v12 = DynamicTerrain._v1; const v22 = DynamicTerrain._v2; const v3 = DynamicTerrain._v3; @@ -409413,45 +409488,39 @@ class DynamicTerrain { v22.copyFromFloats(mapData[idx2], mapData[idx2 + 1], mapData[idx2 + 2]); v3.copyFromFloats(mapData[idx3], mapData[idx3 + 1], mapData[idx3 + 2]); v4.copyFromFloats(mapData[idx4], mapData[idx4 + 1], mapData[idx4 + 2]); - const vAvB = DynamicTerrain._vAvB; - const vAvC = DynamicTerrain._vAvC; - const norm = DynamicTerrain._norm; - const vA = v12; - let vB; - let vC; - let v; - const xv4v1 = v4.x - v12.x; - const zv4v1 = v4.z - v12.z; - if (xv4v1 == 0 || zv4v1 == 0) { - return v12.y; - } - const cd = zv4v1 / xv4v1; - const h = v12.z - cd * v12.x; - if (z < cd * x + h) { - vB = v4; - vC = v22; - v = vA; - } else { - vB = v3; - vC = v4; - v = vB; - } - vB.subtractToRef(vA, vAvB); - vC.subtractToRef(vA, vAvC); - Vector3.CrossToRef(vAvB, vAvC, norm); - norm.normalize(); - if (inverted) { - norm.scaleInPlace(-1); + let isInUpperTriangle = (z - v12.z) / (v3.z - v12.z) < (x - v12.x) / (v22.x - v12.x); + let height; + if (isInUpperTriangle) { + height = DynamicTerrain._BarycentricInterpolation(x, z, v12, v22, v4); + } else { + height = DynamicTerrain._BarycentricInterpolation(x, z, v12, v3, v4); } if (options && options.normal) { - options.normal.copyFrom(norm); - } - const d = -(norm.x * v.x + norm.y * v.y + norm.z * v.z); - let y = v.y; - if (norm.y != 0) { - y = -(norm.x * x + norm.z * z + d) / norm.y; + const edge1 = DynamicTerrain._vAvB; + const edge2 = DynamicTerrain._vAvC; + const normal = DynamicTerrain._norm; + if (isInUpperTriangle) { + v22.subtractToRef(v12, edge1); + v4.subtractToRef(v12, edge2); + } else { + v3.subtractToRef(v12, edge1); + v4.subtractToRef(v12, edge2); + } + Vector3.CrossToRef(edge1, edge2, normal); + normal.normalize(); + if (inverted) { + normal.scaleInPlace(-1); + } + options.normal.copyFrom(normal); } - return y; + return height; + } + static _BarycentricInterpolation(x, z, v12, v22, v3) { + let det = (v22.z - v3.z) * (v12.x - v3.x) + (v3.x - v22.x) * (v12.z - v3.z); + let l1 = ((v22.z - v3.z) * (x - v3.x) + (v3.x - v22.x) * (z - v3.z)) / det; + let l2 = ((v3.z - v12.z) * (x - v3.x) + (v12.x - v3.x) * (z - v3.z)) / det; + let l3 = 1 - l1 - l2; + return l1 * v12.y + l2 * v22.y + l3 * v3.y; } static ComputeNormalsFromMapToRef(mapData, mapSubX, mapSubZ, normals, inverted) { const mapIndices = []; @@ -409929,48 +409998,33 @@ class DynamicTerrainInitSystem extends import_tick_knock10.IterativeSystem { return; } dynamicTerrainComponent.initializationStatus = InitializationStatus.Initializing; - const mapSubX = 1000; - const mapSubZ = 400; - const seed = 0.3; - const noiseScale = 0.005; - const elevationScale = 2; - const prng = import_alea.default(seed); + const prng = import_alea.default(dynamicTerrainComponent.seed); const noise2D = createNoise2D(prng); - const mapData = this.makeMapData(mapSubX, mapSubZ, noise2D, noiseScale, elevationScale, dynamicTerrainComponent.flatPoints, 20); + const mapData = this.makeMapData(dynamicTerrainComponent.mapSubX, dynamicTerrainComponent.mapSubZ, noise2D, dynamicTerrainComponent.noiseScale, dynamicTerrainComponent.elevationScale, dynamicTerrainComponent.flatPoints, 20); const mapParams = { mapData, - mapSubX, - mapSubZ, + mapSubX: dynamicTerrainComponent.mapSubX, + mapSubZ: dynamicTerrainComponent.mapSubZ, terrainSub: MAX_VIEW_DISTANCE * 2 }; const terrain = new DynamicTerrain("dynamicTerrain", mapParams, scene36); dynamicTerrainComponent.dynamicTerrain = terrain; - const terrainWidth = mapSubX; - const terrainHeight = mapSubZ; - const noiseResolution = 1; - const noiseValues = new Float32Array(terrainWidth * terrainHeight); - for (let z = 0;z < terrainHeight; z++) { - for (let x = 0;x < terrainWidth; x++) { - const noiseValue = noise2D(x * noiseResolution, z * noiseResolution); - const normalizedNoiseValue = (noiseValue + 1) / 2; - noiseValues[z * terrainWidth + x] = normalizedNoiseValue; - } - } - const noiseTextureWidth = Math.sqrt(mapSubX * mapSubZ); - const noiseTextureHeight = noiseTextureWidth; - const noiseTextureData = new Uint8Array(noiseTextureWidth * noiseTextureHeight * 4); - for (let i = 0, l = noiseTextureData.length;i < l; i += 4) { - const noiseValue = Math.floor(i / 4); - const normalizedNoiseValue = Math.round(noiseValues[noiseValue] * 255); - noiseTextureData[i] = normalizedNoiseValue; - noiseTextureData[i + 1] = normalizedNoiseValue; - noiseTextureData[i + 2] = normalizedNoiseValue; - noiseTextureData[i + 3] = 255; - } - const noiseTexture = new RawTexture(noiseTextureData, noiseTextureWidth, noiseTextureHeight, Engine2.TEXTUREFORMAT_RGBA, scene36, false, false, Texture.TRILINEAR_SAMPLINGMODE); + const noiseTexture = this.makeTexture(dynamicTerrainComponent, noise2D); const terrainShaderMaterial = await this.makeShaders(noiseTexture); dynamicTerrainComponent.dynamicTerrain.mesh.material = terrainShaderMaterial; dynamicTerrainComponent.dynamicTerrain.mesh.isPickable = false; + dynamicTerrainComponent.dynamicTerrain.beforeUpdate = () => { + const _activeCamera = scene36.activeCamera; + if (!_activeCamera) { + console.error("No active camera"); + return; + } + const offset = Vector3.Center(_activeCamera.position, _activeCamera.target); + terrain.shiftFromCamera = { + x: -offset._x, + z: -offset._z + }; + }; dynamicTerrainComponent.initializationStatus = InitializationStatus.Initialized; } layeredNoise(x, z, noise2D, noiseScale, elevationScale) { @@ -409978,17 +410032,6 @@ class DynamicTerrainInitSystem extends import_tick_knock10.IterativeSystem { const detailNoise = noise2D(x * (noiseScale * 2), z * (noiseScale * 2)) * (elevationScale / 2); return baseNoise + detailNoise; } - getColorForHeight(height) { - const seaLevel = 0; - const mountainLevel = 20; - if (height < seaLevel) { - return new Color3(0, 0, 1); - } else if (height < mountainLevel) { - return new Color3(0, 1, 0); - } else { - return new Color3(1, 1, 1); - } - } makeMapData(mapSubX, mapSubZ, noise2D, noiseScale, elevationScale, trackPoints, flatRadius) { const mapData = new Float32Array(mapSubX * mapSubZ * 3); for (let l = 0;l < mapSubZ; l++) { @@ -410013,6 +410056,32 @@ class DynamicTerrainInitSystem extends import_tick_knock10.IterativeSystem { } return mapData; } + makeTexture(dynamicTerrainComponent, noise2D) { + const terrainWidth = dynamicTerrainComponent.mapSubX; + const terrainHeight = dynamicTerrainComponent.mapSubZ; + const noiseResolution = 1; + const noiseValues = new Float32Array(terrainWidth * terrainHeight); + for (let z = 0;z < terrainHeight; z++) { + for (let x = 0;x < terrainWidth; x++) { + const noiseValue = noise2D(x * noiseResolution, z * noiseResolution); + const normalizedNoiseValue = (noiseValue + 1) / 2; + noiseValues[z * terrainWidth + x] = normalizedNoiseValue; + } + } + const noiseTextureWidth = Math.sqrt(dynamicTerrainComponent.mapSubX * dynamicTerrainComponent.mapSubZ); + const noiseTextureHeight = noiseTextureWidth; + const noiseTextureData = new Uint8Array(noiseTextureWidth * noiseTextureHeight * 4); + for (let i = 0, l = noiseTextureData.length;i < l; i += 4) { + const noiseValue = Math.floor(i / 4); + const normalizedNoiseValue = Math.round(noiseValues[noiseValue] * 255); + noiseTextureData[i] = normalizedNoiseValue; + noiseTextureData[i + 1] = normalizedNoiseValue; + noiseTextureData[i + 2] = normalizedNoiseValue; + noiseTextureData[i + 3] = 255; + } + const noiseTexture = new RawTexture(noiseTextureData, noiseTextureWidth, noiseTextureHeight, Engine2.TEXTUREFORMAT_RGBA, scene36, false, false, Texture.TRILINEAR_SAMPLINGMODE); + return noiseTexture; + } async makeShaders(noiseTexture) { const vertexShader = await loadShader("assets/shaders/terrain/terrainVertexShader.glsl"); const fragmentShader = await loadShader("assets/shaders/terrain/terrainFragmentShader.glsl"); @@ -410056,6 +410125,7 @@ var import_tick_knock11 = __toESM(require_lib(), 1); // src/components/tree.component.ts class TreeComponent { + treeInstance = null; initializationStatus = InitializationStatus.NotInitialized; constructor() { } @@ -410134,6 +410204,7 @@ class TreeInitSystem extends import_tick_knock11.IterativeSystem { trunkInstance.setParent(treeInstance); foliageInstance.alwaysSelectAsActiveMesh = true; trunkInstance.alwaysSelectAsActiveMesh = true; + treeComponent.treeInstance = treeInstance; const transformNodeComponent = new TransformNodeComponent(treeInstance); entity.add(transformNodeComponent); treeComponent.initializationStatus = InitializationStatus.Initialized; @@ -410156,7 +410227,6 @@ class PlacerComponent { } // src/systems/terrain/tree/treePlacer.system.ts -init_core(); class TreePlacerSystem extends import_tick_knock12.IterativeSystem { dynamicTerrainQuery = new import_tick_knock12.Query((entity) => entity.hasComponent(DynamicTerrainComponent)); constructor() { @@ -410174,12 +410244,17 @@ class TreePlacerSystem extends import_tick_knock12.IterativeSystem { return; } entity.remove(PlacerComponent); - const normal = Vector3.Zero(); - let height = dynamicTerrainComponent.dynamicTerrain?.getHeightFromMap(placerComponent.x, placerComponent.z, { normal }); + let height = dynamicTerrainComponent.dynamicTerrain?.getHeightFromMap(placerComponent.x, placerComponent.z); if (!height) { console.error("No height found for tree"); + treeComponent.treeInstance?.dispose(); + entity.remove(TreeComponent); return; } + if (height < 0 || height > 35) { + treeComponent.treeInstance?.dispose(); + entity.remove(TreeComponent); + } const positionComponent = new PositionComponent({ x: placerComponent.x, y: height, @@ -410192,6 +410267,91 @@ class TreePlacerSystem extends import_tick_knock12.IterativeSystem { TreePlacerSystem = __legacyDecorateClassTS([ RegisterSystem() ], TreePlacerSystem); +// src/systems/mapInit.system.ts +var import_tick_knock13 = __toESM(require_lib(), 1); + +// src/components/map.component.ts +class MapComponent { + dynamicTerrain3; + trees; + InitializationStatus = InitializationStatus.NotInitialized; + constructor(dynamicTerrain3, trees = true) { + this.dynamicTerrain = dynamicTerrain3; + this.trees = trees; + } +} + +// src/systems/mapInit.system.ts +class MapInitSystem extends import_tick_knock13.IterativeSystem { + constructor() { + super((entity) => entity.hasComponent(MapComponent)); + } + async updateEntity(entity) { + const mapComponent = entity.get(MapComponent); + if (mapComponent.InitializationStatus !== InitializationStatus.NotInitialized) { + return; + } + mapComponent.InitializationStatus = InitializationStatus.Initializing; + this.createMap(mapComponent); + mapComponent.InitializationStatus = InitializationStatus.Initialized; + } + createMap(mapComponent) { + this.placeTrees(mapComponent); + } + placeTrees(mapComponent) { + if (!mapComponent.trees) { + return; + } + const trackPoints = mapComponent.dynamicTerrain.flatPoints; + const ecsEngine3 = EcsEngine.getInstance(); + const treeRowCount = 30; + const isTooCloseToTrack = (x, y, trackPoints2, minDistance) => { + for (let i = 0;i < trackPoints2.length; i += 2) { + const point = trackPoints2[i]; + const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)); + if (distance < minDistance) { + return true; + } + } + return false; + }; + for (let i = 0;i < treeRowCount; i++) { + for (let j = 0;j < treeRowCount; j++) { + const randomDistanceVariation = Math.random() * 10; + const distanceFromTrack = 5; + const x = (i + 1) * 10 + randomDistanceVariation; + const y = (j + 1) * 10 + randomDistanceVariation + distanceFromTrack; + if (!isTooCloseToTrack(x, y, trackPoints, 10)) { + const treeEntity = new import_tick_knock13.Entity; + const treeComponent = new TreeComponent; + treeEntity.add(treeComponent); + const placerComponent = new PlacerComponent(x, y); + treeEntity.add(placerComponent); + ecsEngine3.addEntity(treeEntity); + } + } + } + for (let i = 0;i < treeRowCount; i++) { + for (let j = 0;j < treeRowCount; j++) { + const randomDistanceVariation = Math.random() * 10; + const distanceFromTrack = 15; + const x = (i + 1) * 10 + randomDistanceVariation; + const y = (j + 1) * -10 + randomDistanceVariation - distanceFromTrack; + if (!isTooCloseToTrack(x, y, trackPoints, 20)) { + const treeEntity = new import_tick_knock13.Entity; + const treeComponent = new TreeComponent; + treeEntity.add(treeComponent); + const placerComponent = new PlacerComponent(x, y); + treeEntity.add(placerComponent); + ecsEngine3.addEntity(treeEntity); + } + } + } + } +} +MapInitSystem = __legacyDecorateClassTS([ + RegisterSystem() +], MapInitSystem); // src/startup/systemRegistration.ts function RegisterSystem() { return function(constructor) { @@ -410203,17 +410363,17 @@ var getRegisteredSystems = function() { return SystemRegistry; }; async function initSystems() { - const ecsEngine3 = EcsEngine.getInstance(); + const ecsEngine4 = EcsEngine.getInstance(); const systemConstructors = getRegisteredSystems(); for (const SystemConstructor of systemConstructors) { const system = new SystemConstructor; - ecsEngine3.addSystem(system); + ecsEngine4.addSystem(system); } } // src/game.ts init_core(); -var import_tick_knock13 = __toESM(require_lib(), 1); +var import_tick_knock14 = __toESM(require_lib(), 1); init_glTF(); var inspector = __toESM(require_babylon_inspector_bundle_max(), 1); async function startGame() { @@ -410239,10 +410399,10 @@ async function startGame() { engine48.runRenderLoop(() => { scene36.render(); }); - const ecsEngine4 = EcsEngine.getInstance(); + const ecsEngine5 = EcsEngine.getInstance(); await initSystems(); scene36.onBeforeRenderObservable.add(() => { - ecsEngine4.update(engine48.getDeltaTime() / 1000); + ecsEngine5.update(engine48.getDeltaTime() / 1000); }); const trackSections = [ "s", @@ -410273,80 +410433,24 @@ async function startGame() { "s" ]; const trackComponent = createTrack(trackSections); - makeDynamicTerrain(trackComponent.points); - createLocomotive(trackComponent); - makeTrees(); -} -var addCurveSection = function(points, turnDirection, turnAngle) { - const currentPoint = points[points.length - 1]; - let secondLastPoint = points[points.length - 2]; - let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); - const initialAngle = Math.atan2(lastDirection.z, lastDirection.x); - const turnAngleRadians = turnAngle * Math.PI / 180; - const ratio = 1.1666666666666667; - const r = 30; - const curvePoints = Math.round(ratio * Math.abs(turnAngle)); - let newPoints = []; - for (let i = 1;i < curvePoints; i++) { - let angle = turnAngleRadians * (i / curvePoints); - let dx = r * Math.sin(angle); - let dz = r * (1 - Math.cos(angle)); - if (turnDirection === "left") { - dz = -dz; - } - let rotatedDx = dx * Math.cos(initialAngle) - dz * Math.sin(initialAngle); - let rotatedDz = dx * Math.sin(initialAngle) + dz * Math.cos(initialAngle); - let newPoint = currentPoint.add(new Vector3(rotatedDx, 0, rotatedDz)); - newPoints.push(newPoint); - } - return newPoints; -}; -var addStraightSection = function(points, n) { - let newPoints = []; - if (points.length === 0) { - for (let i = 0;i < n; i++) { - newPoints.push(new Vector3(i * 0.5, trackHeight, 0)); - } - } else { - const currentPoint = points[points.length - 1]; - let secondLastPoint = points[points.length - 2]; - let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); - for (let i = 0;i < n; i++) { - newPoints.push(currentPoint.add(lastDirection.scale((i + 1) * 0.5))); - } + while (trackComponent.initializationStatus !== InitializationStatus.Initialized) { + await new Promise((resolve) => setTimeout(resolve, 100)); } - return newPoints; -}; + const dynamicTerrainComponent = makeDynamicTerrain(trackComponent.points); + createLocomotive(trackComponent); + makeMap(dynamicTerrainComponent); +} var createTrack = function(trackSections) { - const points = []; - const n = 100; - let sections = []; - for (let trackSection of trackSections) { - const section = new Section(points.length); - if (trackSection === "straight" || trackSection === "w" || trackSection === "s") { - const newPoints = addStraightSection(points, n); - points.push(...newPoints); - } else if (trackSection === "left" || trackSection === "a" || trackSection === "l") { - const newPoints = addCurveSection(points, "left", 90); - points.push(...newPoints); - } else if (trackSection === "right" || trackSection === "d" || trackSection === "r") { - const newPoints = addCurveSection(points, "right", 90); - points.push(...newPoints); - } else { - console.error("Invalid track section"); - } - sections.push(section); - } - const ecsEngine4 = EcsEngine.getInstance(); - const entity = new import_tick_knock13.Entity; - const trackComponent = new TrackComponent(points, sections); + const ecsEngine5 = EcsEngine.getInstance(); + const entity = new import_tick_knock14.Entity; + const trackComponent = new TrackComponent(trackSections); entity.add(trackComponent); - ecsEngine4.addEntity(entity); + ecsEngine5.addEntity(entity); return trackComponent; }; var createLocomotive = function(trackComponent) { - const ecsEngine4 = EcsEngine.getInstance(); - const entity = new import_tick_knock13.Entity; + const ecsEngine5 = EcsEngine.getInstance(); + const entity = new import_tick_knock14.Entity; const locomotiveComponent = new LocomotiveComponent; entity.add(locomotiveComponent); const car1 = new CarComponent(40); @@ -410358,44 +410462,24 @@ var createLocomotive = function(trackComponent) { const keysComponent = new KeysComponent; entity.add(keysComponent); entity.add(trackComponent); - ecsEngine4.addEntity(entity); -}; -var makeTrees = function() { - const ecsEngine4 = EcsEngine.getInstance(); - const treeRowCount = 10; - for (let i = 0;i < treeRowCount; i++) { - for (let j = 0;j < treeRowCount; j++) { - const treeEntity = new import_tick_knock13.Entity; - const treeComponent = new TreeComponent; - treeEntity.add(treeComponent); - const randomDistanceVariation = Math.random() * 10; - const distanceFromTrack = 5; - const placerComponent = new PlacerComponent(i * 10 + randomDistanceVariation, j * 10 + randomDistanceVariation + distanceFromTrack); - treeEntity.add(placerComponent); - ecsEngine4.addEntity(treeEntity); - } - } - for (let i = 0;i < treeRowCount; i++) { - for (let j = 0;j < treeRowCount; j++) { - const treeEntity = new import_tick_knock13.Entity; - const treeComponent = new TreeComponent; - treeEntity.add(treeComponent); - const randomDistanceVariation = Math.random() * 10; - const distanceFromTrack = 15; - const placerComponent = new PlacerComponent(i * 10 + randomDistanceVariation, j * -10 + randomDistanceVariation - distanceFromTrack); - treeEntity.add(placerComponent); - ecsEngine4.addEntity(treeEntity); - } - } + ecsEngine5.addEntity(entity); }; var makeDynamicTerrain = function(flatPoints = []) { - const ecsEngine4 = EcsEngine.getInstance(); - const dynamicTerrainEntity = new import_tick_knock13.Entity; - dynamicTerrainEntity.add(new DynamicTerrainComponent(flatPoints)); - ecsEngine4.addEntity(dynamicTerrainEntity); + const ecsEngine5 = EcsEngine.getInstance(); + const dynamicTerrainEntity = new import_tick_knock14.Entity; + const dynamicTerrainComponent = new DynamicTerrainComponent(flatPoints); + dynamicTerrainEntity.add(dynamicTerrainComponent); + ecsEngine5.addEntity(dynamicTerrainEntity); + return dynamicTerrainComponent; +}; +var makeMap = function(dynamicTerrainComponent) { + const ecsEngine5 = EcsEngine.getInstance(); + const mapEntity = new import_tick_knock14.Entity; + const mapComponent = new MapComponent(dynamicTerrainComponent, true); + mapEntity.add(mapComponent); + ecsEngine5.addEntity(mapEntity); }; var scene36; -var trackHeight = 0.5; var MAX_VIEW_DISTANCE = 300; // index.ts diff --git a/index.ts b/index.ts index 20b9ae0..7127f07 100644 --- a/index.ts +++ b/index.ts @@ -1,3 +1,3 @@ -import { startGame } from "./src/game"; +import { startGame } from './src/game'; -startGame(); \ No newline at end of file +startGame(); diff --git a/package.json b/package.json index af7fa27..22d88df 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@babylonjs/core": "^6.31.0", "@babylonjs/gui": "^6.35.0", + "@babylonjs/havok": "^1.3.1", "@babylonjs/inspector": "^6.31.0", "@babylonjs/loaders": "^6.31.0", "alea": "^1.0.1", diff --git a/src/components/dynamicTerrain.component.ts b/src/components/dynamicTerrain.component.ts index 212be04..432d203 100644 --- a/src/components/dynamicTerrain.component.ts +++ b/src/components/dynamicTerrain.component.ts @@ -5,5 +5,12 @@ import { InitializationStatus } from '../utils/types'; export class DynamicTerrainComponent { public dynamicTerrain: DynamicTerrain | null = null; public initializationStatus: InitializationStatus = InitializationStatus.NotInitialized; + public mapSubX = 1000; + public mapSubZ = 1000; + public seed = 0.3; + public noiseScale = 0.005; + public elevationScale = 2; + public flatRadius = 20; + public noiseResolution = 1.0; constructor(public flatPoints: Vector3[] = []) {} } diff --git a/src/components/map.component.ts b/src/components/map.component.ts new file mode 100644 index 0000000..0aa7e2f --- /dev/null +++ b/src/components/map.component.ts @@ -0,0 +1,12 @@ +import { DynamicTerrain } from '../externals/babylon.dynamicTerrain_modular'; +import { InitializationStatus } from '../utils/types'; +import { DynamicTerrainComponent } from './dynamicTerrain.component'; +import { TrackComponent } from './track.component'; + +export class MapComponent { + public InitializationStatus = InitializationStatus.NotInitialized; + constructor( + public dynamicTerrain: DynamicTerrainComponent, + public trees = true, + ) {} +} diff --git a/src/components/track.component.ts b/src/components/track.component.ts index c9e7099..63c88c5 100644 --- a/src/components/track.component.ts +++ b/src/components/track.component.ts @@ -4,10 +4,9 @@ import { InitializationStatus } from '../utils/types'; export class TrackComponent { public initializationStatus: InitializationStatus = InitializationStatus.NotInitialized; public rotations: Quaternion[] = []; - constructor( - public points: Vector3[], - public sections: Section[], - ) {} + public points: Vector3[] = []; + public sections: Section[] = []; + constructor(public trackDefinition: string[]) {} } export class Section { diff --git a/src/components/tree.component.ts b/src/components/tree.component.ts index d00bd7c..ba36e34 100644 --- a/src/components/tree.component.ts +++ b/src/components/tree.component.ts @@ -1,6 +1,8 @@ +import { TransformNode } from '@babylonjs/core'; import { InitializationStatus } from '../utils/types'; export class TreeComponent { + public treeInstance: TransformNode | null = null; public initializationStatus: InitializationStatus = InitializationStatus.NotInitialized; constructor() {} } diff --git a/src/externals/babylon.dynamicTerrain_modular.ts b/src/externals/babylon.dynamicTerrain_modular.ts index c12a835..16a2718 100644 --- a/src/externals/babylon.dynamicTerrain_modular.ts +++ b/src/externals/babylon.dynamicTerrain_modular.ts @@ -1216,22 +1216,23 @@ export class DynamicTerrain { options?: { normal: Vector3 }, inverted?: boolean, ): number { - const x0 = mapData[0]; - const z0 = mapData[2]; + let x0 = mapData[0]; + let z0 = mapData[2]; - // reset x and z in the map space so they are between 0 and the map size + // Reset x and z in the map space so they are between 0 and the map size x = x - Math.floor((x - x0) / mapSizeX) * mapSizeX; z = z - Math.floor((z - z0) / mapSizeZ) * mapSizeZ; - const col1 = Math.floor(((x - x0) * mapSubX) / mapSizeX); - const row1 = Math.floor(((z - z0) * mapSubZ) / mapSizeZ); - const col2 = (col1 + 1) % mapSubX; - const row2 = (row1 + 1) % mapSubZ; - // starting indexes of the positions of 4 vertices defining a quad on the map - const idx1 = 3 * (row1 * mapSubX + col1); - const idx2 = 3 * (row1 * mapSubX + col2); - const idx3 = 3 * (row2 * mapSubX + col1); - const idx4 = 3 * (row2 * mapSubX + col2); + let col1 = Math.floor(((x - x0) * mapSubX) / mapSizeX); + let row1 = Math.floor(((z - z0) * mapSubZ) / mapSizeZ); + let col2 = (col1 + 1) % mapSubX; + let row2 = (row1 + 1) % mapSubZ; + + // Starting indexes of the positions of 4 vertices defining a quad on the map + let idx1 = 3 * (row1 * mapSubX + col1); + let idx2 = 3 * (row1 * mapSubX + col2); + let idx3 = 3 * (row2 * mapSubX + col1); + let idx4 = 3 * (row2 * mapSubX + col2); const v1 = DynamicTerrain._v1; const v2 = DynamicTerrain._v2; @@ -1242,47 +1243,47 @@ export class DynamicTerrain { v3.copyFromFloats(mapData[idx3], mapData[idx3 + 1], mapData[idx3 + 2]); v4.copyFromFloats(mapData[idx4], mapData[idx4 + 1], mapData[idx4 + 2]); - const vAvB = DynamicTerrain._vAvB; - const vAvC = DynamicTerrain._vAvC; - const norm = DynamicTerrain._norm; - const vA = v1; - let vB; - let vC; - let v; - - const xv4v1 = v4.x - v1.x; - const zv4v1 = v4.z - v1.z; - if (xv4v1 == 0 || zv4v1 == 0) { - return v1.y; - } - const cd = zv4v1 / xv4v1; - const h = v1.z - cd * v1.x; - if (z < cd * x + h) { - vB = v4; - vC = v2; - v = vA; + // Determine if the point is in the upper or lower triangle of the quad + let isInUpperTriangle = (z - v1.z) / (v3.z - v1.z) < (x - v1.x) / (v2.x - v1.x); + + // Perform barycentric interpolation to find the height at the point (x, z) + let height; + if (isInUpperTriangle) { + height = DynamicTerrain._BarycentricInterpolation(x, z, v1, v2, v4); } else { - vB = v3; - vC = v4; - v = vB; - } - vB.subtractToRef(vA, vAvB); - vC.subtractToRef(vA, vAvC); - Vector3.CrossToRef(vAvB, vAvC, norm); - norm.normalize(); - if (inverted) { - norm.scaleInPlace(-1.0); + height = DynamicTerrain._BarycentricInterpolation(x, z, v1, v3, v4); } + + // If a normal is requested, compute it using the cross product of two edges of the triangle if (options && options.normal) { - options.normal.copyFrom(norm); - } - const d = -(norm.x * v.x + norm.y * v.y + norm.z * v.z); - let y = v.y; - if (norm.y != 0.0) { - y = -(norm.x * x + norm.z * z + d) / norm.y; + const edge1 = DynamicTerrain._vAvB; + const edge2 = DynamicTerrain._vAvC; + const normal = DynamicTerrain._norm; + if (isInUpperTriangle) { + v2.subtractToRef(v1, edge1); + v4.subtractToRef(v1, edge2); + } else { + v3.subtractToRef(v1, edge1); + v4.subtractToRef(v1, edge2); + } + Vector3.CrossToRef(edge1, edge2, normal); + normal.normalize(); + if (inverted) { + normal.scaleInPlace(-1.0); + } + options.normal.copyFrom(normal); } - return y; + return height; + } + + // Helper function for barycentric interpolation + private static _BarycentricInterpolation(x: number, z: number, v1: Vector3, v2: Vector3, v3: Vector3): number { + let det = (v2.z - v3.z) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.z - v3.z); + let l1 = ((v2.z - v3.z) * (x - v3.x) + (v3.x - v2.x) * (z - v3.z)) / det; + let l2 = ((v3.z - v1.z) * (x - v3.x) + (v1.x - v3.x) * (z - v3.z)) / det; + let l3 = 1 - l1 - l2; + return l1 * v1.y + l2 * v2.y + l3 * v3.y; } /** diff --git a/src/game.ts b/src/game.ts index 2bde955..b71e7b5 100644 --- a/src/game.ts +++ b/src/game.ts @@ -2,23 +2,21 @@ /// import { initSystems } from './startup/systemRegistration'; -import { ArcRotateCamera, Color3, Color4, Engine, HemisphericLight, Mesh, PointLight, Scene, Vector3 } from '@babylonjs/core'; +import { ArcRotateCamera, Color3, Color4, Engine, HavokPlugin, HemisphericLight, Scene, Vector3 } from '@babylonjs/core'; import { EcsEngine } from './singletons/ecsEngine'; -import { Section, TrackComponent } from './components/track.component'; +import { TrackComponent } from './components/track.component'; import { Entity } from 'tick-knock'; import { LocomotiveComponent } from './components/locomotive/locomotive.component'; import '@babylonjs/loaders/glTF'; -import { TreeComponent } from './components/tree.component'; -import { PositionComponent } from './components/babylonPrimitives/position.component'; import { LocomotiveInputComponent } from './components/locomotive/locomotiveInput.component'; import { KeysComponent } from './components/keys.component'; import { CarComponent } from './components/locomotive/car.component'; import { DynamicTerrainComponent } from './components/dynamicTerrain.component'; import { Inspector } from '@babylonjs/inspector'; -import { PlacerComponent } from './components/placer.component'; +import { MapComponent } from './components/map.component'; +import { InitializationStatus } from './utils/types'; export let scene: Scene; -const trackHeight = 0.5; export const MAX_VIEW_DISTANCE = 300; export async function startGame() { @@ -44,7 +42,6 @@ export async function startGame() { camera.maxZ = MAX_VIEW_DISTANCE; camera.attachControl(canvas, true); const light = new HemisphericLight('light', new Vector3(0, 1, 0), scene); - // const pointLight = new PointLight('pointLight', new Vector3(0, 20, 10), scene); engine.runRenderLoop(() => { scene.render(); @@ -56,6 +53,7 @@ export async function startGame() { ecsEngine.update(engine.getDeltaTime() / 1000); }); + // Track initialization might be moved to be part of the map system...dynamic terrain, trees, track, etc. are all interconected and depend on each other const trackSections = [ 's', 's', @@ -85,96 +83,18 @@ export async function startGame() { 's', ]; const trackComponent = createTrack(trackSections); - makeDynamicTerrain(trackComponent.points); - createLocomotive(trackComponent); - makeTrees(); -} - -function addCurveSection(points: Vector3[], turnDirection: 'left' | 'right', turnAngle: number): Vector3[] { - const currentPoint = points[points.length - 1]; - // Calculate the direction of the last segment - let secondLastPoint = points[points.length - 2]; - let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); - - const initialAngle = Math.atan2(lastDirection.z, lastDirection.x); - - const turnAngleRadians = (turnAngle * Math.PI) / 180; - - // ratio of number of points to the turn angle is ~105:90 - const ratio = 105 / 90; - - const r = 30; - const curvePoints = Math.round(ratio * Math.abs(turnAngle)); - - let newPoints: Vector3[] = []; - - for (let i = 1; i < curvePoints; i++) { - // Angle in radians for each point in the curve - let angle = turnAngleRadians * (i / curvePoints); - let dx = r * Math.sin(angle); - let dz = r * (1 - Math.cos(angle)); - - // flip the direction if turning right - if (turnDirection === 'left') { - dz = -dz; - } - - // Rotate the offsets by the initial angle - let rotatedDx = dx * Math.cos(initialAngle) - dz * Math.sin(initialAngle); - let rotatedDz = dx * Math.sin(initialAngle) + dz * Math.cos(initialAngle); - - let newPoint = currentPoint.add(new Vector3(rotatedDx, 0, rotatedDz)); - newPoints.push(newPoint); - } - - return newPoints; -} - -function addStraightSection(points: Vector3[], n: number): Vector3[] { - let newPoints = []; - // First section - if (points.length === 0) { - for (let i = 0; i < n; i++) { - newPoints.push(new Vector3(i * 0.5, trackHeight, 0)); - } - } else { - const currentPoint = points[points.length - 1]; - // base the direction of the new section on the direction of the last segment - let secondLastPoint = points[points.length - 2]; - let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); - - // Create the new section - for (let i = 0; i < n; i++) { - newPoints.push(currentPoint.add(lastDirection.scale((i + 1) * 0.5))); - } + while (trackComponent.initializationStatus !== InitializationStatus.Initialized) { + await new Promise((resolve) => setTimeout(resolve, 100)); } - return newPoints; + const dynamicTerrainComponent = makeDynamicTerrain(trackComponent.points); + createLocomotive(trackComponent); + makeMap(dynamicTerrainComponent); } function createTrack(trackSections: string[]): TrackComponent { - const points = []; - const n = 100; - let sections = []; - - for (let trackSection of trackSections) { - const section = new Section(points.length); - if (trackSection === 'straight' || trackSection === 'w' || trackSection === 's') { - const newPoints = addStraightSection(points, n); - points.push(...newPoints); - } else if (trackSection === 'left' || trackSection === 'a' || trackSection === 'l') { - const newPoints = addCurveSection(points, 'left', 90); - points.push(...newPoints); - } else if (trackSection === 'right' || trackSection === 'd' || trackSection === 'r') { - const newPoints = addCurveSection(points, 'right', 90); - points.push(...newPoints); - } else { - console.error('Invalid track section'); - } - sections.push(section); - } const ecsEngine = EcsEngine.getInstance(); const entity = new Entity(); - const trackComponent = new TrackComponent(points, sections); + const trackComponent = new TrackComponent(trackSections); entity.add(trackComponent); ecsEngine.addEntity(entity); return trackComponent; @@ -197,41 +117,19 @@ function createLocomotive(trackComponent: TrackComponent) { ecsEngine.addEntity(entity); } -function makeTrees() { +function makeDynamicTerrain(flatPoints: Vector3[] = []): DynamicTerrainComponent { const ecsEngine = EcsEngine.getInstance(); - const treeRowCount = 10; - - // Make a bunch of trees - for (let i = 0; i < treeRowCount; i++) { - for (let j = 0; j < treeRowCount; j++) { - const treeEntity = new Entity(); - const treeComponent = new TreeComponent(); - treeEntity.add(treeComponent); - const randomDistanceVariation = Math.random() * 10; - const distanceFromTrack = 5; - const placerComponent = new PlacerComponent(i * 10 + randomDistanceVariation, j * 10 + randomDistanceVariation + distanceFromTrack); - treeEntity.add(placerComponent); - ecsEngine.addEntity(treeEntity); - } - } - - for (let i = 0; i < treeRowCount; i++) { - for (let j = 0; j < treeRowCount; j++) { - const treeEntity = new Entity(); - const treeComponent = new TreeComponent(); - treeEntity.add(treeComponent); - const randomDistanceVariation = Math.random() * 10; - const distanceFromTrack = 15; - const placerComponent = new PlacerComponent(i * 10 + randomDistanceVariation, j * -10 + randomDistanceVariation - distanceFromTrack); - treeEntity.add(placerComponent); - ecsEngine.addEntity(treeEntity); - } - } + const dynamicTerrainEntity = new Entity(); + const dynamicTerrainComponent = new DynamicTerrainComponent(flatPoints); + dynamicTerrainEntity.add(dynamicTerrainComponent); + ecsEngine.addEntity(dynamicTerrainEntity); + return dynamicTerrainComponent; } -function makeDynamicTerrain(flatPoints: Vector3[] = []) { +function makeMap(dynamicTerrainComponent: DynamicTerrainComponent) { const ecsEngine = EcsEngine.getInstance(); - const dynamicTerrainEntity = new Entity(); - dynamicTerrainEntity.add(new DynamicTerrainComponent(flatPoints)); - ecsEngine.addEntity(dynamicTerrainEntity); + const mapEntity = new Entity(); + const mapComponent = new MapComponent(dynamicTerrainComponent, true); + mapEntity.add(mapComponent); + ecsEngine.addEntity(mapEntity); } diff --git a/src/systems/index.ts b/src/systems/index.ts index 395ef78..d2a778b 100644 --- a/src/systems/index.ts +++ b/src/systems/index.ts @@ -5,3 +5,4 @@ export * from './babylonPrimitives/transformNodePosition.system'; export * from './keys.system'; export * from './terrain/dynamicTerrainInit.system'; export * from './terrain/tree'; +export * from './mapInit.system'; diff --git a/src/systems/mapInit.system.ts b/src/systems/mapInit.system.ts new file mode 100644 index 0000000..77740ef --- /dev/null +++ b/src/systems/mapInit.system.ts @@ -0,0 +1,92 @@ +import { Entity, IterativeSystem } from 'tick-knock'; +import { RegisterSystem } from '../startup/systemRegistration'; +import { MapComponent } from '../components/map.component'; +import { InitializationStatus } from '../utils/types'; +import { TreeComponent } from '../components/tree.component'; +import { PlacerComponent } from '../components/placer.component'; +import { EcsEngine } from '../singletons/ecsEngine'; +import { Vector3 } from '@babylonjs/core'; + +@RegisterSystem() +export class MapInitSystem extends IterativeSystem { + public constructor() { + super((entity) => entity.hasComponent(MapComponent)); + } + + protected async updateEntity(entity: Entity): Promise { + const mapComponent = entity.get(MapComponent)!; + if (mapComponent.InitializationStatus !== InitializationStatus.NotInitialized) { + return; + } + mapComponent.InitializationStatus = InitializationStatus.Initializing; + this.createMap(mapComponent); + mapComponent.InitializationStatus = InitializationStatus.Initialized; + } + + private createMap(mapComponent: MapComponent): void { + this.placeTrees(mapComponent); + } + + private placeTrees(mapComponent: MapComponent): void { + if (!mapComponent.trees) { + return; + } + + const trackPoints = mapComponent.dynamicTerrain.flatPoints; + + const ecsEngine = EcsEngine.getInstance(); + const treeRowCount = 30; + + // Helper function to check if a point is too close to any track point + const isTooCloseToTrack = (x: number, y: number, trackPoints: Vector3[], minDistance: number): boolean => { + for (let i = 0; i < trackPoints.length; i += 2) { + // Check every 5th point + const point = trackPoints[i]; + const distance = Math.sqrt(Math.pow(x - point.x, 2) + Math.pow(y - point.y, 2)); + if (distance < minDistance) { + return true; + } + } + return false; + }; + + // Make a bunch of trees + for (let i = 0; i < treeRowCount; i++) { + for (let j = 0; j < treeRowCount; j++) { + const randomDistanceVariation = Math.random() * 10; + const distanceFromTrack = 5; + const x = (i + 1) * 10 + randomDistanceVariation; + const y = (j + 1) * 10 + randomDistanceVariation + distanceFromTrack; + + // Check if the tree is too close to the track + if (!isTooCloseToTrack(x, y, trackPoints, 10)) { + const treeEntity = new Entity(); + const treeComponent = new TreeComponent(); + treeEntity.add(treeComponent); + const placerComponent = new PlacerComponent(x, y); + treeEntity.add(placerComponent); + ecsEngine.addEntity(treeEntity); + } + } + } + + for (let i = 0; i < treeRowCount; i++) { + for (let j = 0; j < treeRowCount; j++) { + const randomDistanceVariation = Math.random() * 10; + const distanceFromTrack = 15; + const x = (i + 1) * 10 + randomDistanceVariation; + const y = (j + 1) * -10 + randomDistanceVariation - distanceFromTrack; + + // Check if the tree is too close to the track + if (!isTooCloseToTrack(x, y, trackPoints, 20)) { + const treeEntity = new Entity(); + const treeComponent = new TreeComponent(); + treeEntity.add(treeComponent); + const placerComponent = new PlacerComponent(x, y); + treeEntity.add(placerComponent); + ecsEngine.addEntity(treeEntity); + } + } + } + } +} diff --git a/src/systems/terrain/dynamicTerrainInit.system.ts b/src/systems/terrain/dynamicTerrainInit.system.ts index 686933d..6026912 100644 --- a/src/systems/terrain/dynamicTerrainInit.system.ts +++ b/src/systems/terrain/dynamicTerrainInit.system.ts @@ -7,16 +7,14 @@ import alea from 'alea'; import { NoiseFunction2D, createNoise2D } from 'simplex-noise'; import { InitializationStatus } from '../../utils/types'; import { - Color3, Engine, RawTexture, ShaderLanguage, ShaderMaterial, ShaderStore, Texture, - Vector2, + UniversalCamera, Vector3, - VertexBuffer, } from '@babylonjs/core'; import { loadShader } from '../../utils/loadShaders'; @@ -32,79 +30,49 @@ export class DynamicTerrainInitSystem extends IterativeSystem { return; } dynamicTerrainComponent.initializationStatus = InitializationStatus.Initializing; - const mapSubX = 1000; - const mapSubZ = 400; - const seed = 0.3; - const noiseScale = 0.005; - const elevationScale = 2; - const prng = alea(seed); + + const prng = alea(dynamicTerrainComponent.seed); const noise2D = createNoise2D(prng); - const mapData = this.makeMapData(mapSubX, mapSubZ, noise2D, noiseScale, elevationScale, dynamicTerrainComponent.flatPoints, 20); + const mapData = this.makeMapData( + dynamicTerrainComponent.mapSubX, + dynamicTerrainComponent.mapSubZ, + noise2D, + dynamicTerrainComponent.noiseScale, + dynamicTerrainComponent.elevationScale, + dynamicTerrainComponent.flatPoints, + 20, + ); const mapParams = { mapData: mapData, - mapSubX: mapSubX, - mapSubZ: mapSubZ, + mapSubX: dynamicTerrainComponent.mapSubX, + mapSubZ: dynamicTerrainComponent.mapSubZ, terrainSub: MAX_VIEW_DISTANCE * 2, }; const terrain = new DynamicTerrain('dynamicTerrain', mapParams, scene); dynamicTerrainComponent.dynamicTerrain = terrain; - const terrainWidth = mapSubX; - const terrainHeight = mapSubZ; - const noiseResolution = 1.0; // Smaller values make smoother noise - - // Create a 1D array to hold the noise values - const noiseValues = new Float32Array(terrainWidth * terrainHeight); - - // Populate the noise array with values - for (let z = 0; z < terrainHeight; z++) { - for (let x = 0; x < terrainWidth; x++) { - // Calculate the noise value at this point - const noiseValue = noise2D(x * noiseResolution, z * noiseResolution); - - // Optionally, normalize and scale the noise value - const normalizedNoiseValue = (noiseValue + 1) / 2; // Normalize to [0, 1] - - // Store the noise value in the array - noiseValues[z * terrainWidth + x] = normalizedNoiseValue; - } - } - - const noiseTextureWidth = Math.sqrt(mapSubX * mapSubZ); - const noiseTextureHeight = noiseTextureWidth; // Square texture for simplicity - - // Create an empty array for the texture - const noiseTextureData = new Uint8Array(noiseTextureWidth * noiseTextureHeight * 4); // *4 for RGBA - - // Populate the texture array - for (let i = 0, l = noiseTextureData.length; i < l; i += 4) { - // Assuming 'noiseValues' is a 1D array of your noise data normalized to [0, 1] - // Map the 1D noise array to the 2D texture array - const noiseValue = Math.floor(i / 4); // This maps directly, but you might need a more complex mapping - const normalizedNoiseValue = Math.round(noiseValues[noiseValue] * 255); - noiseTextureData[i] = normalizedNoiseValue; // R - noiseTextureData[i + 1] = normalizedNoiseValue; // G - noiseTextureData[i + 2] = normalizedNoiseValue; // B - noiseTextureData[i + 3] = 255; // A - } - - // Create the texture - const noiseTexture = new RawTexture( - noiseTextureData, - noiseTextureWidth, - noiseTextureHeight, - Engine.TEXTUREFORMAT_RGBA, - scene, - false, - false, - Texture.TRILINEAR_SAMPLINGMODE, - ); + const noiseTexture = this.makeTexture(dynamicTerrainComponent, noise2D); const terrainShaderMaterial = await this.makeShaders(noiseTexture); dynamicTerrainComponent.dynamicTerrain.mesh.material = terrainShaderMaterial; + dynamicTerrainComponent.dynamicTerrain.mesh.isPickable = false; + // Keep the terrain centered in the camera's view + dynamicTerrainComponent.dynamicTerrain.beforeUpdate = (): void => { + const _activeCamera = scene.activeCamera as UniversalCamera; + if (!_activeCamera) { + console.error('No active camera'); + return; + } + const offset = Vector3.Center(_activeCamera.position, _activeCamera.target); + terrain.shiftFromCamera = { + x: -offset._x, + z: -offset._z, + }; + }; + dynamicTerrainComponent.initializationStatus = InitializationStatus.Initialized; } @@ -114,19 +82,6 @@ export class DynamicTerrainInitSystem extends IterativeSystem { return baseNoise + detailNoise; } - protected getColorForHeight(height: number) { - const seaLevel = 0; // Define sea level or base level - const mountainLevel = 20; // Define the level where hills become mountains - - if (height < seaLevel) { - return new Color3(0, 0, 1); // Blue for water - } else if (height < mountainLevel) { - return new Color3(0, 1, 0); // Green for lowlands - } else { - return new Color3(1, 1, 1); // White for mountain tops - } - } - protected makeMapData( mapSubX: number, mapSubZ: number, @@ -162,6 +117,54 @@ export class DynamicTerrainInitSystem extends IterativeSystem { return mapData; } + protected makeTexture(dynamicTerrainComponent: DynamicTerrainComponent, noise2D: NoiseFunction2D): RawTexture { + const terrainWidth = dynamicTerrainComponent.mapSubX; + const terrainHeight = dynamicTerrainComponent.mapSubZ; + const noiseResolution = 1.0; // Smaller values make smoother noise + const noiseValues = new Float32Array(terrainWidth * terrainHeight); + for (let z = 0; z < terrainHeight; z++) { + for (let x = 0; x < terrainWidth; x++) { + const noiseValue = noise2D(x * noiseResolution, z * noiseResolution); + + // Optionally, normalize and scale the noise value + const normalizedNoiseValue = (noiseValue + 1) / 2; // Normalize to [0, 1] + + noiseValues[z * terrainWidth + x] = normalizedNoiseValue; + } + } + + const noiseTextureWidth = Math.sqrt(dynamicTerrainComponent.mapSubX * dynamicTerrainComponent.mapSubZ); + const noiseTextureHeight = noiseTextureWidth; // Square texture for simplicity + + // Create an empty array for the texture + const noiseTextureData = new Uint8Array(noiseTextureWidth * noiseTextureHeight * 4); // *4 for RGBA + + // Populate the texture array + for (let i = 0, l = noiseTextureData.length; i < l; i += 4) { + // Assuming 'noiseValues' is a 1D array of your noise data normalized to [0, 1] + // Map the 1D noise array to the 2D texture array + const noiseValue = Math.floor(i / 4); // This maps directly, but you might need a more complex mapping + const normalizedNoiseValue = Math.round(noiseValues[noiseValue] * 255); + noiseTextureData[i] = normalizedNoiseValue; // R + noiseTextureData[i + 1] = normalizedNoiseValue; // G + noiseTextureData[i + 2] = normalizedNoiseValue; // B + noiseTextureData[i + 3] = 255; // A + } + + // Create the texture + const noiseTexture = new RawTexture( + noiseTextureData, + noiseTextureWidth, + noiseTextureHeight, + Engine.TEXTUREFORMAT_RGBA, + scene, + false, + false, + Texture.TRILINEAR_SAMPLINGMODE, + ); + return noiseTexture; + } + protected async makeShaders(noiseTexture: RawTexture): Promise { const vertexShader = await loadShader('assets/shaders/terrain/terrainVertexShader.glsl'); const fragmentShader = await loadShader('assets/shaders/terrain/terrainFragmentShader.glsl'); diff --git a/src/systems/terrain/tree/treeInit.system.ts b/src/systems/terrain/tree/treeInit.system.ts index bdd3276..99e6034 100644 --- a/src/systems/terrain/tree/treeInit.system.ts +++ b/src/systems/terrain/tree/treeInit.system.ts @@ -120,6 +120,7 @@ export class TreeInitSystem extends IterativeSystem { trunkInstance.setParent(treeInstance); foliageInstance.alwaysSelectAsActiveMesh = true; trunkInstance.alwaysSelectAsActiveMesh = true; + treeComponent.treeInstance = treeInstance; const transformNodeComponent = new TransformNodeComponent(treeInstance); entity.add(transformNodeComponent); treeComponent.initializationStatus = InitializationStatus.Initialized; diff --git a/src/systems/terrain/tree/treePlacer.system.ts b/src/systems/terrain/tree/treePlacer.system.ts index 55fe1f8..b722c89 100644 --- a/src/systems/terrain/tree/treePlacer.system.ts +++ b/src/systems/terrain/tree/treePlacer.system.ts @@ -6,7 +6,6 @@ import { PlacerComponent } from '../../../components/placer.component'; import { DynamicTerrainComponent } from '../../../components/dynamicTerrain.component'; import { PositionComponent } from '../../../components/babylonPrimitives/position.component'; import { EcsEngine } from '../../../singletons/ecsEngine'; -import { Vector3 } from '@babylonjs/core'; @RegisterSystem() export class TreePlacerSystem extends IterativeSystem { @@ -27,12 +26,17 @@ export class TreePlacerSystem extends IterativeSystem { return; } entity.remove(PlacerComponent); - const normal = Vector3.Zero(); - let height = dynamicTerrainComponent.dynamicTerrain?.getHeightFromMap(placerComponent.x, placerComponent.z, { normal: normal }); + let height = dynamicTerrainComponent.dynamicTerrain?.getHeightFromMap(placerComponent.x, placerComponent.z); if (!height) { console.error('No height found for tree'); + treeComponent.treeInstance?.dispose(); + entity.remove(TreeComponent); return; } + if (height < 0 || height > 35) { + treeComponent.treeInstance?.dispose(); + entity.remove(TreeComponent); + } const positionComponent = new PositionComponent({ x: placerComponent.x, y: height, diff --git a/src/systems/track/trackInit.system.ts b/src/systems/track/trackInit.system.ts index ac3e1a1..a8f24f7 100644 --- a/src/systems/track/trackInit.system.ts +++ b/src/systems/track/trackInit.system.ts @@ -5,6 +5,8 @@ import { InitializationStatus } from '../../utils/types'; import { RegisterSystem } from '../../startup/systemRegistration'; import { scene } from '../../game'; +const TRACK_HEIGHT = 0.5; + @RegisterSystem() export class TrackInitSystem extends IterativeSystem { public constructor() { @@ -18,6 +20,10 @@ export class TrackInitSystem extends IterativeSystem { } trackComponent.initializationStatus = InitializationStatus.Initializing; + const trackComponentDefinition = trackComponent.trackDefinition; + const { points, sections } = this.createPointsAndSections(trackComponentDefinition); + trackComponent.points = points; + trackComponent.sections = sections; const track = this.createTrack(trackComponent.points, trackComponent.sections); this.createMeshes(track, trackComponent.points); @@ -33,6 +39,91 @@ export class TrackInitSystem extends IterativeSystem { return node; } + private createPointsAndSections(trackDefinition: string[]): { points: Vector3[]; sections: Section[] } { + const points = []; + const n = 100; + let sections: Section[] = []; + + for (let trackSection of trackDefinition) { + const section = new Section(points.length); + if (trackSection === 'straight' || trackSection === 'w' || trackSection === 's') { + const newPoints = this.addStraightSection(points, n); + points.push(...newPoints); + } else if (trackSection === 'left' || trackSection === 'a' || trackSection === 'l') { + const newPoints = this.addCurveSection(points, 'left', 90); + points.push(...newPoints); + } else if (trackSection === 'right' || trackSection === 'd' || trackSection === 'r') { + const newPoints = this.addCurveSection(points, 'right', 90); + points.push(...newPoints); + } else { + console.error('Invalid track section'); + } + sections.push(section); + } + return { points, sections }; + } + + private addCurveSection(points: Vector3[], turnDirection: 'left' | 'right', turnAngle: number): Vector3[] { + const currentPoint = points[points.length - 1]; + // Calculate the direction of the last segment + let secondLastPoint = points[points.length - 2]; + let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); + + const initialAngle = Math.atan2(lastDirection.z, lastDirection.x); + + const turnAngleRadians = (turnAngle * Math.PI) / 180; + + // ratio of number of points to the turn angle is ~105:90 + const ratio = 105 / 90; + + const r = 30; + const curvePoints = Math.round(ratio * Math.abs(turnAngle)); + + let newPoints: Vector3[] = []; + + for (let i = 1; i < curvePoints; i++) { + // Angle in radians for each point in the curve + let angle = turnAngleRadians * (i / curvePoints); + let dx = r * Math.sin(angle); + let dz = r * (1 - Math.cos(angle)); + + // flip the direction if turning right + if (turnDirection === 'left') { + dz = -dz; + } + + // Rotate the offsets by the initial angle + let rotatedDx = dx * Math.cos(initialAngle) - dz * Math.sin(initialAngle); + let rotatedDz = dx * Math.sin(initialAngle) + dz * Math.cos(initialAngle); + + let newPoint = currentPoint.add(new Vector3(rotatedDx, 0, rotatedDz)); + newPoints.push(newPoint); + } + + return newPoints; + } + + private addStraightSection(points: Vector3[], n: number): Vector3[] { + let newPoints = []; + // First section + if (points.length === 0) { + for (let i = 0; i < n; i++) { + newPoints.push(new Vector3(i * 0.5, TRACK_HEIGHT, 0)); + } + } else { + const currentPoint = points[points.length - 1]; + // base the direction of the new section on the direction of the last segment + let secondLastPoint = points[points.length - 2]; + let lastDirection = currentPoint.subtract(secondLastPoint).normalize(); + + // Create the new section + for (let i = 0; i < n; i++) { + newPoints.push(currentPoint.add(lastDirection.scale((i + 1) * 0.5))); + } + } + return newPoints; + } + private createTrack(points: Vector3[], sections: Section[]): TrackData { const trackData: TrackData = { directions: [],