WebGPU Cloth Simulation 06 - WebGPU Cloth Simulation Example (Mass Spring System) - Part 1
Real-Time Web-Based Cloth Simulation With WebGPU, Typescript, and Mass Spring System
GitHub Repository: https://github.com/PINKDIAMONDVVS/webGPUTutorial
Live Demo (Chrome): https://webgpudemo-pinkdiamondvvs-projects.vercel.app/clothParticle
Live Demo (Chrome): https://webgpudemo-pinkdiamondvvs-projects.vercel.app/clothParticle
1. Overview
This project is a comprehensive exploration of the capabilities of WebGPU, the next-generation graphics and computation web API. The primary focus is on simulating cloth behavior in real-time using a mass-spring system. By leveraging WebGPU, the project demonstrates the potential for high-performance simulations directly in the browser, making it a cutting-edge example of modern web technologies in action.The cloth simulation model implemented in this project uses a grid of particles connected by springs, which can represent various types of fabrics by adjusting physical parameters. These parameters include structural, shear, and bending spring constants, as well as damping coefficients, allowing the simulation to replicate the behavior of different materials ranging from stiff denim to soft silk.
Key aspects of the project include the setup of multiple compute pipelines to handle various aspects of the simulation. These pipelines manage the calculations for particle updates, spring forces, node force summation, collision detection, and normal updates. Each pipeline is designed to leverage the parallel processing capabilities of modern GPUs, ensuring efficient and real-time updates to the simulation.
2. Features
a. Real-time Cloth Simulation:
- Utilizes a mass-spring system to simulate realistic cloth behavior.
- Dynamic updates and rendering ensure responsive and interactive simulations.
- Dynamic updates and rendering ensure responsive and interactive simulations.
b. WebGPU Integration:
- Leverages WebGPU for high-performance computations and rendering.
- Demonstrates the capabilities of WebGPU in handling complex simulations.
- Demonstrates the capabilities of WebGPU in handling complex simulations.
c. Customizable Parameters:
- Cloth model parameters, such as the number of particles and spring constants, can be adjusted to simulate different types of fabrics.
- External forces like wind can be dynamically applied to the cloth.
- External forces like wind can be dynamically applied to the cloth.
d. Compute Pipelines:
- Particle Pipeline: Updates particle positions and velocities based on applied forces.
- Spring Force Compute Pipeline: Calculates forces exerted by springs connecting particles.
- Node Force Summation Pipeline: Sums forces at each node to determine the net effect.
- Intersection Pipeline: Detects and resolves intersections within the cloth and with external objects.
- Tri-Tri Intersection Pipeline: Handles intersections between cloth triangles for realistic collision responses.
- Update Normal Pipeline: Updates normals for accurate lighting and shading.
- Spring Force Compute Pipeline: Calculates forces exerted by springs connecting particles.
- Node Force Summation Pipeline: Sums forces at each node to determine the net effect.
- Intersection Pipeline: Detects and resolves intersections within the cloth and with external objects.
- Tri-Tri Intersection Pipeline: Handles intersections between cloth triangles for realistic collision responses.
- Update Normal Pipeline: Updates normals for accurate lighting and shading.
e. Interactive Camera Controls:
- Supports mouse-based interactions for rotating, panning, and zooming the camera.
- Provides an intuitive interface for exploring the simulation from different angles.
- Provides an intuitive interface for exploring the simulation from different angles.
f. Comprehensive Rendering:
- Render Pipeline: Renders the cloth with appropriate textures, lighting, and shading.
- Spring Visualization: Optionally renders springs to visualize the underlying mass-spring structure.
- Spring Visualization: Optionally renders springs to visualize the underlying mass-spring structure.
g. Efficient Resource Management:
- Utilizes GPU buffers for storing particle positions, velocities, forces, and other simulation data.
- Employs bind groups and layouts for efficient resource binding and management.
- Employs bind groups and layouts for efficient resource binding and management.
h. Performance Metrics:
- Displays frame statistics such as milliseconds per frame and frames per second.
3. Function Descriptions
a. startClothSimulation
Initializes the cloth simulation environment, sets up mouse event handlers for camera control, creates the cloth model, and starts the animation loop.b. createClothModel
Initializes the cloth model by creating particles and springs based on the specified parameters, defining the cloth's structure and physical properties.c. createParticles
Generates a grid of particles for the cloth simulation, initializing their positions, velocities, and UV coordinates.d. createSprings
Creates springs between particles to form the mass-spring system, defining structural, shear, and bending springs.e. calculateNormal
Computes the normal vector for a triangle formed by three vertices, used for lighting calculations.f. createClothBuffers
Sets up various GPU buffers required for the cloth simulation, storing data such as particle positions, velocities, forces, and normals.g. createRenderPipeline
Configures the rendering pipeline for particles, setting up shaders, buffers, and the pipeline layout for rendering the cloth.h. createSpringPipeline
Configures the rendering pipeline for visualizing springs, defining shaders, buffers, and the pipeline layout for rendering springs.i. createTrianglePipeline
Sets up the rendering pipeline for rendering the cloth's surface, applying textures, lighting, and shading.j. createParticlePipeline
Configures the compute pipeline for updating particle states in the simulation, including positions, velocities, and forces.k. createUpdateNormalPipeline
Sets up compute pipelines for updating normals, calculating triangle normals, and summing these normals at each vertex for accurate lighting.l. createSpringForceComputePipeline
Establishes the compute pipeline for calculating spring forces, updating the forces exerted by springs connecting particles.m. createNodeForceSummationPipeline
Configures compute pipelines for initializing and summing node forces, ensuring accurate force calculations at each particle.n. createIntersectionPipeline
Sets up the compute pipeline for detecting and handling intersections within the cloth and with external objects.o. createTriTriIntersectionPipeline
Configures the compute pipeline for detecting and resolving intersections between cloth triangles, ensuring realistic collision responses.p. render
Executes a single frame of the simulation, performing compute passes to update the simulation state and rendering the updated cloth. Updates frame statistics to monitor performance.4. Code Breakdown
a. Initialization
The startClothSimulation function is the core initialization and execution routine for the real-time cloth simulation component. This function sets up the environment, handles user interactions, and manages the rendering and simulation pipelines using WebGPU.
export const Initialize = async () => {
await startClothSimluation();
};
const startClothSimluation = async () => {
const canvas = document.querySelector("canvas#gfx-main") as HTMLCanvasElement;
// `as HTMLCanvasElement` use type assertions
if (!canvas) {
console.error("Canvas element not found");
return;
}
let isLeftMouseDown = false;
let isRightMouseDown = false;
let lastMouseX: number, lastMouseY: number;
// Updates mouse button states and logs clicks.
// Stores the current mouse position.
canvas.addEventListener("mousedown", (event: MouseEvent) => {
// `MouseEvent` Specify type
if (event.button === 0) {
// Left click
isLeftMouseDown = true;
console.log("Left click");
} else if (event.button === 2) {
// Right click
isRightMouseDown = true;
console.log("Right click");
}
lastMouseX = event.clientX;
lastMouseY = event.clientY;
});
// reset mouse button states when the mouse button is released
document.addEventListener("mouseup", (event) => {
isLeftMouseDown = false;
isRightMouseDown = false;
});
const sceneManager = new ClothRenderer("gfx-main");
sceneManager.init().then(() => {
// Rotates or pans the camera based on mouse movement and button states.
canvas.addEventListener("mousemove", (event) => {
if (isLeftMouseDown) {
// Implement camera rotation logic
const dx = event.clientX - lastMouseX;
const dy = event.clientY - lastMouseY;
//console.log("rotate");
sceneManager.rotateCamera(dx, dy);
} else if (isRightMouseDown) {
// Implement camera panning logic
const dx = event.clientX - lastMouseX;
const dy = event.clientY - lastMouseY;
// Execute panning logic
sceneManager.panCamera(dx, dy);
}
lastMouseX = event.clientX;
lastMouseY = event.clientY;
});
// Zooms the camera based on the wheel event.
canvas.addEventListener("wheel", (event) => {
// Implement camera zoom in/out logic
sceneManager.zoomCamera(event.deltaY / 100);
});
// createClothModel(
// x: number,
// y: number,
// structuralKs: number = 5000.0,
// shearKs: number = 2000.0,
// bendKs: number = 500.0,
// kd: number = 0.25
// ) {
sceneManager.createClothModel(60, 60, 555000.0, 545000.0, 550000.0, 1000);
// creates the buffers for the cloth sceneManager.createClothBuffers();
// creates the pipeline for rendering the cloth
sceneManager.createRenderPipeline();
// creates the pipeline for updating the springs
sceneManager.createSpringPipeline();
// creates the pipeline for updating the triangles
sceneManager.createTrianglePipeline();
// creates the pipeline for rendering the particles
sceneManager.createParticlePipeline();
// creates the pipeline for updating the normals
sceneManager.createUpdateNormalPipeline();
// creates the pipeline for computing the spring forces
sceneManager.createSpringForceComputePipeline();
// creates the pipeline for summing the node forces
sceneManager.createNodeForceSummationPipeline();
// creates the pipeline for computing the intersections
sceneManager.createIntersectionPipeline();
// creates the pipeline for computing the triangle-triangle intersections
sceneManager.createTriTriIntersectionPipeline();
animate();
});
// Create an animation loop function
function animate() {
sceneManager.render();
requestAnimationFrame(animate);
}
};
b1. Create the cloth model in the scene
The createClothModel function is responsible for initializing the cloth simulation by creating a grid of particles and connecting them with springs. This function takes in parameters that define the size of the cloth and its physical properties, which are then used to set up the mass-spring system. - x:
Number of particles along the width of the cloth.
- y:
- y:
Number of particles along the height of the cloth.
- structuralKs:
- structuralKs:
Structural spring constant, affecting the stiffness of the cloth.
- shearKs:
- shearKs:
Shear spring constant, affecting the cloth's resistance to diagonal deformations.
- bendKs:
- bendKs:
Bend spring constant, influencing the rigidity or flexibility of the cloth.
- kd:
- kd:
Damping coefficient, balancing the movement to prevent excessive bouncing.
createClothModel(
x: number,
y: number,
structuralKs: number = 5000.0,
shearKs: number = 2000.0,
bendKs: number = 500.0,
kd: number = 0.25
) {
this.N = x;
this.M = y;
// Structural Spring Constant.
//High for stiff fabrics like denim, low for soft fabrics like silk.
this.structuralKs = structuralKs;
// Shear Spring Constant.
// High for fabrics that don't easily deform diagonally, low for those that do.
this.shearKs = shearKs;
// Bend Spring Constant.
// High for rigid fabrics, low for flexible fabrics.
this.bendKs = bendKs;
// Damping Coefficient.
// Balanced to prevent excessive bouncing, creating a more natural movement.
this.kD = kd;
this.createParticles();
this.createSprings();
}
b2.Create Particles for Cloth Simulation
The createParticles function generates a grid of particles (nodes) that form the cloth. Each particle is positioned based on the grid size and has an initial velocity and position. This function also computes UV coordinates for texturing and initializes normals for lighting calculations.- Iterates through the grid dimensions to create particles.
- Positions particles in a 3D space, adjusting their height based on the distance from the center to create a natural curvature.
- Stores position and UV coordinates for each particle.
- Initializes an array to store the normals for lighting calculations.
- Calculates and accumulates triangle normals for each particle.
- Normalizes the accumulated normals for accurate lighting.
createParticles() {
// N * M Logic for generating nodes of the grid
//20x20 cloth
const start_x = 30;
const start_y = 30;
const dist_x = this.xSize / this.N;
const dist_y = this.ySize / this.M;
const maxHeight = 27.0; // Set maximum height
const minHeight = 20.0; // Set minimum height
// Calculate center point position
const centerX = (this.N - 1) / 2;
const centerY = (this.M - 1) / 2;
for (let i = 0; i < this.N; i++) {
for (let j = 0; j < this.M; j++) {
// Adjust height based on distance from the center
let distanceFromCenter = Math.sqrt(
Math.pow(i - centerX, 2) + Math.pow(j - centerY, 2)
);
let heightFactor =
(distanceFromCenter / Math.max(centerX, centerY)) *
(maxHeight - minHeight);
let yPos = maxHeight + heightFactor - 7.0;
var pos = vec3.fromValues(
start_x - dist_x * j,
yPos,
start_y - dist_y * i
);
var vel = vec3.fromValues(0, 0, 0);
const n = new Node(pos, vel);
let u = j / (this.M - 1);
let v = i / (this.N - 1);
this.uvIndices.push([u, v]);
this.particles.push(n);
}
}
// An array to store combined position and UV coordinates for each particle.
const combinedVertices: number[] = [];
this.particles.forEach((particle, index) => {
combinedVertices.push(...particle.position, ...this.uvIndices[index]);
});
// Converts the UV coordinates array to a Float32Array for efficient processing.
const uvs: number[] = [];
this.uvIndices.forEach((uv, index) => {
uvs.push(...uv);
});
this.uv = new Float32Array(uvs);
let indices: number[] = [];
this.normals = new Array(this.particles.length);
this.normals.fill(vec3.create());
for (let i = 0; i < this.N - 1; i++) {
for (let j = 0; j < this.M - 1; j++) {
const topLeft = i * this.M + j;
const topRight = topLeft + 1;
const bottomLeft = (i + 1) * this.M + j;
const bottomRight = bottomLeft + 1;
var triangle1 = new Triangle(topLeft, bottomLeft, topRight);
this.triangles.push(triangle1);
this.particles[topLeft].triangles.push(triangle1);
this.particles[bottomLeft].triangles.push(triangle1);
this.particles[topRight].triangles.push(triangle1);
var triangle2 = new Triangle(topRight, bottomLeft, bottomRight);
this.triangles.push(triangle2);
this.particles[topRight].triangles.push(triangle2);
this.particles[bottomLeft].triangles.push(triangle2);
this.particles[bottomRight].triangles.push(triangle2);
indices.push(topLeft, bottomLeft, topRight);
indices.push(topRight, bottomLeft, bottomRight);
// Triangle vertex position
let v0 = this.particles[topLeft].position;
let v1 = this.particles[bottomLeft].position;
let v2 = this.particles[topRight].position;
let v3 = this.particles[bottomRight].position;
// Calculate triangle surface normal
let triangleNormal1 = this.calculateNormal(v0, v1, v2);
let triangleNormal2 = this.calculateNormal(v2, v1, v3);
// Accumulate the triangle's normal to each vertex's normal
vec3.add(this.normals[topLeft], this.normals[topLeft], triangleNormal1);
vec3.add(
this.normals[bottomLeft],
this.normals[bottomLeft],
triangleNormal1
);
vec3.add(
this.normals[topRight],
this.normals[topRight],
triangleNormal1
);
vec3.add(
this.normals[topRight],
this.normals[topRight],
triangleNormal2
);
vec3.add(
this.normals[bottomLeft],
this.normals[bottomLeft],
triangleNormal2
);
vec3.add(
this.normals[bottomRight],
this.normals[bottomRight],
triangleNormal2
);
}
}
// Normalizes the accumulated normals.
this.normals.forEach((normal) => {
vec3.normalize(normal, normal);
});
// Converts the indices array to a Uint32Array for efficient processing.
this.triangleIndices = new Uint32Array(indices);
this.numParticles = this.particles.length;
console.log("make #", this.numParticles, " particles create success");
for (let i = 0; i < this.particles.length; i++) {
let nConnectedTriangle = this.particles[i].triangles.length;
this.maxTriangleConnected = Math.max(
this.maxTriangleConnected,
nConnectedTriangle
);
}
console.log(this.maxTriangleConnected);
}
b3. Calculate Normal Function
The calculateNormal function computes the normal vector for a triangle formed by three vertices. This is essential for lighting calculations, as normals determine how light interacts with the surface. - Computes two edge vectors of the triangle.
- Uses the cross product of the edge vectors to find the normal vector.
- Normalizes the resulting normal vector to ensure it has a unit length.
- Uses the cross product of the edge vectors to find the normal vector.
- Normalizes the resulting normal vector to ensure it has a unit length.
calculateNormal(v0: vec3, v1: vec3, v2: vec3) {
let edge1 = vec3.create();
let edge2 = vec3.create();
let normal = vec3.create();
vec3.subtract(edge1, v1, v0);
vec3.subtract(edge2, v2, v0);
vec3.cross(normal, edge1, edge2);
vec3.normalize(normal, normal);
return normal;
}
b4. Create Springs Between Particles
The createSprings function connects the particles with springs to form the mass-spring system. It creates structural, shear, and bending springs based on the specified spring constants and damping coefficient.Horizontal and vertical springs connecting adjacent particles.
- Shear Springs:
Diagonal springs connecting particles to resist diagonal deformation.
- Bending Springs:
Springs connecting particles that are two steps apart, both horizontally and vertically, to simulate bending resistance.
- Iterates through the grid and creates springs between appropriate particles.
- Adds the springs to the particles, ensuring each particle knows about its connected springs.
- Calculates the maximum number of springs connected to any particle.
- Adjusts spring indices for efficient processing.
- Logs the total number of springs created and the maximum spring connectivity.
createSprings() {
let index = 0;
for (let i = 0; i < this.M; i++) {
for (let j = 0; j < this.N - 1; j++) {
if (i > 0 && j === 0) index++;
// Creates a spring between adjacent horizontal particles
const sp = new Spring(
this.particles[index],
this.particles[index + 1],
this.structuralKs,
this.kD,
"structural",
index,
index + 1
);
sp.targetIndex1 = this.particles[sp.index1].springs.length;
sp.targetIndex2 = this.particles[sp.index2].springs.length;
this.springs.push(sp);
this.particles[sp.index1].springs.push(sp);
this.particles[sp.index2].springs.push(sp);
index++;
}
}
// 2. Structural - Vertical
for (let i = 0; i < this.N - 1; i++) {
for (let j = 0; j < this.M; j++) {
++index;
const sp = new Spring(
this.particles[this.N * i + j],
this.particles[this.N * i + j + this.N],
this.structuralKs,
this.kD,
"structural",
this.N * i + j,
this.N * i + j + this.N
);
sp.targetIndex1 = this.particles[sp.index1].springs.length;
sp.targetIndex2 = this.particles[sp.index2].springs.length;
this.springs.push(sp);
this.particles[sp.index1].springs.push(sp);
this.particles[sp.index2].springs.push(sp);
}
}
// 3. Shear - Top left to bottom right
index = 0;
for (let i = 0; i < this.N * (this.M - 1); i++) {
if (i % this.N === this.N - 1) {
index++;
continue;
}
const sp = new Spring(
this.particles[index],
this.particles[index + this.N + 1],
this.shearKs,
this.kD,
"shear",
index,
index + this.N + 1
);
sp.targetIndex1 = this.particles[sp.index1].springs.length;
sp.targetIndex2 = this.particles[sp.index2].springs.length;
this.springs.push(sp);
this.particles[sp.index1].springs.push(sp);
this.particles[sp.index2].springs.push(sp);
index++;
}
// 4. Shear - Top right to bottom left
index = 0;
for (let i = 0; i < this.N * (this.M - 1); i++) {
if (i % this.N === 0) {
index++;
continue;
}
const sp = new Spring(
this.particles[index],
this.particles[index + this.N - 1],
this.shearKs,
this.kD,
"shear",
index,
index + this.N - 1
);
sp.targetIndex1 = this.particles[sp.index1].springs.length;
sp.targetIndex2 = this.particles[sp.index2].springs.length;
this.springs.push(sp);
this.particles[sp.index1].springs.push(sp);
this.particles[sp.index2].springs.push(sp);
index++;
}
// 5. Bending - Horizontal
index = 0;
for (let i = 0; i < this.N * this.M; i++) {
if (i % this.N > this.N - 3) {
index++;
continue;
}
const sp = new Spring(
this.particles[index],
this.particles[index + 2],
this.bendKs,
this.kD,
"bending",
index,
index + 2
);
sp.targetIndex1 = this.particles[sp.index1].springs.length;
sp.targetIndex2 = this.particles[sp.index2].springs.length;
this.springs.push(sp);
this.particles[sp.index1].springs.push(sp);
this.particles[sp.index2].springs.push(sp);
index++;
}
// //6. Bending - Vertical
for (let i = 0; i < this.N; i++) {
for (let j = 0; j < this.M - 3; j++) {
const sp = new Spring(
this.particles[i + j * this.M],
this.particles[i + (j + 3) * this.M],
this.bendKs,
this.kD,
"bending",
i + j * this.M,
i + (j + 3) * this.M
);
sp.targetIndex1 = this.particles[sp.index1].springs.length;
sp.targetIndex2 = this.particles[sp.index2].springs.length;
this.springs.push(sp);
this.particles[sp.index1].springs.push(sp);
this.particles[sp.index2].springs.push(sp);
}
}
for (let i = 0; i < this.particles.length; i++) {
let nConnectedSpring = this.particles[i].springs.length;
this.maxSpringConnected = Math.max(
this.maxSpringConnected,
nConnectedSpring
);
}
for (let i = 0; i < this.springs.length; i++) {
var sp = this.springs[i];
sp.targetIndex1 += this.maxSpringConnected * sp.index1;
sp.targetIndex2 += this.maxSpringConnected * sp.index2;
}
console.log("maxSpringConnected : #", this.maxSpringConnected);
console.log("make #", this.springs.length, " spring create success");
}
c. Create buffers for the cloth
The createClothBuffers function is responsible for setting up various GPU buffers that are essential for the cloth simulation. These buffers store data such as particle positions, velocities, forces, normals, and spring connections. By transferring this data to the GPU, the function enables efficient processing and rendering of the cloth simulation using WebGPU. - Position Buffer:
Description: Stores the current positions of each particle in the simulation.
Type: Float32Array
Size: Number of particles * 3 (for x, y, z coordinates)
Usage:
Type: Float32Array
Size: Number of particles * 3 (for x, y, z coordinates)
Usage:
- GPUBufferUsage.STORAGE: Allows the buffer to be used as a storage buffer.
- GPUBufferUsage.COPY_DST: Allows data to be copied into the buffer.
- Previous Position Buffer:
- GPUBufferUsage.COPY_DST: Allows data to be copied into the buffer.
- Previous Position Buffer:
Description: Stores the previous positions of each particle, used for calculating movements.
Type: Float32Array
Size: Same as the Position Buffer
Usage:
Size: Same as the Position Buffer
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_DST
- Velocity Buffer:
Description: Stores the velocities of each particle.
Type: Float32ArraySize: Number of particles * 3 (for velocity components x, y, z)
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
- Force Buffer
GPUBufferUsage.COPY_DST
- Force Buffer
Description: Stores the forces acting on each particle, initially set to zero.
Type: Float32Array
Size: Number of particles * 3 (for force components x, y, z)
Usage:
Size: Number of particles * 3 (for force components x, y, z)
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
- Normal BufferGPUBufferUsage.COPY_DST
Description: Stores the normals for each particle, used for lighting calculations.
- Fixed Buffer
- Pipeline Layout Creation:Includes the bind group layouts, defining how resources are structured within the pipeline.
- Pipeline Layout Creation:Includes the bind group layouts, defining how resources are structured within the pipeline.
- binding: 0: Uniform buffer for the MVP matrix, visible in the vertex shader.
- binding: 1: Texture resource, visible in the fragment shader.
- binding: 2: Sampler resource, visible in the fragment shader.
- binding: 3: Uniform buffer for camera position, visible in the fragment shader.
- binding: 4: Uniform buffer for light data, visible in the fragment shader.
- binding: 5: Uniform buffer for alpha value, visible in the fragment shader.
- binding: 1: Texture resource.
- binding: 2: Sampler resource.
- binding: 3: Camera position buffer.
- binding: 4: Light data buffer.
- binding: 5: Alpha value buffer.
- binding: 0: Storage buffer for particle positions.
- binding: 1: Storage buffer for particle velocities.
- binding: 2: Storage buffer for fixed particle states.
- binding: 3: Storage buffer for forces acting on particles.
- binding: 4: Storage buffer for previous particle positions.
- binding: 5: Uniform buffer for external forces (e.g., gravity).
Size: 3 (stores a vec3<f32> for external forces)
Usage:
- Entry Point: "main" (entry point for the compute shader).
Binds the storage buffers and uniform buffer to the compute shader.
- Bind Group Layout Creation (Normal Update):
- Compute Pipeline Creation (Normal Update):
- Bind Group Layout Creation (Normal Summation):
- Bind Group Layout Creation:
- Compute Pipeline Creation (Node Force Summation):
- Compute Pipeline Creation:
Type: Float32Array
Size: Number of particles * 3 (for normal vector components x, y, z)
Usage:
Size: Number of particles * 3 (for normal vector components x, y, z)
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_DST
- Fixed Buffer
Description: Indicates whether each particle is fixed in place (1) or free to move (0).
Type: Uint32Array
Size: Number of particles (1 for fixed, 0 for non-fixed)
Usage:
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_DST
- Spring Indices Buffer
Description: Stores the indices of particles connected by springs.
Type: Uint32Array
Size: Number of springs * 2 (each spring connects two particles)
Usage:
Size: Number of springs * 2 (each spring connects two particles)
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
- UV Buffer
GPUBufferUsage.COPY_DST
Description: Stores the UV coordinates for texturing the cloth.
Type: Float32Array
Size: Number of particles * 2 (for UV coordinates u, v)
Usage:
Size: Number of particles * 2 (for UV coordinates u, v)
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
- Triangle Indices Buffer
- Spring Calculation Buffer
GPUBufferUsage.COPY_DST
- Triangle Indices Buffer
Description: Stores the indices of particles that form the triangles in the cloth mesh
Type: Uint32Array
Size: Number of triangles * 3 (each triangle connects three particles)
Usage:
Size: Number of triangles * 3 (each triangle connects three particles)
Usage:
GPUBufferUsage.INDEX
GPUBufferUsage.COPY_DST
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.STORAGE
- Spring Calculation Buffer
Description: Stores the data needed for spring force calculations.
Type: Float32Array
Size: Number of springs * 7
Size: Number of springs * 7
(each spring has 7 attributes: two indices, two spring constants, rest length, and two target indices)
Usage:
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
- Triangle Calculation Buffer
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
Description: Stores the vertex indices for each triangle, used in collision and normal calculations.
Type: Float32Array
Size: Number of triangles * 3 (each triangle is defined by three vertex indices)
Usage:
Size: Number of triangles * 3 (each triangle is defined by three vertex indices)
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
- Number of Particles Buffer
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
Description: Stores the total number of particles in the simulation.
Type: Uint32Array
Size: 1 (stores the total number of particles)
Usage:
Size: 1 (stores the total number of particles)
Usage:
GPUBufferUsage.UNIFORM
GPUBufferUsage.COPY_DST
Usage:
GPUBufferUsage.COPY_DST
- Temporary Spring Force Buffer
Description: Temporary buffer for storing spring forces during calculations.
Type: Int32Array
Size: Number of particles * 3Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
- Temporary Triangle Normal Buffer
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
- Temporary Triangle Normal Buffer
Description: Temporary buffer for storing triangle normals during calculations.
Type: Uint32Array
Size: Number of particles * 3
Usage:
Size: Number of particles * 3
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
- Collision Temporary Buffer
Description: Temporary buffer for storing collision data during calculations.
Type: Int32Array
Size: Number of particles * 3
Usage:
Size: Number of particles * 3
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
- Collision Count Temporary Buffer
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
- Collision Count Temporary Buffer
Description: Stores the count of collisions detected for each particle.
Type: Int32Array
Size: Number of particles
Usage:
Size: Number of particles
Usage:
GPUBufferUsage.STORAGE
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
GPUBufferUsage.COPY_DST
GPUBufferUsage.COPY_SRC
createClothBuffers() {
// This creates a flat array of particle positions.
const positionData = new Float32Array(
this.particles.flatMap((p) => [
p.position[0],
p.position[1],
p.position[2],
])
);
// This creates a flat array of particle velocities.
const velocityData = new Float32Array(
this.particles.flatMap((p) => [
p.velocity[0],
p.velocity[1],
p.velocity[2],
])
);
// This initializes forces to zero for each particle.
const forceData = new Float32Array(
this.particles.flatMap((p) => [0.0, 0.0, 0.0])
);
// This creates a flat array of normals.
const normalData = new Float32Array(
this.normals.flatMap((p) => [p[0], p[1], p[2]])
);
// Create the GPU buffer for positions
this.positionBuffer = makeFloat32ArrayBufferStorage(
this.device,
positionData
);
// Create the GPU buffer for previous positions
this.prevPositionBuffer = makeFloat32ArrayBufferStorage(
this.device,
positionData
);
// Create the GPU buffer for velocities
this.velocityBuffer = makeFloat32ArrayBufferStorage(
this.device,
velocityData
);
// Create the GPU buffer for forces
this.forceBuffer = makeFloat32ArrayBufferStorage(this.device, forceData);
// Create the GPU buffer for normals
this.vertexNormalBuffer = makeFloat32ArrayBufferStorage(
this.device,
normalData
);
// Create the GPU buffer for object index
const fixedData = new Uint32Array(this.particles.length);
this.particles.forEach((particle, i) => {
fixedData[i] = particle.fixed ? 1 : 0;
});
this.fixedBuffer = this.device.createBuffer({
size: fixedData.byteLength,
// Use as STORAGE and add the COPY_DST flag
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Uint32Array(this.fixedBuffer.getMappedRange()).set(fixedData);
this.fixedBuffer.unmap();
// Creates a buffer to store indices of particles connected by springs.
this.springIndices = new Uint32Array(this.springs.length * 2);
this.springs.forEach((spring, i) => {
let offset = i * 2;
this.springIndices[offset] = spring.index1;
this.springIndices[offset + 1] = spring.index2;
});
this.springRenderBuffer = makeUInt32IndexArrayBuffer(
this.device,
this.springIndices
);
this.uvBuffer = makeFloat32ArrayBuffer(this.device, this.uv);
this.triangleRenderBuffer = this.device.createBuffer({
size: this.triangleIndices.byteLength,
usage:
GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
mappedAtCreation: true,
});
new Uint32Array(this.triangleRenderBuffer.getMappedRange()).set(
this.triangleIndices
);
this.triangleRenderBuffer.unmap();
// Spring Calculation Buffer
// 7 elements per spring
const springCalcData = new Float32Array(this.springs.length * 7);
this.springs.forEach((spring, i) => {
let offset = i * 7;
springCalcData[offset] = spring.index1;
springCalcData[offset + 1] = spring.index2;
springCalcData[offset + 2] = spring.kS;
springCalcData[offset + 3] = spring.kD;
springCalcData[offset + 4] = spring.mRestLen;
springCalcData[offset + 5] = spring.targetIndex1;
springCalcData[offset + 6] = spring.targetIndex2;
});
// Create the GPU buffer for springs
this.springCalculationBuffer = this.device.createBuffer({
size: springCalcData.byteLength,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Float32Array(this.springCalculationBuffer.getMappedRange()).set(
springCalcData
);
this.springCalculationBuffer.unmap();
// Triangle Calculation Buffer
// 7 elements per spring
const triangleCalcData = new Float32Array(this.triangles.length * 3);
this.triangles.forEach((triangle, i) => {
let offset = i * 3;
triangleCalcData[offset] = triangle.v1;
triangleCalcData[offset + 1] = triangle.v2;
triangleCalcData[offset + 2] = triangle.v3;
});
this.triangleCalculationBuffer = this.device.createBuffer({
size: triangleCalcData.byteLength,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Float32Array(this.triangleCalculationBuffer.getMappedRange()).set(
triangleCalcData
);
this.triangleCalculationBuffer.unmap();
// Number of Particles Buffer
const numParticlesData = new Uint32Array([this.numParticles]);
this.numParticlesBuffer = this.device.createBuffer({
size: numParticlesData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Uint32Array(this.numParticlesBuffer.getMappedRange()).set(
numParticlesData
);
this.numParticlesBuffer.unmap();
const nodeSpringConnectedData = new Int32Array(this.numParticles * 3);
this.tempSpringForceBuffer = this.device.createBuffer({
size: nodeSpringConnectedData.byteLength,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Int32Array(this.tempSpringForceBuffer.getMappedRange()).set(
nodeSpringConnectedData
);
this.tempSpringForceBuffer.unmap();
const nodeTriangleConnectedData = new Uint32Array(this.numParticles * 3);
this.tempTriangleNormalBuffer = this.device.createBuffer({
size: nodeTriangleConnectedData.byteLength,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Uint32Array(this.tempTriangleNormalBuffer.getMappedRange()).set(
nodeTriangleConnectedData
);
this.tempTriangleNormalBuffer.unmap();
const collisionTempData = new Int32Array(this.numParticles * 3);
this.collisionTempBuffer = this.device.createBuffer({
size: collisionTempData.byteLength,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Int32Array(this.collisionTempBuffer.getMappedRange()).set(
collisionTempData
);
this.collisionTempBuffer.unmap();
const collisionCountTempData = new Int32Array(this.numParticles);
this.collisionCountTempBuffer = this.device.createBuffer({
size: collisionCountTempData.byteLength,
usage:
GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
mappedAtCreation: true,
});
new Int32Array(this.collisionCountTempBuffer.getMappedRange()).set(
collisionCountTempData
);
this.collisionCountTempBuffer.unmap();
}
d1. Create the pipeline for rendering the cloth
The createRenderPipeline function sets up the rendering pipeline for the cloth simulation. This pipeline is responsible for defining how particles are rendered on the screen, including shader configuration, resource bindings, and rendering settings. The function leverages WebGPU to create and configure the necessary resources and states for rendering. - Shader Module Creation:
It contains the vertex and fragment shaders necessary for rendering particles. - Bind Group Layout Creation:
Defines the layout of the resources (buffers) that will be bound to the shader. - Uniform Buffer for MVP Matrix:
Size: 64 bytes * 3 (for three 4x4 matrices - model, view, projection)
Usage: The buffer will be updated with the MVP matrix before each render, allowing the shader to access the current transformation matrices.
Usage: The buffer will be updated with the MVP matrix before each render, allowing the shader to access the current transformation matrices.
- Bind Group Creation:
Binds the previously created MVP uniform buffer to the shader.
- Pipeline Layout Creation:
- Render Pipeline Creation:
- Vertex State
- Fragment State
- Primitive State
- Depth Stencil State
- Multisample State
createRenderPipeline() {
const particleShaderModule = this.device.createShaderModule({
code: this.particleShader.getParticleShader(),
});
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.VERTEX, // Accessible from the vertex shader
buffer: {}, // Specifies that this binding will be a buffer
},
],
});
// Create a uniform buffer for the MVP matrix. The size is 64 bytes * 3, assuming
// you're storing three 4x4 matrices (model, view, projection) as 32-bit floats.
// This buffer will be updated with the MVP matrix before each render.
// S bind group that binds the previously created uniform buffer to the shader.
// This allows the shader to access the buffer as defined in the bind group layout
this.renderBindGroup = this.device.createBindGroup({
layout: bindGroupLayout, // The layout created earlier
entries: [
{
binding: 0,
resource: {
buffer: this.mvpUniformBuffer,
},
},
],
});
// Create a pipeline layout that includes the bind group layouts.
// This layout is for the render pipeline to know how resources are structured.
const pipelineLayout = this.device.createPipelineLayout({
// Include the bind group layout created above
bindGroupLayouts: [bindGroupLayout],
});
this.particlePipeline = this.device.createRenderPipeline({
// Simplified layout, assuming no complex bindings needed
layout: pipelineLayout,
vertex: {
module: particleShaderModule,
// Ensure your shader has appropriate entry points
entryPoint: "vs_main",
buffers: [
{
// Assuming each particle position is a vec3<f32>
arrayStride: 12,
attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }],
},
],
},
fragment: {
module: particleShaderModule,
entryPoint: "fs_main",
targets: [
{
format: this.format,
blend: {
color: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
alpha: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
},
},
],
},
primitive: {
topology: "point-list", // Render particles as points
},
// Include depthStencil state if depth testing is required
depthStencil: {
depthWriteEnabled: true,
depthCompare: "less",
format: "depth32float",
},
multisample: {
count: this.sampleCount,
},
});
console.log("create render pipeline success");
}
d2. Create the pipeline for updating the springs
The createSpringPipeline function sets up the rendering pipeline specifically for visualizing the springs in the cloth simulation. This pipeline defines how the springs, which connect the particles, are rendered on the screen, including shader configuration, resource bindings, and rendering settings. The function leverages WebGPU to create and configure the necessary resources and states for rendering the springs. - Shader Module Creation:
It contains the vertex and fragment shaders necessary for rendering the springs. - Bind Group Layout Creation:
Defines the layout of the resources (buffers) that will be bound to the shader.- Pipeline Layout Creation:
- Render Pipeline Creation:
- Vertex State
- Fragment State
- Primitive State
- Depth Stencil State
- Multisample State
createSpringPipeline() {
const springShaderModule = this.device.createShaderModule({
code: this.particleShader.getSpringShader(),
});
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.VERTEX, // Accessible from the vertex shader
buffer: {}, // Specifies that this binding will be a buffer
},
],
});
const pipelineLayout = this.device.createPipelineLayout({
// Include the bind group layout created above
bindGroupLayouts: [bindGroupLayout],
});
this.springPipeline = this.device.createRenderPipeline({
layout: pipelineLayout, // Reuse or create as needed
vertex: {
module: springShaderModule,
entryPoint: "vs_main",
buffers: [
{
arrayStride: 12, // vec3<f32> for spring start and end positions
attributes: [{ shaderLocation: 0, offset: 0, format: "float32x3" }],
},
],
},
fragment: {
module: springShaderModule,
entryPoint: "fs_main",
targets: [
{
format: this.format,
blend: {
color: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
alpha: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
},
},
],
},
primitive: {
topology: "line-list",
// Additional configurations as needed
},
// Reuse depthStencil configuration
depthStencil: {
depthWriteEnabled: true,
depthCompare: "less",
format: "depth32float",
},
multisample: {
count: this.sampleCount,
},
});
}
d3. Create the pipeline for updating the triangles
The createTrianglePipeline function sets up the rendering pipeline for rendering triangles in the cloth simulation. This pipeline handles the drawing of the cloth's surface, applying textures, lighting, and other visual effects. The function leverages WebGPU to create and configure the necessary resources and states for rendering the triangles. - Shader Module Creation:
It contains the vertex and fragment shaders necessary for rendering the cloth with textures and lighting. - Bind Group Layout Creation:
Defines the layout of the resources (buffers, textures, samplers) that will be bound to the shader.- binding: 1: Texture resource, visible in the fragment shader.
- binding: 2: Sampler resource, visible in the fragment shader.
- binding: 3: Uniform buffer for camera position, visible in the fragment shader.
- binding: 4: Uniform buffer for light data, visible in the fragment shader.
- binding: 5: Uniform buffer for alpha value, visible in the fragment shader.
- Alpha Value Buffer Creation:
Description: Stores the alpha value for blending.
Type: Float32Array
Size: 1 (stores a single alpha value)
Usage:
Size: 1 (stores a single alpha value)
Usage:
GPUBufferUsage.UNIFORM
GPUBufferUsage.COPY_DST
- binding: 0: MVP uniform buffer. - Bind Group Creation:
Binds the uniform buffers, texture, and sampler to the shader.- binding: 1: Texture resource.
- binding: 2: Sampler resource.
- binding: 3: Camera position buffer.
- binding: 4: Light data buffer.
- binding: 5: Alpha value buffer.
- Pipeline Layout Creation:
Includes the bind group layouts, defining how resources are structured within the pipeline. - Render Pipeline Creation:
- Vertex State
- Fragment State
- Primitive State
- Depth Stencil State
- Multisample State
createTrianglePipeline() {
const textureShaderModule = this.device.createShaderModule({
code: this.particleShader.getTextureShader(),
});
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {},
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: {},
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
sampler: {},
},
{
binding: 3,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: "uniform",
},
},
{
binding: 4,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: "uniform",
},
},
{
binding: 5,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: "uniform",
},
},
],
});
const alphaData = new Float32Array([1.0]);
this.alphaValueBuffer = this.device.createBuffer({
size: alphaData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(this.alphaValueBuffer.getMappedRange()).set(alphaData);
this.alphaValueBuffer.unmap();
this.triangleBindGroup = this.device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: this.mvpUniformBuffer,
},
},
{
binding: 1,
resource: this.view,
},
{
binding: 2,
resource: this.sampler,
},
{
binding: 3,
resource: {
buffer: this.camPosBuffer,
},
},
{
binding: 4,
resource: {
buffer: this.lightDataBuffer,
},
},
{
binding: 5,
resource: {
buffer: this.alphaValueBuffer,
},
},
],
});
const pipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.trianglePipeline = this.device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: textureShaderModule,
entryPoint: "vs_main",
buffers: [
{
arrayStride: 12,
attributes: [
{
shaderLocation: 0,
format: "float32x3",
offset: 0,
},
],
},
{
arrayStride: 8,
attributes: [
{
shaderLocation: 1,
format: "float32x2",
offset: 0,
},
],
},
{
arrayStride: 12,
attributes: [
{
shaderLocation: 2,
format: "float32x2",
offset: 0,
},
],
},
],
},
fragment: {
module: textureShaderModule,
entryPoint: "fs_main",
targets: [
{
format: this.format,
blend: {
color: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
alpha: {
srcFactor: "src-alpha",
dstFactor: "one-minus-src-alpha",
operation: "add",
},
},
},
],
},
primitive: {
topology: "triangle-list",
cullMode: "none",
//topology: 'line-list',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: "less",
format: "depth32float",
},
multisample: {
count: this.sampleCount,
},
});
}
d4. Create the pipeline for rendering the particles
The createParticlePipeline function sets up the compute pipeline for simulating the particles in the cloth simulation. This pipeline is responsible for updating the particle positions, velocities, and forces based on the physical simulation. The function leverages WebGPU to create and configure the necessary resources and states for executing the compute shader. - Compute Shader Module Creation:
It contains the logic for updating particle states in the simulation. - Bind Group Layout Creation:
Defines the layout of the resources (buffers) that will be bound to the compute shader.- binding: 1: Storage buffer for particle velocities.
- binding: 2: Storage buffer for fixed particle states.
- binding: 3: Storage buffer for forces acting on particles.
- binding: 4: Storage buffer for previous particle positions.
- binding: 5: Uniform buffer for external forces (e.g., gravity).
- External Force Buffer Creation:
Description: Stores external forces applied to particles, such as gravity.
Type: Float32ArraySize: 3 (stores a vec3<f32> for external forces)
Usage:
GPUBufferUsage.UNIFORM
GPUBufferUsage.COPY_DST
Initialization: The buffer is initialized with zero values for the external force.
Initialization: The buffer is initialized with zero values for the external force.
- Pipeline Layout Creation:
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Compute Pipeline Creation:
- Module: computeShaderModule (compute shader).- Entry Point: "main" (entry point for the compute shader).
- Bind Group Creation:
- binding: 0: Particle position buffer.
- binding: 1: Particle velocity buffer.
- binding: 2: Fixed particle state buffer.
- binding: 3: Force buffer.
- binding: 4: Previous particle position buffer.
- binding: 5: External force buffer.
- binding: 1: Particle velocity buffer.
- binding: 2: Fixed particle state buffer.
- binding: 3: Force buffer.
- binding: 4: Previous particle position buffer.
- binding: 5: External force buffer.
createParticlePipeline() {
const computeShaderModule = this.device.createShaderModule({
code: this.particleShader.getComputeShader(),
});
// Create bind group layout for storage buffers
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // matches @group(0) @binding(0)
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 3,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 4,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 5, // This matches @group(0) @binding(5) in the WGSL shader
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "uniform",
minBindingSize: 0, // Specify the size of vec3<f32>
},
},
],
});
const initialExternalForce = new Float32Array([0.0, 0.0, 0.0]);
// externalForceBuffer
this.externalForceBuffer = this.device.createBuffer({
size: initialExternalForce.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(this.externalForceBuffer.getMappedRange()).set(
initialExternalForce
);
this.externalForceBuffer.unmap();
// Use the bind group layout to create a pipeline layout
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
const computePipeline = this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: computeShaderModule,
entryPoint: "main",
},
});
this.computePipeline = computePipeline;
this.computeBindGroup = this.device.createBindGroup({
layout: this.computePipeline.getBindGroupLayout(0),
entries: [
{
binding: 0,
resource: {
buffer: this.positionBuffer,
},
},
{
binding: 1,
resource: {
buffer: this.velocityBuffer,
},
},
{
binding: 2,
resource: {
buffer: this.fixedBuffer,
},
},
{
binding: 3,
resource: {
buffer: this.forceBuffer,
},
},
{
binding: 4,
resource: {
buffer: this.prevPositionBuffer,
},
},
{
binding: 5,
resource: {
buffer: this.externalForceBuffer,
},
},
],
});
}
d5. Create the pipeline for updating the normals
The createUpdateNormalPipeline function sets up two compute pipelines for updating the normals of the cloth's vertices. This is essential for accurate lighting and shading in the simulation. The first pipeline calculates the normals for each triangle, and the second pipeline sums these normals for each vertex. The function leverages WebGPU to create and configure the necessary resources and states for these compute operations. - Normal Update Compute Shader Module Creation:
This shader calculates the normals for each triangle.
- Bind Group Layout Creation (Normal Update):
Defines the layout of the resources (buffers) that will be bound to the normal update compute shader.
- binding: 0: Storage buffer for particle positions.
- binding: 1: Storage buffer for triangle calculation data.
- binding: 2: Storage buffer for temporary triangle normals.
- binding: 3: Uniform buffer for the number of triangles.
- Pipeline Layout Creation (Normal Update):
- binding: 1: Storage buffer for triangle calculation data.
- binding: 2: Storage buffer for temporary triangle normals.
- binding: 3: Uniform buffer for the number of triangles.
- Pipeline Layout Creation (Normal Update):
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Compute Pipeline Creation (Normal Update):
- Module: normalComputeShaderModule (compute shader).
- Entry Point: "main" (entry point for the compute shader).
- Number of Triangles Buffer Creation:
- Entry Point: "main" (entry point for the compute shader).
- Number of Triangles Buffer Creation:
- Description: Stores the number of triangles in the cloth simulation.
- Initialization: The buffer is initialized with the number of triangles.
- Initialization: The buffer is initialized with the number of triangles.
- Type: Uint32Array
- Size: 1 (stores the number of triangles)
- Usage:
- Size: 1 (stores the number of triangles)
- Usage:
GPUBufferUsage.UNIFORM |
GPUBufferUsage.COPY_DST
- Bind Group Creation (Normal Update):
- Bind Group Creation (Normal Update):
Binds the storage buffers and uniform buffer to the normal update compute shader.
- binding: 0: Particle position buffer.
- binding: 1: Triangle calculation buffer.
- binding: 2: Temporary triangle normal buffer.
- binding: 3: Number of triangles buffer.
- Normal Summation Compute Shader Module Creation:
- binding: 1: Triangle calculation buffer.
- binding: 2: Temporary triangle normal buffer.
- binding: 3: Number of triangles buffer.
- Normal Summation Compute Shader Module Creation:
The shader module is created using the normal summation compute shader code. This shader sums the triangle normals for each vertex.
- Bind Group Layout Creation (Normal Summation):
Defines the layout of the resources (buffers) that will be bound to the normal summation compute shader.
- binding: 0: Storage buffer for temporary triangle normals.
- binding: 0: Storage buffer for temporary triangle normals.
- binding: 1: Storage buffer for vertex normals.
- binding: 2: Uniform buffer for the maximum number of triangles connected to a vertex.
- binding: 3: Uniform buffer for the number of particles.
- Pipeline Layout Creation (Normal Summation):
- binding: 2: Uniform buffer for the maximum number of triangles connected to a vertex.
- binding: 3: Uniform buffer for the number of particles.
- Pipeline Layout Creation (Normal Summation):
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Compute Pipeline Creation (Normal Summation):
- Compute Pipeline Creation (Normal Summation):
- Layout: computePipelineLayout (created earlier).
- Module: normalSummationComputeShaderModule (compute shader).
- Entry Point: "main" (entry point for the compute shader).
- Max Connected Triangles Buffer Creation:
- Entry Point: "main" (entry point for the compute shader).
- Max Connected Triangles Buffer Creation:
- Description: Stores the maximum number of triangles connected to any vertex in the cloth simulation.
- Initialization: The buffer is initialized with the maximum number of connected triangles.
- Initialization: The buffer is initialized with the maximum number of connected triangles.
- Type: Uint32Array
- Size: 1 (stores the maximum number of triangles connected to a vertex)
- Usage:
- Size: 1 (stores the maximum number of triangles connected to a vertex)
- Usage:
GPUBufferUsage.UNIFORM |
GPUBufferUsage.COPY_DST
- Bind Group Creation (Normal Summation):
- Bind Group Creation (Normal Summation):
Binds the storage buffers and uniform buffers to the normal summation compute shader.
- Layout: bindGroupLayout (created earlier).
- Layout: bindGroupLayout (created earlier).
- binding: 0: Temporary triangle normal buffer.
- binding: 1: Vertex normal buffer.
- binding: 2: Maximum connected triangles buffer.
- binding: 3: Number of particles buffer.
- binding: 1: Vertex normal buffer.
- binding: 2: Maximum connected triangles buffer.
- binding: 3: Number of particles buffer.
createUpdateNormalPipeline() {
const normalComputeShaderModule = this.device.createShaderModule({
code: this.normalShader.getNormalUpdateComputeShader(),
});
{
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 1, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 2, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 3, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
],
});
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.computeNormalPipeline = this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: normalComputeShaderModule,
entryPoint: "main",
},
});
const numTriangleData = new Uint32Array([this.triangles.length]);
this.numTriangleBuffer = this.device.createBuffer({
size: numTriangleData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Uint32Array(this.numTriangleBuffer.getMappedRange()).set(
numTriangleData
);
this.numTriangleBuffer.unmap();
this.computeNormalBindGroup = this.device.createBindGroup({
layout: bindGroupLayout, // The layout created earlier
entries: [
{
binding: 0,
resource: {
buffer: this.positionBuffer,
},
},
{
binding: 1,
resource: {
buffer: this.triangleCalculationBuffer,
},
},
{
binding: 2,
resource: {
buffer: this.tempTriangleNormalBuffer,
},
},
{
binding: 3,
resource: {
buffer: this.numTriangleBuffer,
},
},
],
});
}
const normalSummationComputeShaderModule = this.device.createShaderModule({
code: this.normalShader.getNormalSummationComputeShader(),
});
{
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 1, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 2, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
{
binding: 3, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
],
});
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.computeNormalSummationPipeline = this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: normalSummationComputeShaderModule,
entryPoint: "main",
},
});
const maxConnectedTriangleData = new Uint32Array([
this.maxTriangleConnected,
]);
this.maxConnectedTriangleBuffer = this.device.createBuffer({
size: maxConnectedTriangleData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Uint32Array(this.maxConnectedTriangleBuffer.getMappedRange()).set(
maxConnectedTriangleData
);
this.maxConnectedTriangleBuffer.unmap();
this.computeNormalSummationBindGroup = this.device.createBindGroup({
layout: bindGroupLayout, // The layout created earlier
entries: [
{
binding: 0,
resource: {
buffer: this.tempTriangleNormalBuffer,
},
},
{
binding: 1,
resource: {
buffer: this.vertexNormalBuffer,
},
},
{
binding: 2,
resource: {
buffer: this.maxConnectedTriangleBuffer,
},
},
{
binding: 3,
resource: {
buffer: this.numParticlesBuffer,
},
},
],
});
}
}
d6. Creates the pipeline for computing the spring forces
The createSpringForceComputePipeline function sets up the compute pipeline for calculating spring forces in the cloth simulation. This pipeline computes the forces exerted by the springs connecting the particles, which are then used to update the particle positions and velocities. The function leverages WebGPU to create and configure the necessary resources and states for executing the compute shader responsible for spring force calculations. - Spring Compute Shader Module Creation:
This shader contains the logic for calculating spring forces.
Defines the layout of the resources (buffers) that will be bound to the compute shader.
- binding: 0: Storage buffer for particle positions.
- binding: 1: Storage buffer for particle velocities.
- binding: 2: Read-only storage buffer for spring calculation data.
- binding: 3: Uniform buffer for the number of springs.
- binding: 4: Storage buffer for temporary spring forces.
- binding: 5: Uniform buffer for the number of particles.
- Pipeline Layout Creation:
- Compute Pipeline Creation:
- binding: 1: Storage buffer for particle velocities.
- binding: 2: Read-only storage buffer for spring calculation data.
- binding: 3: Uniform buffer for the number of springs.
- binding: 4: Storage buffer for temporary spring forces.
- binding: 5: Uniform buffer for the number of particles.
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Module: springComputeShaderModule (compute shader).
- Entry Point: "main" (entry point for the compute shader).
- Number of Springs Buffer Creation:
- Bind Group Layout Creation:
- Entry Point: "main" (entry point for the compute shader).
- Number of Springs Buffer Creation:
Description: Stores the number of springs in the cloth simulation.
Initialization: The buffer is initialized with the number of springs.
Initialization: The buffer is initialized with the number of springs.
Type: Uint32Array
Size: 1 (stores the number of springs)
Usage:
Size: 1 (stores the number of springs)
Usage:
GPUBufferUsage.UNIFORM |
GPUBufferUsage.COPY_DST
- Bind Group Creation:
Binds the storage buffers and uniform buffers to the compute shader.
- binding: 0: Particle position buffer.
- binding: 1: Particle velocity buffer.
- binding: 2: Spring calculation buffer.
- binding: 3: Number of springs buffer.
- binding: 4: Temporary spring force buffer.
- binding: 5: Number of particles buffer.
- binding: 1: Particle velocity buffer.
- binding: 2: Spring calculation buffer.
- binding: 3: Number of springs buffer.
- binding: 4: Temporary spring force buffer.
- binding: 5: Number of particles buffer.
createSpringForceComputePipeline() {
const springComputeShaderModule = this.device.createShaderModule({
code: this.springShader.getSpringUpdateShader(),
});
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 1, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 2, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: {
type: "read-only-storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 3, // The binding number in the shader
// Accessible from the vertex shader
visibility: GPUShaderStage.COMPUTE,
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
{
binding: 4, // The binding number in the shader
// Accessible from the vertex shader
visibility: GPUShaderStage.COMPUTE,
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 5, // The binding number in the shader
// Accessible from the vertex shader
visibility: GPUShaderStage.COMPUTE,
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
],
});
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.computeSpringPipeline = this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: springComputeShaderModule,
entryPoint: "main",
},
});
const numSpringsData = new Uint32Array([this.springs.length]);
this.numSpringsBuffer = this.device.createBuffer({
size: numSpringsData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Uint32Array(this.numSpringsBuffer.getMappedRange()).set(numSpringsData);
this.numSpringsBuffer.unmap();
this.computeSpringBindGroup = this.device.createBindGroup({
layout: bindGroupLayout, // The layout created earlier
entries: [
{
binding: 0,
resource: { buffer: this.positionBuffer },
},
{
binding: 1,
resource: { buffer: this.velocityBuffer },
},
{
binding: 2,
resource: { buffer: this.springCalculationBuffer },
},
{
binding: 3,
resource: { buffer: this.numSpringsBuffer },
},
{
binding: 4,
resource: { buffer: this.tempSpringForceBuffer },
},
{
binding: 5,
resource: { buffer: this.numParticlesBuffer },
},
],
});
}
d7. Create the pipeline for summing the node forces
The createNodeForceSummationPipeline function sets up the compute pipelines for summing the forces at each node (particle) in the cloth simulation. This function creates two pipelines: one for initializing node forces and another for summing the spring forces at each node. The function leverages WebGPU to create and configure the necessary resources and states for executing the compute shaders responsible for these tasks. - Node Force Compute Shader Module Creation:
This shader contains the logic for summing the forces at each node.
Defines the layout of the resources (buffers) that will be bound to the compute shader.
- binding: 0: Storage buffer for temporary spring forces.
- binding: 1: Storage buffer for the final node forces.
- binding: 2: Uniform buffer for the number of particles.
- Pipeline Layout Creation (Node Force Summation):
- binding: 1: Storage buffer for the final node forces.
- binding: 2: Uniform buffer for the number of particles.
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Layout: computePipelineLayout (created earlier).
- Module: nodeForceComputeShaderModule (compute shader).
- Entry Point: "main" (entry point for the compute shader that sums the forces).
- Bind Group Creation (Node Force Summation):
- Entry Point: "main" (entry point for the compute shader that sums the forces).
Type: Bind Group
Usage: Binds the storage buffers and uniform buffer to the compute shader.
Layout: bindGroupLayout (created earlier).
Entries:
Usage: Binds the storage buffers and uniform buffer to the compute shader.
Layout: bindGroupLayout (created earlier).
Entries:
- binding: 0: Temporary spring force buffer.
- binding: 1: Node force buffer.
- binding: 2: Number of particles buffer.
- Pipeline Layout Creation (Node Force Initialization):
- binding: 1: Node force buffer.
- binding: 2: Number of particles buffer.
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Compute Pipeline Creation (Node Force Initialization):
- Compute Pipeline Creation (Node Force Initialization):
- Layout: computePipelineLayout.
- Module: nodeForceComputeShaderModule (compute shader).
- Entry Point: "initialize" (entry point for the compute shader that initializes the node forces).
- Bind Group Layout Creation:
- Entry Point: "initialize" (entry point for the compute shader that initializes the node forces).
createNodeForceSummationPipeline() {
const nodeForceComputeShaderModule = this.device.createShaderModule({
code: this.springShader.getNodeForceShader(),
});
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 1, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 2, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
],
});
{
/* Node Force Merge Equation */
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.computeNodeForcePipeline = this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: nodeForceComputeShaderModule,
entryPoint: "main",
},
});
this.computeNodeForceBindGroup = this.device.createBindGroup({
layout: bindGroupLayout, // The layout created earlier
entries: [
{
binding: 0,
resource: {
buffer: this.tempSpringForceBuffer,
},
},
{
binding: 1,
resource: {
buffer: this.forceBuffer,
},
},
{
binding: 2,
resource: {
buffer: this.numParticlesBuffer,
},
},
],
});
}
{
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.computeNodeForceInitPipeline = this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: nodeForceComputeShaderModule,
entryPoint: "initialize",
},
});
}
}
d8. Create the pipeline for computing the intersections
The createIntersectionPipeline function sets up the compute pipeline for detecting and handling intersections in the cloth simulation. This pipeline ensures that the cloth interacts realistically with itself and other objects by calculating intersections and applying appropriate responses. The function leverages WebGPU to create and configure the necessary resources and states for executing the compute shader responsible for intersection calculations. - Intersection Compute Shader Module Creation:
This shader contains the logic for detecting and responding to intersections.
Defines the layout of the resources (buffers) that will be bound to the compute shader.
- binding: 0: Storage buffer for particle positions.
- binding: 1: Storage buffer for particle velocities.
- binding: 2: Storage buffer for object positions.
- binding: 3: Storage buffer for object indices.
- binding: 4: Uniform buffer for the number of particles.
- binding: 5: Uniform buffer for the number of object triangles.
- binding: 6: Storage buffer for temporary collision data.
- binding: 7: Storage buffer for fixed particle states.
- binding: 8: Storage buffer for collision count data.
- binding: 9: Storage buffer for previous particle positions.
- binding: 1: Storage buffer for particle velocities.
- binding: 2: Storage buffer for object positions.
- binding: 3: Storage buffer for object indices.
- binding: 4: Uniform buffer for the number of particles.
- binding: 5: Uniform buffer for the number of object triangles.
- binding: 6: Storage buffer for temporary collision data.
- binding: 7: Storage buffer for fixed particle states.
- binding: 8: Storage buffer for collision count data.
- binding: 9: Storage buffer for previous particle positions.
- Pipeline Layout Creation:
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Layout: computePipelineLayout (created earlier).
- Module: intersectionComputeShaderModule (compute shader).
- Entry Point: "response" (entry point for the compute shader that handles intersection responses).
- Bind Group Creation:
- Entry Point: "response" (entry point for the compute shader that handles intersection responses).
Binds the storage buffers and uniform buffers to the compute shader.
- Layout: bindGroupLayout (created earlier).
- binding: 0: Particle position buffer.
- binding: 1: Particle velocity buffer.
- binding: 2: Object position buffer.
- binding: 3: Object index buffer.
- binding: 4: Number of particles buffer.
- binding: 5: Number of object triangles buffer.
- binding: 6: Temporary collision data buffer.
- binding: 7: Fixed particle state buffer.
- binding: 8: Collision count data buffer.
- binding: 9: Previous particle position buffer.
- Triangle-Triangle Intersection Compute Shader Module Creation:
- binding: 1: Particle velocity buffer.
- binding: 2: Object position buffer.
- binding: 3: Object index buffer.
- binding: 4: Number of particles buffer.
- binding: 5: Number of object triangles buffer.
- binding: 6: Temporary collision data buffer.
- binding: 7: Fixed particle state buffer.
- binding: 8: Collision count data buffer.
- binding: 9: Previous particle position buffer.
createIntersectionPipeline() {
const intersectionComputeShaderModule = this.device.createShaderModule({
code: this.interesectionShader.getIntersectionShader(),
});
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 1, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 2, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: {
type: "storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 3, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 4, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "uniform", minBindingSize: 4 },
},
{
binding: 5, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
{
binding: 6, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 7, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 8, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 9, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
],
});
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.computeIntersectionSummationPipeline =
this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: intersectionComputeShaderModule,
entryPoint: "response",
},
});
this.computeIntersectionBindGroup2 = this.device.createBindGroup({
layout: bindGroupLayout, // The layout created earlier
entries: [
{
binding: 0,
resource: { buffer: this.positionBuffer },
},
{
binding: 1,
resource: { buffer: this.velocityBuffer },
},
{
binding: 2,
resource: { buffer: this.ObjectPosBuffer },
},
{
binding: 3,
resource: { buffer: this.objectIndexBuffer },
},
{
binding: 4,
resource: { buffer: this.numParticlesBuffer },
},
{
binding: 5,
resource: { buffer: this.objectNumTriangleBuffer },
},
{
binding: 6,
resource: { buffer: this.collisionTempBuffer },
},
{
binding: 7,
resource: { buffer: this.fixedBuffer },
},
{
binding: 8,
resource: { buffer: this.collisionCountTempBuffer },
},
{
binding: 9,
resource: { buffer: this.prevPositionBuffer },
},
],
});
}
d9. Create the pipeline for computing the triangle-triangle intersections
The createTriTriIntersectionPipeline function sets up the compute pipeline for detecting and handling triangle-triangle intersections in the cloth simulation. This pipeline ensures that collisions between the triangles of the cloth and other objects are accurately detected and resolved. The function leverages WebGPU to create and configure the necessary resources and states for executing the compute shader responsible for these intersection calculations.- Triangle-Triangle Intersection Compute Shader Module Creation:
This shader contains the logic for detecting and handling intersections between triangles.
- Bind Group Layout Creation:
Defines the layout of the resources (buffers) that will be bound to the compute shader.
- binding: 0: Storage buffer for particle positions.
- binding: 1: Storage buffer for triangle indices.
- binding: 2: Storage buffer for object positions.
- binding: 3: Storage buffer for object indices.
- binding: 4: Uniform buffer for the number of triangles.
- binding: 5: Uniform buffer for the number of object triangles.
- binding: 6: Storage buffer for temporary collision data.
- binding: 7: Storage buffer for collision count data.
- binding: 8: Storage buffer for particle velocities.
- Compute Pipeline Creation:
- Compute Passes: - Update Objects: Calls this.updateObjects(commandEncoder) to update object positions and states.
- Initialize Node Force: Calls this.InitNodeForce(commandEncoder) to initialize forces at each node.
- Update Springs: Calls this.updateSprings(commandEncoder) to update the spring forces between particles.
- Sum Node Forces: Calls this.summationNodeForce(commandEncoder) to sum the forces acting on each node.
- Intersection Handling: Calls this.Intersections(commandEncoder) to detect and handle intersections within the cloth and with other objects.
- Update Particles: Calls this.updateParticles(commandEncoder) to update particle positions and velocities based on the computed forces.
- Update Normals: Calls this.updateNormals(commandEncoder) to update the normals of the cloth for accurate lighting and shading.
- Frame Statistics Update:
- binding: 1: Storage buffer for triangle indices.
- binding: 2: Storage buffer for object positions.
- binding: 3: Storage buffer for object indices.
- binding: 4: Uniform buffer for the number of triangles.
- binding: 5: Uniform buffer for the number of object triangles.
- binding: 6: Storage buffer for temporary collision data.
- binding: 7: Storage buffer for collision count data.
- binding: 8: Storage buffer for particle velocities.
- Pipeline Layout Creation:
Includes the bind group layouts, defining how resources are structured within the compute pipeline.
- Layout: computePipelineLayout (created earlier).
- Module: intersectionComputeShaderModule (compute shader).
- Entry Point: "main" (entry point for the compute shader that handles triangle-triangle intersections).
- Entry Point: "main" (entry point for the compute shader that handles triangle-triangle intersections).
- Bind Group Creation:
Binds the storage buffers and uniform buffers to the compute shader.
- Layout: bindGroupLayout (created earlier).
- binding: 0: Particle position buffer.
- binding: 1: Triangle index buffer.
- binding: 2: Object position buffer.
- binding: 3: Object index buffer.
- binding: 4: Number of triangles buffer.
- binding: 5: Number of object triangles buffer.
- binding: 6: Temporary collision data buffer.
- binding: 7: Collision count data buffer.
- binding: 8: Particle velocity buffer.
- binding: 1: Triangle index buffer.
- binding: 2: Object position buffer.
- binding: 3: Object index buffer.
- binding: 4: Number of triangles buffer.
- binding: 5: Number of object triangles buffer.
- binding: 6: Temporary collision data buffer.
- binding: 7: Collision count data buffer.
- binding: 8: Particle velocity buffer.
createTriTriIntersectionPipeline() {
const intersectionComputeShaderModule = this.device.createShaderModule({
code: this.interesectionShader.getTriTriIntersectionShader(),
});
const bindGroupLayout = this.device.createBindGroupLayout({
entries: [
{
binding: 0, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 1, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 2, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: {
type: "storage",
minBindingSize: 0, // or specify the actual size
},
},
{
binding: 3, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 4, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
buffer: { type: "uniform", minBindingSize: 4 },
},
{
binding: 5, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "uniform", minBindingSize: 4 },
},
{
binding: 6, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 7, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
{
binding: 8, // The binding number in the shader
visibility: GPUShaderStage.COMPUTE, // Accessible from the vertex shader
// Ensure this matches the shader's expectation
buffer: { type: "storage", minBindingSize: 0 },
},
],
});
const computePipelineLayout = this.device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
this.computeIntersectionPipeline = this.device.createComputePipeline({
layout: computePipelineLayout,
compute: {
module: intersectionComputeShaderModule,
entryPoint: "main",
},
});
this.computeIntersectionBindGroup = this.device.createBindGroup({
layout: bindGroupLayout, // The layout created earlier
entries: [
{
binding: 0,
resource: { buffer: this.positionBuffer },
},
{
binding: 1,
resource: { buffer: this.triangleRenderBuffer },
},
{
binding: 2,
resource: { buffer: this.ObjectPosBuffer },
},
{
binding: 3,
resource: { buffer: this.objectIndexBuffer },
},
{
binding: 4,
resource: { buffer: this.numTriangleBuffer },
},
{
binding: 5,
resource: { buffer: this.objectNumTriangleBuffer },
},
{
binding: 6,
resource: { buffer: this.collisionTempBuffer },
},
{
binding: 7,
resource: { buffer: this.collisionCountTempBuffer },
},
{
binding: 8,
resource: { buffer: this.velocityBuffer },
},
],
});
}
e. Begin render
The render function is responsible for executing a single frame of the cloth simulation and rendering it to the screen. This function updates the simulation state, processes various compute passes, and then renders the updated state. It leverages WebGPU to perform these tasks efficiently, ensuring real-time performance and responsiveness. - Time Tracking:
Captures the current time using performance.now(), which is used for calculating frame statistics. - Camera Update:
Updates the camera's position and orientation using this.setCamera(this.camera). - Render Pass Descriptor Update:
Creates or updates the render pass descriptor with this.makeRenderpassDescriptor(). - Command Encoder Creation:
Creates a command encoder using this.device.createCommandEncoder() to record GPU commands. - Wind Force Application (Conditional):
Applies a wind force to the cloth if this.renderOptions.wind is enabled.
- Compute Passes:
- Initialize Node Force: Calls this.InitNodeForce(commandEncoder) to initialize forces at each node.
- Update Springs: Calls this.updateSprings(commandEncoder) to update the spring forces between particles.
- Sum Node Forces: Calls this.summationNodeForce(commandEncoder) to sum the forces acting on each node.
- Intersection Handling: Calls this.Intersections(commandEncoder) to detect and handle intersections within the cloth and with other objects.
- Update Particles: Calls this.updateParticles(commandEncoder) to update particle positions and velocities based on the computed forces.
- Update Normals: Calls this.updateNormals(commandEncoder) to update the normals of the cloth for accurate lighting and shading.
- Render Pass:
Calls this.renderCloth(commandEncoder) to render the updated cloth state to the screen.
- Submit Commands:
- Submits the recorded commands to the GPU queue using this.device.queue.submit([commandEncoder.finish()]).
- Waits for the GPU to finish processing the submitted commands using await this.device.queue.onSubmittedWorkDone().
- Submit Commands:
- Submits the recorded commands to the GPU queue using this.device.queue.submit([commandEncoder.finish()]).
- Waits for the GPU to finish processing the submitted commands using await this.device.queue.onSubmittedWorkDone().
- Update MS
- Update FPS
- Update Last Time
- Increment Frame Count
- Update FPS
- Update Last Time
- Increment Frame Count
async render() {
const currentTime = performance.now();
// updates camera and render pass descriptor.
this.setCamera(this.camera);
this.makeRenderpassDescriptor();
const commandEncoder = this.device.createCommandEncoder();
if (this.renderOptions.wind) {
const newExternalForce = new Float32Array([0.0, 0.0, 20.0]);
this.device.queue.writeBuffer(
this.externalForceBuffer,
0, // Buffer Starting position within
newExternalForce.buffer, // new data
newExternalForce.byteOffset,
newExternalForce.byteLength
);
}
//compute pass
this.updateObjects(commandEncoder);
this.InitNodeForce(commandEncoder);
this.updateSprings(commandEncoder);
this.summationNodeForce(commandEncoder);
this.Intersections(commandEncoder);
this.updateParticles(commandEncoder);
this.updateNormals(commandEncoder);
//render pass
this.renderCloth(commandEncoder);
this.device.queue.submit([commandEncoder.finish()]);
await this.device.queue.onSubmittedWorkDone();
// updates frame statistics
this.stats.ms = (currentTime - this.lastTime).toFixed(2);
this.stats.fps = Math.round(1000.0 / (currentTime - this.lastTime));
this.lastTime = currentTime;
this.localFrameCount++;
}


Comments
Post a Comment