Updated the webgpu stuff to have webgl as fallback
This commit is contained in:
48
src/app/pages/algorithms/cloth/cloth-glsl.shader.ts
Normal file
48
src/app/pages/algorithms/cloth/cloth-glsl.shader.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* GLSL shaders for cloth rendering on WebGL.
|
||||
* Replicates the visual output of the WGSL cloth shaders:
|
||||
* checkerboard pattern with Lambertian lighting.
|
||||
*/
|
||||
|
||||
export const CLOTH_VERTEX_SHADER_GLSL = `
|
||||
precision highp float;
|
||||
|
||||
attribute vec3 position;
|
||||
attribute vec2 uv;
|
||||
|
||||
uniform mat4 viewProjection;
|
||||
|
||||
varying vec2 vUV;
|
||||
varying vec3 vWorldPos;
|
||||
|
||||
void main() {
|
||||
vUV = uv;
|
||||
vWorldPos = position;
|
||||
gl_Position = viewProjection * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const CLOTH_FRAGMENT_SHADER_GLSL = `
|
||||
#extension GL_OES_standard_derivatives : enable
|
||||
precision highp float;
|
||||
|
||||
varying vec2 vUV;
|
||||
varying vec3 vWorldPos;
|
||||
|
||||
void main() {
|
||||
vec3 dx = dFdx(vWorldPos);
|
||||
vec3 dy = dFdy(vWorldPos);
|
||||
vec3 normal = normalize(cross(dx, dy));
|
||||
|
||||
vec3 lightDir = normalize(vec3(1.0, 1.0, 0.5));
|
||||
float diffuse = max(0.0, abs(dot(normal, lightDir)));
|
||||
float ambient = 0.3;
|
||||
float lightIntensity = ambient + (diffuse * 0.7);
|
||||
|
||||
float grid = mod(floor(vUV.x * 20.0) + floor(vUV.y * 20.0), 2.0);
|
||||
vec3 baseColor = mix(vec3(0.8, 0.4, 0.15), vec3(0.9, 0.5, 0.2), grid);
|
||||
vec3 finalColor = baseColor * lightIntensity;
|
||||
|
||||
gl_FragColor = vec4(finalColor, 1.0);
|
||||
}
|
||||
`;
|
||||
@@ -1,27 +1,17 @@
|
||||
/**
|
||||
* File: cloth.component.ts
|
||||
* Description: Component for cloth simulation using WebGPU compute shaders.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCard, MatCardContent, MatCardHeader, MatCardTitle } from '@angular/material/card';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { BabylonCanvas, RenderConfig, SceneEventData } from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {ComputeShader, StorageBuffer, MeshBuilder, ShaderMaterial, ShaderLanguage, ArcRotateCamera, GroundMesh, WebGPUEngine, Scene} from '@babylonjs/core';
|
||||
import {
|
||||
CLOTH_FRAGMENT_SHADER_WGSL,
|
||||
CLOTH_INTEGRATE_COMPUTE_WGSL,
|
||||
CLOTH_SOLVE_COMPUTE_WGSL,
|
||||
CLOTH_VELOCITY_COMPUTE_WGSL,
|
||||
CLOTH_VERTEX_SHADER_WGSL
|
||||
} from './cloth.shader';
|
||||
import {Component} from '@angular/core';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import {TranslatePipe} from '@ngx-translate/core';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {ClothBuffers, ClothConfig, ClothData, ClothPipelines} from './cloth.model';
|
||||
import {ClothConfig} from './cloth.model';
|
||||
import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {ClothSimulationStrategy} from './strategies/cloth-simulation.strategy';
|
||||
import {ClothGpuStrategy} from './strategies/cloth-gpu.strategy';
|
||||
import {ClothCpuStrategy} from './strategies/cloth-cpu.strategy';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cloth',
|
||||
@@ -43,17 +33,16 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
export class ClothComponent {
|
||||
private currentSceneData: SceneEventData | null = null;
|
||||
private simulationTime: number = 0;
|
||||
private clothMesh: GroundMesh | null = null;
|
||||
private strategy: ClothSimulationStrategy | null = null;
|
||||
|
||||
public isWindActive: boolean = false;
|
||||
public isOutlineActive: boolean = false;
|
||||
public stiffness: number = 80;
|
||||
// Elongation along the vertical (Y) axis, 0.5 = compressed, 2.0 = stretched
|
||||
public elongation: number = 1.0;
|
||||
|
||||
public renderConfig: RenderConfig = {
|
||||
mode: '3D',
|
||||
initialViewSize: 20,
|
||||
shaderLanguage: ShaderLanguage.WGSL
|
||||
initialViewSize: 20
|
||||
};
|
||||
|
||||
algoInformation: AlgorithmInformation = {
|
||||
@@ -89,10 +78,6 @@ export class ClothComponent {
|
||||
disclaimerListEntry: ['CLOTH.EXPLANATION.DISCLAIMER_1', 'CLOTH.EXPLANATION.DISCLAIMER_2', 'CLOTH.EXPLANATION.DISCLAIMER_3', 'CLOTH.EXPLANATION.DISCLAIMER_4']
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the Babylon scene is ready.
|
||||
* @param event The scene event data.
|
||||
*/
|
||||
public onSceneReady(event: SceneEventData): void {
|
||||
this.currentSceneData = event;
|
||||
this.createSimulation();
|
||||
@@ -104,10 +89,11 @@ export class ClothComponent {
|
||||
|
||||
public toggleMesh(): void {
|
||||
this.isOutlineActive = !this.isOutlineActive;
|
||||
if (!this.clothMesh?.material) {
|
||||
const mesh = this.strategy?.getMesh();
|
||||
if (!mesh?.material) {
|
||||
return;
|
||||
}
|
||||
this.clothMesh.material.wireframe = this.isOutlineActive;
|
||||
mesh.material.wireframe = this.isOutlineActive;
|
||||
}
|
||||
|
||||
public restartSimulation(): void {
|
||||
@@ -115,36 +101,43 @@ export class ClothComponent {
|
||||
this.createSimulation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and starts the cloth simulation.
|
||||
*/
|
||||
private createSimulation(): void {
|
||||
if (!this.currentSceneData) return;
|
||||
if (!this.currentSceneData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { engine, scene } = this.currentSceneData;
|
||||
|
||||
// 1. Define physics parameters
|
||||
const {engine, scene, gpuTier} = this.currentSceneData;
|
||||
const config = this.getClothConfig();
|
||||
|
||||
// 2. Generate initial CPU data (positions, constraints)
|
||||
const clothData = this.generateClothData(config);
|
||||
if (this.strategy) {
|
||||
this.strategy.dispose();
|
||||
}
|
||||
|
||||
// 3. Upload to GPU
|
||||
const buffers = this.createStorageBuffers(engine, clothData);
|
||||
this.strategy = gpuTier === 'webgpu'
|
||||
? new ClothGpuStrategy()
|
||||
: new ClothCpuStrategy();
|
||||
|
||||
// 4. Create Compute Shaders
|
||||
const pipelines = this.setupComputePipelines(engine, buffers);
|
||||
|
||||
// 5. Setup Rendering (Mesh, Material, Camera)
|
||||
this.setupRenderMesh(scene, config, buffers.positions);
|
||||
|
||||
// 6. Start the physics loop
|
||||
this.startRenderLoop(engine, scene, config, buffers, pipelines);
|
||||
this.strategy.init(scene, engine, config);
|
||||
this.startParamUpdateLoop(scene, engine);
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: any, engine: any): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.simulationTime += engine.getDeltaTime() / 1000.0;
|
||||
|
||||
if (this.strategy) {
|
||||
this.strategy.updateParams({
|
||||
stiffness: this.stiffness,
|
||||
elongation: this.elongation,
|
||||
isWindActive: this.isWindActive,
|
||||
simulationTime: this.simulationTime,
|
||||
deltaTime: engine.getDeltaTime() / 1000.0
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 1. CONFIGURATION
|
||||
// ========================================================================
|
||||
private getClothConfig(): ClothConfig {
|
||||
const gridWidth = 100;
|
||||
const gridHeight = 100;
|
||||
@@ -162,239 +155,4 @@ export class ClothComponent {
|
||||
particleInvMass: 1.0 / particleMass
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 2. DATA GENERATION (CPU)
|
||||
// ========================================================================
|
||||
private generateClothData(config: ClothConfig): ClothData {
|
||||
const positionsData = new Float32Array(config.numVertices * 4);
|
||||
const prevPositionsData = new Float32Array(config.numVertices * 4);
|
||||
const velocitiesData = new Float32Array(config.numVertices * 4);
|
||||
|
||||
const constraintsP0: number[] = [];
|
||||
const constraintsP1: number[] = [];
|
||||
const constraintsP2: number[] = [];
|
||||
const constraintsP3: number[] = [];
|
||||
|
||||
// Type 1.0 = horizontal/diagonal (no elongation), Type 2.0 = vertical (elongation applies)
|
||||
const addHorizontalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 1.0);
|
||||
};
|
||||
const addVerticalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 2.0);
|
||||
};
|
||||
|
||||
// Fill positions (Pin top row)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth; x++) {
|
||||
const idx = (y * config.gridWidth + x) * 4;
|
||||
positionsData[idx + 0] = (x - config.gridWidth / 2) * config.spacing;
|
||||
positionsData[idx + 1] = 5.0 - (y * config.spacing);
|
||||
positionsData[idx + 2] = 0.0;
|
||||
positionsData[idx + 3] = (y === 0) ? 0.0 : config.particleInvMass;
|
||||
|
||||
prevPositionsData[idx + 0] = positionsData[idx + 0];
|
||||
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
||||
prevPositionsData[idx + 2] = positionsData[idx + 2];
|
||||
prevPositionsData[idx + 3] = positionsData[idx + 3];
|
||||
}
|
||||
}
|
||||
|
||||
// Graph Coloring (4 Phases)
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) addHorizontalConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) addHorizontalConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1);
|
||||
}
|
||||
for (let y = 0; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addVerticalConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
for (let y = 1; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) addVerticalConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
|
||||
const constraintsP4: number[] = [];
|
||||
const constraintsP5: number[] = [];
|
||||
const constraintsP6: number[] = [];
|
||||
const constraintsP7: number[] = [];
|
||||
|
||||
const diagSpacing = config.spacing * Math.SQRT2;
|
||||
const addDiagConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, diagSpacing, 1.0);
|
||||
};
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP4 : constraintsP5;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + x, (y + 1) * config.gridWidth + (x + 1));
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP6 : constraintsP7;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + (x + 1), (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
positions: positionsData,
|
||||
prevPositions: prevPositionsData,
|
||||
velocities: velocitiesData,
|
||||
constraints: [
|
||||
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||
],
|
||||
params: new Float32Array(9)
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 3. BUFFER CREATION (GPU)
|
||||
// ========================================================================
|
||||
private createStorageBuffers(engine: WebGPUEngine, data: ClothData): ClothBuffers {
|
||||
const createBuffer = (arrayData: Float32Array | number[]): StorageBuffer => {
|
||||
const buffer = new StorageBuffer(engine, arrayData.length * 4);
|
||||
buffer.update(arrayData instanceof Float32Array ? arrayData : new Float32Array(arrayData));
|
||||
return buffer;
|
||||
};
|
||||
|
||||
return {
|
||||
positions: createBuffer(data.positions),
|
||||
prevPositions: createBuffer(data.prevPositions),
|
||||
velocities: createBuffer(data.velocities),
|
||||
params: createBuffer(data.params),
|
||||
constraints: data.constraints.map(cData => createBuffer(cData))
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 4. COMPUTE SHADERS
|
||||
// ========================================================================
|
||||
private setupComputePipelines(engine: WebGPUEngine, buffers: ClothBuffers): ClothPipelines {
|
||||
|
||||
// Helper for integrating & velocity
|
||||
const createBasicShader = (name: string, source: string) => {
|
||||
const cs = new ComputeShader(name, engine, { computeSource: source }, {
|
||||
bindingsMapping: {
|
||||
"p": { group: 0, binding: 0 },
|
||||
"positions": { group: 0, binding: 1 },
|
||||
"prev_positions": { group: 0, binding: 2 },
|
||||
"velocities": { group: 0, binding: 3 }
|
||||
}
|
||||
});
|
||||
cs.setStorageBuffer("p", buffers.params);
|
||||
cs.setStorageBuffer("positions", buffers.positions);
|
||||
cs.setStorageBuffer("prev_positions", buffers.prevPositions);
|
||||
cs.setStorageBuffer("velocities", buffers.velocities);
|
||||
return cs;
|
||||
};
|
||||
|
||||
// Helper for solvers
|
||||
const createSolverShader = (name: string, constraintBuffer: StorageBuffer) => {
|
||||
const cs = new ComputeShader(name, engine, { computeSource: CLOTH_SOLVE_COMPUTE_WGSL }, {
|
||||
bindingsMapping: {
|
||||
"p": { group: 0, binding: 0 },
|
||||
"positions": { group: 0, binding: 1 },
|
||||
"constraints": { group: 0, binding: 2 }
|
||||
}
|
||||
});
|
||||
cs.setStorageBuffer("p", buffers.params);
|
||||
cs.setStorageBuffer("positions", buffers.positions);
|
||||
cs.setStorageBuffer("constraints", constraintBuffer);
|
||||
return cs;
|
||||
};
|
||||
|
||||
return {
|
||||
integrate: createBasicShader("integrate", CLOTH_INTEGRATE_COMPUTE_WGSL),
|
||||
solvers: buffers.constraints.map((cBuffer, i) => createSolverShader(`solve${i}`, cBuffer)),
|
||||
velocity: createBasicShader("velocity", CLOTH_VELOCITY_COMPUTE_WGSL)
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 5. RENDERING SETUP
|
||||
// ========================================================================
|
||||
private setupRenderMesh(scene: Scene, config: ClothConfig, positionsBuffer: StorageBuffer): void {
|
||||
if (this.clothMesh) {
|
||||
scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
|
||||
this.clothMesh = MeshBuilder.CreateGround("cloth", { width: 10, height: 10, subdivisions: config.gridWidth - 1 }, scene);
|
||||
|
||||
const clothMaterial = new ShaderMaterial("clothMat", scene, {
|
||||
vertexSource: CLOTH_VERTEX_SHADER_WGSL,
|
||||
fragmentSource: CLOTH_FRAGMENT_SHADER_WGSL
|
||||
}, {
|
||||
attributes: ["position", "uv"],
|
||||
uniforms: ["viewProjection"],
|
||||
storageBuffers: ["positions"],
|
||||
shaderLanguage: ShaderLanguage.WGSL
|
||||
});
|
||||
|
||||
clothMaterial.backFaceCulling = false;
|
||||
clothMaterial.setStorageBuffer("positions", positionsBuffer);
|
||||
this.clothMesh.material = clothMaterial;
|
||||
|
||||
const camera = scene.activeCamera as ArcRotateCamera;
|
||||
if (camera) {
|
||||
camera.alpha = Math.PI / 4;
|
||||
camera.beta = Math.PI / 2.5;
|
||||
camera.radius = 15;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// 6. RENDER LOOP
|
||||
// ========================================================================
|
||||
private startRenderLoop(engine: WebGPUEngine, scene: Scene, config: ClothConfig, buffers: ClothBuffers, pipelines: ClothPipelines): void {
|
||||
const paramsData = new Float32Array(9);
|
||||
|
||||
// Pre-calculate constraint dispatch sizes for the 4 phases
|
||||
const constraintsLength = buffers.constraints.map(b => (b as any)._buffer.capacity / 4 / 4); // Elements / vec4 length
|
||||
const dispatchXConstraints = constraintsLength.map(len => Math.ceil(len / 64));
|
||||
const dispatchXVertices = Math.ceil(config.numVertices / 64);
|
||||
const substeps = 15;
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
this.simulationTime += engine.getDeltaTime() / 1000.0;
|
||||
|
||||
// Update Physics Parameters
|
||||
const windX = this.isWindActive ? 5.0 : 0.0;
|
||||
const windY = 0.0;
|
||||
const windZ = this.isWindActive ? 15.0 : 0.0;
|
||||
|
||||
// Logarithmic compliance: stiffness=1 → very soft fabric, stiffness=100 → rigid metal sheet.
|
||||
// alpha = compliance / dt² must be >> wSum (≈800) to be soft, << wSum to be rigid.
|
||||
const softCompliance = 10.0;
|
||||
const rigidCompliance = 0.00001;
|
||||
const t = (this.stiffness - 1) / 99.0;
|
||||
const compliance = softCompliance * Math.pow(rigidCompliance / softCompliance, t);
|
||||
|
||||
paramsData[0] = 0.016; // dt
|
||||
paramsData[1] = -9.81; // gravity
|
||||
paramsData[2] = compliance;
|
||||
paramsData[3] = config.numVertices;
|
||||
paramsData[4] = windX;
|
||||
paramsData[5] = windY;
|
||||
paramsData[6] = windZ;
|
||||
paramsData[7] = this.simulationTime;
|
||||
paramsData[8] = this.elongation;
|
||||
|
||||
buffers.params.update(paramsData);
|
||||
|
||||
// 1. Predict positions
|
||||
pipelines.integrate.dispatch(dispatchXVertices, 1, 1);
|
||||
|
||||
// 2. XPBD Solver (Substeps) - Graph Coloring Phase
|
||||
for (let i = 0; i < substeps; i++) {
|
||||
for (let phase = 0; phase < pipelines.solvers.length; phase++) {
|
||||
pipelines.solvers[phase].dispatch(dispatchXConstraints[phase], 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update velocities
|
||||
pipelines.velocity.dispatch(dispatchXVertices, 1, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// --- SIMULATION CONFIGURATION ---
|
||||
import {ComputeShader, StorageBuffer} from '@babylonjs/core';
|
||||
|
||||
export interface ClothConfig {
|
||||
gridWidth: number;
|
||||
gridHeight: number;
|
||||
@@ -10,27 +7,10 @@ export interface ClothConfig {
|
||||
particleInvMass: number;
|
||||
}
|
||||
|
||||
// --- RAW CPU DATA ---
|
||||
export interface ClothData {
|
||||
positions: Float32Array;
|
||||
prevPositions: Float32Array;
|
||||
velocities: Float32Array;
|
||||
constraints: number[][]; // Array containing the 4 phases
|
||||
constraints: number[][];
|
||||
params: Float32Array;
|
||||
}
|
||||
|
||||
// --- WEBGPU BUFFERS ---
|
||||
export interface ClothBuffers {
|
||||
positions: StorageBuffer;
|
||||
prevPositions: StorageBuffer;
|
||||
velocities: StorageBuffer;
|
||||
params: StorageBuffer;
|
||||
constraints: StorageBuffer[]; // 4 phase buffers
|
||||
}
|
||||
|
||||
// --- COMPUTE PIPELINES ---
|
||||
export interface ClothPipelines {
|
||||
integrate: ComputeShader;
|
||||
solvers: ComputeShader[]; // 4 solve shaders
|
||||
velocity: ComputeShader;
|
||||
}
|
||||
|
||||
149
src/app/pages/algorithms/cloth/strategies/cloth-cpu-physics.ts
Normal file
149
src/app/pages/algorithms/cloth/strategies/cloth-cpu-physics.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* CPU-side cloth physics mirroring the WGSL compute shaders.
|
||||
* All data uses the same Float32Array vec4 layout: [x, y, z, invMass] per vertex.
|
||||
*/
|
||||
|
||||
export interface ClothPhysicsParams {
|
||||
dt: number;
|
||||
gravityY: number;
|
||||
compliance: number;
|
||||
numVertices: number;
|
||||
windX: number;
|
||||
windY: number;
|
||||
windZ: number;
|
||||
time: number;
|
||||
elongation: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors CLOTH_INTEGRATE_COMPUTE_WGSL:
|
||||
* Applies gravity and wind forces, predicts new positions.
|
||||
*/
|
||||
export function integratePositions(
|
||||
positions: Float32Array,
|
||||
prevPositions: Float32Array,
|
||||
velocities: Float32Array,
|
||||
params: ClothPhysicsParams
|
||||
): void {
|
||||
for (let idx = 0; idx < params.numVertices; idx++) {
|
||||
const base = idx * 4;
|
||||
const invMass = positions[base + 3];
|
||||
|
||||
if (invMass <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
velocities[base + 1] += params.gravityY * params.dt;
|
||||
|
||||
const posX = positions[base + 0];
|
||||
const posY = positions[base + 1];
|
||||
|
||||
const flutter = Math.sin(posX * 2.0 + params.time * 5.0) * Math.cos(posY * 2.0 + params.time * 3.0);
|
||||
|
||||
const windForceX = params.windX + (flutter * params.windX * 0.8);
|
||||
const windForceY = params.windY + (flutter * 2.0);
|
||||
const windForceZ = params.windZ + (flutter * params.windZ * 0.8);
|
||||
|
||||
velocities[base + 0] += windForceX * params.dt;
|
||||
velocities[base + 1] += windForceY * params.dt;
|
||||
velocities[base + 2] += windForceZ * params.dt;
|
||||
|
||||
prevPositions[base + 0] = positions[base + 0];
|
||||
prevPositions[base + 1] = positions[base + 1];
|
||||
prevPositions[base + 2] = positions[base + 2];
|
||||
prevPositions[base + 3] = positions[base + 3];
|
||||
|
||||
positions[base + 0] += velocities[base + 0] * params.dt;
|
||||
positions[base + 1] += velocities[base + 1] * params.dt;
|
||||
positions[base + 2] += velocities[base + 2] * params.dt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors CLOTH_SOLVE_COMPUTE_WGSL:
|
||||
* XPBD constraint solving for one phase of constraints.
|
||||
* Each constraint is stored as [idA, idB, restLength, type] (4 floats).
|
||||
*/
|
||||
export function solveConstraints(
|
||||
positions: Float32Array,
|
||||
constraints: Float32Array,
|
||||
params: ClothPhysicsParams
|
||||
): void {
|
||||
const numConstraints = constraints.length / 4;
|
||||
|
||||
for (let idx = 0; idx < numConstraints; idx++) {
|
||||
const cBase = idx * 4;
|
||||
const constraintType = constraints[cBase + 3];
|
||||
|
||||
if (constraintType < 0.5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const idA = constraints[cBase + 0];
|
||||
const idB = constraints[cBase + 1];
|
||||
const restLength = constraints[cBase + 2] * params.elongation;
|
||||
|
||||
const baseA = idA * 4;
|
||||
const baseB = idB * 4;
|
||||
|
||||
const wA = positions[baseA + 3];
|
||||
const wB = positions[baseB + 3];
|
||||
const wSum = wA + wB;
|
||||
|
||||
if (wSum <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dirX = positions[baseA + 0] - positions[baseB + 0];
|
||||
const dirY = positions[baseA + 1] - positions[baseB + 1];
|
||||
const dirZ = positions[baseA + 2] - positions[baseB + 2];
|
||||
const dist = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ);
|
||||
|
||||
if (dist < 0.0001) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nX = dirX / dist;
|
||||
const nY = dirY / dist;
|
||||
const nZ = dirZ / dist;
|
||||
const c = dist - restLength;
|
||||
|
||||
const alpha = params.compliance / (params.dt * params.dt);
|
||||
const lambda = -c / (wSum + alpha);
|
||||
|
||||
if (wA > 0.0) {
|
||||
positions[baseA + 0] += nX * (lambda * wA);
|
||||
positions[baseA + 1] += nY * (lambda * wA);
|
||||
positions[baseA + 2] += nZ * (lambda * wA);
|
||||
}
|
||||
if (wB > 0.0) {
|
||||
positions[baseB + 0] += nX * (-lambda * wB);
|
||||
positions[baseB + 1] += nY * (-lambda * wB);
|
||||
positions[baseB + 2] += nZ * (-lambda * wB);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors CLOTH_VELOCITY_COMPUTE_WGSL:
|
||||
* Derives velocity from position changes: v = (pos - prevPos) / dt
|
||||
*/
|
||||
export function updateVelocities(
|
||||
positions: Float32Array,
|
||||
prevPositions: Float32Array,
|
||||
velocities: Float32Array,
|
||||
params: ClothPhysicsParams
|
||||
): void {
|
||||
for (let idx = 0; idx < params.numVertices; idx++) {
|
||||
const base = idx * 4;
|
||||
const invMass = positions[base + 3];
|
||||
|
||||
if (invMass <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
velocities[base + 0] = (positions[base + 0] - prevPositions[base + 0]) / params.dt;
|
||||
velocities[base + 1] = (positions[base + 1] - prevPositions[base + 1]) / params.dt;
|
||||
velocities[base + 2] = (positions[base + 2] - prevPositions[base + 2]) / params.dt;
|
||||
}
|
||||
}
|
||||
216
src/app/pages/algorithms/cloth/strategies/cloth-cpu.strategy.ts
Normal file
216
src/app/pages/algorithms/cloth/strategies/cloth-cpu.strategy.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import {ArcRotateCamera, Engine, GroundMesh, MeshBuilder, Scene, ShaderMaterial, VertexBuffer, WebGPUEngine} from '@babylonjs/core';
|
||||
import {ClothConfig, ClothData} from '../cloth.model';
|
||||
import {CLOTH_FRAGMENT_SHADER_GLSL, CLOTH_VERTEX_SHADER_GLSL} from '../cloth-glsl.shader';
|
||||
import {ClothPhysicsParams, integratePositions, solveConstraints, updateVelocities} from './cloth-cpu-physics';
|
||||
import {ClothSimulationParams, ClothSimulationStrategy} from './cloth-simulation.strategy';
|
||||
|
||||
export class ClothCpuStrategy implements ClothSimulationStrategy {
|
||||
private clothMesh: GroundMesh | null = null;
|
||||
private scene: Scene | null = null;
|
||||
private config: ClothConfig | null = null;
|
||||
|
||||
private positions!: Float32Array;
|
||||
private prevPositions!: Float32Array;
|
||||
private velocities!: Float32Array;
|
||||
private constraintPhases!: Float32Array[];
|
||||
|
||||
private physicsParams: ClothPhysicsParams = {
|
||||
dt: 0.016,
|
||||
gravityY: -9.81,
|
||||
compliance: 0.00001,
|
||||
numVertices: 0,
|
||||
windX: 0,
|
||||
windY: 0,
|
||||
windZ: 0,
|
||||
time: 0,
|
||||
elongation: 1.0
|
||||
};
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig): void {
|
||||
this.scene = scene;
|
||||
this.config = config;
|
||||
this.physicsParams.numVertices = config.numVertices;
|
||||
|
||||
const clothData = this.generateClothData(config);
|
||||
this.positions = clothData.positions;
|
||||
this.prevPositions = clothData.prevPositions;
|
||||
this.velocities = clothData.velocities;
|
||||
this.constraintPhases = clothData.constraints.map(c => new Float32Array(c));
|
||||
|
||||
this.setupRenderMesh(scene, config);
|
||||
this.startRenderLoop(scene, config);
|
||||
}
|
||||
|
||||
updateParams(params: ClothSimulationParams): void {
|
||||
const windX = params.isWindActive ? 5.0 : 0.0;
|
||||
const windZ = params.isWindActive ? 15.0 : 0.0;
|
||||
|
||||
const softCompliance = 10.0;
|
||||
const rigidCompliance = 0.00001;
|
||||
const t = (params.stiffness - 1) / 99.0;
|
||||
|
||||
this.physicsParams.compliance = softCompliance * Math.pow(rigidCompliance / softCompliance, t);
|
||||
this.physicsParams.windX = windX;
|
||||
this.physicsParams.windY = 0.0;
|
||||
this.physicsParams.windZ = windZ;
|
||||
this.physicsParams.time = params.simulationTime;
|
||||
this.physicsParams.elongation = params.elongation;
|
||||
}
|
||||
|
||||
getMesh(): GroundMesh | null {
|
||||
return this.clothMesh;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene && this.clothMesh) {
|
||||
this.scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
this.clothMesh = null;
|
||||
}
|
||||
|
||||
private generateClothData(config: ClothConfig): ClothData {
|
||||
const positionsData = new Float32Array(config.numVertices * 4);
|
||||
const prevPositionsData = new Float32Array(config.numVertices * 4);
|
||||
const velocitiesData = new Float32Array(config.numVertices * 4);
|
||||
|
||||
const constraintsP0: number[] = [];
|
||||
const constraintsP1: number[] = [];
|
||||
const constraintsP2: number[] = [];
|
||||
const constraintsP3: number[] = [];
|
||||
|
||||
const addHorizontalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 1.0);
|
||||
};
|
||||
const addVerticalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 2.0);
|
||||
};
|
||||
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth; x++) {
|
||||
const idx = (y * config.gridWidth + x) * 4;
|
||||
positionsData[idx + 0] = (x - config.gridWidth / 2) * config.spacing;
|
||||
positionsData[idx + 1] = 5.0 - (y * config.spacing);
|
||||
positionsData[idx + 2] = 0.0;
|
||||
positionsData[idx + 3] = (y === 0) ? 0.0 : config.particleInvMass;
|
||||
|
||||
prevPositionsData[idx + 0] = positionsData[idx + 0];
|
||||
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
||||
prevPositionsData[idx + 2] = positionsData[idx + 2];
|
||||
prevPositionsData[idx + 3] = positionsData[idx + 3];
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) { addHorizontalConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1); }
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) { addHorizontalConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1); }
|
||||
}
|
||||
for (let y = 0; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) { addVerticalConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x); }
|
||||
}
|
||||
for (let y = 1; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) { addVerticalConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x); }
|
||||
}
|
||||
|
||||
const constraintsP4: number[] = [];
|
||||
const constraintsP5: number[] = [];
|
||||
const constraintsP6: number[] = [];
|
||||
const constraintsP7: number[] = [];
|
||||
const diagSpacing = config.spacing * Math.SQRT2;
|
||||
const addDiagConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, diagSpacing, 1.0);
|
||||
};
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP4 : constraintsP5;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + x, (y + 1) * config.gridWidth + (x + 1));
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP6 : constraintsP7;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + (x + 1), (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
positions: positionsData,
|
||||
prevPositions: prevPositionsData,
|
||||
velocities: velocitiesData,
|
||||
constraints: [
|
||||
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||
],
|
||||
params: new Float32Array(9)
|
||||
};
|
||||
}
|
||||
|
||||
private setupRenderMesh(scene: Scene, config: ClothConfig): void {
|
||||
if (this.clothMesh) {
|
||||
scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
|
||||
this.clothMesh = MeshBuilder.CreateGround("cloth", {
|
||||
width: 10,
|
||||
height: 10,
|
||||
subdivisions: config.gridWidth - 1,
|
||||
updatable: true
|
||||
}, scene);
|
||||
|
||||
const clothMaterial = new ShaderMaterial("clothMat", scene, {
|
||||
vertexSource: CLOTH_VERTEX_SHADER_GLSL,
|
||||
fragmentSource: CLOTH_FRAGMENT_SHADER_GLSL
|
||||
}, {
|
||||
attributes: ["position", "uv"],
|
||||
uniforms: ["viewProjection"]
|
||||
});
|
||||
|
||||
clothMaterial.backFaceCulling = false;
|
||||
this.clothMesh.material = clothMaterial;
|
||||
|
||||
const camera = scene.activeCamera as ArcRotateCamera;
|
||||
if (camera) {
|
||||
camera.alpha = Math.PI / 4;
|
||||
camera.beta = Math.PI / 2.5;
|
||||
camera.radius = 15;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts xyz from the vec4 positions array into a vec3 array for mesh vertex update.
|
||||
*/
|
||||
private extractPositionsVec3(positions: Float32Array, numVertices: number): Float32Array {
|
||||
const result = new Float32Array(numVertices * 3);
|
||||
for (let i = 0; i < numVertices; i++) {
|
||||
result[i * 3 + 0] = positions[i * 4 + 0];
|
||||
result[i * 3 + 1] = positions[i * 4 + 1];
|
||||
result[i * 3 + 2] = positions[i * 4 + 2];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private startRenderLoop(scene: Scene, config: ClothConfig): void {
|
||||
const substeps = 15;
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.clothMesh) {
|
||||
return;
|
||||
}
|
||||
|
||||
integratePositions(this.positions, this.prevPositions, this.velocities, this.physicsParams);
|
||||
|
||||
for (let i = 0; i < substeps; i++) {
|
||||
for (const phase of this.constraintPhases) {
|
||||
solveConstraints(this.positions, phase, this.physicsParams);
|
||||
}
|
||||
}
|
||||
|
||||
updateVelocities(this.positions, this.prevPositions, this.velocities, this.physicsParams);
|
||||
|
||||
const posVec3 = this.extractPositionsVec3(this.positions, config.numVertices);
|
||||
this.clothMesh.updateVerticesData(VertexBuffer.PositionKind, posVec3);
|
||||
});
|
||||
}
|
||||
}
|
||||
275
src/app/pages/algorithms/cloth/strategies/cloth-gpu.strategy.ts
Normal file
275
src/app/pages/algorithms/cloth/strategies/cloth-gpu.strategy.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import {ArcRotateCamera, ComputeShader, Engine, GroundMesh, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, StorageBuffer, WebGPUEngine} from '@babylonjs/core';
|
||||
import {ClothConfig, ClothData} from '../cloth.model';
|
||||
import {
|
||||
CLOTH_FRAGMENT_SHADER_WGSL,
|
||||
CLOTH_INTEGRATE_COMPUTE_WGSL,
|
||||
CLOTH_SOLVE_COMPUTE_WGSL,
|
||||
CLOTH_VELOCITY_COMPUTE_WGSL,
|
||||
CLOTH_VERTEX_SHADER_WGSL
|
||||
} from '../cloth.shader';
|
||||
import {ClothSimulationParams, ClothSimulationStrategy} from './cloth-simulation.strategy';
|
||||
|
||||
interface GpuBuffers {
|
||||
positions: StorageBuffer;
|
||||
prevPositions: StorageBuffer;
|
||||
velocities: StorageBuffer;
|
||||
params: StorageBuffer;
|
||||
constraints: StorageBuffer[];
|
||||
}
|
||||
|
||||
interface GpuPipelines {
|
||||
integrate: ComputeShader;
|
||||
solvers: ComputeShader[];
|
||||
velocity: ComputeShader;
|
||||
}
|
||||
|
||||
export class ClothGpuStrategy implements ClothSimulationStrategy {
|
||||
private clothMesh: GroundMesh | null = null;
|
||||
private scene: Scene | null = null;
|
||||
private paramsData = new Float32Array(9);
|
||||
private buffers: GpuBuffers | null = null;
|
||||
private pipelines: GpuPipelines | null = null;
|
||||
private dispatchXConstraints: number[] = [];
|
||||
private dispatchXVertices = 0;
|
||||
private numVertices = 0;
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig): void {
|
||||
this.scene = scene;
|
||||
this.numVertices = config.numVertices;
|
||||
const gpuEngine = engine as WebGPUEngine;
|
||||
|
||||
const clothData = this.generateClothData(config);
|
||||
this.buffers = this.createStorageBuffers(gpuEngine, clothData);
|
||||
this.pipelines = this.setupComputePipelines(gpuEngine, this.buffers);
|
||||
this.setupRenderMesh(scene, config, this.buffers.positions);
|
||||
this.setupDispatchSizes(config, this.buffers);
|
||||
this.startRenderLoop(scene);
|
||||
}
|
||||
|
||||
updateParams(params: ClothSimulationParams): void {
|
||||
if (!this.buffers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windX = params.isWindActive ? 5.0 : 0.0;
|
||||
const windZ = params.isWindActive ? 15.0 : 0.0;
|
||||
|
||||
const softCompliance = 10.0;
|
||||
const rigidCompliance = 0.00001;
|
||||
const t = (params.stiffness - 1) / 99.0;
|
||||
const compliance = softCompliance * Math.pow(rigidCompliance / softCompliance, t);
|
||||
|
||||
this.paramsData[0] = 0.016;
|
||||
this.paramsData[1] = -9.81;
|
||||
this.paramsData[2] = compliance;
|
||||
this.paramsData[3] = this.numVertices;
|
||||
this.paramsData[4] = windX;
|
||||
this.paramsData[5] = 0.0;
|
||||
this.paramsData[6] = windZ;
|
||||
this.paramsData[7] = params.simulationTime;
|
||||
this.paramsData[8] = params.elongation;
|
||||
}
|
||||
|
||||
getMesh(): GroundMesh | null {
|
||||
return this.clothMesh;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene && this.clothMesh) {
|
||||
this.scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
this.clothMesh = null;
|
||||
this.buffers = null;
|
||||
this.pipelines = null;
|
||||
}
|
||||
|
||||
private generateClothData(config: ClothConfig): ClothData {
|
||||
const positionsData = new Float32Array(config.numVertices * 4);
|
||||
const prevPositionsData = new Float32Array(config.numVertices * 4);
|
||||
const velocitiesData = new Float32Array(config.numVertices * 4);
|
||||
|
||||
const constraintsP0: number[] = [];
|
||||
const constraintsP1: number[] = [];
|
||||
const constraintsP2: number[] = [];
|
||||
const constraintsP3: number[] = [];
|
||||
|
||||
const addHorizontalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 1.0);
|
||||
};
|
||||
const addVerticalConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, config.spacing, 2.0);
|
||||
};
|
||||
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth; x++) {
|
||||
const idx = (y * config.gridWidth + x) * 4;
|
||||
positionsData[idx + 0] = (x - config.gridWidth / 2) * config.spacing;
|
||||
positionsData[idx + 1] = 5.0 - (y * config.spacing);
|
||||
positionsData[idx + 2] = 0.0;
|
||||
positionsData[idx + 3] = (y === 0) ? 0.0 : config.particleInvMass;
|
||||
|
||||
prevPositionsData[idx + 0] = positionsData[idx + 0];
|
||||
prevPositionsData[idx + 1] = positionsData[idx + 1];
|
||||
prevPositionsData[idx + 2] = positionsData[idx + 2];
|
||||
prevPositionsData[idx + 3] = positionsData[idx + 3];
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < config.gridHeight; y++) {
|
||||
for (let x = 0; x < config.gridWidth - 1; x += 2) { addHorizontalConstraint(constraintsP0, y * config.gridWidth + x, y * config.gridWidth + x + 1); }
|
||||
for (let x = 1; x < config.gridWidth - 1; x += 2) { addHorizontalConstraint(constraintsP1, y * config.gridWidth + x, y * config.gridWidth + x + 1); }
|
||||
}
|
||||
for (let y = 0; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) { addVerticalConstraint(constraintsP2, y * config.gridWidth + x, (y + 1) * config.gridWidth + x); }
|
||||
}
|
||||
for (let y = 1; y < config.gridHeight - 1; y += 2) {
|
||||
for (let x = 0; x < config.gridWidth; x++) { addVerticalConstraint(constraintsP3, y * config.gridWidth + x, (y + 1) * config.gridWidth + x); }
|
||||
}
|
||||
|
||||
const constraintsP4: number[] = [];
|
||||
const constraintsP5: number[] = [];
|
||||
const constraintsP6: number[] = [];
|
||||
const constraintsP7: number[] = [];
|
||||
const diagSpacing = config.spacing * Math.SQRT2;
|
||||
const addDiagConstraint = (arr: number[], a: number, b: number): void => {
|
||||
arr.push(a, b, diagSpacing, 1.0);
|
||||
};
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP4 : constraintsP5;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + x, (y + 1) * config.gridWidth + (x + 1));
|
||||
}
|
||||
}
|
||||
|
||||
for (let y = 0; y < config.gridHeight - 1; y++) {
|
||||
const arr = (y % 2 === 0) ? constraintsP6 : constraintsP7;
|
||||
for (let x = 0; x < config.gridWidth - 1; x++) {
|
||||
addDiagConstraint(arr, y * config.gridWidth + (x + 1), (y + 1) * config.gridWidth + x);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
positions: positionsData,
|
||||
prevPositions: prevPositionsData,
|
||||
velocities: velocitiesData,
|
||||
constraints: [
|
||||
constraintsP0, constraintsP1, constraintsP2, constraintsP3,
|
||||
constraintsP4, constraintsP5, constraintsP6, constraintsP7
|
||||
],
|
||||
params: new Float32Array(9)
|
||||
};
|
||||
}
|
||||
|
||||
private createStorageBuffers(engine: WebGPUEngine, data: ClothData): GpuBuffers {
|
||||
const createBuffer = (arrayData: Float32Array | number[]): StorageBuffer => {
|
||||
const buffer = new StorageBuffer(engine, arrayData.length * 4);
|
||||
buffer.update(arrayData instanceof Float32Array ? arrayData : new Float32Array(arrayData));
|
||||
return buffer;
|
||||
};
|
||||
|
||||
return {
|
||||
positions: createBuffer(data.positions),
|
||||
prevPositions: createBuffer(data.prevPositions),
|
||||
velocities: createBuffer(data.velocities),
|
||||
params: createBuffer(data.params),
|
||||
constraints: data.constraints.map(cData => createBuffer(cData))
|
||||
};
|
||||
}
|
||||
|
||||
private setupComputePipelines(engine: WebGPUEngine, buffers: GpuBuffers): GpuPipelines {
|
||||
const createBasicShader = (name: string, source: string) => {
|
||||
const cs = new ComputeShader(name, engine, {computeSource: source}, {
|
||||
bindingsMapping: {
|
||||
"p": {group: 0, binding: 0},
|
||||
"positions": {group: 0, binding: 1},
|
||||
"prev_positions": {group: 0, binding: 2},
|
||||
"velocities": {group: 0, binding: 3}
|
||||
}
|
||||
});
|
||||
cs.setStorageBuffer("p", buffers.params);
|
||||
cs.setStorageBuffer("positions", buffers.positions);
|
||||
cs.setStorageBuffer("prev_positions", buffers.prevPositions);
|
||||
cs.setStorageBuffer("velocities", buffers.velocities);
|
||||
return cs;
|
||||
};
|
||||
|
||||
const createSolverShader = (name: string, constraintBuffer: StorageBuffer) => {
|
||||
const cs = new ComputeShader(name, engine, {computeSource: CLOTH_SOLVE_COMPUTE_WGSL}, {
|
||||
bindingsMapping: {
|
||||
"p": {group: 0, binding: 0},
|
||||
"positions": {group: 0, binding: 1},
|
||||
"constraints": {group: 0, binding: 2}
|
||||
}
|
||||
});
|
||||
cs.setStorageBuffer("p", buffers.params);
|
||||
cs.setStorageBuffer("positions", buffers.positions);
|
||||
cs.setStorageBuffer("constraints", constraintBuffer);
|
||||
return cs;
|
||||
};
|
||||
|
||||
return {
|
||||
integrate: createBasicShader("integrate", CLOTH_INTEGRATE_COMPUTE_WGSL),
|
||||
solvers: buffers.constraints.map((cBuffer, i) => createSolverShader(`solve${i}`, cBuffer)),
|
||||
velocity: createBasicShader("velocity", CLOTH_VELOCITY_COMPUTE_WGSL)
|
||||
};
|
||||
}
|
||||
|
||||
private setupRenderMesh(scene: Scene, config: ClothConfig, positionsBuffer: StorageBuffer): void {
|
||||
if (this.clothMesh) {
|
||||
scene.removeMesh(this.clothMesh);
|
||||
}
|
||||
|
||||
this.clothMesh = MeshBuilder.CreateGround("cloth", {width: 10, height: 10, subdivisions: config.gridWidth - 1}, scene);
|
||||
|
||||
const clothMaterial = new ShaderMaterial("clothMat", scene, {
|
||||
vertexSource: CLOTH_VERTEX_SHADER_WGSL,
|
||||
fragmentSource: CLOTH_FRAGMENT_SHADER_WGSL
|
||||
}, {
|
||||
attributes: ["position", "uv"],
|
||||
uniforms: ["viewProjection"],
|
||||
storageBuffers: ["positions"],
|
||||
shaderLanguage: ShaderLanguage.WGSL
|
||||
});
|
||||
|
||||
clothMaterial.backFaceCulling = false;
|
||||
clothMaterial.setStorageBuffer("positions", positionsBuffer);
|
||||
this.clothMesh.material = clothMaterial;
|
||||
|
||||
const camera = scene.activeCamera as ArcRotateCamera;
|
||||
if (camera) {
|
||||
camera.alpha = Math.PI / 4;
|
||||
camera.beta = Math.PI / 2.5;
|
||||
camera.radius = 15;
|
||||
}
|
||||
}
|
||||
|
||||
private setupDispatchSizes(config: ClothConfig, buffers: GpuBuffers): void {
|
||||
const constraintsLength = buffers.constraints.map(b => (b as any)._buffer.capacity / 4 / 4);
|
||||
this.dispatchXConstraints = constraintsLength.map(len => Math.ceil(len / 64));
|
||||
this.dispatchXVertices = Math.ceil(config.numVertices / 64);
|
||||
}
|
||||
|
||||
private startRenderLoop(scene: Scene): void {
|
||||
const substeps = 15;
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.buffers || !this.pipelines) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buffers.params.update(this.paramsData);
|
||||
|
||||
this.pipelines.integrate.dispatch(this.dispatchXVertices, 1, 1);
|
||||
|
||||
for (let i = 0; i < substeps; i++) {
|
||||
for (let phase = 0; phase < this.pipelines.solvers.length; phase++) {
|
||||
this.pipelines.solvers[phase].dispatch(this.dispatchXConstraints[phase], 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.pipelines.velocity.dispatch(this.dispatchXVertices, 1, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {Engine, GroundMesh, Scene, WebGPUEngine} from '@babylonjs/core';
|
||||
import {ClothConfig} from '../cloth.model';
|
||||
|
||||
export interface ClothSimulationParams {
|
||||
stiffness: number;
|
||||
elongation: number;
|
||||
isWindActive: boolean;
|
||||
simulationTime: number;
|
||||
deltaTime: number;
|
||||
}
|
||||
|
||||
export interface ClothSimulationStrategy {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine, config: ClothConfig): void;
|
||||
updateParams(params: ClothSimulationParams): void;
|
||||
getMesh(): GroundMesh | null;
|
||||
dispose(): void;
|
||||
}
|
||||
134
src/app/pages/algorithms/pendulum/pendulum-glsl.shader.ts
Normal file
134
src/app/pages/algorithms/pendulum/pendulum-glsl.shader.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* GLSL shaders for pendulum rendering on WebGL.
|
||||
* Replicates the visual output of the WGSL pendulum compute+fragment shaders.
|
||||
* Uses a feedback texture (ping-pong) for trail persistence.
|
||||
*
|
||||
* Coordinate note: WGSL pixel buffer has Y=0 at top (screen space).
|
||||
* GLSL UVs have Y=0 at bottom. We flip Y via (1.0 - uv.y) to match.
|
||||
*/
|
||||
|
||||
export const PENDULUM_VERTEX_SHADER_GLSL = `
|
||||
precision highp float;
|
||||
|
||||
attribute vec3 position;
|
||||
attribute vec2 uv;
|
||||
|
||||
varying vec2 vUV;
|
||||
|
||||
void main() {
|
||||
vUV = uv;
|
||||
gl_Position = vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Shader for the feedback pass: renders pendulum state to a render target texture.
|
||||
* The R channel stores trail1 intensity, G stores trail2 intensity.
|
||||
*/
|
||||
export const PENDULUM_FEEDBACK_FRAGMENT_GLSL = `
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 resolution;
|
||||
uniform float theta1;
|
||||
uniform float theta2;
|
||||
uniform float l1;
|
||||
uniform float l2;
|
||||
uniform float trailDecay;
|
||||
uniform sampler2D previousFrame;
|
||||
|
||||
varying vec2 vUV;
|
||||
|
||||
void main() {
|
||||
// Flip Y to match WGSL screen-space (Y=0 at top)
|
||||
vec2 uv = vec2(vUV.x, 1.0 - vUV.y);
|
||||
float aspect = resolution.x / resolution.y;
|
||||
vec2 uvCorrected = vec2(uv.x * aspect, uv.y);
|
||||
|
||||
vec4 prev = texture2D(previousFrame, vUV);
|
||||
float trail1 = prev.r * trailDecay;
|
||||
float trail2 = prev.g * trailDecay;
|
||||
|
||||
vec2 origin = vec2(0.5 * aspect, 0.3);
|
||||
float displayScale = 0.15;
|
||||
|
||||
vec2 p1 = origin + vec2(sin(theta1), cos(theta1)) * l1 * displayScale;
|
||||
vec2 p2 = p1 + vec2(sin(theta2), cos(theta2)) * l2 * displayScale;
|
||||
|
||||
float dMass1 = length(uvCorrected - p1);
|
||||
float dMass2 = length(uvCorrected - p2);
|
||||
|
||||
if (dMass1 < 0.02) {
|
||||
trail1 = 1.0;
|
||||
}
|
||||
if (dMass2 < 0.02) {
|
||||
trail2 = 1.0;
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(trail1, trail2, 0.0, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Display shader: reads trail data from feedback texture and renders final colors.
|
||||
*/
|
||||
export const PENDULUM_DISPLAY_FRAGMENT_GLSL = `
|
||||
precision highp float;
|
||||
|
||||
uniform vec2 resolution;
|
||||
uniform float theta1;
|
||||
uniform float theta2;
|
||||
uniform float l1;
|
||||
uniform float l2;
|
||||
uniform sampler2D trailTexture;
|
||||
|
||||
varying vec2 vUV;
|
||||
|
||||
float sdSegment(vec2 p, vec2 a, vec2 b) {
|
||||
vec2 pa = p - a;
|
||||
vec2 ba = b - a;
|
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
return length(pa - ba * h);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Flip Y to match WGSL screen-space (Y=0 at top)
|
||||
vec2 uv = vec2(vUV.x, 1.0 - vUV.y);
|
||||
float aspect = resolution.x / resolution.y;
|
||||
vec2 uvCorrected = vec2(uv.x * aspect, uv.y);
|
||||
|
||||
vec4 trailData = texture2D(trailTexture, vUV);
|
||||
float trail1 = trailData.r;
|
||||
float trail2 = trailData.g;
|
||||
|
||||
vec2 origin = vec2(0.5 * aspect, 0.3);
|
||||
float displayScale = 0.15;
|
||||
|
||||
vec2 p1 = origin + vec2(sin(theta1), cos(theta1)) * l1 * displayScale;
|
||||
vec2 p2 = p1 + vec2(sin(theta2), cos(theta2)) * l2 * displayScale;
|
||||
|
||||
float dLine1 = sdSegment(uvCorrected, origin, p1);
|
||||
float dLine2 = sdSegment(uvCorrected, p1, p2);
|
||||
float dMass1 = length(uvCorrected - p1);
|
||||
float dMass2 = length(uvCorrected - p2);
|
||||
|
||||
vec3 bgColor = vec3(0.1, 0.1, 0.15);
|
||||
vec3 mass1Color = vec3(1.0, 0.0, 0.0);
|
||||
vec3 mass2Color = vec3(0.0, 1.0, 0.0);
|
||||
vec3 line1Color = vec3(1.0, 1.0, 0.0);
|
||||
vec3 line2Color = vec3(1.0, 0.0, 1.0);
|
||||
|
||||
vec3 finalColor = bgColor;
|
||||
finalColor = mix(finalColor, mass1Color, clamp(trail1, 0.0, 1.0));
|
||||
finalColor = mix(finalColor, mass2Color, clamp(trail2, 0.0, 1.0));
|
||||
|
||||
if (dMass1 >= 0.02 && dMass2 >= 0.02) {
|
||||
if (dLine1 < 0.003) {
|
||||
finalColor = line1Color;
|
||||
} else if (dLine2 < 0.003) {
|
||||
finalColor = line2Color;
|
||||
}
|
||||
}
|
||||
|
||||
gl_FragColor = vec4(finalColor, 1.0);
|
||||
}
|
||||
`;
|
||||
@@ -1,8 +1,7 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {BabylonCanvas, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {ComputeShader, ShaderLanguage, StorageBuffer} from '@babylonjs/core';
|
||||
import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PENDULUM_RENDER_COMPUTE_SHADER_WGSL, PENDULUM_VERTEX_SHADER_WGSL} from './pendulum.shader';
|
||||
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||
import {DEFAULT_DAMPING, DEFAULT_G, DEFAULT_L1_LENGTH, DEFAULT_M1_MASS, DEFAULT_L2_LENGTH, DEFAULT_M2_MASS, DEFAULT_TRAIL_DECAY, MAX_DAMPING, MAX_G, MAX_LENGTH, MAX_MASS, MAX_TRAIL_DECAY, MIN_DAMPING, MIN_G, MIN_LENGTH, MIN_MASS, MIN_TRAIL_DECAY, IMPULSE_M2, IMPULSE_M1} from './pendulum.model';
|
||||
@@ -11,6 +10,9 @@ import {MatButton} from '@angular/material/button';
|
||||
import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {PendulumSimulationStrategy} from './strategies/pendulum-simulation.strategy';
|
||||
import {PendulumGpuStrategy} from './strategies/pendulum-gpu.strategy';
|
||||
import {PendulumCpuStrategy} from './strategies/pendulum-cpu.strategy';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pendulum',
|
||||
@@ -31,7 +33,6 @@ import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
})
|
||||
class PendulumComponent {
|
||||
|
||||
// --- CONFIGURATION ---
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'PENDULUM.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
@@ -46,15 +47,9 @@ class PendulumComponent {
|
||||
disclaimerListEntry: ['PENDULUM.EXPLANATION.DISCLAIMER_1', 'PENDULUM.EXPLANATION.DISCLAIMER_2', 'PENDULUM.EXPLANATION.DISCLAIMER_3', 'PENDULUM.EXPLANATION.DISCLAIMER_4']
|
||||
};
|
||||
|
||||
|
||||
renderConfig: RenderConfig = {
|
||||
mode: '2D',
|
||||
initialViewSize: 2,
|
||||
shaderLanguage: ShaderLanguage.WGSL,
|
||||
vertexShader: PENDULUM_VERTEX_SHADER_WGSL,
|
||||
fragmentShader: PENDULUM_FRAGMENT_SHADER_WGSL,
|
||||
uniformNames: [],
|
||||
uniformBufferNames: []
|
||||
initialViewSize: 2
|
||||
};
|
||||
|
||||
trailDecayOptions: Options = {
|
||||
@@ -107,7 +102,6 @@ class PendulumComponent {
|
||||
hidePointerLabels: false
|
||||
};
|
||||
|
||||
// Central management of physics parameters
|
||||
readonly simParams = {
|
||||
time: 0,
|
||||
dt: 0.015,
|
||||
@@ -123,6 +117,7 @@ class PendulumComponent {
|
||||
};
|
||||
|
||||
private currentSceneData: SceneEventData | null = null;
|
||||
private strategy: PendulumSimulationStrategy | null = null;
|
||||
|
||||
onSceneReady(event: SceneEventData) {
|
||||
this.currentSceneData = event;
|
||||
@@ -130,83 +125,34 @@ class PendulumComponent {
|
||||
}
|
||||
|
||||
private createSimulation() {
|
||||
if (!this.currentSceneData){
|
||||
if (!this.currentSceneData) {
|
||||
return;
|
||||
}
|
||||
const {engine, scene} = this.currentSceneData;
|
||||
engine.resize();
|
||||
|
||||
const width = engine.getRenderWidth();
|
||||
const height = engine.getRenderHeight();
|
||||
const totalPixels = width * height;
|
||||
const {engine, scene, gpuTier} = this.currentSceneData;
|
||||
|
||||
// --- 1. BUFFERS ---
|
||||
const pixelBuffer = new StorageBuffer(engine, totalPixels * 4);
|
||||
|
||||
const stateBuffer = new StorageBuffer(engine, 4 * 4);
|
||||
stateBuffer.update(new Float32Array([Math.PI / 4, Math.PI / 2, 0, 0])); // Initial angles
|
||||
|
||||
const paramsBuffer = new StorageBuffer(engine, 14 * 4);
|
||||
const paramsData = new Float32Array(14);
|
||||
|
||||
// --- 2. SHADERS ---
|
||||
const csPhysics = new ComputeShader("physics", engine,
|
||||
{computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL},
|
||||
{bindingsMapping: {"state": {group: 0, binding: 0}, "p": {group: 0, binding: 1}}}
|
||||
);
|
||||
csPhysics.setStorageBuffer("state", stateBuffer);
|
||||
csPhysics.setStorageBuffer("p", paramsBuffer);
|
||||
|
||||
const csRender = new ComputeShader("render", engine,
|
||||
{computeSource: PENDULUM_RENDER_COMPUTE_SHADER_WGSL},
|
||||
{bindingsMapping: {"pixelBuffer": {group: 0, binding: 0}, "p": {group: 0, binding: 1}, "state": {group: 0, binding: 2}}}
|
||||
);
|
||||
csRender.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||
csRender.setStorageBuffer("p", paramsBuffer);
|
||||
csRender.setStorageBuffer("state", stateBuffer);
|
||||
|
||||
// --- 3. MATERIAL ---
|
||||
const plane = scene.getMeshByName("plane");
|
||||
if (plane?.material) {
|
||||
const mat = plane.material as any;
|
||||
mat.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||
mat.setStorageBuffer("p", paramsBuffer);
|
||||
if (this.strategy) {
|
||||
this.strategy.dispose();
|
||||
}
|
||||
|
||||
//remove old observables if available
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
// --- 4. RENDER LOOP ---
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
this.strategy = gpuTier === 'webgpu'
|
||||
? new PendulumGpuStrategy()
|
||||
: new PendulumCpuStrategy();
|
||||
|
||||
this.strategy.init(scene, engine);
|
||||
this.startParamUpdateLoop(scene);
|
||||
}
|
||||
|
||||
private startParamUpdateLoop(scene: any): void {
|
||||
scene.onAfterRenderObservable.clear();
|
||||
scene.onAfterRenderObservable.add(() => {
|
||||
this.simParams.time += this.simParams.dt;
|
||||
|
||||
const currentWidth = engine.getRenderWidth();
|
||||
const currentHeight = engine.getRenderHeight();
|
||||
|
||||
// Fill parameter array (must match the exact order of the WGSL struct!)
|
||||
paramsData[0] = currentWidth;
|
||||
paramsData[1] = currentHeight;
|
||||
paramsData[2] = this.simParams.time;
|
||||
paramsData[3] = this.simParams.dt;
|
||||
paramsData[4] = this.simParams.g;
|
||||
paramsData[5] = this.simParams.m1;
|
||||
paramsData[6] = this.simParams.m2;
|
||||
paramsData[7] = this.simParams.l1;
|
||||
paramsData[8] = this.simParams.l2;
|
||||
paramsData[9] = this.simParams.damping;
|
||||
paramsData[10] = this.simParams.trailDecay;
|
||||
paramsData[11] = this.simParams.impulseM1;
|
||||
paramsData[12] = this.simParams.impulseM2;
|
||||
paramsData[13] = 0; // Pad
|
||||
if (this.strategy) {
|
||||
this.strategy.updateParams({...this.simParams});
|
||||
}
|
||||
|
||||
this.resetImpulses();
|
||||
|
||||
paramsBuffer.update(paramsData);
|
||||
|
||||
// Trigger simulation and rendering
|
||||
csPhysics.dispatch(1, 1, 1);
|
||||
|
||||
const dispatchCount = Math.ceil((currentWidth * currentHeight) / 64);
|
||||
csRender.dispatch(dispatchCount, 1, 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -221,8 +167,7 @@ class PendulumComponent {
|
||||
}
|
||||
|
||||
pushPendulum(m1: boolean) {
|
||||
if (m1)
|
||||
{
|
||||
if (m1) {
|
||||
this.simParams.impulseM1 = IMPULSE_M1;
|
||||
return;
|
||||
}
|
||||
@@ -235,6 +180,4 @@ class PendulumComponent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default PendulumComponent
|
||||
export default PendulumComponent;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* CPU-side double pendulum physics mirroring PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL.
|
||||
* Equations from: https://en.wikipedia.org/wiki/Double_pendulum
|
||||
*/
|
||||
|
||||
export interface PendulumState {
|
||||
theta1: number;
|
||||
theta2: number;
|
||||
v1: number;
|
||||
v2: number;
|
||||
}
|
||||
|
||||
export interface PendulumPhysicsParams {
|
||||
dt: number;
|
||||
g: number;
|
||||
m1: number;
|
||||
m2: number;
|
||||
l1: number;
|
||||
l2: number;
|
||||
damping: number;
|
||||
impulseM1: number;
|
||||
impulseM2: number;
|
||||
}
|
||||
|
||||
export function stepPendulumPhysics(state: PendulumState, params: PendulumPhysicsParams): void {
|
||||
const t1 = state.theta1;
|
||||
const t2 = state.theta2;
|
||||
const v1 = state.v1;
|
||||
const v2 = state.v2;
|
||||
|
||||
const deltaT = t1 - t2;
|
||||
|
||||
const num1 = -params.g * (2.0 * params.m1 + params.m2) * Math.sin(t1)
|
||||
- params.m2 * params.g * Math.sin(t1 - 2.0 * t2)
|
||||
- 2.0 * Math.sin(deltaT) * params.m2 * (v2 * v2 * params.l2 + v1 * v1 * params.l1 * Math.cos(deltaT));
|
||||
const den1 = params.l1 * (2.0 * params.m1 + params.m2 - params.m2 * Math.cos(2.0 * deltaT));
|
||||
const a1 = num1 / den1;
|
||||
|
||||
const num2 = 2.0 * Math.sin(deltaT) * (v1 * v1 * params.l1 * (params.m1 + params.m2) + params.g * (params.m1 + params.m2) * Math.cos(t1) + v2 * v2 * params.l2 * params.m2 * Math.cos(deltaT));
|
||||
const den2 = params.l2 * (2.0 * params.m1 + params.m2 - params.m2 * Math.cos(2.0 * deltaT));
|
||||
const a2 = num2 / den2;
|
||||
|
||||
const newV1 = (v1 + a1 * params.dt) * params.damping + params.impulseM1;
|
||||
const newV2 = (v2 + a2 * params.dt) * params.damping + params.impulseM2;
|
||||
|
||||
state.v1 = newV1;
|
||||
state.v2 = newV2;
|
||||
state.theta1 = t1 + newV1 * params.dt;
|
||||
state.theta2 = t2 + newV2 * params.dt;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import {Constants, Engine, MeshBuilder, RenderTargetTexture, Scene, ShaderMaterial, Texture, Vector2, WebGPUEngine} from '@babylonjs/core';
|
||||
import {PENDULUM_DISPLAY_FRAGMENT_GLSL, PENDULUM_FEEDBACK_FRAGMENT_GLSL, PENDULUM_VERTEX_SHADER_GLSL} from '../pendulum-glsl.shader';
|
||||
import {PendulumState, stepPendulumPhysics} from './pendulum-cpu-physics';
|
||||
import {PendulumSimParams, PendulumSimulationStrategy} from './pendulum-simulation.strategy';
|
||||
|
||||
export class PendulumCpuStrategy implements PendulumSimulationStrategy {
|
||||
private scene: Scene | null = null;
|
||||
private state: PendulumState = {theta1: Math.PI / 4, theta2: Math.PI / 2, v1: 0, v2: 0};
|
||||
private params: PendulumSimParams | null = null;
|
||||
private feedbackMaterial: ShaderMaterial | null = null;
|
||||
private displayMaterial: ShaderMaterial | null = null;
|
||||
private rttA: RenderTargetTexture | null = null;
|
||||
private rttB: RenderTargetTexture | null = null;
|
||||
private pingPong = false;
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine): void {
|
||||
this.scene = scene;
|
||||
|
||||
const existingPlane = scene.getMeshByName("plane");
|
||||
if (existingPlane) {
|
||||
scene.removeMesh(existingPlane);
|
||||
}
|
||||
|
||||
const width = engine.getRenderWidth();
|
||||
const height = engine.getRenderHeight();
|
||||
|
||||
this.rttA = new RenderTargetTexture("rttA", {width, height}, scene, false, true, Constants.TEXTURETYPE_FLOAT);
|
||||
this.rttB = new RenderTargetTexture("rttB", {width, height}, scene, false, true, Constants.TEXTURETYPE_FLOAT);
|
||||
|
||||
this.rttA.wrapU = Texture.CLAMP_ADDRESSMODE;
|
||||
this.rttA.wrapV = Texture.CLAMP_ADDRESSMODE;
|
||||
this.rttB.wrapU = Texture.CLAMP_ADDRESSMODE;
|
||||
this.rttB.wrapV = Texture.CLAMP_ADDRESSMODE;
|
||||
|
||||
// Size 2 maps positions to -1..1 which fills clip space (vertex shader bypasses camera)
|
||||
const feedbackPlane = MeshBuilder.CreatePlane("feedbackPlane", {size: 2}, scene);
|
||||
feedbackPlane.alwaysSelectAsActiveMesh = true;
|
||||
// Use layer mask to exclude from main scene rendering (only rendered via RTT)
|
||||
feedbackPlane.layerMask = 0x10000000;
|
||||
|
||||
this.feedbackMaterial = new ShaderMaterial("feedbackMat", scene, {
|
||||
vertexSource: PENDULUM_VERTEX_SHADER_GLSL,
|
||||
fragmentSource: PENDULUM_FEEDBACK_FRAGMENT_GLSL
|
||||
}, {
|
||||
attributes: ["position", "uv"],
|
||||
uniforms: ["resolution", "theta1", "theta2", "l1", "l2", "trailDecay"],
|
||||
samplers: ["previousFrame"]
|
||||
});
|
||||
this.feedbackMaterial.backFaceCulling = false;
|
||||
feedbackPlane.material = this.feedbackMaterial;
|
||||
|
||||
this.rttA.renderList!.push(feedbackPlane);
|
||||
this.rttB.renderList!.push(feedbackPlane);
|
||||
|
||||
// Size 2 maps positions to -1..1 which fills clip space (vertex shader bypasses camera)
|
||||
const displayPlane = MeshBuilder.CreatePlane("displayPlane", {size: 2}, scene);
|
||||
displayPlane.alwaysSelectAsActiveMesh = true;
|
||||
|
||||
this.displayMaterial = new ShaderMaterial("displayMat", scene, {
|
||||
vertexSource: PENDULUM_VERTEX_SHADER_GLSL,
|
||||
fragmentSource: PENDULUM_DISPLAY_FRAGMENT_GLSL
|
||||
}, {
|
||||
attributes: ["position", "uv"],
|
||||
uniforms: ["resolution", "theta1", "theta2", "l1", "l2"],
|
||||
samplers: ["trailTexture"]
|
||||
});
|
||||
this.displayMaterial.backFaceCulling = false;
|
||||
this.displayMaterial.disableDepthWrite = true;
|
||||
displayPlane.material = this.displayMaterial;
|
||||
|
||||
// RTTs are rendered manually via writeTarget.render() -- do NOT add to customRenderTargets
|
||||
this.startRenderLoop(scene, engine, width, height);
|
||||
}
|
||||
|
||||
updateParams(params: PendulumSimParams): void {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene) {
|
||||
this.scene.onBeforeRenderObservable.clear();
|
||||
}
|
||||
this.rttA?.dispose();
|
||||
this.rttB?.dispose();
|
||||
this.feedbackMaterial?.dispose();
|
||||
this.displayMaterial?.dispose();
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
private startRenderLoop(scene: Scene, engine: WebGPUEngine | Engine, width: number, height: number): void {
|
||||
const resolution = new Vector2(width, height);
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.params || !this.feedbackMaterial || !this.displayMaterial || !this.rttA || !this.rttB) {
|
||||
return;
|
||||
}
|
||||
|
||||
stepPendulumPhysics(this.state, {
|
||||
dt: this.params.dt,
|
||||
g: this.params.g,
|
||||
m1: this.params.m1,
|
||||
m2: this.params.m2,
|
||||
l1: this.params.l1,
|
||||
l2: this.params.l2,
|
||||
damping: this.params.damping,
|
||||
impulseM1: this.params.impulseM1,
|
||||
impulseM2: this.params.impulseM2
|
||||
});
|
||||
|
||||
const readTarget = this.pingPong ? this.rttA : this.rttB;
|
||||
const writeTarget = this.pingPong ? this.rttB : this.rttA;
|
||||
|
||||
this.feedbackMaterial.setTexture("previousFrame", readTarget);
|
||||
this.feedbackMaterial.setVector2("resolution", resolution);
|
||||
this.feedbackMaterial.setFloat("theta1", this.state.theta1);
|
||||
this.feedbackMaterial.setFloat("theta2", this.state.theta2);
|
||||
this.feedbackMaterial.setFloat("l1", this.params.l1);
|
||||
this.feedbackMaterial.setFloat("l2", this.params.l2);
|
||||
this.feedbackMaterial.setFloat("trailDecay", this.params.trailDecay);
|
||||
|
||||
writeTarget.render();
|
||||
|
||||
this.displayMaterial.setTexture("trailTexture", writeTarget);
|
||||
this.displayMaterial.setVector2("resolution", resolution);
|
||||
this.displayMaterial.setFloat("theta1", this.state.theta1);
|
||||
this.displayMaterial.setFloat("theta2", this.state.theta2);
|
||||
this.displayMaterial.setFloat("l1", this.params.l1);
|
||||
this.displayMaterial.setFloat("l2", this.params.l2);
|
||||
|
||||
this.pingPong = !this.pingPong;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import {Camera, ComputeShader, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, StorageBuffer, WebGPUEngine} from '@babylonjs/core';
|
||||
import {PENDULUM_FRAGMENT_SHADER_WGSL, PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL, PENDULUM_RENDER_COMPUTE_SHADER_WGSL, PENDULUM_VERTEX_SHADER_WGSL} from '../pendulum.shader';
|
||||
import {PendulumSimParams, PendulumSimulationStrategy} from './pendulum-simulation.strategy';
|
||||
|
||||
export class PendulumGpuStrategy implements PendulumSimulationStrategy {
|
||||
private scene: Scene | null = null;
|
||||
private paramsData = new Float32Array(14);
|
||||
private paramsBuffer: StorageBuffer | null = null;
|
||||
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine): void {
|
||||
this.scene = scene;
|
||||
const gpuEngine = engine as WebGPUEngine;
|
||||
gpuEngine.resize();
|
||||
|
||||
const width = gpuEngine.getRenderWidth();
|
||||
const height = gpuEngine.getRenderHeight();
|
||||
const totalPixels = width * height;
|
||||
|
||||
const pixelBuffer = new StorageBuffer(gpuEngine, totalPixels * 4);
|
||||
|
||||
const stateBuffer = new StorageBuffer(gpuEngine, 4 * 4);
|
||||
stateBuffer.update(new Float32Array([Math.PI / 4, Math.PI / 2, 0, 0]));
|
||||
|
||||
this.paramsBuffer = new StorageBuffer(gpuEngine, 14 * 4);
|
||||
|
||||
const csPhysics = new ComputeShader("physics", gpuEngine,
|
||||
{computeSource: PENDULUM_PHYSIC_COMPUTE_SHADER_WGSL},
|
||||
{bindingsMapping: {"state": {group: 0, binding: 0}, "p": {group: 0, binding: 1}}}
|
||||
);
|
||||
csPhysics.setStorageBuffer("state", stateBuffer);
|
||||
csPhysics.setStorageBuffer("p", this.paramsBuffer);
|
||||
|
||||
const csRender = new ComputeShader("render", gpuEngine,
|
||||
{computeSource: PENDULUM_RENDER_COMPUTE_SHADER_WGSL},
|
||||
{bindingsMapping: {"pixelBuffer": {group: 0, binding: 0}, "p": {group: 0, binding: 1}, "state": {group: 0, binding: 2}}}
|
||||
);
|
||||
csRender.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||
csRender.setStorageBuffer("p", this.paramsBuffer);
|
||||
csRender.setStorageBuffer("state", stateBuffer);
|
||||
|
||||
// Create WGSL display plane and material
|
||||
const displayMaterial = new ShaderMaterial("pendulumWgslMat", scene, {
|
||||
vertexSource: PENDULUM_VERTEX_SHADER_WGSL,
|
||||
fragmentSource: PENDULUM_FRAGMENT_SHADER_WGSL
|
||||
}, {
|
||||
attributes: ["position"],
|
||||
uniforms: [],
|
||||
storageBuffers: ["pixelBuffer", "p"],
|
||||
shaderLanguage: ShaderLanguage.WGSL
|
||||
});
|
||||
displayMaterial.setStorageBuffer("pixelBuffer", pixelBuffer);
|
||||
displayMaterial.setStorageBuffer("p", this.paramsBuffer);
|
||||
displayMaterial.backFaceCulling = false;
|
||||
displayMaterial.disableDepthWrite = true;
|
||||
|
||||
const plane = MeshBuilder.CreatePlane("pendulumPlane", {size: 100}, scene);
|
||||
const camera = scene.activeCamera as Camera;
|
||||
if (camera) {
|
||||
plane.lookAt(camera.position);
|
||||
}
|
||||
plane.alwaysSelectAsActiveMesh = true;
|
||||
plane.material = displayMaterial;
|
||||
|
||||
scene.onBeforeRenderObservable.clear();
|
||||
scene.onBeforeRenderObservable.add(() => {
|
||||
if (!this.paramsBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWidth = gpuEngine.getRenderWidth();
|
||||
const currentHeight = gpuEngine.getRenderHeight();
|
||||
this.paramsData[0] = currentWidth;
|
||||
this.paramsData[1] = currentHeight;
|
||||
|
||||
this.paramsBuffer.update(this.paramsData);
|
||||
|
||||
csPhysics.dispatch(1, 1, 1);
|
||||
|
||||
const dispatchCount = Math.ceil((currentWidth * currentHeight) / 64);
|
||||
csRender.dispatch(dispatchCount, 1, 1);
|
||||
});
|
||||
}
|
||||
|
||||
updateParams(params: PendulumSimParams): void {
|
||||
// paramsData[0] (width) and paramsData[1] (height) are set in the render loop
|
||||
this.paramsData[2] = params.time;
|
||||
this.paramsData[3] = params.dt;
|
||||
this.paramsData[4] = params.g;
|
||||
this.paramsData[5] = params.m1;
|
||||
this.paramsData[6] = params.m2;
|
||||
this.paramsData[7] = params.l1;
|
||||
this.paramsData[8] = params.l2;
|
||||
this.paramsData[9] = params.damping;
|
||||
this.paramsData[10] = params.trailDecay;
|
||||
this.paramsData[11] = params.impulseM1;
|
||||
this.paramsData[12] = params.impulseM2;
|
||||
this.paramsData[13] = 0;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.scene) {
|
||||
this.scene.onBeforeRenderObservable.clear();
|
||||
const plane = this.scene.getMeshByName("pendulumPlane");
|
||||
if (plane) {
|
||||
this.scene.removeMesh(plane);
|
||||
plane.dispose();
|
||||
}
|
||||
}
|
||||
this.paramsBuffer = null;
|
||||
this.scene = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {Engine, Scene, WebGPUEngine} from '@babylonjs/core';
|
||||
|
||||
export interface PendulumSimParams {
|
||||
time: number;
|
||||
dt: number;
|
||||
g: number;
|
||||
m1: number;
|
||||
m2: number;
|
||||
l1: number;
|
||||
l2: number;
|
||||
damping: number;
|
||||
trailDecay: number;
|
||||
impulseM1: number;
|
||||
impulseM2: number;
|
||||
}
|
||||
|
||||
export interface PendulumSimulationStrategy {
|
||||
init(scene: Scene, engine: WebGPUEngine | Engine): void;
|
||||
updateParams(params: PendulumSimParams): void;
|
||||
dispose(): void;
|
||||
}
|
||||
55
src/app/service/gpu-capability.service.ts
Normal file
55
src/app/service/gpu-capability.service.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {Injectable, signal} from '@angular/core';
|
||||
|
||||
export type GpuTier = 'webgpu' | 'webgl' | 'none';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class GpuCapabilityService {
|
||||
private cachedTier: GpuTier | null = null;
|
||||
readonly tier = signal<GpuTier | null>(null);
|
||||
|
||||
async detect(): Promise<GpuTier> {
|
||||
if (this.cachedTier) {
|
||||
return this.cachedTier;
|
||||
}
|
||||
|
||||
const result = await this.probe();
|
||||
this.cachedTier = result;
|
||||
this.tier.set(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async probe(): Promise<GpuTier> {
|
||||
if (await this.isWebGpuAvailable()) {
|
||||
return 'webgpu';
|
||||
}
|
||||
|
||||
if (this.isWebGlAvailable()) {
|
||||
return 'webgl';
|
||||
}
|
||||
|
||||
return 'none';
|
||||
}
|
||||
|
||||
private async isWebGpuAvailable(): Promise<boolean> {
|
||||
if (!navigator.gpu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const adapter = await navigator.gpu.requestAdapter();
|
||||
return adapter !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private isWebGlAvailable(): boolean {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('webgl2');
|
||||
return context !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
|
||||
import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ArcRotateCamera, Camera, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
||||
import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
|
||||
import {GpuCapabilityService, GpuTier} from '../../../service/gpu-capability.service';
|
||||
|
||||
export interface RenderConfig {
|
||||
mode: '2D' | '3D';
|
||||
@@ -17,7 +18,8 @@ export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas:
|
||||
|
||||
export interface SceneEventData {
|
||||
scene: Scene;
|
||||
engine: WebGPUEngine;
|
||||
engine: WebGPUEngine | Engine;
|
||||
gpuTier: GpuTier;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -30,6 +32,7 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
readonly ngZone = inject(NgZone);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly gpuCapability = inject(GpuCapabilityService);
|
||||
|
||||
@ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@@ -38,11 +41,13 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
|
||||
@Output() sceneReady = new EventEmitter<SceneEventData>();
|
||||
@Output() sceneResized = new EventEmitter<SceneEventData>();
|
||||
@Output() engineUnavailable = new EventEmitter<{ reason: string }>();
|
||||
|
||||
private engine!: WebGPUEngine;
|
||||
private engine!: WebGPUEngine | Engine;
|
||||
private scene!: Scene;
|
||||
private shaderMaterial!: ShaderMaterial;
|
||||
private camera!: Camera;
|
||||
private gpuTier: GpuTier = 'none';
|
||||
|
||||
//Listener
|
||||
private readonly resizeHandler = () => this.handleResize();
|
||||
@@ -63,26 +68,60 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
|
||||
private async initBabylon(): Promise<void> {
|
||||
const canvas = this.canvasRef.nativeElement;
|
||||
const tmpEngine = new WebGPUEngine(canvas);
|
||||
await tmpEngine.initAsync().then(() => {
|
||||
this.engine = tmpEngine;
|
||||
this.scene = new Scene(this.engine);
|
||||
this.setupCamera(canvas);
|
||||
this.addListener(canvas);
|
||||
this.createShaderMaterial();
|
||||
this.createFullScreenRect();
|
||||
this.sceneReady.emit({
|
||||
scene: this.scene,
|
||||
engine: this.engine
|
||||
});
|
||||
this.addRenderLoop(canvas);
|
||||
const tier = await this.gpuCapability.detect();
|
||||
this.gpuTier = tier;
|
||||
|
||||
})
|
||||
.catch(() => {
|
||||
const message = this.translate.instant('WEBGPU.NOT_SUPPORTED');
|
||||
this.snackBar.open(message, 'OK', { duration: 8000, horizontalPosition: "center", verticalPosition: "top" });
|
||||
this.engine = null!;
|
||||
});
|
||||
if (tier === 'webgpu') {
|
||||
await this.initWebGpuEngine(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tier === 'webgl') {
|
||||
this.showSnackBar('GPU.WEBGL_FALLBACK');
|
||||
this.initWebGlEngine(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
this.showSnackBar('GPU.NOT_SUPPORTED');
|
||||
this.engineUnavailable.emit({reason: 'no_gpu'});
|
||||
}
|
||||
|
||||
private async initWebGpuEngine(canvas: HTMLCanvasElement): Promise<void> {
|
||||
const tmpEngine = new WebGPUEngine(canvas);
|
||||
|
||||
try {
|
||||
await tmpEngine.initAsync();
|
||||
this.engine = tmpEngine;
|
||||
this.setupScene(canvas);
|
||||
} catch {
|
||||
this.showSnackBar('GPU.WEBGL_FALLBACK');
|
||||
this.gpuTier = 'webgl';
|
||||
this.initWebGlEngine(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
private initWebGlEngine(canvas: HTMLCanvasElement): void {
|
||||
this.engine = new Engine(canvas, true);
|
||||
this.setupScene(canvas);
|
||||
}
|
||||
|
||||
private setupScene(canvas: HTMLCanvasElement): void {
|
||||
this.scene = new Scene(this.engine);
|
||||
this.setupCamera(canvas);
|
||||
this.addListener(canvas);
|
||||
this.createShaderMaterial();
|
||||
this.createFullScreenRect();
|
||||
this.sceneReady.emit({
|
||||
scene: this.scene,
|
||||
engine: this.engine,
|
||||
gpuTier: this.gpuTier
|
||||
});
|
||||
this.addRenderLoop(canvas);
|
||||
}
|
||||
|
||||
private showSnackBar(translationKey: string): void {
|
||||
const message = this.translate.instant(translationKey);
|
||||
this.snackBar.open(message, 'OK', {duration: 8000, horizontalPosition: 'center', verticalPosition: 'top'});
|
||||
}
|
||||
|
||||
private addListener(canvas: HTMLCanvasElement) {
|
||||
@@ -198,7 +237,8 @@ export class BabylonCanvas implements AfterViewInit, OnDestroy {
|
||||
|
||||
this.sceneResized?.emit({
|
||||
scene: this.scene,
|
||||
engine: this.engine
|
||||
engine: this.engine,
|
||||
gpuTier: this.gpuTier
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,8 +513,9 @@
|
||||
"DISCLAIMER_4": "Der XPBD-Kompromiss: XPBD umgeht dieses komplexe Matrix-Problem völlig, indem es als lokaler Löser arbeitet. Es kombiniert die unbedingte Stabilität eines impliziten Lösers mit der enormen Geschwindigkeit, Parallelisierbarkeit und dynamischen Anpassungsfähigkeit eines expliziten Systems."
|
||||
}
|
||||
},
|
||||
"WEBGPU": {
|
||||
"NOT_SUPPORTED": "WebGPU konnte nicht gestartet werden. Bitte prüfe, ob dein Browser WebGPU unterstützt."
|
||||
"GPU": {
|
||||
"WEBGL_FALLBACK": "WebGPU ist nicht verfügbar. WebGL wird als Fallback verwendet. Die Leistung kann eingeschränkt sein.",
|
||||
"NOT_SUPPORTED": "Weder WebGPU noch WebGL konnten initialisiert werden. GPU-Visualisierungen sind nicht verfügbar."
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithmen",
|
||||
|
||||
@@ -512,8 +512,9 @@
|
||||
"DISCLAIMER_4": "The XPBD Compromise: XPBD completely bypasses this complex matrix problem by acting as a local solver. It combines the absolute stability of an implicit solver with the enormous speed, parallelizability, and dynamic adaptability of an explicit system."
|
||||
}
|
||||
},
|
||||
"WEBGPU": {
|
||||
"NOT_SUPPORTED": "WebGPU could not be started. Please check if your browser supports WebGPU."
|
||||
"GPU": {
|
||||
"WEBGL_FALLBACK": "WebGPU is not available. Using WebGL as fallback. Performance may be reduced.",
|
||||
"NOT_SUPPORTED": "Neither WebGPU nor WebGL could be initialized. GPU visualizations are unavailable."
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithms",
|
||||
|
||||
Reference in New Issue
Block a user