diff --git a/README.md b/README.md index 7e38a3f..882238a 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A procedural asset scattering system built with BabylonJS. It can render millions of blades of grass with LoD and cute butterflies. +The instance buffers are generated using compute shaders when available, otherwise they are generated on the CPU. To see the CPU only version, checkout the branch `cpu`. + ## Online demo Main demo with procedural terrain, lod, collisions, butterflies and trees: [here](https://barthpaleologue.github.io/AssetScattering/) @@ -19,6 +21,10 @@ Minimal example of a dense patch of grass: [here](https://barthpaleologue.github If you can't run the demo, there is a video on YouTube [here](https://www.youtube.com/watch?v=0I5Kd784K6A). +### WebGPU + +All demos are WebGPU compatible, simply add `?webgpu` to the URL to use WebGPU instead of WebGL. If your browser doesn't support WebGPU, it will throw an error in the console. + ## How to use The files for the asset scattering are located in the folder `src/ts/instancing`. diff --git a/src/ts/compute/vertexData/computeVertexData.ts b/src/ts/compute/Terrain2dVertexData/computeVertexData.ts similarity index 100% rename from src/ts/compute/vertexData/computeVertexData.ts rename to src/ts/compute/Terrain2dVertexData/computeVertexData.ts diff --git a/src/ts/compute/vertexData/vertexData.wgsl b/src/ts/compute/Terrain2dVertexData/vertexData.wgsl similarity index 100% rename from src/ts/compute/vertexData/vertexData.wgsl rename to src/ts/compute/Terrain2dVertexData/vertexData.wgsl diff --git a/src/ts/compute/Terrain3dVertexData/computeVertexData.ts b/src/ts/compute/Terrain3dVertexData/computeVertexData.ts new file mode 100644 index 0000000..eab1bf8 --- /dev/null +++ b/src/ts/compute/Terrain3dVertexData/computeVertexData.ts @@ -0,0 +1,85 @@ +import heightMapComputeSource from "./vertexData.wgsl"; +import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData"; +import { Engine } from "@babylonjs/core/Engines/engine"; +import { ComputeShader } from "@babylonjs/core/Compute/computeShader"; +import { StorageBuffer } from "@babylonjs/core/Buffers/storageBuffer"; +import { UniformBuffer } from "@babylonjs/core/Materials/uniformBuffer"; +import { Matrix, Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector"; + +export async function computeVertexData(nbVerticesPerRow: number, position: Vector3, rotation: Quaternion, size: number, engine: Engine): Promise { + const computeShader = new ComputeShader( + "heightMap", + engine, + { computeSource: heightMapComputeSource }, + { + bindingsMapping: { + positions: { group: 0, binding: 0 }, + normals: { group: 0, binding: 1 }, + indices: { group: 0, binding: 2 }, + params: { group: 0, binding: 3 } + } + } + ); + + const positions = new Float32Array(nbVerticesPerRow * nbVerticesPerRow * 3); + const normals = new Float32Array(nbVerticesPerRow * nbVerticesPerRow * 3); + const indices = new Uint32Array((nbVerticesPerRow - 1) * (nbVerticesPerRow - 1) * 6); + + const positionsBuffer = new StorageBuffer(engine, positions.byteLength); + positionsBuffer.update(positions); + computeShader.setStorageBuffer("positions", positionsBuffer); + + const normalsBuffer = new StorageBuffer(engine, normals.byteLength); + normalsBuffer.update(normals); + computeShader.setStorageBuffer("normals", normalsBuffer); + + const indicesBuffer = new StorageBuffer(engine, indices.byteLength); + indicesBuffer.update(indices); + computeShader.setStorageBuffer("indices", indicesBuffer); + + const paramsBuffer = new UniformBuffer(engine); + + paramsBuffer.addUniform("nbVerticesPerRow", 1); + paramsBuffer.addUniform("size", 1); + paramsBuffer.addUniform("position", 3); + paramsBuffer.addUniform("rotationMatrix", 16); + + paramsBuffer.updateUInt("nbVerticesPerRow", nbVerticesPerRow); + paramsBuffer.updateFloat("size", size); + paramsBuffer.updateVector3("position", position); + paramsBuffer.updateMatrix("rotationMatrix", rotation.toRotationMatrix(new Matrix())); + paramsBuffer.update(); + + computeShader.setUniformBuffer("params", paramsBuffer); + + return new Promise((resolve, reject) => { + computeShader + .dispatchWhenReady(nbVerticesPerRow, nbVerticesPerRow, 1) + .then(async () => { + try { + const [positionsBufferView, normalsBufferView, indicesBufferView] = await Promise.all([positionsBuffer.read(), normalsBuffer.read(), indicesBuffer.read()]); + + const positions = new Float32Array(positionsBufferView.buffer); + positionsBuffer.dispose(); + + const normals = new Float32Array(normalsBufferView.buffer); + normalsBuffer.dispose(); + + const indices = new Uint32Array(indicesBufferView.buffer); + indicesBuffer.dispose(); + + const vertexData = new VertexData(); + vertexData.positions = positions; + vertexData.indices = indices; + vertexData.normals = normals; + + resolve(vertexData); + } catch (error) { + reject(new Error("Error: " + error)); + } + }) + .catch((error) => { + reject(new Error("Error: " + error)); + }); + }); +} diff --git a/src/ts/compute/Terrain3dVertexData/vertexData.wgsl b/src/ts/compute/Terrain3dVertexData/vertexData.wgsl new file mode 100644 index 0000000..a5ac06b --- /dev/null +++ b/src/ts/compute/Terrain3dVertexData/vertexData.wgsl @@ -0,0 +1,57 @@ +struct Params { + nbVerticesPerRow : u32, + size : f32, + position : vec3, + rotationMatrix : mat4x4, +}; + +struct FloatArray { + elements : array, +}; +struct UIntArray { + elements : array>, +}; + +@group(0) @binding(0) var positions : FloatArray; +@group(0) @binding(1) var normals : FloatArray; +@group(0) @binding(2) var indices : UIntArray; +@group(0) @binding(3) var params : Params; + +@compute @workgroup_size(1,1,1) +fn main(@builtin(global_invocation_id) id: vec3) +{ + let x : f32 = f32(id.x); + let y : f32 = f32(id.y); + + let index: u32 = id.x + id.y * u32(params.nbVerticesPerRow); + + let vertex_position = vec3(params.size * x / f32(params.nbVerticesPerRow - 1) - params.size / 2.0, params.size * y / f32(params.nbVerticesPerRow - 1) - params.size / 2.0, 0.0); + var vertex_position_world: vec3 = vertex_position + vec3(0.0, 0.0, -params.size / 2.0); + vertex_position_world = (params.rotationMatrix * vec4(vertex_position_world, 1.0)).xyz; + + let normal: vec3 = normalize(vertex_position_world); + + vertex_position_world = normal * params.size / 2.0; + + //vertex_position_world = vertex_position_world - params.position; + + positions.elements[index * 3 + 0] = vertex_position_world.x; + positions.elements[index * 3 + 1] = vertex_position_world.y; + positions.elements[index * 3 + 2] = vertex_position_world.z; + + normals.elements[index * 3 + 0] = normal.x; + normals.elements[index * 3 + 1] = normal.y; + normals.elements[index * 3 + 2] = normal.z; + + if(x > 0 && y > 0) { + let indexIndex = ((id.x - 1) + (id.y - 1) * (params.nbVerticesPerRow - 1)) * 6; + + atomicStore(&indices.elements[indexIndex + 0], index - 1); + atomicStore(&indices.elements[indexIndex + 1], index - params.nbVerticesPerRow - 1); + atomicStore(&indices.elements[indexIndex + 2], index); + + atomicStore(&indices.elements[indexIndex + 3], index); + atomicStore(&indices.elements[indexIndex + 4], index - params.nbVerticesPerRow - 1); + atomicStore(&indices.elements[indexIndex + 5], index - params.nbVerticesPerRow); + } +} \ No newline at end of file diff --git a/src/ts/compute/scatterSquare/computeSquareScatterPoints.ts b/src/ts/compute/scatterSquare/computeSquareScatterPoints.ts index 7775da4..91ea322 100644 --- a/src/ts/compute/scatterSquare/computeSquareScatterPoints.ts +++ b/src/ts/compute/scatterSquare/computeSquareScatterPoints.ts @@ -5,10 +5,7 @@ import { StorageBuffer } from "@babylonjs/core/Buffers/storageBuffer"; import { UniformBuffer } from "@babylonjs/core/Materials/uniformBuffer"; import { Vector3 } from "@babylonjs/core/Maths/math.vector"; -export async function computeSquareScatterPoints( - position: Vector3, size: number, resolution: number, - engine: Engine -): Promise { +export async function computeSquareScatterPoints(position: Vector3, size: number, resolution: number, engine: Engine): Promise { const computeShader = new ComputeShader( "scatter", engine, @@ -45,9 +42,7 @@ export async function computeSquareScatterPoints( .dispatchWhenReady(resolution, resolution, 1) .then(async () => { try { - const [instanceMatricesBufferView] = await Promise.all([ - instanceMatricesBuffer.read() - ]); + const [instanceMatricesBufferView] = await Promise.all([instanceMatricesBuffer.read()]); const instanceMatrices = new Float32Array(instanceMatricesBufferView.buffer); instanceMatricesBuffer.dispose(); diff --git a/src/ts/flatfield.ts b/src/ts/flatfield.ts index ebd27e4..8d6ee32 100644 --- a/src/ts/flatfield.ts +++ b/src/ts/flatfield.ts @@ -32,15 +32,14 @@ import { HavokPlugin } from "@babylonjs/core/Physics/v2/Plugins/havokPlugin"; import { IPatch } from "./instancing/iPatch"; import { createButterfly } from "./butterfly/butterfly"; import { createButterflyMaterial } from "./butterfly/butterflyMaterial"; -import { EngineFactory } from "@babylonjs/core/Engines/engineFactory"; -import "@babylonjs/core/Engines"; +import { createEngine } from "./utils/createEngine"; // Init babylonjs const canvas = document.getElementById("renderer") as HTMLCanvasElement; canvas.width = window.innerWidth; canvas.height = window.innerHeight; -const engine = await EngineFactory.CreateAsync(canvas, {}); +const engine = await createEngine(canvas); engine.displayLoadingUI(); if (engine.getCaps().supportComputeShaders) { @@ -58,7 +57,6 @@ scene.executeWhenReady(() => { engine.runRenderLoop(() => scene.render()); }); - const camera = new ArcRotateCamera("camera", (-3.14 * 3) / 4, 1.4, 6, Vector3.Zero(), scene); camera.minZ = 0.1; camera.attachControl(); @@ -93,10 +91,9 @@ const grassManager = new PatchManager([lowQualityGrassBlade, highQualityGrassBla return distance < patchSize * 3 ? 1 : 0; }); -const grassPromise = PatchManager.circleInit(fieldRadius, patchSize, patchResolution, engine).then((patches) => { - grassManager.addPatches(patches); - grassManager.initInstances(); -}); +const grassPatches = await PatchManager.circleInit(fieldRadius, patchSize, patchResolution, engine); +grassManager.addPatches(grassPatches); +grassManager.initInstances(); const ground = MeshBuilder.CreateGround( "ground", @@ -118,9 +115,8 @@ butterfly.isVisible = false; const butterflyMaterial = createButterflyMaterial(light, scene, character); butterfly.material = butterflyMaterial; -const butterflyPromise = ThinInstancePatch.CreateSquare(Vector3.Zero(), patchSize * fieldRadius * 2, 100, engine).then((patch) => { - patch.createInstances(butterfly); -}); +const butterflyPatch = await ThinInstancePatch.CreateSquare(Vector3.Zero(), patchSize * fieldRadius * 2, 100, engine); +butterflyPatch.createInstances(butterfly); const ui = new UI(scene); @@ -131,12 +127,12 @@ document.addEventListener("keypress", (e) => { } }); -scene.onBeforeRenderObservable.add(() =>{ +scene.onBeforeRenderObservable.add(() => { ui.setText(`${grassManager.getNbInstances().toLocaleString()} grass blades\n${grassManager.getNbVertices().toLocaleString()} vertices | ${engine.getFps().toFixed(0)} FPS`); - grassManager.update(camera.position); + grassManager.update(); }); -Promise.all([grassPromise, butterflyPromise]).then(() => { +scene.executeWhenReady(() => { engine.loadingScreen.hideLoadingUI(); }); diff --git a/src/ts/index.ts b/src/ts/index.ts index c011fc4..aae95d9 100644 --- a/src/ts/index.ts +++ b/src/ts/index.ts @@ -42,14 +42,14 @@ import { TerrainChunk } from "./terrain/terrainChunk"; import { createButterfly } from "./butterfly/butterfly"; import { createButterflyMaterial } from "./butterfly/butterflyMaterial"; import { createTree } from "./utils/tree"; -import { EngineFactory } from "@babylonjs/core/Engines/engineFactory"; -import "@babylonjs/core/Engines"; +import { createEngine } from "./utils/createEngine"; const canvas = document.getElementById("renderer") as HTMLCanvasElement; canvas.width = window.innerWidth; canvas.height = window.innerHeight; -const engine = await EngineFactory.CreateAsync(canvas, {}); +const engine = await createEngine(canvas); + engine.displayLoadingUI(); if (engine.getCaps().supportComputeShaders) { @@ -141,10 +141,10 @@ const terrain = new Terrain( scene ); terrain.onCreateChunkObservable.add((chunk: TerrainChunk) => { - if(chunk.instancesMatrixBuffer === null) { + if (chunk.instancesMatrixBuffer === null) { throw new Error("Instances matrix buffer is null"); } - if(chunk.alignedInstancesMatrixBuffer === null) { + if (chunk.alignedInstancesMatrixBuffer === null) { throw new Error("Aligned instance matrices are null"); } const grassPatch = new ThinInstancePatch(chunk.mesh.position, chunk.instancesMatrixBuffer); @@ -179,7 +179,7 @@ terrain.onCreateChunkObservable.add((chunk: TerrainChunk) => { }); const renderDistance = 6; -terrain.init(character.position, renderDistance); +await terrain.init(character.position, renderDistance); grassManager.initInstances(); cubeManager.initInstances(); @@ -201,10 +201,10 @@ scene.onBeforeRenderObservable.add(() => { ui.setText(`${grassManager.getNbInstances().toLocaleString()} grass blades\n${treeManager.getNbInstances().toLocaleString()} trees | ${engine.getFps().toFixed(0)} FPS`); - grassManager.update(camera.position); - cubeManager.update(camera.position); - butterflyManager.update(camera.position); - treeManager.update(camera.position); + grassManager.update(); + cubeManager.update(); + butterflyManager.update(); + treeManager.update(); // do not update terrain every frame to prevent lag spikes if (terrainUpdateCounter % 30 === 0) { @@ -213,8 +213,9 @@ scene.onBeforeRenderObservable.add(() => { } }); - -engine.loadingScreen.hideLoadingUI(); +scene.executeWhenReady(() => { + engine.loadingScreen.hideLoadingUI(); +}); window.addEventListener("resize", () => { engine.resize(); diff --git a/src/ts/instancing/patchManager.ts b/src/ts/instancing/patchManager.ts index 3d9e3b7..56b3971 100644 --- a/src/ts/instancing/patchManager.ts +++ b/src/ts/instancing/patchManager.ts @@ -16,7 +16,7 @@ export class PatchManager { private readonly computeLodLevel: (patch: IPatch) => number; private readonly queue: Array<{ newLOD: number; patch: IPatch }> = []; - constructor(meshesFromLod: Mesh[], computeLodLevel = (patch: IPatch) => 0) { + constructor(meshesFromLod: Mesh[], computeLodLevel = (_patch: IPatch) => 0) { this.meshesFromLod = meshesFromLod; this.nbVertexFromLod = this.meshesFromLod.map((mesh) => { if (mesh instanceof Mesh) return mesh.getTotalVertices(); @@ -62,16 +62,18 @@ export class PatchManager { const patchPosition = new Vector3(x * patchSize, 0, z * patchSize); - promises.push(createSquareMatrixBuffer(patchPosition, patchSize, patchResolution, engine).then((buffer) => { - return new ThinInstancePatch(patchPosition, buffer); - })); + promises.push( + createSquareMatrixBuffer(patchPosition, patchSize, patchResolution, engine).then((buffer) => { + return new ThinInstancePatch(patchPosition, buffer); + }) + ); } } return Promise.all(promises); } - public update(playerPosition: Vector3) { + public update() { if (this.meshesFromLod.length > 1) this.updateLOD(); else this.updateQueue(this.patchUpdateRate); } diff --git a/src/ts/minimal.ts b/src/ts/minimal.ts index 87f9c32..135fb33 100644 --- a/src/ts/minimal.ts +++ b/src/ts/minimal.ts @@ -12,15 +12,14 @@ import { ThinInstancePatch } from "./instancing/thinInstancePatch"; import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera"; import "@babylonjs/materials"; -import { EngineFactory } from "@babylonjs/core/Engines/engineFactory"; -import "@babylonjs/core/Engines"; +import { createEngine } from "./utils/createEngine"; // Init babylonjs const canvas = document.getElementById("renderer") as HTMLCanvasElement; canvas.width = window.innerWidth; canvas.height = window.innerHeight; -const engine = await EngineFactory.CreateAsync(canvas, {}); +const engine = await createEngine(canvas); engine.displayLoadingUI(); const scene = new Scene(engine); diff --git a/src/ts/modules.ts b/src/ts/modules.ts index 6334937..d49d8c8 100644 --- a/src/ts/modules.ts +++ b/src/ts/modules.ts @@ -4,4 +4,4 @@ declare module "*.glb"; declare module "*.babylon"; declare module "*.tga"; declare module "*.mp3"; -declare module "*.wgsl"; \ No newline at end of file +declare module "*.wgsl"; diff --git a/src/ts/planet.ts b/src/ts/planet.ts index 4ed3f3e..2be283a 100644 --- a/src/ts/planet.ts +++ b/src/ts/planet.ts @@ -20,17 +20,14 @@ import "@babylonjs/core/Physics/physicsEngineComponent"; import HavokPhysics from "@babylonjs/havok"; import { HavokPlugin } from "@babylonjs/core/Physics/v2/Plugins/havokPlugin"; import { createCharacterController } from "./utils/character"; -import { setUpVector } from "./utils/algebra"; -import { EngineFactory } from "@babylonjs/core/Engines/engineFactory"; -import "@babylonjs/core/Engines"; - +import { createEngine } from "./utils/createEngine"; // Init babylonjs const canvas = document.getElementById("renderer") as HTMLCanvasElement; canvas.width = window.innerWidth; canvas.height = window.innerHeight; -const engine = await EngineFactory.CreateAsync(canvas, {}); +const engine = await createEngine(canvas); engine.displayLoadingUI(); if (engine.getCaps().supportComputeShaders) { @@ -59,18 +56,10 @@ ambient.intensity = 0.2; createSkybox(scene, light.direction.scale(-1)); -const character = await createCharacterController(scene, camera); -scene.onAfterPhysicsObservable.add(() => { - setUpVector(character, character.position.subtract(planet.node.position).normalize()); - - camera.upVector = character.up; - - character.computeWorldMatrix(true); - camera.getViewMatrix(true); -}); +await createCharacterController(scene, camera, true); // Interesting part starts here -const planet = new Planet(planetRadius, scene); +new Planet(planetRadius, scene); document.addEventListener("keypress", (e) => { if (e.key === "p") { @@ -79,7 +68,9 @@ document.addEventListener("keypress", (e) => { } }); -engine.loadingScreen.hideLoadingUI(); +scene.executeWhenReady(() => { + engine.loadingScreen.hideLoadingUI(); +}); window.addEventListener("resize", () => { engine.resize(); diff --git a/src/ts/planet/createPlanet.ts b/src/ts/planet/createPlanet.ts index 0e35d2d..dc21ebe 100644 --- a/src/ts/planet/createPlanet.ts +++ b/src/ts/planet/createPlanet.ts @@ -23,9 +23,10 @@ export class Planet { material.specularColor.scaleInPlace(0); //material.wireframe = true; - this.chunks.forEach(chunk => { + this.chunks.forEach((chunk) => { chunk.mesh.parent = this.node; chunk.mesh.material = material; + chunk.init(scene); }); } -} \ No newline at end of file +} diff --git a/src/ts/planet/planetChunk.ts b/src/ts/planet/planetChunk.ts index 28ce9c0..c5947fb 100644 --- a/src/ts/planet/planetChunk.ts +++ b/src/ts/planet/planetChunk.ts @@ -10,12 +10,11 @@ import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight"; import { ThinInstancePatch } from "../instancing/thinInstancePatch"; import { MeshBuilder } from "@babylonjs/core/Meshes/meshBuilder"; import { downSample } from "../utils/matrixBuffer"; -import { createTree } from "../utils/tree"; -import { createButterfly } from "../butterfly/butterfly"; -import { createButterflyMaterial } from "../butterfly/butterflyMaterial"; import { PhysicsAggregate } from "@babylonjs/core/Physics/v2/physicsAggregate"; import { PhysicsShapeType } from "@babylonjs/core/Physics/v2/IPhysicsEnginePlugin"; import { InstancePatch } from "../instancing/instancePatch"; +import { computeVertexData } from "../compute/Terrain3dVertexData/computeVertexData"; +import { computeScatterPoints } from "../compute/scatterTerrain/computeScatterPoints"; export enum Direction { FRONT, @@ -45,134 +44,169 @@ function rotationFromDirection(direction: Direction) { export class PlanetChunk { readonly mesh: Mesh; - readonly instancesMatrixBuffer: Float32Array; - readonly alignedInstancesMatrixBuffer: Float32Array; + instancesMatrixBuffer: Float32Array | null = null; + alignedInstancesMatrixBuffer: Float32Array | null = null; - constructor(direction: Direction, planetRadius: number, scene: Scene) { - this.mesh = new Mesh("chunk", scene); - - const nbVerticesPerRow = 64; - - const positions = new Float32Array(nbVerticesPerRow * nbVerticesPerRow * 3); - const normals = new Float32Array(nbVerticesPerRow * nbVerticesPerRow * 3); - const indices = new Uint32Array((nbVerticesPerRow - 1) * (nbVerticesPerRow - 1) * 6); - - const size = planetRadius * 2; - const stepSize = size / (nbVerticesPerRow - 1); - - const scatterPerSquareMeter = 300; - - const flatArea = size * size; - const maxNbInstances = Math.floor(flatArea * scatterPerSquareMeter * 2.0); - this.instancesMatrixBuffer = new Float32Array(16 * maxNbInstances); - this.alignedInstancesMatrixBuffer = new Float32Array(16 * maxNbInstances); - - const rotationQuaternion = rotationFromDirection(direction); - - const chunkPosition = new Vector3(0, 0, -size / 2); - const rotatedChunkPosition = chunkPosition.applyRotationQuaternion(rotationQuaternion); - - this.mesh.position = rotatedChunkPosition; - - let indexIndex = 0; - let instanceIndex = 0; - let excessInstanceNumber = 0; - for (let x = 0; x < nbVerticesPerRow; x++) { - for (let y = 0; y < nbVerticesPerRow; y++) { - const index = x * nbVerticesPerRow + y; - const positionX = x * stepSize - size / 2; - const positionY = y * stepSize - size / 2; - const positionZ = 0; - - const vertexPosition = chunkPosition.add(new Vector3(positionX, positionY, positionZ)); - vertexPosition.applyRotationQuaternionInPlace(rotationQuaternion); - - const vertexNormalToPlanet = vertexPosition.normalizeToNew(); - - vertexPosition.copyFrom(vertexNormalToPlanet.scale(planetRadius)); - - const [height, gradient] = terrainFunction(vertexPosition); - - const projectedGradient = gradient.subtract(vertexNormalToPlanet.scale(Vector3.Dot(gradient, vertexNormalToPlanet))); - - vertexPosition.addInPlace(vertexNormalToPlanet.scale(height)); - vertexPosition.subtractInPlace(this.mesh.position); - - positions[3 * index + 0] = vertexPosition.x; - positions[3 * index + 1] = vertexPosition.y; - positions[3 * index + 2] = vertexPosition.z; - - const normal = vertexNormalToPlanet.subtract(projectedGradient).normalize(); + private readonly nbVerticesPerRow = 64; + private readonly size: number; + private readonly scatterPerSquareMeter = 300; + private readonly direction: Direction; - normals[3 * index + 0] = normal.x; - normals[3 * index + 1] = normal.y; - normals[3 * index + 2] = normal.z; + private vertexData: VertexData | null = null; - if (x == 0 || y == 0) continue; - - indices[indexIndex++] = index - 1; - indices[indexIndex++] = index; - indices[indexIndex++] = index - nbVerticesPerRow - 1; - - const triangleArea1 = triangleAreaFromBuffer(positions, index - 1, index, index - nbVerticesPerRow - 1); - const nbInstances1 = Math.floor(triangleArea1 * scatterPerSquareMeter + excessInstanceNumber); - excessInstanceNumber = triangleArea1 * scatterPerSquareMeter + excessInstanceNumber - nbInstances1; - instanceIndex = scatterInTriangle( - this.mesh.position, - nbInstances1, - instanceIndex, - this.instancesMatrixBuffer, - this.alignedInstancesMatrixBuffer, - positions, - normals, - vertexNormalToPlanet, - index - 1, - index, - index - nbVerticesPerRow - 1 - ); - if (instanceIndex >= maxNbInstances) { - throw new Error("Too many instances"); - } + constructor(direction: Direction, planetRadius: number, scene: Scene) { + this.mesh = new Mesh("chunk", scene); + this.size = planetRadius * 2; + this.direction = direction; + } - indices[indexIndex++] = index; - indices[indexIndex++] = index - nbVerticesPerRow; - indices[indexIndex++] = index - nbVerticesPerRow - 1; - - const triangleArea2 = triangleAreaFromBuffer(positions, index, index - nbVerticesPerRow, index - nbVerticesPerRow - 1); - const nbInstances2 = Math.floor(triangleArea2 * scatterPerSquareMeter + excessInstanceNumber); - excessInstanceNumber = triangleArea2 * scatterPerSquareMeter + excessInstanceNumber - nbInstances2; - instanceIndex = scatterInTriangle( - this.mesh.position, - nbInstances2, - instanceIndex, - this.instancesMatrixBuffer, - this.alignedInstancesMatrixBuffer, - positions, - normals, - vertexNormalToPlanet, - index, - index - nbVerticesPerRow, - index - nbVerticesPerRow - 1 - ); - if (instanceIndex >= maxNbInstances) { - throw new Error("Too many instances"); + async init(scene: Scene) { + if (scene.getEngine().getCaps().supportComputeShaders) { + const vertexData = await computeVertexData(this.nbVerticesPerRow, this.mesh.position, rotationFromDirection(this.direction), this.size, scene.getEngine()); + vertexData.applyToMesh(this.mesh); + this.vertexData = vertexData; + } else { + const positions = new Float32Array(this.nbVerticesPerRow * this.nbVerticesPerRow * 3); + const normals = new Float32Array(this.nbVerticesPerRow * this.nbVerticesPerRow * 3); + const indices = new Uint32Array((this.nbVerticesPerRow - 1) * (this.nbVerticesPerRow - 1) * 6); + + const flatArea = this.size * this.size; + const maxNbInstances = Math.floor(flatArea * this.scatterPerSquareMeter * 2.0); + this.instancesMatrixBuffer = new Float32Array(16 * maxNbInstances); + this.alignedInstancesMatrixBuffer = new Float32Array(16 * maxNbInstances); + + const rotationQuaternion = rotationFromDirection(this.direction); + + const chunkPosition = new Vector3(0, 0, -this.size / 2); + const rotatedChunkPosition = chunkPosition.applyRotationQuaternion(rotationQuaternion); + + this.mesh.position = rotatedChunkPosition; + + const stepSize = this.size / (this.nbVerticesPerRow - 1); + let indexIndex = 0; + let instanceIndex = 0; + let excessInstanceNumber = 0; + for (let x = 0; x < this.nbVerticesPerRow; x++) { + for (let y = 0; y < this.nbVerticesPerRow; y++) { + const index = x * this.nbVerticesPerRow + y; + const positionX = x * stepSize - this.size / 2; + const positionY = y * stepSize - this.size / 2; + const positionZ = 0; + + const vertexPosition = chunkPosition.add(new Vector3(positionX, positionY, positionZ)); + vertexPosition.applyRotationQuaternionInPlace(rotationQuaternion); + + const vertexNormalToPlanet = vertexPosition.normalizeToNew(); + + vertexPosition.copyFrom(vertexNormalToPlanet.scale(this.size / 2)); + + const [height, gradient] = terrainFunction(vertexPosition); + + const projectedGradient = gradient.subtract(vertexNormalToPlanet.scale(Vector3.Dot(gradient, vertexNormalToPlanet))); + + vertexPosition.addInPlace(vertexNormalToPlanet.scale(height)); + vertexPosition.subtractInPlace(this.mesh.position); + + positions[3 * index + 0] = vertexPosition.x; + positions[3 * index + 1] = vertexPosition.y; + positions[3 * index + 2] = vertexPosition.z; + + const normal = vertexNormalToPlanet.subtract(projectedGradient).normalize(); + + normals[3 * index + 0] = normal.x; + normals[3 * index + 1] = normal.y; + normals[3 * index + 2] = normal.z; + + if (x == 0 || y == 0) continue; + + indices[indexIndex++] = index - 1; + indices[indexIndex++] = index; + indices[indexIndex++] = index - this.nbVerticesPerRow - 1; + + const triangleArea1 = triangleAreaFromBuffer(positions, index - 1, index, index - this.nbVerticesPerRow - 1); + const nbInstances1 = Math.floor(triangleArea1 * this.scatterPerSquareMeter + excessInstanceNumber); + excessInstanceNumber = triangleArea1 * this.scatterPerSquareMeter + excessInstanceNumber - nbInstances1; + instanceIndex = scatterInTriangle( + this.mesh.position, + nbInstances1, + instanceIndex, + this.instancesMatrixBuffer, + this.alignedInstancesMatrixBuffer, + positions, + normals, + vertexNormalToPlanet, + index - 1, + index, + index - this.nbVerticesPerRow - 1 + ); + if (instanceIndex >= maxNbInstances) { + throw new Error("Too many instances"); + } + + indices[indexIndex++] = index; + indices[indexIndex++] = index - this.nbVerticesPerRow; + indices[indexIndex++] = index - this.nbVerticesPerRow - 1; + + const triangleArea2 = triangleAreaFromBuffer(positions, index, index - this.nbVerticesPerRow, index - this.nbVerticesPerRow - 1); + const nbInstances2 = Math.floor(triangleArea2 * this.scatterPerSquareMeter + excessInstanceNumber); + excessInstanceNumber = triangleArea2 * this.scatterPerSquareMeter + excessInstanceNumber - nbInstances2; + instanceIndex = scatterInTriangle( + this.mesh.position, + nbInstances2, + instanceIndex, + this.instancesMatrixBuffer, + this.alignedInstancesMatrixBuffer, + positions, + normals, + vertexNormalToPlanet, + index, + index - this.nbVerticesPerRow, + index - this.nbVerticesPerRow - 1 + ); + if (instanceIndex >= maxNbInstances) { + throw new Error("Too many instances"); + } } } - } - const vertexData = new VertexData(); - vertexData.positions = positions; - vertexData.indices = indices; - vertexData.normals = normals; + const vertexData = new VertexData(); + vertexData.positions = positions; + vertexData.indices = indices; + vertexData.normals = normals; + this.vertexData = vertexData; - vertexData.applyToMesh(this.mesh); + vertexData.applyToMesh(this.mesh); + } new PhysicsAggregate(this.mesh, PhysicsShapeType.MESH, { mass: 0 }, scene); - this.scatterAssets(scene); + await this.scatterAssets(scene); } - scatterAssets(scene: Scene) { + async scatterAssets(scene: Scene) { + if (scene.getEngine().getCaps().supportComputeShaders) { + if (this.vertexData === null) { + throw new Error("Vertex data is null"); + } + + const flatArea = this.size * this.size; + [this.instancesMatrixBuffer, this.alignedInstancesMatrixBuffer] = await computeScatterPoints( + this.vertexData, + this.mesh.position, + 2 * flatArea, + this.nbVerticesPerRow, + this.scatterPerSquareMeter, + scene.getEngine() + ); + } + + if (this.instancesMatrixBuffer === null) { + throw new Error("Instances matrix buffer is null"); + } + if (this.alignedInstancesMatrixBuffer === null) { + throw new Error("Aligned instance matrices are null"); + } + const grassBlade = createGrassBlade(scene, 2); grassBlade.isVisible = false; grassBlade.material = createGrassMaterial(scene.lights[0] as DirectionalLight, scene); @@ -180,30 +214,13 @@ export class PlanetChunk { const patch = new ThinInstancePatch(this.mesh.position, this.alignedInstancesMatrixBuffer); patch.createInstances(grassBlade); - /*createTree(scene).then((tree) => { - tree.scaling.scaleInPlace(3); - tree.position.y = -1; - tree.bakeCurrentTransformIntoVertices(); - tree.isVisible = false; - const treePatch = new ThinInstancePatch(this.mesh.position, downSample(this.instancesMatrixBuffer, 20000)); - treePatch.createInstances(tree); - });*/ - const cube = MeshBuilder.CreateBox("cube", { size: 1 }, scene); cube.position.y = 0.5; cube.bakeCurrentTransformIntoVertices(); cube.isVisible = false; cube.checkCollisions = true; - const cubePatch = new InstancePatch(this.mesh.position, downSample(this.instancesMatrixBuffer, 5000)); + const cubePatch = new InstancePatch(this.mesh.position, downSample(this.alignedInstancesMatrixBuffer, 5000)); cubePatch.createInstances(cube); - - /*const butterfly = createButterfly(scene); - //butterfly.position.y = 1; - //butterfly.bakeCurrentTransformIntoVertices(); - butterfly.material = createButterflyMaterial(scene.lights[0] as DirectionalLight, scene); - butterfly.isVisible = false; - const butterflyPatch = new ThinInstancePatch(this.mesh.position, downSample(this.instancesMatrixBuffer, 1000)); - butterflyPatch.createInstances(butterfly);*/ } } @@ -255,4 +272,4 @@ function scatterInTriangle( } return instanceIndex; -} \ No newline at end of file +} diff --git a/src/ts/terrain/terrain.ts b/src/ts/terrain/terrain.ts index 4e8af92..8a4f3a2 100644 --- a/src/ts/terrain/terrain.ts +++ b/src/ts/terrain/terrain.ts @@ -71,16 +71,19 @@ export class Terrain { private buildNextChunks(n: number) { // dequeue chunks to create + const promises: Promise[] = []; for (let i = 0; i < n; i++) { const data = this.creationQueue.shift(); if (data === undefined) break; const [position, scatterPerSquareMeter] = data; - this.createChunk(position, scatterPerSquareMeter); + promises.push(this.createChunk(position, scatterPerSquareMeter)); } + + return Promise.all(promises); } - public init(playerPosition: Vector3, renderDistance: number) { + public async init(playerPosition: Vector3, renderDistance: number) { this.update(playerPosition, renderDistance, 0); - this.buildNextChunks(this.creationQueue.length); + await this.buildNextChunks(this.creationQueue.length); } } diff --git a/src/ts/terrain/terrainChunk.ts b/src/ts/terrain/terrainChunk.ts index c009145..cb3f353 100644 --- a/src/ts/terrain/terrainChunk.ts +++ b/src/ts/terrain/terrainChunk.ts @@ -8,9 +8,8 @@ import { PhysicsShapeType } from "@babylonjs/core/Physics/v2/IPhysicsEnginePlugi import { Observable } from "@babylonjs/core/Misc/observable"; import { randomPointInTriangleFromBuffer, triangleAreaFromBuffer } from "../utils/triangle"; import { getTransformationQuaternion } from "../utils/algebra"; -import { computeVertexData } from "../compute/vertexData/computeVertexData"; +import { computeVertexData } from "../compute/Terrain2dVertexData/computeVertexData"; import { computeScatterPoints } from "../compute/scatterTerrain/computeScatterPoints"; -import { showNormals } from "../utils/debug"; function scatterInTriangle( chunkPosition: Vector3, @@ -88,13 +87,19 @@ export class TerrainChunk { async init(scene: Scene): Promise { const flatArea = this.size * this.size; - if(scene.getEngine().getCaps().supportComputeShaders) { + if (scene.getEngine().getCaps().supportComputeShaders) { const vertexData = await computeVertexData(this.nbVerticesPerRow, this.mesh.position, this.size, scene.getEngine()); vertexData.applyToMesh(this.mesh); this.aggregate = new PhysicsAggregate(this.mesh, PhysicsShapeType.MESH, { mass: 0 }, scene); - [this.instancesMatrixBuffer, this.alignedInstancesMatrixBuffer] = - await computeScatterPoints(vertexData, this.mesh.position, flatArea, this.nbVerticesPerRow, this.scatterPerSquareMeter, scene.getEngine()); + [this.instancesMatrixBuffer, this.alignedInstancesMatrixBuffer] = await computeScatterPoints( + vertexData, + this.mesh.position, + flatArea, + this.nbVerticesPerRow, + this.scatterPerSquareMeter, + scene.getEngine() + ); } else { const positions = new Float32Array(this.nbVerticesPerRow * this.nbVerticesPerRow * 3); const normals = new Float32Array(this.nbVerticesPerRow * this.nbVerticesPerRow * 3); diff --git a/src/ts/utils/algebra.ts b/src/ts/utils/algebra.ts index bfee105..5d5d596 100644 --- a/src/ts/utils/algebra.ts +++ b/src/ts/utils/algebra.ts @@ -10,6 +10,6 @@ export function getTransformationQuaternion(from: Vector3, to: Vector3): Quatern export function setUpVector(transformNode: TransformNode, newUpVector: Vector3) { const currentUpVector = transformNode.up; const rotationQuaternion = getTransformationQuaternion(currentUpVector, newUpVector); - if(transformNode.rotationQuaternion === null) transformNode.rotationQuaternion = rotationQuaternion; + if (transformNode.rotationQuaternion === null) transformNode.rotationQuaternion = rotationQuaternion; else transformNode.rotationQuaternion = rotationQuaternion.multiply(transformNode.rotationQuaternion); -} \ No newline at end of file +} diff --git a/src/ts/utils/character.ts b/src/ts/utils/character.ts index 0b81c4c..902450b 100644 --- a/src/ts/utils/character.ts +++ b/src/ts/utils/character.ts @@ -15,7 +15,7 @@ import { PhysicsEngineV2 } from "@babylonjs/core/Physics/v2"; import { ActionManager, ExecuteCodeAction } from "@babylonjs/core/Actions"; import { setUpVector } from "./algebra"; -export async function createCharacterController(scene: Scene, camera: ArcRotateCamera): Promise { +export async function createCharacterController(scene: Scene, camera: ArcRotateCamera, planet = false): Promise { const result = await SceneLoader.ImportMeshAsync("", "", character, scene); const hero = result.meshes[0]; @@ -61,6 +61,9 @@ export async function createCharacterController(scene: Scene, camera: ArcRotateC const raycastResult = new PhysicsRaycastResult(); + //FIXME when the position is 0 then character cannot be rotated (this makes no sense) + hero.position = new Vector3(0, 0.000000001, 0); + //Rendering loop (executed for everyframe) scene.onBeforePhysicsObservable.add(() => { const deltaTime = scene.getEngine().getDeltaTime() / 1000; @@ -116,12 +119,17 @@ export async function createCharacterController(scene: Scene, camera: ArcRotateC } } + if (planet) { + setUpVector(hero, hero.position.normalizeToNew()); + camera.upVector = hero.up; + } + // downward raycast const start = hero.position.add(hero.up.scale(50)); const end = hero.position.add(hero.up.scale(-50)); (scene.getPhysicsEngine() as PhysicsEngineV2).raycastToRef(start, end, raycastResult); if (raycastResult.hasHit) { - hero.position.y = raycastResult.hitPointWorld.y + 0.01; + hero.position = raycastResult.hitPointWorld.add(hero.up.scale(0.01)); } }); diff --git a/src/ts/utils/createEngine.ts b/src/ts/utils/createEngine.ts new file mode 100644 index 0000000..c795504 --- /dev/null +++ b/src/ts/utils/createEngine.ts @@ -0,0 +1,20 @@ +import { Engine } from "@babylonjs/core/Engines/engine"; +import { WebGPUEngine } from "@babylonjs/core/Engines"; + +export async function createEngine(canvas: HTMLCanvasElement): Promise { + if (window.location.href.includes("?webgpu")) { + if (!WebGPUEngine.IsSupportedAsync) { + throw new Error("WebGPU is not supported on this device"); + } + console.log("BACKEND: WebGPU"); + const engine = new WebGPUEngine(canvas); + await engine.initAsync(); + return engine; + } else if (window.location.href.includes("?webgl")) { + console.log("BACKEND: WebGL"); + return new Engine(canvas, true); + } + + console.log("DEFAULT BACKEND: WebGL"); + return new Engine(canvas); +} diff --git a/src/ts/utils/matrixBuffer.ts b/src/ts/utils/matrixBuffer.ts index e484d70..70d9e9f 100644 --- a/src/ts/utils/matrixBuffer.ts +++ b/src/ts/utils/matrixBuffer.ts @@ -25,7 +25,7 @@ export function randomDownSample(matrixBuffer: Float32Array, stride: number): Fl } export async function createSquareMatrixBuffer(position: Vector3, size: number, resolution: number, engine: Engine) { - if(engine.getCaps().supportComputeShaders) { + if (engine.getCaps().supportComputeShaders) { return computeSquareScatterPoints(position, size, resolution, engine); } diff --git a/src/ts/utils/triangle.ts b/src/ts/utils/triangle.ts index 88411e5..61c866a 100644 --- a/src/ts/utils/triangle.ts +++ b/src/ts/utils/triangle.ts @@ -70,4 +70,4 @@ export function randomPointInTriangleFromBuffer(positions: Float32Array, normals const nz = f1 * n1z + f2 * n2z + f3 * n3z; return [x, y, z, nx, ny, nz]; -} \ No newline at end of file +}