Skip to content

Commit

Permalink
Compute shaders feature parity
Browse files Browse the repository at this point in the history
- Compute shaders work for the planet terrain
- Fixed character walking on sphere
- added custom url parameter to enable webgpu (it will always default to webgl for stability)
- formatting
  • Loading branch information
BarthPaleologue committed Nov 26, 2023
1 parent ed76a78 commit 7b97bcf
Show file tree
Hide file tree
Showing 21 changed files with 397 additions and 211 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand All @@ -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`.
Expand Down
File renamed without changes.
85 changes: 85 additions & 0 deletions src/ts/compute/Terrain3dVertexData/computeVertexData.ts
Original file line number Diff line number Diff line change
@@ -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<VertexData> {
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));
});
});
}
57 changes: 57 additions & 0 deletions src/ts/compute/Terrain3dVertexData/vertexData.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
struct Params {
nbVerticesPerRow : u32,
size : f32,
position : vec3<f32>,
rotationMatrix : mat4x4<f32>,
};

struct FloatArray {
elements : array<f32>,
};
struct UIntArray {
elements : array<atomic<u32>>,
};

@group(0) @binding(0) var<storage, read_write> positions : FloatArray;
@group(0) @binding(1) var<storage, read_write> normals : FloatArray;
@group(0) @binding(2) var<storage, read_write> indices : UIntArray;
@group(0) @binding(3) var<uniform> params : Params;

@compute @workgroup_size(1,1,1)
fn main(@builtin(global_invocation_id) id: vec3<u32>)
{
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<f32>(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<f32> = vertex_position + vec3(0.0, 0.0, -params.size / 2.0);
vertex_position_world = (params.rotationMatrix * vec4<f32>(vertex_position_world, 1.0)).xyz;

let normal: vec3<f32> = 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);
}
}
9 changes: 2 additions & 7 deletions src/ts/compute/scatterSquare/computeSquareScatterPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Float32Array> {
export async function computeSquareScatterPoints(position: Vector3, size: number, resolution: number, engine: Engine): Promise<Float32Array> {
const computeShader = new ComputeShader(
"scatter",
engine,
Expand Down Expand Up @@ -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();
Expand Down
24 changes: 10 additions & 14 deletions src/ts/flatfield.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -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",
Expand All @@ -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);

Expand All @@ -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();
});

Expand Down
25 changes: 13 additions & 12 deletions src/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -213,8 +213,9 @@ scene.onBeforeRenderObservable.add(() => {
}
});


engine.loadingScreen.hideLoadingUI();
scene.executeWhenReady(() => {
engine.loadingScreen.hideLoadingUI();
});

window.addEventListener("resize", () => {
engine.resize();
Expand Down
12 changes: 7 additions & 5 deletions src/ts/instancing/patchManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 2 additions & 3 deletions src/ts/minimal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/ts/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ declare module "*.glb";
declare module "*.babylon";
declare module "*.tga";
declare module "*.mp3";
declare module "*.wgsl";
declare module "*.wgsl";
Loading

0 comments on commit 7b97bcf

Please sign in to comment.