Initial commit
98
Demos/Graphics/GpuWorker.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// worker.js
|
||||||
|
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
|
||||||
|
setup( request ) {
|
||||||
|
|
||||||
|
var canvas = request.canvas;
|
||||||
|
|
||||||
|
console.log( canvas );
|
||||||
|
|
||||||
|
particleSimulation.setup( canvas, request.width, request.height );
|
||||||
|
|
||||||
|
//self.postMessage({ type: "pong", id: request.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousemove( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( request ) {
|
||||||
|
|
||||||
|
console.log("onMouseDown");
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousedown( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseup( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseleave( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.wheel( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions( );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller();
|
||||||
|
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||||
|
console.warn("Invalid request received:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||||
|
// const request = new Request(data.method, data.payload);
|
||||||
|
|
||||||
|
// Or just use plain data object
|
||||||
|
const request = data;
|
||||||
|
|
||||||
|
const methodName = request.method;
|
||||||
|
|
||||||
|
if (typeof controller[methodName] !== "function") {
|
||||||
|
console.warn("No method found for:", request.method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller[methodName](request);
|
||||||
|
};
|
||||||
544
Demos/Graphics/demo.js
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Matrix4 from "../../framework/Matrix4.js"
|
||||||
|
|
||||||
|
import Vector3 from "../../framework/Vector3.js"
|
||||||
|
|
||||||
|
import Camera from "../../framework/Camera.js";
|
||||||
|
|
||||||
|
import EventManager from "../../framework/eventManager.js";
|
||||||
|
|
||||||
|
import ShaderInpector from "../../framework/ShaderInpector.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class ParticleSimulation {
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
device;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
useLocalSort = true;
|
||||||
|
|
||||||
|
eventManager = new EventManager();
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.eventManager.setCanvas( canvas );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup( offscreenCanvas, width, height ) {
|
||||||
|
|
||||||
|
offscreenCanvas.width = width;
|
||||||
|
|
||||||
|
offscreenCanvas.height = height;
|
||||||
|
|
||||||
|
this.canvas = offscreenCanvas;
|
||||||
|
|
||||||
|
//console.log( this.canvas.width, this.canvas.height );
|
||||||
|
|
||||||
|
const context = offscreenCanvas.getContext("webgpu");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.camera = new Camera( [0, 0, 5], [0, -.3, 0], [0, 1, 0] );
|
||||||
|
|
||||||
|
this.eventManager.setup( offscreenCanvas, this.camera );
|
||||||
|
|
||||||
|
//this.eventManager.registerEventListenersNode();
|
||||||
|
|
||||||
|
const adapter = await self.navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if ( !adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
this.particleCount = 8192 * 2;
|
||||||
|
|
||||||
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: presentationFormat,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
});
|
||||||
|
|
||||||
|
let boundsMinimum = new Vector3( -2, -2, -2 );
|
||||||
|
|
||||||
|
let boundsMaximum = new Vector3( 2, 2, 2 );
|
||||||
|
|
||||||
|
let cellsPerDimension = 12;
|
||||||
|
|
||||||
|
let gravity = -2.3;
|
||||||
|
|
||||||
|
this.workgroupSize = 64;
|
||||||
|
|
||||||
|
this.numWorkgroups = Math.ceil( this.particleCount / this.workgroupSize );
|
||||||
|
|
||||||
|
|
||||||
|
var jArray = new Array();
|
||||||
|
|
||||||
|
var kArray = new Array();
|
||||||
|
|
||||||
|
if( this.useLocalSort ) {
|
||||||
|
|
||||||
|
var startK = this.workgroupSize * 2; // 2 * 256; 256 = this.workgroupSize
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
var startK = 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let k = startK; k <= this.particleCount; k <<= 1 ) {
|
||||||
|
|
||||||
|
for (let j = k >> 1; j > 0; j >>= 1 ) {
|
||||||
|
|
||||||
|
jArray.push( j );
|
||||||
|
|
||||||
|
kArray.push( k );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.gravityShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.gravityShader.setup( "../../shaders/gravity.wgsl" );
|
||||||
|
|
||||||
|
this.gravityShader.setVariable("gravity", gravity );
|
||||||
|
|
||||||
|
this.gravityShader.setVariable("updateDistancesAndIndices", 1);
|
||||||
|
|
||||||
|
|
||||||
|
this.resetParticlePositions();
|
||||||
|
|
||||||
|
|
||||||
|
this.copyIndicesShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.copyIndicesShader.setup( "../../shaders/copyBuffer.wgsl" );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader = new Shader( this.device );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
await this.renderShader.setup("../../shaders/points.wgsl");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const quadOffsets = new Float32Array( [
|
||||||
|
// Triangle 1
|
||||||
|
-1, -1, // bottom-left
|
||||||
|
1, -1, // bottom-right
|
||||||
|
1, 1, // top-right
|
||||||
|
|
||||||
|
// Triangle 2
|
||||||
|
-1, -1, // bottom-left
|
||||||
|
1, 1, // top-right
|
||||||
|
-1, 1 // top-left
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "quadOffset", quadOffsets );
|
||||||
|
|
||||||
|
|
||||||
|
this.findGridHashShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.findGridHashShader.setup( "../../shaders/findGridHash.wgsl" );
|
||||||
|
|
||||||
|
//console.log("this.gravityShader.getBuffer(\"positions\")", this.gravityShader.getBuffer("positions"));
|
||||||
|
|
||||||
|
this.findGridHashShader.setBuffer( "positions", this.gravityShader.getBuffer("positions") );
|
||||||
|
|
||||||
|
this.findGridHashShader.setVariable( "cellCount", cellsPerDimension );
|
||||||
|
|
||||||
|
this.findGridHashShader.setVariable( "gridMin", boundsMinimum );
|
||||||
|
|
||||||
|
this.findGridHashShader.setVariable( "gridMax", boundsMaximum );
|
||||||
|
|
||||||
|
|
||||||
|
if( this.useLocalSort ) {
|
||||||
|
|
||||||
|
this.localSortShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.localSortShader.setup( "../../shaders/localSort.wgsl" );
|
||||||
|
|
||||||
|
this.localSortShader.setBuffer( "gridHashes", this.findGridHashShader.getBuffer("gridHashes" ) );
|
||||||
|
|
||||||
|
this.localSortShader.setBuffer( "indices", this.findGridHashShader.getBuffer("indices" ) );
|
||||||
|
|
||||||
|
this.localSortShader.setVariable( "totalCount", this.particleCount );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.bitonicSortGridHashShader.setup( "../../shaders/bitonicSortUIntMultiPass.wgsl" );
|
||||||
|
|
||||||
|
if( this.useLocalSort ) {
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setBuffer( "gridHashes", this.localSortShader.getBuffer("gridHashes") );
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setBuffer( "indices", this.localSortShader.getBuffer("indices") );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setBuffer( "gridHashes", this.findGridHashShader.getBuffer("gridHashes" ) );
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setBuffer( "indices", this.findGridHashShader.getBuffer("indices") );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setVariable( "totalCount", this.particleCount );
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setVariable( "jArray", jArray );
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setVariable( "kArray", kArray );
|
||||||
|
|
||||||
|
|
||||||
|
this.findGridHashRangeShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.findGridHashRangeShader.setup( "../../shaders/findGridHashRanges.wgsl" );
|
||||||
|
|
||||||
|
|
||||||
|
this.collisionDetectionShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.collisionDetectionShader.setup( "../../shaders/collisionDetection.wgsl" );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "positions", this.gravityShader.getBuffer("positions" ) );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "velocities", this.gravityShader.getBuffer("velocities") );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "positions", this.gravityShader.getBuffer("positions" ) );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "velocities", this.gravityShader.getBuffer("velocities") );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "gridHashes", this.findGridHashShader.getBuffer("gridHashes") );
|
||||||
|
|
||||||
|
|
||||||
|
if( this.useLocalSort ) {
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "hashSortedIndices", this.localSortShader.getBuffer("indices") );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "hashSortedIndices", this.bitonicSortGridHashShader.getBuffer("indices") );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setVariable( "cellCount", cellsPerDimension );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setVariable( "gridMin", boundsMinimum );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setVariable( "gridMax", boundsMaximum );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "startIndices", this.findGridHashRangeShader.getBuffer("startIndices") );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setBuffer( "endIndices", this.findGridHashRangeShader.getBuffer("endIndices") );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setVariable( "collisionRadius", 0.06 );
|
||||||
|
|
||||||
|
|
||||||
|
this.bitonicSortShader = new Shader(this.device, "../../shaders/bitonicSort.wgsl");
|
||||||
|
|
||||||
|
await this.bitonicSortShader.addStage("main", GPUShaderStage.COMPUTE );
|
||||||
|
|
||||||
|
|
||||||
|
this.bitonicSortShader.setBuffer( "compare", this.gravityShader.getBuffer("distances") );
|
||||||
|
|
||||||
|
this.bitonicSortShader.setBuffer( "indices", this.gravityShader.getBuffer("indices") );
|
||||||
|
|
||||||
|
this.bitonicSortShader.setVariable( "totalCount", this.particleCount );
|
||||||
|
|
||||||
|
|
||||||
|
this.findGridHashRangeShader.setBuffer( "gridHashes", this.bitonicSortGridHashShader.getBuffer("gridHashes") );
|
||||||
|
|
||||||
|
this.findGridHashRangeShader.setBuffer( "indices", this.bitonicSortGridHashShader.getBuffer("indices") );
|
||||||
|
|
||||||
|
this.findGridHashRangeShader.setVariable( "totalCount", this.particleCount );
|
||||||
|
|
||||||
|
|
||||||
|
this.copyIndicesShader.setBuffer( "indices", this.bitonicSortShader.getBuffer("indices") );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.setBuffer( "positions", this.gravityShader.getBuffer("positions") );
|
||||||
|
|
||||||
|
this.renderShader.setBuffer( "sortedIndices", this.copyIndicesShader.getBuffer("sortedIndices") );
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
//var simulation = new particleSimulation();
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
//document.getElementById("resetParticlesButton").addEventListener("click", resetParticlePositions);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
const positions = new Float32Array( this.particleCount * 3 );
|
||||||
|
|
||||||
|
const velocities = new Float32Array( this.particleCount * 3 );
|
||||||
|
|
||||||
|
|
||||||
|
for (var i = 0; i < this.particleCount; i++) {
|
||||||
|
|
||||||
|
positions[ i * 3 + 0 ] = Math.random() * 2.0 - 1.0;
|
||||||
|
|
||||||
|
positions[ i * 3 + 1 ] = Math.random() * 2.0 - 1.0;
|
||||||
|
|
||||||
|
positions[ i * 3 + 2 ] = Math.random() * 2.0 - 1.0;
|
||||||
|
|
||||||
|
velocities[ i * 3 + 0 ] = 0;
|
||||||
|
|
||||||
|
velocities[ i * 3 + 1 ] = 0;
|
||||||
|
|
||||||
|
velocities[ i * 3 + 2 ] = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gravityShader.setVariable("positions", positions);
|
||||||
|
|
||||||
|
this.gravityShader.setVariable("velocities", velocities);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
updateTimeDelta() {
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000;
|
||||||
|
|
||||||
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async sortGridHash() {
|
||||||
|
|
||||||
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
|
|
||||||
|
// Local sort 256 items
|
||||||
|
if( this.useLocalSort ) {
|
||||||
|
|
||||||
|
const passEncoder = commandEncoder.beginComputePass();
|
||||||
|
|
||||||
|
passEncoder.setPipeline( this.localSortShader.pipeline );
|
||||||
|
|
||||||
|
for ( const [ index, bindGroup ] of this.localSortShader.bindGroups.entries() ) {
|
||||||
|
|
||||||
|
passEncoder.setBindGroup( index, bindGroup );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
passEncoder.dispatchWorkgroups( this.numWorkgroups );
|
||||||
|
|
||||||
|
passEncoder.end();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitonic global merge
|
||||||
|
{
|
||||||
|
const threadPassIndices = new Uint32Array( this.particleCount );
|
||||||
|
|
||||||
|
for (var i = 0; i < this.particleCount; i++) {
|
||||||
|
|
||||||
|
threadPassIndices[0] = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bitonicSortGridHashShader.setVariable("threadPassIndices", threadPassIndices);
|
||||||
|
|
||||||
|
const passEncoder = commandEncoder.beginComputePass();
|
||||||
|
|
||||||
|
passEncoder.setPipeline( this.bitonicSortGridHashShader.pipeline );
|
||||||
|
|
||||||
|
for ( const [ index, bindGroup ] of this.bitonicSortGridHashShader.bindGroups.entries() ) {
|
||||||
|
|
||||||
|
passEncoder.setBindGroup( index, bindGroup );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if( this.useLocalSort ) {
|
||||||
|
|
||||||
|
var startK = this.workgroupSize * 2;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
var startK = 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let k = startK; k <= this.particleCount; k <<= 1 ) {
|
||||||
|
|
||||||
|
for (let j = k >> 1; j > 0; j >>= 1 ) {
|
||||||
|
|
||||||
|
passEncoder.dispatchWorkgroups( this.numWorkgroups );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
passEncoder.end();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandBuffer = commandEncoder.finish();
|
||||||
|
|
||||||
|
await this.device.queue.submit( [ commandBuffer ] );
|
||||||
|
|
||||||
|
await this.device.queue.onSubmittedWorkDone();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async sortDistance() {
|
||||||
|
|
||||||
|
await this.gravityShader.execute( this.numWorkgroups );
|
||||||
|
|
||||||
|
this.gravityShader.setVariable("updateDistancesAndIndices", 0);
|
||||||
|
|
||||||
|
for (let k = 2; k <= this.particleCount; k <<= 1) {
|
||||||
|
|
||||||
|
for (let j = k >> 1; j > 0; j >>= 1) {
|
||||||
|
|
||||||
|
const commandBuffers = new Array();
|
||||||
|
|
||||||
|
await this.bitonicSortShader.setVariable( "j", j ); // Must await uniform update
|
||||||
|
await this.bitonicSortShader.setVariable( "k", k );
|
||||||
|
|
||||||
|
const commandEncoder = this.device.createCommandEncoder();
|
||||||
|
|
||||||
|
const passEncoder = commandEncoder.beginComputePass();
|
||||||
|
|
||||||
|
passEncoder.setPipeline( this.bitonicSortShader.pipeline );
|
||||||
|
|
||||||
|
for ( const [ index, bindGroup ] of this.bitonicSortShader.bindGroups.entries() ) {
|
||||||
|
|
||||||
|
passEncoder.setBindGroup( index, bindGroup );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
passEncoder.dispatchWorkgroups( this.numWorkgroups );
|
||||||
|
|
||||||
|
passEncoder.end();
|
||||||
|
|
||||||
|
commandBuffers.push( commandEncoder.finish() );
|
||||||
|
|
||||||
|
this.device.queue.submit( commandBuffers );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gravityShader.setVariable( "updateDistancesAndIndices", 1 );
|
||||||
|
|
||||||
|
await this.copyIndicesShader.execute( this.numWorkgroups );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async findStartEndIndices( logBuffers ) {
|
||||||
|
|
||||||
|
await this.gravityShader.execute( this.numWorkgroups );
|
||||||
|
|
||||||
|
await this.gravityShader.setVariable("updateDistancesAndIndices", 0);
|
||||||
|
|
||||||
|
await this.findGridHashShader.execute( this.numWorkgroups );
|
||||||
|
|
||||||
|
await this.sortGridHash();
|
||||||
|
|
||||||
|
await this.findGridHashRangeShader.execute( this.workgroupSize );
|
||||||
|
|
||||||
|
if( logBuffers ) {
|
||||||
|
|
||||||
|
await this.bitonicSortGridHashShader.debugBuffer("indices");
|
||||||
|
|
||||||
|
await this.bitonicSortGridHashShader.debugBuffer("gridHashes");
|
||||||
|
|
||||||
|
await this.findGridHashRangeShader.debugBuffer("startIndices");
|
||||||
|
|
||||||
|
await this.findGridHashRangeShader.debugBuffer("endIndices");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.gravityShader.setVariable("updateDistancesAndIndices", 1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
|
||||||
|
this.updateTimeDelta();
|
||||||
|
|
||||||
|
const viewMatrixData = this.camera.getViewMatrix();
|
||||||
|
|
||||||
|
const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas )
|
||||||
|
|
||||||
|
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||||
|
|
||||||
|
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||||
|
|
||||||
|
const cameraRight = Matrix4.getColumn( cameraWorldMatrix, 0 );
|
||||||
|
|
||||||
|
const cameraUp = Matrix4.getColumn( cameraWorldMatrix, 1 );
|
||||||
|
|
||||||
|
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||||
|
|
||||||
|
|
||||||
|
this.gravityShader.execute( this.numWorkgroups );
|
||||||
|
|
||||||
|
this.gravityShader.setVariable( "cameraPosition", cameraPosition );
|
||||||
|
|
||||||
|
this.gravityShader.setVariable( "deltaTimeSeconds", this.deltaTimeValue );
|
||||||
|
|
||||||
|
if ( this.frameCount % 20 === 0 ) {
|
||||||
|
|
||||||
|
this.sortDistance();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "cameraRight", cameraRight );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "cameraUp", cameraUp );
|
||||||
|
|
||||||
|
this.renderShader.renderToCanvas( 6, this.particleCount, 0 );
|
||||||
|
|
||||||
|
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
if ( this.frameCount % 6 === 0 ) {
|
||||||
|
|
||||||
|
this.findStartEndIndices();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collisionDetectionShader.setVariable( "deltaTimeSeconds", this.deltaTimeValue );
|
||||||
|
|
||||||
|
this.collisionDetectionShader.execute( this.numWorkgroups );
|
||||||
|
|
||||||
|
requestAnimationFrame( this.render.bind( this ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
171
Demos/Graphics/index.html
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||||
|
</head>
|
||||||
|
<base href="Graphics/" />
|
||||||
|
<link rel="stylesheet" href="./style/main.css" >
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
|
||||||
|
var useWebWorker = false;
|
||||||
|
|
||||||
|
const canvas = document.querySelector(".mainCanvas");
|
||||||
|
|
||||||
|
particleSimulation.setCanvas( canvas );
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
|
||||||
|
var worker;
|
||||||
|
|
||||||
|
if ( !useWebWorker ) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||||
|
|
||||||
|
console.log("document.bufferMap", document.bufferMap);
|
||||||
|
|
||||||
|
} else if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker = new Worker("./GpuWorker.js", { type: "module" });
|
||||||
|
|
||||||
|
worker.onmessage = function ( event ) {
|
||||||
|
|
||||||
|
console.log("From worker:", event.data);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const offscreen = canvas.transferControlToOffscreen();
|
||||||
|
|
||||||
|
worker.addEventListener("error", function ( event ) {
|
||||||
|
|
||||||
|
console.error( "Worker failed:",
|
||||||
|
event.message, "at",
|
||||||
|
event.filename + ":" +
|
||||||
|
event.lineno + ":" +
|
||||||
|
event.colno );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "setup",
|
||||||
|
canvas: offscreen,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage( request, [offscreen] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||||
|
|
||||||
|
for ( var i = 0; i < events.length; i++ ) {
|
||||||
|
|
||||||
|
let eventName = events[i];
|
||||||
|
|
||||||
|
console.log("createEvent", eventName);
|
||||||
|
|
||||||
|
canvas.addEventListener( eventName, function ( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: eventName,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
deltaY: event.deltaY
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager[ eventName ]( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resetParticlePositions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize( canvas ) {
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resize",
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( "resize", function () {
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="controlPanel">
|
||||||
|
<div class="inputRow">
|
||||||
|
<button id="resetParticlesButton">Reset Spheres</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
Demos/Graphics/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background:#111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
114
Demos/Graphics/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Simplifying WebGPU with This Framework
|
||||||
|
|
||||||
|
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||||
|
|
||||||
|
## 1. Loading and Using a Shader
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||||
|
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||||
|
const pipeline = device.createComputePipeline({ ... });
|
||||||
|
// Setup bind groups and command encoder manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const shader = new Shader( "myComputeShader.wgsl" );
|
||||||
|
|
||||||
|
await shader.compile();
|
||||||
|
|
||||||
|
shader.setVariable( "someUniform", 42 );
|
||||||
|
|
||||||
|
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||||
|
|
||||||
|
## 2. Setting Buffers
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create GPU buffer, write data, create bind group manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||||
|
|
||||||
|
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers are bound by name with a simple call.
|
||||||
|
|
||||||
|
## 3. Rendering to Canvas
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create render pipeline, set vertex buffers, encode commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
shader.setCanvas( canvasElement );
|
||||||
|
|
||||||
|
shader.setAttributes( vertexData );
|
||||||
|
|
||||||
|
shader.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||||
|
|
||||||
|
## 4. Camera and Matrix Utilities
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const camera = new Camera();
|
||||||
|
|
||||||
|
camera.position.set( 0, 1, 5 );
|
||||||
|
|
||||||
|
camera.updateViewMatrix();
|
||||||
|
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework provides built-in classes for common math tasks.
|
||||||
|
|
||||||
|
## 5. Event Handling
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const events = new EventManager( canvasElement );
|
||||||
|
|
||||||
|
events.on( "mouseMove", (event) => {
|
||||||
|
camera.rotate( event.deltaX, event.deltaY );
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
shader.execute();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Separates input handling cleanly from GPU logic.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Task | WebGPU Native API | This Framework |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||||
|
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||||
|
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||||
|
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||||
|
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||||
|
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||||
|
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||||
97
Demos/Texture/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// worker.js
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
|
||||||
|
setup( request ) {
|
||||||
|
|
||||||
|
var canvas = request.canvas;
|
||||||
|
|
||||||
|
console.log( canvas );
|
||||||
|
|
||||||
|
particleSimulation.setup( canvas, request.width, request.height );
|
||||||
|
|
||||||
|
//self.postMessage({ type: "pong", id: request.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousemove( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( request ) {
|
||||||
|
|
||||||
|
console.log("onMouseDown");
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousedown( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseup( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseleave( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.wheel( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions( );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller();
|
||||||
|
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||||
|
console.warn("Invalid request received:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||||
|
// const request = new Request(data.method, data.payload);
|
||||||
|
|
||||||
|
// Or just use plain data object
|
||||||
|
const request = data;
|
||||||
|
|
||||||
|
const methodName = request.method;
|
||||||
|
|
||||||
|
if (typeof controller[methodName] !== "function") {
|
||||||
|
console.warn("No method found for:", request.method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller[methodName](request);
|
||||||
|
};
|
||||||
300
Demos/Texture/demo.js
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Matrix4 from "../../framework/Matrix4.js"
|
||||||
|
|
||||||
|
import Vector3 from "../../framework/Vector3.js"
|
||||||
|
|
||||||
|
import Camera from "../../framework/Camera.js";
|
||||||
|
|
||||||
|
import EventManager from "../../framework/eventManager.js";
|
||||||
|
|
||||||
|
import ShaderInpector from "../../framework/ShaderInpector.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class ParticleSimulation {
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
device;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
useLocalSort = true;
|
||||||
|
|
||||||
|
eventManager = new EventManager();
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.eventManager.setCanvas( canvas );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
createTextureFromImageBitmap( device, imageBitmap ) {
|
||||||
|
|
||||||
|
const texture = device.createTexture( {
|
||||||
|
|
||||||
|
size: [ imageBitmap.width, imageBitmap.height, 1 ],
|
||||||
|
|
||||||
|
format: 'rgba8unorm',
|
||||||
|
|
||||||
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
|
device.queue.copyExternalImageToTexture(
|
||||||
|
|
||||||
|
{ source: imageBitmap },
|
||||||
|
|
||||||
|
{ texture: texture },
|
||||||
|
|
||||||
|
[ imageBitmap.width, imageBitmap.height, 1 ]
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImageBitmap(url) {
|
||||||
|
|
||||||
|
const response = await fetch( url );
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const imageBitmap = await createImageBitmap( blob );
|
||||||
|
|
||||||
|
return imageBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async loadTexture( url ) {
|
||||||
|
|
||||||
|
const imageBitmap = await this.loadImageBitmap( url );
|
||||||
|
|
||||||
|
const texture = this.createTextureFromImageBitmap( this.device, imageBitmap );
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlane(width, height, repeatU, repeatV) {
|
||||||
|
|
||||||
|
const vertices = new Float32Array( 18 ); // 6 vertices (2 triangles) * 3 coords
|
||||||
|
const normals = new Float32Array( 18 ); // same count as vertices
|
||||||
|
const uvs = new Float32Array( 12 ); // 6 vertices * 2 coords
|
||||||
|
|
||||||
|
// Positions (two triangles forming a plane on XY plane at z=0)
|
||||||
|
// Large plane from (-width/2, -height/2) to (width/2, height/2)
|
||||||
|
vertices.set([
|
||||||
|
-width / 2, -height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
width / 2, height / 2, 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Normals all pointing +Z
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
normals[i * 3 + 0] = 0;
|
||||||
|
normals[i * 3 + 1] = 0;
|
||||||
|
normals[i * 3 + 2] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UVs scaled by repeatU, repeatV to repeat texture over the plane
|
||||||
|
uvs.set([
|
||||||
|
0, 0,
|
||||||
|
repeatU, 0,
|
||||||
|
0, repeatV,
|
||||||
|
|
||||||
|
0, repeatV,
|
||||||
|
repeatU, 0,
|
||||||
|
repeatU, repeatV
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { vertices, normals, uvs };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup( offscreenCanvas, width, height ) {
|
||||||
|
|
||||||
|
offscreenCanvas.width = width;
|
||||||
|
|
||||||
|
offscreenCanvas.height = height;
|
||||||
|
|
||||||
|
this.canvas = offscreenCanvas;
|
||||||
|
|
||||||
|
const context = offscreenCanvas.getContext("webgpu");
|
||||||
|
|
||||||
|
this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||||
|
|
||||||
|
this.eventManager.setup( offscreenCanvas, this.camera );
|
||||||
|
|
||||||
|
|
||||||
|
const adapter = await self.navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if ( !adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
this.renderShader = new Shader( this.device );
|
||||||
|
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: presentationFormat,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
});
|
||||||
|
const instanceCount = 100;
|
||||||
|
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||||
|
|
||||||
|
for (let i = 0; i < instanceCount; i++) {
|
||||||
|
|
||||||
|
const x = (i % 10) * 300.0;
|
||||||
|
const y = Math.floor(i / 10) * 350.0;
|
||||||
|
|
||||||
|
instancePositions[i * 4 + 0] = x - 1000;
|
||||||
|
instancePositions[i * 4 + 1] = 0;
|
||||||
|
instancePositions[i * 4 + 2] = y - 1000;
|
||||||
|
instancePositions[i * 4 + 3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var model = await this.loadJSON("demo.json");
|
||||||
|
|
||||||
|
var mesh = model.meshes[0];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||||
|
|
||||||
|
var faces = mesh.faces;
|
||||||
|
|
||||||
|
const indexArray = new Uint32Array(faces.length * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < faces.length; i++) {
|
||||||
|
|
||||||
|
indexArray[i * 3 + 0] = faces[i][0];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 1] = faces[i][1];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 2] = faces[i][2];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderShader.setIndices( indexArray );
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||||
|
|
||||||
|
/*
|
||||||
|
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", uvs );
|
||||||
|
|
||||||
|
|
||||||
|
this.vertexCount = vertices.length / 3
|
||||||
|
|
||||||
|
*/
|
||||||
|
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||||
|
|
||||||
|
var texture = await this.loadTexture("./textures/defaultnouvs.png");
|
||||||
|
|
||||||
|
const sampler = this.device.createSampler({
|
||||||
|
minFilter: 'linear',
|
||||||
|
magFilter: 'linear',
|
||||||
|
mipmapFilter: 'linear',
|
||||||
|
addressModeU: 'repeat',
|
||||||
|
addressModeV: 'repeat',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "mySampler", sampler );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "myTexture", texture );
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeDelta() {
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000;
|
||||||
|
|
||||||
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
|
||||||
|
this.updateTimeDelta();
|
||||||
|
|
||||||
|
const viewMatrixData = this.camera.getViewMatrix();
|
||||||
|
|
||||||
|
const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas )
|
||||||
|
|
||||||
|
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||||
|
|
||||||
|
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||||
|
|
||||||
|
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "cameraPosition", cameraPosition );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.renderToCanvas( this.vertexCount, 74, 0 );
|
||||||
|
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
requestAnimationFrame( this.render.bind( this ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadJSON( pathName ) {
|
||||||
|
|
||||||
|
const response = await fetch( pathName );
|
||||||
|
|
||||||
|
if ( !response.ok ){
|
||||||
|
|
||||||
|
throw new Error( `Failed to load shader: ${ pathName }` );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
844040
Demos/Texture/demo.json
Executable file
181
Demos/Texture/index.html
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<base href="Texture/" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./style/main.css" >
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
|
||||||
|
var useWebWorker = false;
|
||||||
|
|
||||||
|
const canvas = document.querySelector(".mainCanvas");
|
||||||
|
|
||||||
|
particleSimulation.setCanvas( canvas );
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var worker;
|
||||||
|
|
||||||
|
if ( !useWebWorker ) {
|
||||||
|
|
||||||
|
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||||
|
|
||||||
|
console.log("document.bufferMap", document.bufferMap);
|
||||||
|
|
||||||
|
} else if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||||
|
|
||||||
|
worker.onmessage = function ( event ) {
|
||||||
|
|
||||||
|
console.log("From worker:", event.data);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const offscreen = canvas.transferControlToOffscreen();
|
||||||
|
|
||||||
|
worker.addEventListener("error", function ( event ) {
|
||||||
|
|
||||||
|
console.error( "Worker failed:",
|
||||||
|
event.message, "at",
|
||||||
|
event.filename + ":" +
|
||||||
|
event.lineno + ":" +
|
||||||
|
event.colno );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "setup",
|
||||||
|
canvas: offscreen,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage( request, [offscreen] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||||
|
|
||||||
|
for ( var i = 0; i < events.length; i++ ) {
|
||||||
|
|
||||||
|
let eventName = events[i];
|
||||||
|
|
||||||
|
console.log("createEvent", eventName);
|
||||||
|
|
||||||
|
canvas.addEventListener( eventName, function ( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: eventName,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
deltaY: event.deltaY
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager[ eventName ]( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resetParticlePositions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize( canvas ) {
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resize",
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( "resize", function () {
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="controlPanel">
|
||||||
|
<div class="inputRow">
|
||||||
|
<button id="resetParticlesButton">Reset Spheres</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
Demos/Texture/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background:#111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
BIN
Demos/Texture/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
Demos/Texture/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Demos/Texture/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
Demos/Texture/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Simplifying WebGPU with This Framework
|
||||||
|
|
||||||
|
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||||
|
|
||||||
|
## 1. Loading and Using a Shader
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||||
|
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||||
|
const pipeline = device.createComputePipeline({ ... });
|
||||||
|
// Setup bind groups and command encoder manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const shader = new Shader( "myComputeShader.wgsl" );
|
||||||
|
|
||||||
|
await shader.compile();
|
||||||
|
|
||||||
|
shader.setVariable( "someUniform", 42 );
|
||||||
|
|
||||||
|
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||||
|
|
||||||
|
## 2. Setting Buffers
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create GPU buffer, write data, create bind group manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||||
|
|
||||||
|
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers are bound by name with a simple call.
|
||||||
|
|
||||||
|
## 3. Rendering to Canvas
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create render pipeline, set vertex buffers, encode commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
shader.setCanvas( canvasElement );
|
||||||
|
|
||||||
|
shader.setAttributes( vertexData );
|
||||||
|
|
||||||
|
shader.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||||
|
|
||||||
|
## 4. Camera and Matrix Utilities
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const camera = new Camera();
|
||||||
|
|
||||||
|
camera.position.set( 0, 1, 5 );
|
||||||
|
|
||||||
|
camera.updateViewMatrix();
|
||||||
|
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework provides built-in classes for common math tasks.
|
||||||
|
|
||||||
|
## 5. Event Handling
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const events = new EventManager( canvasElement );
|
||||||
|
|
||||||
|
events.on( "mouseMove", (event) => {
|
||||||
|
camera.rotate( event.deltaX, event.deltaY );
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
shader.execute();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Separates input handling cleanly from GPU logic.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Task | WebGPU Native API | This Framework |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||||
|
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||||
|
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||||
|
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||||
|
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||||
|
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||||
|
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||||
97
Demos/Texture2/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// worker.js
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
|
||||||
|
setup( request ) {
|
||||||
|
|
||||||
|
var canvas = request.canvas;
|
||||||
|
|
||||||
|
console.log( canvas );
|
||||||
|
|
||||||
|
particleSimulation.setup( canvas, request.width, request.height );
|
||||||
|
|
||||||
|
//self.postMessage({ type: "pong", id: request.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousemove( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( request ) {
|
||||||
|
|
||||||
|
console.log("onMouseDown");
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousedown( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseup( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseleave( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.wheel( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions( );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller();
|
||||||
|
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||||
|
console.warn("Invalid request received:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||||
|
// const request = new Request(data.method, data.payload);
|
||||||
|
|
||||||
|
// Or just use plain data object
|
||||||
|
const request = data;
|
||||||
|
|
||||||
|
const methodName = request.method;
|
||||||
|
|
||||||
|
if (typeof controller[methodName] !== "function") {
|
||||||
|
console.warn("No method found for:", request.method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller[methodName](request);
|
||||||
|
};
|
||||||
300
Demos/Texture2/demo.js
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Matrix4 from "../../framework/Matrix4.js"
|
||||||
|
|
||||||
|
import Vector3 from "../../framework/Vector3.js"
|
||||||
|
|
||||||
|
import Camera from "../../framework/Camera.js";
|
||||||
|
|
||||||
|
import EventManager from "../../framework/eventManager.js";
|
||||||
|
|
||||||
|
import ShaderInpector from "../../framework/ShaderInpector.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class ParticleSimulation {
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
device;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
useLocalSort = true;
|
||||||
|
|
||||||
|
eventManager = new EventManager();
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.eventManager.setCanvas( canvas );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
createTextureFromImageBitmap( device, imageBitmap ) {
|
||||||
|
|
||||||
|
const texture = device.createTexture( {
|
||||||
|
|
||||||
|
size: [ imageBitmap.width, imageBitmap.height, 1 ],
|
||||||
|
|
||||||
|
format: 'rgba8unorm',
|
||||||
|
|
||||||
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
|
device.queue.copyExternalImageToTexture(
|
||||||
|
|
||||||
|
{ source: imageBitmap },
|
||||||
|
|
||||||
|
{ texture: texture },
|
||||||
|
|
||||||
|
[ imageBitmap.width, imageBitmap.height, 1 ]
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImageBitmap(url) {
|
||||||
|
|
||||||
|
const response = await fetch( url );
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const imageBitmap = await createImageBitmap( blob );
|
||||||
|
|
||||||
|
return imageBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async loadTexture( url ) {
|
||||||
|
|
||||||
|
const imageBitmap = await this.loadImageBitmap( url );
|
||||||
|
|
||||||
|
const texture = this.createTextureFromImageBitmap( this.device, imageBitmap );
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlane(width, height, repeatU, repeatV) {
|
||||||
|
|
||||||
|
const vertices = new Float32Array( 18 ); // 6 vertices (2 triangles) * 3 coords
|
||||||
|
const normals = new Float32Array( 18 ); // same count as vertices
|
||||||
|
const uvs = new Float32Array( 12 ); // 6 vertices * 2 coords
|
||||||
|
|
||||||
|
// Positions (two triangles forming a plane on XY plane at z=0)
|
||||||
|
// Large plane from (-width/2, -height/2) to (width/2, height/2)
|
||||||
|
vertices.set([
|
||||||
|
-width / 2, -height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
width / 2, height / 2, 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Normals all pointing +Z
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
normals[i * 3 + 0] = 0;
|
||||||
|
normals[i * 3 + 1] = 0;
|
||||||
|
normals[i * 3 + 2] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UVs scaled by repeatU, repeatV to repeat texture over the plane
|
||||||
|
uvs.set([
|
||||||
|
0, 0,
|
||||||
|
repeatU, 0,
|
||||||
|
0, repeatV,
|
||||||
|
|
||||||
|
0, repeatV,
|
||||||
|
repeatU, 0,
|
||||||
|
repeatU, repeatV
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { vertices, normals, uvs };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup( offscreenCanvas, width, height ) {
|
||||||
|
|
||||||
|
offscreenCanvas.width = width;
|
||||||
|
|
||||||
|
offscreenCanvas.height = height;
|
||||||
|
|
||||||
|
this.canvas = offscreenCanvas;
|
||||||
|
|
||||||
|
const context = offscreenCanvas.getContext("webgpu");
|
||||||
|
|
||||||
|
this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||||
|
|
||||||
|
this.eventManager.setup( offscreenCanvas, this.camera );
|
||||||
|
|
||||||
|
|
||||||
|
const adapter = await self.navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if ( !adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
this.renderShader = new Shader( this.device );
|
||||||
|
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: presentationFormat,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
});
|
||||||
|
const instanceCount = 100;
|
||||||
|
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||||
|
|
||||||
|
for (let i = 0; i < instanceCount; i++) {
|
||||||
|
|
||||||
|
const x = (i % 10) * 300.0;
|
||||||
|
const y = Math.floor(i / 10) * 350.0;
|
||||||
|
|
||||||
|
instancePositions[i * 4 + 0] = x - 1000;
|
||||||
|
instancePositions[i * 4 + 1] = 0;
|
||||||
|
instancePositions[i * 4 + 2] = y - 1000;
|
||||||
|
instancePositions[i * 4 + 3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var model = await this.loadJSON("demo.json");
|
||||||
|
|
||||||
|
var mesh = model.meshes[0];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||||
|
|
||||||
|
var faces = mesh.faces;
|
||||||
|
|
||||||
|
const indexArray = new Uint32Array(faces.length * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < faces.length; i++) {
|
||||||
|
|
||||||
|
indexArray[i * 3 + 0] = faces[i][0];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 1] = faces[i][1];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 2] = faces[i][2];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderShader.setIndices( indexArray );
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||||
|
|
||||||
|
/*
|
||||||
|
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", uvs );
|
||||||
|
|
||||||
|
|
||||||
|
this.vertexCount = vertices.length / 3
|
||||||
|
|
||||||
|
*/
|
||||||
|
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||||
|
|
||||||
|
var texture = await this.loadTexture("./textures/defaultnouvs.png");
|
||||||
|
|
||||||
|
const sampler = this.device.createSampler({
|
||||||
|
minFilter: 'linear',
|
||||||
|
magFilter: 'linear',
|
||||||
|
mipmapFilter: 'linear',
|
||||||
|
addressModeU: 'repeat',
|
||||||
|
addressModeV: 'repeat',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "mySampler", sampler );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "myTexture", texture );
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeDelta() {
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000;
|
||||||
|
|
||||||
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
|
||||||
|
this.updateTimeDelta();
|
||||||
|
|
||||||
|
const viewMatrixData = this.camera.getViewMatrix();
|
||||||
|
|
||||||
|
const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas )
|
||||||
|
|
||||||
|
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||||
|
|
||||||
|
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||||
|
|
||||||
|
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "cameraPosition", cameraPosition );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.renderToCanvas( this.vertexCount, 74, 0 );
|
||||||
|
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
requestAnimationFrame( this.render.bind( this ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadJSON( pathName ) {
|
||||||
|
|
||||||
|
const response = await fetch( pathName );
|
||||||
|
|
||||||
|
if ( !response.ok ){
|
||||||
|
|
||||||
|
throw new Error( `Failed to load shader: ${ pathName }` );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
844040
Demos/Texture2/demo.json
Executable file
179
Demos/Texture2/index.html
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||||
|
</head>
|
||||||
|
<base href="Texture2/" />
|
||||||
|
<link rel="stylesheet" href="./style/main.css" >
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
|
||||||
|
var useWebWorker = false;
|
||||||
|
|
||||||
|
const canvas = document.querySelector(".mainCanvas");
|
||||||
|
|
||||||
|
particleSimulation.setCanvas( canvas );
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var worker;
|
||||||
|
|
||||||
|
if ( !useWebWorker ) {
|
||||||
|
|
||||||
|
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||||
|
|
||||||
|
console.log("document.bufferMap", document.bufferMap);
|
||||||
|
|
||||||
|
} else if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||||
|
|
||||||
|
worker.onmessage = function ( event ) {
|
||||||
|
|
||||||
|
console.log("From worker:", event.data);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const offscreen = canvas.transferControlToOffscreen();
|
||||||
|
|
||||||
|
worker.addEventListener("error", function ( event ) {
|
||||||
|
|
||||||
|
console.error( "Worker failed:",
|
||||||
|
event.message, "at",
|
||||||
|
event.filename + ":" +
|
||||||
|
event.lineno + ":" +
|
||||||
|
event.colno );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "setup",
|
||||||
|
canvas: offscreen,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage( request, [offscreen] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||||
|
|
||||||
|
for ( var i = 0; i < events.length; i++ ) {
|
||||||
|
|
||||||
|
let eventName = events[i];
|
||||||
|
|
||||||
|
console.log("createEvent", eventName);
|
||||||
|
|
||||||
|
canvas.addEventListener( eventName, function ( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: eventName,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
deltaY: event.deltaY
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager[ eventName ]( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resetParticlePositions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize( canvas ) {
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resize",
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( "resize", function () {
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="controlPanel">
|
||||||
|
<div class="inputRow">
|
||||||
|
<button id="resetParticlesButton">Reset Spheres</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
Demos/Texture2/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background:#111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
BIN
Demos/Texture2/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
Demos/Texture2/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Demos/Texture2/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
Demos/Texture2/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Simplifying WebGPU with This Framework
|
||||||
|
|
||||||
|
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||||
|
|
||||||
|
## 1. Loading and Using a Shader
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||||
|
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||||
|
const pipeline = device.createComputePipeline({ ... });
|
||||||
|
// Setup bind groups and command encoder manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const shader = new Shader( "myComputeShader.wgsl" );
|
||||||
|
|
||||||
|
await shader.compile();
|
||||||
|
|
||||||
|
shader.setVariable( "someUniform", 42 );
|
||||||
|
|
||||||
|
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||||
|
|
||||||
|
## 2. Setting Buffers
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create GPU buffer, write data, create bind group manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||||
|
|
||||||
|
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers are bound by name with a simple call.
|
||||||
|
|
||||||
|
## 3. Rendering to Canvas
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create render pipeline, set vertex buffers, encode commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
shader.setCanvas( canvasElement );
|
||||||
|
|
||||||
|
shader.setAttributes( vertexData );
|
||||||
|
|
||||||
|
shader.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||||
|
|
||||||
|
## 4. Camera and Matrix Utilities
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const camera = new Camera();
|
||||||
|
|
||||||
|
camera.position.set( 0, 1, 5 );
|
||||||
|
|
||||||
|
camera.updateViewMatrix();
|
||||||
|
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework provides built-in classes for common math tasks.
|
||||||
|
|
||||||
|
## 5. Event Handling
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const events = new EventManager( canvasElement );
|
||||||
|
|
||||||
|
events.on( "mouseMove", (event) => {
|
||||||
|
camera.rotate( event.deltaX, event.deltaY );
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
shader.execute();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Separates input handling cleanly from GPU logic.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Task | WebGPU Native API | This Framework |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||||
|
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||||
|
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||||
|
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||||
|
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||||
|
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||||
|
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||||
97
Demos/Texture3/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// worker.js
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
|
||||||
|
setup( request ) {
|
||||||
|
|
||||||
|
var canvas = request.canvas;
|
||||||
|
|
||||||
|
console.log( canvas );
|
||||||
|
|
||||||
|
particleSimulation.setup( canvas, request.width, request.height );
|
||||||
|
|
||||||
|
//self.postMessage({ type: "pong", id: request.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousemove( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( request ) {
|
||||||
|
|
||||||
|
console.log("onMouseDown");
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousedown( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseup( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseleave( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.wheel( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions( );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller();
|
||||||
|
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||||
|
console.warn("Invalid request received:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||||
|
// const request = new Request(data.method, data.payload);
|
||||||
|
|
||||||
|
// Or just use plain data object
|
||||||
|
const request = data;
|
||||||
|
|
||||||
|
const methodName = request.method;
|
||||||
|
|
||||||
|
if (typeof controller[methodName] !== "function") {
|
||||||
|
console.warn("No method found for:", request.method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller[methodName](request);
|
||||||
|
};
|
||||||
307
Demos/Texture3/demo.js
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Matrix4 from "../../framework/Matrix4.js"
|
||||||
|
|
||||||
|
import Vector3 from "../../framework/Vector3.js"
|
||||||
|
|
||||||
|
import Camera from "../../framework/Camera.js";
|
||||||
|
|
||||||
|
import EventManager from "../../framework/eventManager.js";
|
||||||
|
|
||||||
|
import ShaderInpector from "../../framework/ShaderInpector.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class ParticleSimulation {
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
device;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
useLocalSort = true;
|
||||||
|
|
||||||
|
eventManager = new EventManager();
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.eventManager.setCanvas( canvas );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
createTextureFromImageBitmap( device, imageBitmap ) {
|
||||||
|
|
||||||
|
const texture = device.createTexture( {
|
||||||
|
|
||||||
|
size: [ imageBitmap.width, imageBitmap.height, 1 ],
|
||||||
|
|
||||||
|
format: 'rgba8unorm',
|
||||||
|
|
||||||
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
|
device.queue.copyExternalImageToTexture(
|
||||||
|
|
||||||
|
{ source: imageBitmap },
|
||||||
|
|
||||||
|
{ texture: texture },
|
||||||
|
|
||||||
|
[ imageBitmap.width, imageBitmap.height, 1 ]
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImageBitmap(url) {
|
||||||
|
|
||||||
|
const response = await fetch( url );
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const imageBitmap = await createImageBitmap( blob );
|
||||||
|
|
||||||
|
return imageBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async loadTexture( url ) {
|
||||||
|
|
||||||
|
const imageBitmap = await this.loadImageBitmap( url );
|
||||||
|
|
||||||
|
const texture = this.createTextureFromImageBitmap( this.device, imageBitmap );
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlane(width, height, repeatU, repeatV) {
|
||||||
|
|
||||||
|
const vertices = new Float32Array( 18 ); // 6 vertices (2 triangles) * 3 coords
|
||||||
|
const normals = new Float32Array( 18 ); // same count as vertices
|
||||||
|
const uvs = new Float32Array( 12 ); // 6 vertices * 2 coords
|
||||||
|
|
||||||
|
// Positions (two triangles forming a plane on XY plane at z=0)
|
||||||
|
// Large plane from (-width/2, -height/2) to (width/2, height/2)
|
||||||
|
vertices.set([
|
||||||
|
-width / 2, -height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
width / 2, height / 2, 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Normals all pointing +Z
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
normals[i * 3 + 0] = 0;
|
||||||
|
normals[i * 3 + 1] = 0;
|
||||||
|
normals[i * 3 + 2] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UVs scaled by repeatU, repeatV to repeat texture over the plane
|
||||||
|
uvs.set([
|
||||||
|
0, 0,
|
||||||
|
repeatU, 0,
|
||||||
|
0, repeatV,
|
||||||
|
|
||||||
|
0, repeatV,
|
||||||
|
repeatU, 0,
|
||||||
|
repeatU, repeatV
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { vertices, normals, uvs };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup( offscreenCanvas, width, height ) {
|
||||||
|
|
||||||
|
offscreenCanvas.width = width;
|
||||||
|
|
||||||
|
offscreenCanvas.height = height;
|
||||||
|
|
||||||
|
this.canvas = offscreenCanvas;
|
||||||
|
|
||||||
|
const context = offscreenCanvas.getContext("webgpu");
|
||||||
|
|
||||||
|
this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||||
|
|
||||||
|
this.eventManager.setup( offscreenCanvas, this.camera );
|
||||||
|
|
||||||
|
|
||||||
|
const adapter = await self.navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if ( !adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
this.renderShader = new Shader( this.device );
|
||||||
|
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: presentationFormat,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
});
|
||||||
|
const instanceCount = 100;
|
||||||
|
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||||
|
|
||||||
|
for (let i = 0; i < instanceCount; i++) {
|
||||||
|
|
||||||
|
const x = (i % 10) * 300.0;
|
||||||
|
const y = Math.floor(i / 10) * 350.0;
|
||||||
|
|
||||||
|
instancePositions[i * 4 + 0] = x - 1000;
|
||||||
|
instancePositions[i * 4 + 1] = 0;
|
||||||
|
instancePositions[i * 4 + 2] = y - 1000;
|
||||||
|
instancePositions[i * 4 + 3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var model = await this.loadJSON("demo.json");
|
||||||
|
|
||||||
|
var mesh = model.meshes[0];
|
||||||
|
|
||||||
|
console.log("mesh", mesh);
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture-normal.wgsl");
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "bitangent", mesh.bitangents );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||||
|
|
||||||
|
var faces = mesh.faces;
|
||||||
|
|
||||||
|
const indexArray = new Uint32Array(faces.length * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < faces.length; i++) {
|
||||||
|
|
||||||
|
indexArray[i * 3 + 0] = faces[i][0];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 1] = faces[i][1];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 2] = faces[i][2];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderShader.setIndices( indexArray );
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture-normal.wgsl");
|
||||||
|
|
||||||
|
/*
|
||||||
|
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", uvs );
|
||||||
|
|
||||||
|
|
||||||
|
this.vertexCount = vertices.length / 3
|
||||||
|
|
||||||
|
*/
|
||||||
|
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||||
|
|
||||||
|
var texture = await this.loadTexture("./textures/0_floorTiles_diff.png");
|
||||||
|
|
||||||
|
const sampler = this.device.createSampler({
|
||||||
|
minFilter: 'linear',
|
||||||
|
magFilter: 'linear',
|
||||||
|
mipmapFilter: 'linear',
|
||||||
|
addressModeU: 'repeat',
|
||||||
|
addressModeV: 'repeat',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "mySampler", sampler );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "myTexture", texture );
|
||||||
|
|
||||||
|
var normalTexture = await this.loadTexture("./textures/0_floorTiles_ddn.png");
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "normalMapTexture", normalTexture );
|
||||||
|
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeDelta() {
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000;
|
||||||
|
|
||||||
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
|
||||||
|
this.updateTimeDelta();
|
||||||
|
|
||||||
|
const viewMatrixData = this.camera.getViewMatrix();
|
||||||
|
|
||||||
|
const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas )
|
||||||
|
|
||||||
|
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||||
|
|
||||||
|
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||||
|
|
||||||
|
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "cameraPosition", cameraPosition );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.renderToCanvas( this.vertexCount, 74, 0 );
|
||||||
|
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
requestAnimationFrame( this.render.bind( this ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadJSON( pathName ) {
|
||||||
|
|
||||||
|
const response = await fetch( pathName );
|
||||||
|
|
||||||
|
if ( !response.ok ){
|
||||||
|
|
||||||
|
throw new Error( `Failed to load shader: ${ pathName }` );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
844040
Demos/Texture3/demo.json
Executable file
180
Demos/Texture3/index.html
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./style/main.css" >
|
||||||
|
|
||||||
|
<base href="Texture3/" />
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
|
||||||
|
var useWebWorker = false;
|
||||||
|
|
||||||
|
const canvas = document.querySelector(".mainCanvas");
|
||||||
|
|
||||||
|
particleSimulation.setCanvas( canvas );
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var worker;
|
||||||
|
|
||||||
|
if ( !useWebWorker ) {
|
||||||
|
|
||||||
|
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||||
|
|
||||||
|
console.log("document.bufferMap", document.bufferMap);
|
||||||
|
|
||||||
|
} else if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||||
|
|
||||||
|
worker.onmessage = function ( event ) {
|
||||||
|
|
||||||
|
console.log("From worker:", event.data);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const offscreen = canvas.transferControlToOffscreen();
|
||||||
|
|
||||||
|
worker.addEventListener("error", function ( event ) {
|
||||||
|
|
||||||
|
console.error( "Worker failed:",
|
||||||
|
event.message, "at",
|
||||||
|
event.filename + ":" +
|
||||||
|
event.lineno + ":" +
|
||||||
|
event.colno );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "setup",
|
||||||
|
canvas: offscreen,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage( request, [offscreen] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||||
|
|
||||||
|
for ( var i = 0; i < events.length; i++ ) {
|
||||||
|
|
||||||
|
let eventName = events[i];
|
||||||
|
|
||||||
|
console.log("createEvent", eventName);
|
||||||
|
|
||||||
|
canvas.addEventListener( eventName, function ( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: eventName,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
deltaY: event.deltaY
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager[ eventName ]( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resetParticlePositions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize( canvas ) {
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resize",
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( "resize", function () {
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="controlPanel">
|
||||||
|
<div class="inputRow">
|
||||||
|
<button id="resetParticlesButton">Reset Spheres</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
Demos/Texture3/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background:#111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
BIN
Demos/Texture3/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
Demos/Texture3/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Demos/Texture3/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
Demos/Texture3/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Simplifying WebGPU with This Framework
|
||||||
|
|
||||||
|
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||||
|
|
||||||
|
## 1. Loading and Using a Shader
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||||
|
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||||
|
const pipeline = device.createComputePipeline({ ... });
|
||||||
|
// Setup bind groups and command encoder manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const shader = new Shader( "myComputeShader.wgsl" );
|
||||||
|
|
||||||
|
await shader.compile();
|
||||||
|
|
||||||
|
shader.setVariable( "someUniform", 42 );
|
||||||
|
|
||||||
|
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||||
|
|
||||||
|
## 2. Setting Buffers
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create GPU buffer, write data, create bind group manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||||
|
|
||||||
|
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers are bound by name with a simple call.
|
||||||
|
|
||||||
|
## 3. Rendering to Canvas
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create render pipeline, set vertex buffers, encode commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
shader.setCanvas( canvasElement );
|
||||||
|
|
||||||
|
shader.setAttributes( vertexData );
|
||||||
|
|
||||||
|
shader.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||||
|
|
||||||
|
## 4. Camera and Matrix Utilities
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const camera = new Camera();
|
||||||
|
|
||||||
|
camera.position.set( 0, 1, 5 );
|
||||||
|
|
||||||
|
camera.updateViewMatrix();
|
||||||
|
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework provides built-in classes for common math tasks.
|
||||||
|
|
||||||
|
## 5. Event Handling
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const events = new EventManager( canvasElement );
|
||||||
|
|
||||||
|
events.on( "mouseMove", (event) => {
|
||||||
|
camera.rotate( event.deltaX, event.deltaY );
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
shader.execute();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Separates input handling cleanly from GPU logic.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Task | WebGPU Native API | This Framework |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||||
|
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||||
|
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||||
|
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||||
|
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||||
|
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||||
|
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||||
97
Demos/TextureArray/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// worker.js
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
|
||||||
|
setup( request ) {
|
||||||
|
|
||||||
|
var canvas = request.canvas;
|
||||||
|
|
||||||
|
console.log( canvas );
|
||||||
|
|
||||||
|
particleSimulation.setup( canvas, request.width, request.height );
|
||||||
|
|
||||||
|
//self.postMessage({ type: "pong", id: request.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousemove( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( request ) {
|
||||||
|
|
||||||
|
console.log("onMouseDown");
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousedown( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseup( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseleave( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.wheel( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions( );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller();
|
||||||
|
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||||
|
console.warn("Invalid request received:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||||
|
// const request = new Request(data.method, data.payload);
|
||||||
|
|
||||||
|
// Or just use plain data object
|
||||||
|
const request = data;
|
||||||
|
|
||||||
|
const methodName = request.method;
|
||||||
|
|
||||||
|
if (typeof controller[methodName] !== "function") {
|
||||||
|
console.warn("No method found for:", request.method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller[methodName](request);
|
||||||
|
};
|
||||||
351
Demos/TextureArray/demo.js
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Matrix4 from "../../framework/Matrix4.js"
|
||||||
|
|
||||||
|
import Vector3 from "../../framework/Vector3.js"
|
||||||
|
|
||||||
|
import Camera from "../../framework/Camera.js";
|
||||||
|
|
||||||
|
import EventManager from "../../framework/eventManager.js";
|
||||||
|
|
||||||
|
import ShaderInpector from "../../framework/ShaderInpector.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class ParticleSimulation {
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
device;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
useLocalSort = true;
|
||||||
|
|
||||||
|
eventManager = new EventManager();
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.eventManager.setCanvas( canvas );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImagesFromFile( filePaths ) {
|
||||||
|
|
||||||
|
var imageBitmaps = new Array();
|
||||||
|
|
||||||
|
for (var i = 0; i < filePaths.length; i++) {
|
||||||
|
|
||||||
|
var filePath = filePaths[i]
|
||||||
|
|
||||||
|
const imageBitmap = await this.loadImageBitmap( filePath );
|
||||||
|
|
||||||
|
imageBitmaps.push( imageBitmap );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createTextureArray(this.device, 512, 512, imageBitmaps.length, imageBitmaps);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTextureArray(device, width, height, layerCount, imageBitmaps) {
|
||||||
|
|
||||||
|
const texture = device.createTexture({
|
||||||
|
size: {
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
depthOrArrayLayers: layerCount,
|
||||||
|
},
|
||||||
|
format: "rgba8unorm",
|
||||||
|
usage: GPUTextureUsage.TEXTURE_BINDING |
|
||||||
|
GPUTextureUsage.COPY_DST |
|
||||||
|
GPUTextureUsage.RENDER_ATTACHMENT,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let layer = 0; layer < layerCount; layer++) {
|
||||||
|
const imageBitmap = imageBitmaps[layer];
|
||||||
|
|
||||||
|
device.queue.copyExternalImageToTexture(
|
||||||
|
{ source: imageBitmap },
|
||||||
|
{ texture: texture, origin: { x: 0, y: 0, z: layer } },
|
||||||
|
[ imageBitmap.width, imageBitmap.height, 1 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textureView = texture.createView({
|
||||||
|
dimension: "2d-array",
|
||||||
|
baseArrayLayer: 0,
|
||||||
|
arrayLayerCount: layerCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { texture, textureView };
|
||||||
|
}
|
||||||
|
|
||||||
|
createTextureFromImageBitmap( device, imageBitmap ) {
|
||||||
|
|
||||||
|
const texture = device.createTexture( {
|
||||||
|
|
||||||
|
size: [ imageBitmap.width, imageBitmap.height, 1 ],
|
||||||
|
|
||||||
|
format: 'rgba8unorm',
|
||||||
|
|
||||||
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
|
device.queue.copyExternalImageToTexture(
|
||||||
|
|
||||||
|
{ source: imageBitmap },
|
||||||
|
|
||||||
|
{ texture: texture },
|
||||||
|
|
||||||
|
[ imageBitmap.width, imageBitmap.height, 1 ]
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImageBitmap(url) {
|
||||||
|
|
||||||
|
const response = await fetch( url );
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const imageBitmap = await createImageBitmap( blob );
|
||||||
|
|
||||||
|
return imageBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async loadTexture( url ) {
|
||||||
|
|
||||||
|
const imageBitmap = await this.loadImageBitmap( url );
|
||||||
|
|
||||||
|
const texture = this.createTextureFromImageBitmap( this.device, imageBitmap );
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlane(width, height, repeatU, repeatV) {
|
||||||
|
|
||||||
|
const vertices = new Float32Array( 18 ); // 6 vertices (2 triangles) * 3 coords
|
||||||
|
const normals = new Float32Array( 18 ); // same count as vertices
|
||||||
|
const uvs = new Float32Array( 12 ); // 6 vertices * 2 coords
|
||||||
|
|
||||||
|
// Positions (two triangles forming a plane on XY plane at z=0)
|
||||||
|
// Large plane from (-width/2, -height/2) to (width/2, height/2)
|
||||||
|
vertices.set([
|
||||||
|
-width / 2, -height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
width / 2, height / 2, 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Normals all pointing +Z
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
normals[i * 3 + 0] = 0;
|
||||||
|
normals[i * 3 + 1] = 0;
|
||||||
|
normals[i * 3 + 2] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UVs scaled by repeatU, repeatV to repeat texture over the plane
|
||||||
|
uvs.set([
|
||||||
|
0, 0,
|
||||||
|
repeatU, 0,
|
||||||
|
0, repeatV,
|
||||||
|
|
||||||
|
0, repeatV,
|
||||||
|
repeatU, 0,
|
||||||
|
repeatU, repeatV
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { vertices, normals, uvs };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup( offscreenCanvas, width, height ) {
|
||||||
|
|
||||||
|
offscreenCanvas.width = width;
|
||||||
|
|
||||||
|
offscreenCanvas.height = height;
|
||||||
|
|
||||||
|
this.canvas = offscreenCanvas;
|
||||||
|
|
||||||
|
const context = offscreenCanvas.getContext("webgpu");
|
||||||
|
|
||||||
|
this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||||
|
|
||||||
|
this.eventManager.setup( offscreenCanvas, this.camera );
|
||||||
|
|
||||||
|
|
||||||
|
const adapter = await self.navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if ( !adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
this.renderShader = new Shader( this.device );
|
||||||
|
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: presentationFormat,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
});
|
||||||
|
const instanceCount = 100;
|
||||||
|
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||||
|
|
||||||
|
for (let i = 0; i < instanceCount; i++) {
|
||||||
|
|
||||||
|
const x = (i % 10) * 300.0;
|
||||||
|
const y = Math.floor(i / 10) * 350.0;
|
||||||
|
|
||||||
|
instancePositions[i * 4 + 0] = x - 1000;
|
||||||
|
instancePositions[i * 4 + 1] = 0;
|
||||||
|
instancePositions[i * 4 + 2] = y - 1000;
|
||||||
|
instancePositions[i * 4 + 3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var model = await this.loadJSON("demo.json");
|
||||||
|
|
||||||
|
var mesh = model.meshes[0];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture-array.wgsl");
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", mesh.texturecoords[0] );
|
||||||
|
|
||||||
|
var faces = mesh.faces;
|
||||||
|
|
||||||
|
const indexArray = new Uint32Array(faces.length * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < faces.length; i++) {
|
||||||
|
|
||||||
|
indexArray[i * 3 + 0] = faces[i][0];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 1] = faces[i][1];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 2] = faces[i][2];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderShader.setIndices( indexArray );
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list-texture.wgsl");
|
||||||
|
|
||||||
|
/*
|
||||||
|
const { vertices, normals, uvs } = this.createPlane( 1000, 1000, 4, 4 );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "uv", uvs );
|
||||||
|
|
||||||
|
|
||||||
|
this.vertexCount = vertices.length / 3
|
||||||
|
|
||||||
|
*/
|
||||||
|
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||||
|
|
||||||
|
//var texture = await this.loadTexture("./textures/defaultnouvs.png");
|
||||||
|
|
||||||
|
const sampler = this.device.createSampler({
|
||||||
|
minFilter: 'linear',
|
||||||
|
magFilter: 'linear',
|
||||||
|
mipmapFilter: 'linear',
|
||||||
|
addressModeU: 'repeat',
|
||||||
|
addressModeV: 'repeat',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "mySampler", sampler );
|
||||||
|
|
||||||
|
//this.renderShader.setVariable( "myTextureArray", texture );
|
||||||
|
|
||||||
|
//var myTextureArray = this.loadImagesFromFile( ["./textures/defaultnouvs.png", "./textures/0_floorTiles_diff.png"] );
|
||||||
|
//this.renderShader.setVariable( "myTextureArray", texture );
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeDelta() {
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000;
|
||||||
|
|
||||||
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
|
||||||
|
this.updateTimeDelta();
|
||||||
|
|
||||||
|
const viewMatrixData = this.camera.getViewMatrix();
|
||||||
|
|
||||||
|
const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas )
|
||||||
|
|
||||||
|
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||||
|
|
||||||
|
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||||
|
|
||||||
|
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "cameraPosition", cameraPosition );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.renderToCanvas( this.vertexCount, 74, 0 );
|
||||||
|
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
requestAnimationFrame( this.render.bind( this ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadJSON( pathName ) {
|
||||||
|
|
||||||
|
const response = await fetch( pathName );
|
||||||
|
|
||||||
|
if ( !response.ok ){
|
||||||
|
|
||||||
|
throw new Error( `Failed to load shader: ${ pathName }` );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
844040
Demos/TextureArray/demo.json
Executable file
179
Demos/TextureArray/index.html
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||||
|
</head>
|
||||||
|
<base href="TextureArray/" />
|
||||||
|
<link rel="stylesheet" href="./style/main.css" >
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
|
||||||
|
var useWebWorker = false;
|
||||||
|
|
||||||
|
const canvas = document.querySelector(".mainCanvas");
|
||||||
|
|
||||||
|
particleSimulation.setCanvas( canvas );
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var worker;
|
||||||
|
|
||||||
|
if ( !useWebWorker ) {
|
||||||
|
|
||||||
|
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||||
|
|
||||||
|
console.log("document.bufferMap", document.bufferMap);
|
||||||
|
|
||||||
|
} else if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||||
|
|
||||||
|
worker.onmessage = function ( event ) {
|
||||||
|
|
||||||
|
console.log("From worker:", event.data);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const offscreen = canvas.transferControlToOffscreen();
|
||||||
|
|
||||||
|
worker.addEventListener("error", function ( event ) {
|
||||||
|
|
||||||
|
console.error( "Worker failed:",
|
||||||
|
event.message, "at",
|
||||||
|
event.filename + ":" +
|
||||||
|
event.lineno + ":" +
|
||||||
|
event.colno );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "setup",
|
||||||
|
canvas: offscreen,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage( request, [offscreen] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||||
|
|
||||||
|
for ( var i = 0; i < events.length; i++ ) {
|
||||||
|
|
||||||
|
let eventName = events[i];
|
||||||
|
|
||||||
|
console.log("createEvent", eventName);
|
||||||
|
|
||||||
|
canvas.addEventListener( eventName, function ( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: eventName,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
deltaY: event.deltaY
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager[ eventName ]( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resetParticlePositions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize( canvas ) {
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resize",
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( "resize", function () {
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="controlPanel">
|
||||||
|
<div class="inputRow">
|
||||||
|
<button id="resetParticlesButton">Reset Spheres</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
Demos/TextureArray/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background:#111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
BIN
Demos/TextureArray/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
Demos/TextureArray/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Demos/TextureArray/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
Demos/TextureArray/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Simplifying WebGPU with This Framework
|
||||||
|
|
||||||
|
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||||
|
|
||||||
|
## 1. Loading and Using a Shader
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||||
|
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||||
|
const pipeline = device.createComputePipeline({ ... });
|
||||||
|
// Setup bind groups and command encoder manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const shader = new Shader( "myComputeShader.wgsl" );
|
||||||
|
|
||||||
|
await shader.compile();
|
||||||
|
|
||||||
|
shader.setVariable( "someUniform", 42 );
|
||||||
|
|
||||||
|
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||||
|
|
||||||
|
## 2. Setting Buffers
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create GPU buffer, write data, create bind group manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||||
|
|
||||||
|
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers are bound by name with a simple call.
|
||||||
|
|
||||||
|
## 3. Rendering to Canvas
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create render pipeline, set vertex buffers, encode commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
shader.setCanvas( canvasElement );
|
||||||
|
|
||||||
|
shader.setAttributes( vertexData );
|
||||||
|
|
||||||
|
shader.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||||
|
|
||||||
|
## 4. Camera and Matrix Utilities
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const camera = new Camera();
|
||||||
|
|
||||||
|
camera.position.set( 0, 1, 5 );
|
||||||
|
|
||||||
|
camera.updateViewMatrix();
|
||||||
|
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework provides built-in classes for common math tasks.
|
||||||
|
|
||||||
|
## 5. Event Handling
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const events = new EventManager( canvasElement );
|
||||||
|
|
||||||
|
events.on( "mouseMove", (event) => {
|
||||||
|
camera.rotate( event.deltaX, event.deltaY );
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
shader.execute();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Separates input handling cleanly from GPU logic.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Task | WebGPU Native API | This Framework |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||||
|
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||||
|
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||||
|
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||||
|
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||||
|
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||||
|
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||||
97
Demos/Triangles/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// worker.js
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
|
||||||
|
setup( request ) {
|
||||||
|
|
||||||
|
var canvas = request.canvas;
|
||||||
|
|
||||||
|
console.log( canvas );
|
||||||
|
|
||||||
|
particleSimulation.setup( canvas, request.width, request.height );
|
||||||
|
|
||||||
|
//self.postMessage({ type: "pong", id: request.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousemove( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( request ) {
|
||||||
|
|
||||||
|
console.log("onMouseDown");
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousedown( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseup( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseleave( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.wheel( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions( );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller();
|
||||||
|
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||||
|
console.warn("Invalid request received:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||||
|
// const request = new Request(data.method, data.payload);
|
||||||
|
|
||||||
|
// Or just use plain data object
|
||||||
|
const request = data;
|
||||||
|
|
||||||
|
const methodName = request.method;
|
||||||
|
|
||||||
|
if (typeof controller[methodName] !== "function") {
|
||||||
|
console.warn("No method found for:", request.method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller[methodName](request);
|
||||||
|
};
|
||||||
200
Demos/Triangles/demo.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Matrix4 from "../../framework/Matrix4.js"
|
||||||
|
|
||||||
|
import Vector3 from "../../framework/Vector3.js"
|
||||||
|
|
||||||
|
import Camera from "../../framework/Camera.js";
|
||||||
|
|
||||||
|
import EventManager from "../../framework/eventManager.js";
|
||||||
|
|
||||||
|
import ShaderInpector from "../../framework/ShaderInpector.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class ParticleSimulation {
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
device;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
useLocalSort = true;
|
||||||
|
|
||||||
|
eventManager = new EventManager();
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.eventManager.setCanvas( canvas );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup( offscreenCanvas, width, height ) {
|
||||||
|
|
||||||
|
offscreenCanvas.width = width;
|
||||||
|
|
||||||
|
offscreenCanvas.height = height;
|
||||||
|
|
||||||
|
this.canvas = offscreenCanvas;
|
||||||
|
|
||||||
|
const context = offscreenCanvas.getContext("webgpu");
|
||||||
|
|
||||||
|
this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||||
|
|
||||||
|
this.eventManager.setup( offscreenCanvas, this.camera );
|
||||||
|
|
||||||
|
|
||||||
|
const adapter = await self.navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if ( !adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
this.renderShader = new Shader( this.device );
|
||||||
|
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: presentationFormat,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
});
|
||||||
|
|
||||||
|
var model = await this.loadJSON("demo.json");
|
||||||
|
|
||||||
|
var mesh = model.meshes[0];
|
||||||
|
|
||||||
|
console.log( mesh );
|
||||||
|
|
||||||
|
const vertices = new Float32Array([
|
||||||
|
0.0, 0.5, // 0 - top outer point
|
||||||
|
0.1123, 0.1545, // 1 - inner point between top and right
|
||||||
|
0.4755, 0.1545, // 2 - right outer point
|
||||||
|
0.1816, -0.0590, // 3 - inner point between right and bottom right
|
||||||
|
0.2939, -0.4045, // 4 - bottom right outer point
|
||||||
|
0.0, -0.1909, // 5 - inner bottom point
|
||||||
|
-0.2939,-0.4045, // 6 - bottom left outer point
|
||||||
|
-0.1816, -0.0590,// 7 - inner point between bottom left and left
|
||||||
|
-0.4755, 0.1545, // 8 - left outer point
|
||||||
|
-0.1123, 0.1545, // 9 - inner point between left and top
|
||||||
|
]);
|
||||||
|
|
||||||
|
const indices = new Uint16Array([
|
||||||
|
0, 1, 9,
|
||||||
|
1, 2, 3,
|
||||||
|
3, 4, 5,
|
||||||
|
5, 6, 7,
|
||||||
|
7, 8, 9,
|
||||||
|
1, 3, 5,
|
||||||
|
1, 5, 7,
|
||||||
|
1, 7, 9,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const instanceCount = 100;
|
||||||
|
const instancePositions = new Float32Array(instanceCount * 4); // vec4 per instance
|
||||||
|
|
||||||
|
for (let i = 0; i < instanceCount; i++) {
|
||||||
|
|
||||||
|
const x = (i % 10) * 300.0;
|
||||||
|
const y = Math.floor(i / 10) * 350.0;
|
||||||
|
|
||||||
|
instancePositions[i * 4 + 0] = x - 500;
|
||||||
|
instancePositions[i * 4 + 1] = 0;
|
||||||
|
instancePositions[i * 4 + 2] = y - 500;
|
||||||
|
instancePositions[i * 4 + 3] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "triangle-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/triangle-list.wgsl");
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "position", mesh.vertices );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "normal", mesh.normals );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "instancePositions", instancePositions );
|
||||||
|
|
||||||
|
|
||||||
|
var faces = mesh.faces;
|
||||||
|
|
||||||
|
const indexArray = new Uint32Array(faces.length * 3);
|
||||||
|
|
||||||
|
for (let i = 0; i < faces.length; i++) {
|
||||||
|
|
||||||
|
indexArray[i * 3 + 0] = faces[i][0];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 1] = faces[i][1];
|
||||||
|
|
||||||
|
indexArray[i * 3 + 2] = faces[i][2];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderShader.setIndices( indexArray );
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeDelta() {
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000;
|
||||||
|
|
||||||
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
|
||||||
|
this.updateTimeDelta();
|
||||||
|
|
||||||
|
const viewMatrixData = this.camera.getViewMatrix();
|
||||||
|
|
||||||
|
const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas )
|
||||||
|
|
||||||
|
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||||
|
|
||||||
|
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||||
|
|
||||||
|
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "cameraPosition", cameraPosition );
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader.renderToCanvas( this.vertexCount, 60, 0 );
|
||||||
|
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
requestAnimationFrame( this.render.bind( this ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadJSON( pathName ) {
|
||||||
|
|
||||||
|
const response = await fetch( pathName );
|
||||||
|
|
||||||
|
if ( !response.ok ){
|
||||||
|
|
||||||
|
throw new Error( `Failed to load shader: ${ pathName }` );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
844040
Demos/Triangles/demo.json
Executable file
180
Demos/Triangles/index.html
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||||
|
<base href="Triangles/" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="./style/main.css" >
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
|
||||||
|
var useWebWorker = false;
|
||||||
|
|
||||||
|
const canvas = document.querySelector(".mainCanvas");
|
||||||
|
|
||||||
|
particleSimulation.setCanvas( canvas );
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var worker;
|
||||||
|
|
||||||
|
if ( !useWebWorker ) {
|
||||||
|
|
||||||
|
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||||
|
|
||||||
|
console.log("document.bufferMap", document.bufferMap);
|
||||||
|
|
||||||
|
} else if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||||
|
|
||||||
|
worker.onmessage = function ( event ) {
|
||||||
|
|
||||||
|
console.log("From worker:", event.data);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const offscreen = canvas.transferControlToOffscreen();
|
||||||
|
|
||||||
|
worker.addEventListener("error", function ( event ) {
|
||||||
|
|
||||||
|
console.error( "Worker failed:",
|
||||||
|
event.message, "at",
|
||||||
|
event.filename + ":" +
|
||||||
|
event.lineno + ":" +
|
||||||
|
event.colno );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "setup",
|
||||||
|
canvas: offscreen,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage( request, [offscreen] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||||
|
|
||||||
|
for ( var i = 0; i < events.length; i++ ) {
|
||||||
|
|
||||||
|
let eventName = events[i];
|
||||||
|
|
||||||
|
console.log("createEvent", eventName);
|
||||||
|
|
||||||
|
canvas.addEventListener( eventName, function ( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: eventName,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
deltaY: event.deltaY
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager[ eventName ]( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resetParticlePositions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize( canvas ) {
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resize",
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( "resize", function () {
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="controlPanel">
|
||||||
|
<div class="inputRow">
|
||||||
|
<button id="resetParticlesButton">Reset Spheres</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
Demos/Triangles/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background:#111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
114
Demos/Triangles/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Simplifying WebGPU with This Framework
|
||||||
|
|
||||||
|
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||||
|
|
||||||
|
## 1. Loading and Using a Shader
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||||
|
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||||
|
const pipeline = device.createComputePipeline({ ... });
|
||||||
|
// Setup bind groups and command encoder manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const shader = new Shader( "myComputeShader.wgsl" );
|
||||||
|
|
||||||
|
await shader.compile();
|
||||||
|
|
||||||
|
shader.setVariable( "someUniform", 42 );
|
||||||
|
|
||||||
|
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||||
|
|
||||||
|
## 2. Setting Buffers
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create GPU buffer, write data, create bind group manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||||
|
|
||||||
|
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers are bound by name with a simple call.
|
||||||
|
|
||||||
|
## 3. Rendering to Canvas
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create render pipeline, set vertex buffers, encode commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
shader.setCanvas( canvasElement );
|
||||||
|
|
||||||
|
shader.setAttributes( vertexData );
|
||||||
|
|
||||||
|
shader.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||||
|
|
||||||
|
## 4. Camera and Matrix Utilities
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const camera = new Camera();
|
||||||
|
|
||||||
|
camera.position.set( 0, 1, 5 );
|
||||||
|
|
||||||
|
camera.updateViewMatrix();
|
||||||
|
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework provides built-in classes for common math tasks.
|
||||||
|
|
||||||
|
## 5. Event Handling
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const events = new EventManager( canvasElement );
|
||||||
|
|
||||||
|
events.on( "mouseMove", (event) => {
|
||||||
|
camera.rotate( event.deltaX, event.deltaY );
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
shader.execute();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Separates input handling cleanly from GPU logic.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Task | WebGPU Native API | This Framework |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||||
|
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||||
|
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||||
|
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||||
|
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||||
|
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||||
|
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||||
97
Demos/particleHeader/GpuWorker.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// worker.js
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
export class Controller {
|
||||||
|
|
||||||
|
setup( request ) {
|
||||||
|
|
||||||
|
var canvas = request.canvas;
|
||||||
|
|
||||||
|
console.log( canvas );
|
||||||
|
|
||||||
|
particleSimulation.setup( canvas, request.width, request.height );
|
||||||
|
|
||||||
|
//self.postMessage({ type: "pong", id: request.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousemove( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( request ) {
|
||||||
|
|
||||||
|
console.log("onMouseDown");
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mousedown( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseup( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.mouseleave( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.wheel( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( request ) {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resetParticlePositions() {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions( );
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller();
|
||||||
|
|
||||||
|
self.onmessage = function (event) {
|
||||||
|
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
if (typeof data !== "object" || typeof data.method !== "string") {
|
||||||
|
console.warn("Invalid request received:", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: wrap the data into a Request instance (if you need its methods)
|
||||||
|
// const request = new Request(data.method, data.payload);
|
||||||
|
|
||||||
|
// Or just use plain data object
|
||||||
|
const request = data;
|
||||||
|
|
||||||
|
const methodName = request.method;
|
||||||
|
|
||||||
|
if (typeof controller[methodName] !== "function") {
|
||||||
|
console.warn("No method found for:", request.method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
controller[methodName](request);
|
||||||
|
};
|
||||||
431
Demos/particleHeader/demo.js
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Matrix4 from "../../framework/Matrix4.js"
|
||||||
|
|
||||||
|
import Vector3 from "../../framework/Vector3.js"
|
||||||
|
|
||||||
|
import Camera from "../../framework/Camera.js";
|
||||||
|
|
||||||
|
import EventManager from "../../framework/eventManager.js";
|
||||||
|
|
||||||
|
import ShaderInpector from "../../framework/ShaderInpector.js";
|
||||||
|
|
||||||
|
|
||||||
|
export class ParticleSimulation {
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
device;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
useLocalSort = true;
|
||||||
|
|
||||||
|
eventManager = new EventManager();
|
||||||
|
|
||||||
|
frameCount = 0;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.eventManager.setCanvas( canvas );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
createTextureFromImageBitmap( device, imageBitmap ) {
|
||||||
|
|
||||||
|
const texture = device.createTexture( {
|
||||||
|
|
||||||
|
size: [ imageBitmap.width, imageBitmap.height, 1 ],
|
||||||
|
|
||||||
|
format: 'rgba8unorm',
|
||||||
|
|
||||||
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
||||||
|
|
||||||
|
} );
|
||||||
|
|
||||||
|
device.queue.copyExternalImageToTexture(
|
||||||
|
|
||||||
|
{ source: imageBitmap },
|
||||||
|
|
||||||
|
{ texture: texture },
|
||||||
|
|
||||||
|
[ imageBitmap.width, imageBitmap.height, 1 ]
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImageBitmap(url) {
|
||||||
|
|
||||||
|
const response = await fetch( url );
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
const imageBitmap = await createImageBitmap( blob );
|
||||||
|
|
||||||
|
return imageBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async loadTexture( url ) {
|
||||||
|
|
||||||
|
const imageBitmap = await this.loadImageBitmap( url );
|
||||||
|
|
||||||
|
const texture = this.createTextureFromImageBitmap( this.device, imageBitmap );
|
||||||
|
|
||||||
|
return texture;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlane(width, height, repeatU, repeatV) {
|
||||||
|
|
||||||
|
const vertices = new Float32Array( 18 ); // 6 vertices (2 triangles) * 3 coords
|
||||||
|
const normals = new Float32Array( 18 ); // same count as vertices
|
||||||
|
const uvs = new Float32Array( 12 ); // 6 vertices * 2 coords
|
||||||
|
|
||||||
|
// Positions (two triangles forming a plane on XY plane at z=0)
|
||||||
|
// Large plane from (-width/2, -height/2) to (width/2, height/2)
|
||||||
|
vertices.set([
|
||||||
|
-width / 2, -height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
|
||||||
|
-width / 2, height / 2, 0,
|
||||||
|
width / 2, -height / 2, 0,
|
||||||
|
width / 2, height / 2, 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Normals all pointing +Z
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
normals[i * 3 + 0] = 0;
|
||||||
|
normals[i * 3 + 1] = 0;
|
||||||
|
normals[i * 3 + 2] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UVs scaled by repeatU, repeatV to repeat texture over the plane
|
||||||
|
uvs.set([
|
||||||
|
0, 0,
|
||||||
|
repeatU, 0,
|
||||||
|
0, repeatV,
|
||||||
|
|
||||||
|
0, repeatV,
|
||||||
|
repeatU, 0,
|
||||||
|
repeatU, repeatV
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { vertices, normals, uvs };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getParticlePositionsAndIndicesFromDiv(div, canvasSize = 256) {
|
||||||
|
|
||||||
|
const rect = div.getBoundingClientRect();
|
||||||
|
console.log(rect.width, rect.height);
|
||||||
|
|
||||||
|
|
||||||
|
const divWidth = rect.width;
|
||||||
|
const divHeight = rect.height;
|
||||||
|
|
||||||
|
|
||||||
|
const canvas = await html2canvas(div, {
|
||||||
|
backgroundColor: null,
|
||||||
|
width: divWidth,
|
||||||
|
height: divHeight,
|
||||||
|
scale: .6,
|
||||||
|
useCORS: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height );
|
||||||
|
const data = imageData.data;
|
||||||
|
console.log(imageData);
|
||||||
|
const positions = [];
|
||||||
|
|
||||||
|
const samplesPerPixel = 8;
|
||||||
|
const colors = [];
|
||||||
|
|
||||||
|
for (let y = 0; y < canvas.height; y++) {
|
||||||
|
for (let x = 0; x < canvas.width; x++) {
|
||||||
|
|
||||||
|
const i = (y * canvas.width + x) * 4;
|
||||||
|
|
||||||
|
const r = data[i + 0];
|
||||||
|
const g = data[i + 1];
|
||||||
|
const b = data[i + 2];
|
||||||
|
const a = data[i + 3];
|
||||||
|
|
||||||
|
|
||||||
|
if (a > 10) {
|
||||||
|
for (let s = 0; s < samplesPerPixel; s++) {
|
||||||
|
|
||||||
|
const u = (x + Math.random()) / (canvas.width - 1);
|
||||||
|
const v = (y + Math.random()) / (canvas.height - 1);
|
||||||
|
|
||||||
|
const clipX = u * 2 - 1;
|
||||||
|
const clipY = 1 - v * 2;
|
||||||
|
|
||||||
|
positions.push(clipX, clipY, 0, 1);
|
||||||
|
colors.push(r / 255, g / 255, b / 255, a / 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.log("positions", positions.length);
|
||||||
|
|
||||||
|
const positionsArray = new Float32Array(positions);
|
||||||
|
|
||||||
|
const indexCount = positionsArray.length / 4;
|
||||||
|
const indices = new Uint32Array(indexCount);
|
||||||
|
|
||||||
|
for (let i = 0; i < indexCount; i++) {
|
||||||
|
indices[i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
positions: positionsArray,
|
||||||
|
indices: indices,
|
||||||
|
colors: new Float32Array(colors)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setup( offscreenCanvas, width, height ) {
|
||||||
|
|
||||||
|
offscreenCanvas.width = width;
|
||||||
|
|
||||||
|
offscreenCanvas.height = height;
|
||||||
|
|
||||||
|
this.canvas = offscreenCanvas;
|
||||||
|
|
||||||
|
const context = offscreenCanvas.getContext("webgpu");
|
||||||
|
|
||||||
|
this.camera = new Camera( [0, 0, 1115], [0, -.3, 0], [0, 1, 0] );
|
||||||
|
|
||||||
|
this.camera.distance = 2
|
||||||
|
|
||||||
|
this.eventManager.setup( offscreenCanvas, this.camera );
|
||||||
|
|
||||||
|
|
||||||
|
const adapter = await self.navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if ( !adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||||
|
|
||||||
|
this.device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
context.configure({
|
||||||
|
device: this.device,
|
||||||
|
format: presentationFormat,
|
||||||
|
alphaMode: "opaque"
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
//var model = await this.loadJSON("demo.json");
|
||||||
|
|
||||||
|
//var mesh = model.meshes[0];
|
||||||
|
var div = document.querySelector("#particleSource");
|
||||||
|
|
||||||
|
|
||||||
|
const { positions, indices, colors } = await this.getParticlePositionsAndIndicesFromDiv( div );
|
||||||
|
|
||||||
|
|
||||||
|
const rect = div.getBoundingClientRect();
|
||||||
|
|
||||||
|
const divWidth = rect.width;
|
||||||
|
const divHeight = rect.height;
|
||||||
|
|
||||||
|
const aspectRatio = divWidth / divHeight;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.initiateParticlesShader = new Shader( this.device );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
await this.initiateParticlesShader.setup( "../../shaders/initiateParticles.wgsl");
|
||||||
|
|
||||||
|
//this.initiateParticlesShader.setVariable( "aspectRatio", aspectRatio );
|
||||||
|
|
||||||
|
this.initiateParticlesShader.setVariable( "initiationPositions", positions );
|
||||||
|
|
||||||
|
this.initiateParticlesShader.setVariable( "positions", positions );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.simpleGravityShader = new Shader( this.device );
|
||||||
|
|
||||||
|
await this.simpleGravityShader.setup( "../../shaders/simpleGravity.wgsl");
|
||||||
|
|
||||||
|
this.simpleGravityShader.setBuffer( "velocities", this.initiateParticlesShader.getBuffer("velocities") );
|
||||||
|
|
||||||
|
this.simpleGravityShader.setBuffer( "positions", this.initiateParticlesShader.getBuffer("positions") );
|
||||||
|
|
||||||
|
|
||||||
|
this.simpleGravityShader.setVariable( "aspectRatio", aspectRatio );
|
||||||
|
|
||||||
|
//this.simpleGravityShader.setVariable( "gravity", [0, -7, 0] );
|
||||||
|
this.simpleGravityShader.setVariable( "hoverRadius", .2 );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.renderShader = new Shader( this.device );
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "point-list";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log("vertices", positions);
|
||||||
|
|
||||||
|
|
||||||
|
this.vertexCount = positions.length / 4;
|
||||||
|
|
||||||
|
|
||||||
|
//this.renderShader.setAttribute( "normal", mesh.normals );
|
||||||
|
|
||||||
|
this.renderShader.setAttribute( "indices",indices );
|
||||||
|
|
||||||
|
this.renderShader.setCanvas( this.canvas );
|
||||||
|
|
||||||
|
this.renderShader.topology = "point-list";
|
||||||
|
|
||||||
|
await this.renderShader.setup( "../../shaders/particle-header.wgsl");
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "hoverRadius", .2 );
|
||||||
|
|
||||||
|
this.renderShader.setBuffer( "positions", this.simpleGravityShader.getBuffer("positions") );
|
||||||
|
|
||||||
|
//this.renderShader.setVariable( "positions", positions );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "colors", colors );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "aspectRatio", aspectRatio );
|
||||||
|
|
||||||
|
|
||||||
|
div.addEventListener("mousemove", function(event) {
|
||||||
|
const rect = div.getBoundingClientRect();
|
||||||
|
|
||||||
|
const mouseX = event.clientX - rect.left;
|
||||||
|
const mouseY = event.clientY - rect.top;
|
||||||
|
|
||||||
|
const aspectRatio = rect.width / rect.height;
|
||||||
|
|
||||||
|
// Normalize to 0..1
|
||||||
|
const normX = mouseX / rect.width;
|
||||||
|
const normY = mouseY / rect.height;
|
||||||
|
|
||||||
|
// Map to clip space coordinates
|
||||||
|
const clipX = normX * 2 * aspectRatio - aspectRatio; // -aspectRatio to +aspectRatio
|
||||||
|
const clipY = 1 - normY * 2; // +1 to -1 (top to bottom)
|
||||||
|
|
||||||
|
// Update uniform buffer for mousePos
|
||||||
|
this.renderShader.setVariable("mousePos", [clipX, clipY]);
|
||||||
|
this.simpleGravityShader.setVariable("mousePos", [clipX, clipY]);
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
|
||||||
|
var texture = await this.loadTexture("./textures/defaultnouvs.png");
|
||||||
|
|
||||||
|
const sampler = this.device.createSampler({
|
||||||
|
minFilter: 'linear',
|
||||||
|
magFilter: 'linear',
|
||||||
|
mipmapFilter: 'linear',
|
||||||
|
addressModeU: 'repeat',
|
||||||
|
addressModeV: 'repeat',
|
||||||
|
});
|
||||||
|
|
||||||
|
//this.renderShader.setVariable( "mySampler", sampler );
|
||||||
|
|
||||||
|
//this.renderShader.setVariable( "myTexture", texture );
|
||||||
|
|
||||||
|
const workgroupSize = 64;
|
||||||
|
|
||||||
|
const particleCount = positions.length / 4;
|
||||||
|
this.dispatchCount = Math.ceil(particleCount / workgroupSize);
|
||||||
|
|
||||||
|
if( this.dispatchCount * workgroupSize <= particleCount ) {
|
||||||
|
|
||||||
|
console.error( "this.dispatchCount is to small", this.dispatchCount);
|
||||||
|
|
||||||
|
}
|
||||||
|
this.initiateParticlesShader.execute( this.dispatchCount );
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeDelta() {
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
this.deltaTimeValue = ( now - this.lastFrameTime ) / 1000;
|
||||||
|
|
||||||
|
this.lastFrameTime = now;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async render() {
|
||||||
|
|
||||||
|
this.updateTimeDelta();
|
||||||
|
|
||||||
|
const viewMatrixData = this.camera.getViewMatrix();
|
||||||
|
|
||||||
|
const projectionMatrixData = Matrix4.createProjectionMatrix( this.camera, this.canvas )
|
||||||
|
|
||||||
|
const viewProjectionMatrix = Matrix4.multiply( projectionMatrixData, viewMatrixData );
|
||||||
|
|
||||||
|
const cameraWorldMatrix = Matrix4.invert( viewMatrixData );
|
||||||
|
|
||||||
|
const cameraPosition = Matrix4.getColumn( cameraWorldMatrix, 3 );
|
||||||
|
|
||||||
|
this.renderShader.setVariable( "viewProjectionMatrix", viewProjectionMatrix );
|
||||||
|
|
||||||
|
//this.renderShader.setVariable( "time", this.lastFrameTime*.001 );
|
||||||
|
|
||||||
|
|
||||||
|
this.simpleGravityShader.setVariable("deltaTime", this.deltaTimeValue);
|
||||||
|
|
||||||
|
this.simpleGravityShader.execute( this.dispatchCount );
|
||||||
|
|
||||||
|
this.renderShader.renderToCanvas( this.vertexCount , 1, 0 );
|
||||||
|
|
||||||
|
this.frameCount++;
|
||||||
|
|
||||||
|
requestAnimationFrame( this.render.bind( this ) );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadJSON( pathName ) {
|
||||||
|
|
||||||
|
const response = await fetch( pathName );
|
||||||
|
|
||||||
|
if ( !response.ok ){
|
||||||
|
|
||||||
|
throw new Error( `Failed to load shader: ${ pathName }` );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
844040
Demos/particleHeader/demo.json
Executable file
195
Demos/particleHeader/index.html
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>10.000 Gpu Sphere Collision Demo.</title>
|
||||||
|
</head>
|
||||||
|
<base href="particleHeader/" />
|
||||||
|
<link rel="stylesheet" href="./style/main.css" >
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
|
||||||
|
import { ParticleSimulation } from "./demo.js";
|
||||||
|
|
||||||
|
|
||||||
|
var particleSimulation = new ParticleSimulation();
|
||||||
|
|
||||||
|
|
||||||
|
var useWebWorker = false;
|
||||||
|
|
||||||
|
const canvas = document.querySelector(".mainCanvas");
|
||||||
|
|
||||||
|
particleSimulation.setCanvas( canvas );
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var worker;
|
||||||
|
|
||||||
|
if ( !useWebWorker ) {
|
||||||
|
|
||||||
|
await particleSimulation.setup( canvas, canvas.width, canvas.height, true );
|
||||||
|
|
||||||
|
console.log("document.bufferMap", document.bufferMap);
|
||||||
|
|
||||||
|
} else if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker = new Worker("../../framework/GpuWorker.js", { type: "module" });
|
||||||
|
|
||||||
|
worker.onmessage = function ( event ) {
|
||||||
|
|
||||||
|
console.log("From worker:", event.data);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
const offscreen = canvas.transferControlToOffscreen();
|
||||||
|
|
||||||
|
worker.addEventListener("error", function ( event ) {
|
||||||
|
|
||||||
|
console.error( "Worker failed:",
|
||||||
|
event.message, "at",
|
||||||
|
event.filename + ":" +
|
||||||
|
event.lineno + ":" +
|
||||||
|
event.colno );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "setup",
|
||||||
|
canvas: offscreen,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage( request, [offscreen] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var events = new Array( "mousemove", "mousedown", "mouseup", "onwheel", "wheel" );
|
||||||
|
|
||||||
|
for ( var i = 0; i < events.length; i++ ) {
|
||||||
|
|
||||||
|
let eventName = events[i];
|
||||||
|
|
||||||
|
console.log("createEvent", eventName);
|
||||||
|
|
||||||
|
canvas.addEventListener( eventName, function ( event ) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
const x = event.clientX - rect.left;
|
||||||
|
|
||||||
|
const y = event.clientY - rect.top;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: eventName,
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
deltaY: event.deltaY
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager[ eventName ]( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector("#resetParticlesButton").addEventListener("click", function () {
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resetParticlePositions"
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.resetParticlePositions();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToDisplaySize( canvas ) {
|
||||||
|
|
||||||
|
const width = window.innerWidth;
|
||||||
|
const height = window.innerHeight;
|
||||||
|
|
||||||
|
var request = {
|
||||||
|
method: "resize",
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( useWebWorker ) {
|
||||||
|
|
||||||
|
worker.postMessage( request );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
particleSimulation.eventManager.resize( request );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( "resize", function () {
|
||||||
|
|
||||||
|
resizeCanvasToDisplaySize( canvas );
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="controlPanel">
|
||||||
|
<div class="inputRow">
|
||||||
|
<button id="resetParticlesButton">Reset Spheres</button>
|
||||||
|
</div>
|
||||||
|
</div><div id="particleSource" xmlns="http://www.w3.org/1999/xhtml" style="
|
||||||
|
width: 882px;
|
||||||
|
height: 67px;
|
||||||
|
background: linear-gradient(135deg, #ff6600, #ffcc00);
|
||||||
|
color: white;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.3);
|
||||||
|
">
|
||||||
|
Hello GPU!
|
||||||
|
</div>
|
||||||
|
<canvas class="mainCanvas" width="1000" height="1000"></canvas>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
114
Demos/particleHeader/style/main.css
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
background:#111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
BIN
Demos/particleHeader/textures/0_floorTiles_ddn.png
Executable file
|
After Width: | Height: | Size: 447 KiB |
BIN
Demos/particleHeader/textures/0_floorTiles_diff.png
Executable file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Demos/particleHeader/textures/defaultnouvs.png
Executable file
|
After Width: | Height: | Size: 5.0 KiB |
114
Demos/particleHeader/webgpu_framework.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# Simplifying WebGPU with This Framework
|
||||||
|
|
||||||
|
WebGPU’s native API is complex and verbose. This framework reduces that complexity by providing simple classes and methods to manage shaders, buffers, and execution. Instead of handling low-level GPU commands, you focus on your logic.
|
||||||
|
|
||||||
|
## 1. Loading and Using a Shader
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Manual setup of device, pipeline, bind groups, etc. Very verbose and error-prone
|
||||||
|
const shaderModule = device.createShaderModule({ code: wgslSource });
|
||||||
|
const pipeline = device.createComputePipeline({ ... });
|
||||||
|
// Setup bind groups and command encoder manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const shader = new Shader( "myComputeShader.wgsl" );
|
||||||
|
|
||||||
|
await shader.compile();
|
||||||
|
|
||||||
|
shader.setVariable( "someUniform", 42 );
|
||||||
|
|
||||||
|
shader.execute( 64 ); // runs compute with 64 workgroups
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework loads, compiles, sets uniforms, and dispatches the compute shader with simple method calls.
|
||||||
|
|
||||||
|
## 2. Setting Buffers
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create GPU buffer, write data, create bind group manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const dataBuffer = gpu.createBuffer( new Float32Array([1, 2, 3]) );
|
||||||
|
|
||||||
|
shader.setBuffer( "inputBuffer", dataBuffer );
|
||||||
|
```
|
||||||
|
|
||||||
|
Buffers are bound by name with a simple call.
|
||||||
|
|
||||||
|
## 3. Rendering to Canvas
|
||||||
|
|
||||||
|
**Without Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Create render pipeline, set vertex buffers, encode commands manually
|
||||||
|
```
|
||||||
|
|
||||||
|
**With Framework:**
|
||||||
|
|
||||||
|
```
|
||||||
|
shader.setCanvas( canvasElement );
|
||||||
|
|
||||||
|
shader.setAttributes( vertexData );
|
||||||
|
|
||||||
|
shader.execute();
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework handles pipeline creation, drawing commands, and presentation automatically.
|
||||||
|
|
||||||
|
## 4. Camera and Matrix Utilities
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const camera = new Camera();
|
||||||
|
|
||||||
|
camera.position.set( 0, 1, 5 );
|
||||||
|
|
||||||
|
camera.updateViewMatrix();
|
||||||
|
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework provides built-in classes for common math tasks.
|
||||||
|
|
||||||
|
## 5. Event Handling
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```
|
||||||
|
const events = new EventManager( canvasElement );
|
||||||
|
|
||||||
|
events.on( "mouseMove", (event) => {
|
||||||
|
camera.rotate( event.deltaX, event.deltaY );
|
||||||
|
shader.setVariable( "viewMatrix", camera.viewMatrix );
|
||||||
|
shader.execute();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Separates input handling cleanly from GPU logic.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Task | WebGPU Native API | This Framework |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Load and compile shader | Many steps, manual setup | One line with `new Shader()` + `compile()` |
|
||||||
|
| Set uniforms | Define bind groups and layouts | Simple `setVariable()` calls |
|
||||||
|
| Bind buffers | Manual buffer creation and binding | `setBuffer()` by name |
|
||||||
|
| Execute compute | Command encoder and dispatch calls | `execute(workgroupCount)` method |
|
||||||
|
| Render to canvas | Complex pipeline and draw calls | `setCanvas()`, `setAttributes()`, `execute()` |
|
||||||
|
| Handle math and camera | External libs or manual math | Built-in Matrix4, Camera classes |
|
||||||
|
| Input handling | Manual event listeners | `EventManager` handles input cleanly |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This framework hides the low-level complexity behind a clean, simple API that accelerates WebGPU development and makes GPU programming accessible and maintainable.
|
||||||
170
Demos/sort/demo.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
|
||||||
|
import Shader from "../../framework/WebGpu.js"
|
||||||
|
|
||||||
|
import Measure from "../../framework/Measure.js"
|
||||||
|
|
||||||
|
|
||||||
|
const adapter = await navigator.gpu.requestAdapter();
|
||||||
|
|
||||||
|
if (!adapter ) {
|
||||||
|
|
||||||
|
throw new Error("Failed to get GPU adapter");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const particleCount = 131072;
|
||||||
|
|
||||||
|
const workgroupSize = 256;
|
||||||
|
|
||||||
|
const numWorkgroups = Math.ceil( particleCount / workgroupSize );
|
||||||
|
|
||||||
|
const device = await adapter.requestDevice();
|
||||||
|
|
||||||
|
|
||||||
|
var passIndex = 0;
|
||||||
|
|
||||||
|
var jArray = new Array();
|
||||||
|
|
||||||
|
var kArray = new Array();
|
||||||
|
|
||||||
|
|
||||||
|
for ( let k = 512; k <= particleCount; k <<= 1 ) {
|
||||||
|
|
||||||
|
for ( let j = k >> 1; j > 0; j >>= 1 ) {
|
||||||
|
|
||||||
|
jArray.push( j );
|
||||||
|
|
||||||
|
kArray.push( k );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const indices = new Uint32Array( particleCount );
|
||||||
|
|
||||||
|
const threadPassIndices = new Uint32Array( particleCount );
|
||||||
|
|
||||||
|
for (var i = 0; i < particleCount; i++) {
|
||||||
|
|
||||||
|
indices[particleCount-i-1] = i;
|
||||||
|
|
||||||
|
threadPassIndices[0] = 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const localSortShader = new Shader( device, "./shaders/localSort.wgsl");
|
||||||
|
|
||||||
|
await localSortShader.addStage("main", GPUShaderStage.COMPUTE );
|
||||||
|
|
||||||
|
localSortShader.setVariable( "compare", indices );
|
||||||
|
|
||||||
|
localSortShader.setVariable( "totalCount", particleCount );
|
||||||
|
|
||||||
|
|
||||||
|
const bitonicSortGridHashShader = new Shader( device, "./shaders/bitonicSortUIntMultiPass.wgsl");
|
||||||
|
|
||||||
|
await bitonicSortGridHashShader.addStage("main", GPUShaderStage.COMPUTE );
|
||||||
|
|
||||||
|
bitonicSortGridHashShader.setVariable( "totalCount", particleCount );
|
||||||
|
|
||||||
|
bitonicSortGridHashShader.setBuffer( "compare", localSortShader.getBuffer("compare") );
|
||||||
|
|
||||||
|
bitonicSortGridHashShader.setVariable( "jArray", jArray );
|
||||||
|
|
||||||
|
bitonicSortGridHashShader.setVariable( "kArray", kArray );
|
||||||
|
|
||||||
|
|
||||||
|
const measure = new Measure();
|
||||||
|
|
||||||
|
measure.writeToPage = true;
|
||||||
|
|
||||||
|
measure.element = document.querySelector(".result");
|
||||||
|
|
||||||
|
for (var i = 3; i < 59; i++) {
|
||||||
|
|
||||||
|
bitonicSortGridHashShader.setVariable("threadPassIndices", threadPassIndices);
|
||||||
|
|
||||||
|
measure.start( "sort"+i );
|
||||||
|
|
||||||
|
//await localSortShader.execute( numWorkgroups );
|
||||||
|
|
||||||
|
const commandEncoder = device.createCommandEncoder();
|
||||||
|
|
||||||
|
|
||||||
|
// === Local sort ===
|
||||||
|
{
|
||||||
|
const passEncoder = commandEncoder.beginComputePass();
|
||||||
|
|
||||||
|
passEncoder.setPipeline( localSortShader.pipeline );
|
||||||
|
|
||||||
|
for ( const [ index, bindGroup ] of localSortShader.bindGroups.entries() ) {
|
||||||
|
|
||||||
|
passEncoder.setBindGroup( index, bindGroup );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
passEncoder.dispatchWorkgroups( numWorkgroups );
|
||||||
|
|
||||||
|
passEncoder.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Bitonic global merge ===
|
||||||
|
{
|
||||||
|
const passEncoder = commandEncoder.beginComputePass();
|
||||||
|
|
||||||
|
passEncoder.setPipeline( bitonicSortGridHashShader.pipeline );
|
||||||
|
|
||||||
|
for ( const [ index, bindGroup ] of bitonicSortGridHashShader.bindGroups.entries() ) {
|
||||||
|
|
||||||
|
passEncoder.setBindGroup( index, bindGroup );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberOfPasses = 0;
|
||||||
|
|
||||||
|
for ( let k = 512; k <= particleCount; k <<= 1 ) {
|
||||||
|
|
||||||
|
for ( let j = k >> 1; j > 0; j >>= 1 ) {
|
||||||
|
|
||||||
|
numberOfPasses++;
|
||||||
|
|
||||||
|
passEncoder.dispatchWorkgroups( numWorkgroups );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
passEncoder.end();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const commandBuffer = commandEncoder.finish();
|
||||||
|
|
||||||
|
device.queue.submit( [ commandBuffer ] );
|
||||||
|
|
||||||
|
await device.queue.onSubmittedWorkDone();
|
||||||
|
|
||||||
|
measure.end( "sort"+i );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var result = await bitonicSortGridHashShader.debugBuffer("compare");
|
||||||
|
|
||||||
|
await bitonicSortGridHashShader.debugBuffer("threadPassIndices");
|
||||||
|
|
||||||
|
var error = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < particleCount; i++ ) {
|
||||||
|
|
||||||
|
if( result[i] != i ) {
|
||||||
|
|
||||||
|
error = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("There is error?", error, particleCount);
|
||||||
|
|
||||||
206
Demos/sort/index.html
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>WebGPU Attention Example</title>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.result{
|
||||||
|
color: #d9d9d9;
|
||||||
|
margin:0 auto;
|
||||||
|
width: 400px;
|
||||||
|
|
||||||
|
padding: 100px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.8);
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 8px rgba(28, 28, 30, 0.7);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 15px rgba(28, 28, 30, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: rgba(28, 28, 30, 1);
|
||||||
|
box-shadow:
|
||||||
|
0 2px 6px rgba(0, 0, 0, 0.8),
|
||||||
|
0 0 10px rgba(28, 28, 30, 1);
|
||||||
|
}
|
||||||
|
#controlPanel {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(30, 30, 30, 0.6);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 12px rgba(0, 0, 0, 0.5),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.05);
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow label {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button,
|
||||||
|
.inputRow input {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow button:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow input {
|
||||||
|
background: rgba(20, 20, 20, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f0f0f5;
|
||||||
|
background: rgba(28, 28, 30, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 6px rgba(28, 28, 30, 0.5);
|
||||||
|
transition: background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
appearance: none; /* Remove default arrow */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(45deg, transparent 50%, #f0f0f5 50%),
|
||||||
|
linear-gradient(135deg, #f0f0f5 50%, transparent 50%);
|
||||||
|
background-position:
|
||||||
|
calc(100% - 20px) calc(50% - 3px),
|
||||||
|
calc(100% - 15px) calc(50% - 3px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow select:hover {
|
||||||
|
background: rgba(40, 40, 44, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRow option {
|
||||||
|
background: rgba(28, 28, 30, 0.95);
|
||||||
|
color: #f0f0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel{
|
||||||
|
justify-content: flex-start;
|
||||||
|
border-bottom: 1px solid #292929;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
gap: 2.5rem;
|
||||||
|
padding: 1.75rem 2rem;
|
||||||
|
background: rgb(20 20 20 / 95%);
|
||||||
|
backdrop-filter: blur(35px);
|
||||||
|
-webkit-backdrop-filter: blur(35px);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.7);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
|
||||||
|
width: calc(100vw);
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-panel button{
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: #111111;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<base href="sort/" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="top-panel"></div>
|
||||||
|
|
||||||
|
<div class="result">
|
||||||
|
|
||||||
|
<p>LocalSort -> Bitonic Sort Multi step</p>
|
||||||
|
<p> 131 072 items under 10 ms in WebGpu</p>
|
||||||
|
<p>Open Your console to see the results.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<canvas width="1000" height="1000"></canvas>
|
||||||
|
<script type="module" src="demo.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
64
Demos/sort/shaders/bitonicSort.wgsl
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> compare: array<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> k: u32; // current stage size (power of two)
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<uniform> j: u32; // current subsequence size
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
|
||||||
|
|
||||||
|
let ixj = idx ^ j;
|
||||||
|
|
||||||
|
|
||||||
|
if (idx >= totalCount || ixj >= totalCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (ixj > idx && ixj < totalCount) {
|
||||||
|
let ascending = (idx & k) == 0u;
|
||||||
|
|
||||||
|
let dist_idx = compare[idx];
|
||||||
|
let dist_ixj = compare[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
if (dist_idx < dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dist_idx > dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
|
||||||
|
let tempDist = compare[idx];
|
||||||
|
let tempDist2 = compare[ixj];
|
||||||
|
|
||||||
|
let tempIndex = indices[idx];
|
||||||
|
let tempIndex2 = indices[ixj];
|
||||||
|
|
||||||
|
compare[idx] = tempDist2;
|
||||||
|
compare[ixj] = tempDist;
|
||||||
|
|
||||||
|
indices[idx] = tempIndex2;
|
||||||
|
indices[ixj] = tempIndex;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Demos/sort/shaders/bitonicSortUInt.wgsl
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> compare: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> k: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<uniform> j: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
|
||||||
|
let ixj = idx ^ j;
|
||||||
|
|
||||||
|
if (idx >= totalCount || ixj <= idx || ixj >= totalCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ixj > idx) {
|
||||||
|
let ascending = (idx & k) == 0u;
|
||||||
|
|
||||||
|
let dist_idx = compare[idx];
|
||||||
|
let dist_ixj = compare[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
if (dist_idx > dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dist_idx < dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
|
||||||
|
let tempDist = compare[idx];
|
||||||
|
|
||||||
|
compare[idx] = compare[ixj];
|
||||||
|
compare[ixj] = tempDist;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
Demos/sort/shaders/bitonicSortUIntMultiPass.wgsl
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> compare: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> threadPassIndices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage, read_write> kArray: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<storage, read_write> jArray: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@compute @workgroup_size( 256 )
|
||||||
|
fn main( @builtin(global_invocation_id) global_id : vec3<u32> ) {
|
||||||
|
|
||||||
|
let idx = global_id.x;
|
||||||
|
|
||||||
|
let threadPassIndex = threadPassIndices[ idx ];
|
||||||
|
|
||||||
|
threadPassIndices[idx] = threadPassIndices[idx] + 1;
|
||||||
|
|
||||||
|
let j = jArray[ threadPassIndex ];
|
||||||
|
|
||||||
|
let k = kArray[ threadPassIndex ];
|
||||||
|
|
||||||
|
let ixj = idx ^ j;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (idx >= totalCount || ixj <= idx || ixj >= totalCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ixj > idx) {
|
||||||
|
let ascending = (idx & k) == 0u;
|
||||||
|
|
||||||
|
let dist_idx = compare[idx];
|
||||||
|
let dist_ixj = compare[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
if (dist_idx > dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dist_idx < dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
|
||||||
|
let tempDist = compare[idx];
|
||||||
|
|
||||||
|
compare[idx] = compare[ixj];
|
||||||
|
compare[ixj] = tempDist;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
68
Demos/sort/shaders/localSort.wgsl
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> compare: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
var<workgroup> sharedData: array<u32, 256>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(256)
|
||||||
|
fn main(@builtin(local_invocation_id) local_id : vec3<u32>,
|
||||||
|
@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
|
||||||
|
let localIndex = local_id.x;
|
||||||
|
let globalIndex = global_id.x;
|
||||||
|
|
||||||
|
// Load element from global memory into shared memory if in range
|
||||||
|
if (globalIndex < totalCount) {
|
||||||
|
sharedData[localIndex] = compare[globalIndex];
|
||||||
|
} else {
|
||||||
|
sharedData[localIndex] = 0xffffffffu; // Max uint to push invalid values to the end
|
||||||
|
}
|
||||||
|
|
||||||
|
workgroupBarrier();
|
||||||
|
|
||||||
|
// Bitonic sort in shared memory on 256 elements
|
||||||
|
var size = 2u;
|
||||||
|
while (size <= 256u) {
|
||||||
|
var stride = size >> 1u;
|
||||||
|
|
||||||
|
var j = stride;
|
||||||
|
while (j > 0u) {
|
||||||
|
let ixj = localIndex ^ j;
|
||||||
|
|
||||||
|
if (ixj > localIndex) {
|
||||||
|
let ascending = ((localIndex & size) == 0u);
|
||||||
|
|
||||||
|
let valLocal = sharedData[localIndex];
|
||||||
|
let valIxj = sharedData[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
if (ascending) {
|
||||||
|
if (valLocal > valIxj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (valLocal < valIxj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
sharedData[localIndex] = valIxj;
|
||||||
|
sharedData[ixj] = valLocal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workgroupBarrier();
|
||||||
|
j = j >> 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = size << 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write sorted results back to global memory
|
||||||
|
if (globalIndex < totalCount) {
|
||||||
|
compare[globalIndex] = sharedData[localIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
231
README.md
@@ -1,3 +1,230 @@
|
|||||||
# WebGPU-Framework
|
Here is a full **professional** README for your WebGPU Framework — polished, structured, and hire-worthy.
|
||||||
|
|
||||||
High-level GPU Compute + Rendering for Browser & Node.js.
|
If you want, I can also produce a shorter “NPM package style” version or include diagrams/screenshots.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# WebGPU Framework
|
||||||
|
|
||||||
|
**High-level GPU Compute + Rendering for Browser & Node.js**
|
||||||
|
**Write once — run on WebGPU everywhere.**
|
||||||
|
|
||||||
|
WebGPU exposes massive GPU power — but also a very low-level API full of pipelines, descriptor layouts, bind groups, command buffers, and sync issues.
|
||||||
|
|
||||||
|
This framework brings **clarity and simplicity**:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const shader = new Shader(device);
|
||||||
|
|
||||||
|
await shader.setup("path/to/shader.wgsl");
|
||||||
|
|
||||||
|
shader.setVariable("gravity", -2.3);
|
||||||
|
shader.setBuffer("positions", buffer);
|
||||||
|
|
||||||
|
await shader.execute(numWorkgroups);
|
||||||
|
```
|
||||||
|
|
||||||
|
No boilerplate.
|
||||||
|
No bind group confusion.
|
||||||
|
Just GPU work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
| ------------------------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| **Unified API** | Same code works in browser or native Node.js GPU contexts |
|
||||||
|
| **Shader-first design** | Bind uniforms & buffers by name — the framework auto-builds pipelines |
|
||||||
|
| **Automatic bind group management** | No descriptor juggling |
|
||||||
|
| **Compute & Rendering** | Full support for GPU compute pipelines & mesh rendering |
|
||||||
|
| **Camera, Matrix & Vector math utilities** | Real 3D scenes ready to go |
|
||||||
|
| **Event system (browser & desktop)** | Camera & input control mapped seamlessly |
|
||||||
|
| **Debug utilities** | Inspect GPU buffers during runtime |
|
||||||
|
| **Fast iteration** | Live shader reload (optional extension) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Browser:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module" src="./framework/WebGpu.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Node.js (requires headless or windowed WebGPU runtime):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install webgpu-framework
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Shader from "webgpu-framework";
|
||||||
|
```
|
||||||
|
|
||||||
|
> Rendering works **in Node** using a native window — same code as browser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Example — Compute Shader
|
||||||
|
|
||||||
|
```js
|
||||||
|
const device = await navigator.gpu.requestDevice();
|
||||||
|
const shader = new Shader(device);
|
||||||
|
|
||||||
|
await shader.setup("./shaders/addVectors.wgsl");
|
||||||
|
|
||||||
|
shader.setBuffer("inputA", bufferA);
|
||||||
|
shader.setBuffer("inputB", bufferB);
|
||||||
|
shader.setBuffer("output", bufferOut);
|
||||||
|
|
||||||
|
await shader.execute(256);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Example — Rendering Particles
|
||||||
|
|
||||||
|
```js
|
||||||
|
const shader = new Shader(device, "./shaders/points.wgsl");
|
||||||
|
|
||||||
|
shader.setCanvas(canvas);
|
||||||
|
shader.setBuffer("positions", particleBuffer);
|
||||||
|
|
||||||
|
shader.renderToCanvas(
|
||||||
|
vertexCount = particleCount,
|
||||||
|
instanceCount = 1
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
That’s all you need.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Project Included: **3D Particle Simulation**
|
||||||
|
|
||||||
|
* 16K moving particles
|
||||||
|
* Gravity, spatial hashing, collision detection
|
||||||
|
* Bitonic GPU sorting
|
||||||
|
* Full compute → render → compute loop
|
||||||
|
|
||||||
|
Demo code:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { ParticleSimulation } from "./demo/ParticleSimulation.js";
|
||||||
|
|
||||||
|
const sim = new ParticleSimulation();
|
||||||
|
await sim.setup(canvas, 1280, 720);
|
||||||
|
sim.render();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Under the Hood
|
||||||
|
|
||||||
|
This framework builds and manages:
|
||||||
|
|
||||||
|
* GPUDevice, SwapChain, Render Passes
|
||||||
|
* Pipeline creation (compute + render)
|
||||||
|
* Bind groups and layouts
|
||||||
|
* Buffer creation + dynamic sized uniform arrays
|
||||||
|
* Command submission + synchronization
|
||||||
|
|
||||||
|
The developer writes:
|
||||||
|
|
||||||
|
* WGSL shaders
|
||||||
|
* Minimal JavaScript to bind variables
|
||||||
|
|
||||||
|
**All complexity stays internal.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Your Code
|
||||||
|
|
|
||||||
|
| setVariable(), setBuffer(), execute(), render()
|
||||||
|
v
|
||||||
|
WebGPU Framework
|
||||||
|
|
|
||||||
|
| Auto-bind pipelines, sync, command buffers
|
||||||
|
v
|
||||||
|
Native WebGPU (Browser or Node.js)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
GPU 🚀
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Summary
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
| ---------------------------------- | --------------------------------------------------- |
|
||||||
|
| `setup(path)` | Load & compile a WGSL shader |
|
||||||
|
| `setVariable(name, value)` | Upload uniform values (numbers, vectors, matrices…) |
|
||||||
|
| `setBuffer(name, gpuBuffer)` | Bind device buffers by name |
|
||||||
|
| `execute(workgroups)` | Dispatch compute workloads |
|
||||||
|
| `renderToCanvas(verts, instances)` | Draw geometry to output surface |
|
||||||
|
| `debugBuffer(name)` | Inspect GPU buffer contents |
|
||||||
|
|
||||||
|
Camera/Math/Event utilities:
|
||||||
|
|
||||||
|
* `Matrix4`, `Vector3`, `Camera`
|
||||||
|
* `EventManager` for mouse/touch/keyboard (browser + Node)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* Modern GPU with WebGPU support
|
||||||
|
* Browser support: Chromium-based + Firefox Nightly
|
||||||
|
* Node.js support: wgpu-native or compatible environments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| Component | State |
|
||||||
|
| -------------------- | --------------- |
|
||||||
|
| Core Shader API | ✓ Stable |
|
||||||
|
| Browser rendering | ✓ Fully working |
|
||||||
|
| Node.js rendering | ✓ Fully working |
|
||||||
|
| Advanced pipelines | 🚧 In progress |
|
||||||
|
| Cross-platform demos | 🚧 More coming |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
© 2025 Kaj Dijkstra
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
**Kaj Dijkstra**
|
||||||
|
Leeuwarden, Netherlands
|
||||||
|
Portfolio: [https://kajdijkstra.com](https://kajdijkstra.com)
|
||||||
|
Email: [kajdijkstra@protonmail.com](mailto:kajdijkstra@protonmail.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Would you like me to:
|
||||||
|
|
||||||
|
✔ Add diagrams illustrating compute → sort → grid hashing pipeline
|
||||||
|
✔ Add screenshots from your particle simulation
|
||||||
|
✔ Add performance benchmarks
|
||||||
|
✔ Add a “Why use this instead of raw WebGPU?” section
|
||||||
|
✔ Publish this to npm with clean package layout
|
||||||
|
|
||||||
|
Also — should we link this repo from your CV under **Projects** with a short bullet like:
|
||||||
|
|
||||||
|
> *High-level WebGPU framework enabling real-time GPU compute + rendering in browser and native runtimes with a minimal API*
|
||||||
|
|
||||||
|
If you're ready, I can push this README into your Gitea repository automatically.
|
||||||
|
|||||||
83
framework/Camera.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import Vector3 from "./Vector3.js";
|
||||||
|
import Matrix4 from "./Matrix4.js";
|
||||||
|
|
||||||
|
export default class Camera {
|
||||||
|
|
||||||
|
eye = new Vector3();
|
||||||
|
target = new Vector3();
|
||||||
|
up = new Vector3( 0, 1, 0 );
|
||||||
|
|
||||||
|
yaw = 0;
|
||||||
|
pitch = 0;
|
||||||
|
fovRadians = Math.PI / 4;
|
||||||
|
near = 0.1;
|
||||||
|
far = 3000.0;
|
||||||
|
distance = 10;
|
||||||
|
viewMatrix = new Float32Array( 16 );
|
||||||
|
|
||||||
|
constructor( eye = [0, 0, 5], target = [0, 0, 0], up = [0, 1, 0] ) {
|
||||||
|
|
||||||
|
this.eye = new Vector3( ...eye );
|
||||||
|
this.target = new Vector3( ...target );
|
||||||
|
this.up = new Vector3( ...up );
|
||||||
|
|
||||||
|
this.distance = Vector3.subtract( this.eye, this.target ).length();
|
||||||
|
|
||||||
|
this.viewMatrix = Matrix4.lookAt( this.eye, this.target, this.up );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
|
||||||
|
const x = this.distance * Math.cos( this.pitch ) * Math.sin( this.yaw );
|
||||||
|
const y = this.distance * Math.sin( this.pitch );
|
||||||
|
const z = this.distance * Math.cos( this.pitch ) * Math.cos( this.yaw );
|
||||||
|
|
||||||
|
this.eye = new Vector3(
|
||||||
|
x + this.target.x,
|
||||||
|
y + this.target.y,
|
||||||
|
z + this.target.z
|
||||||
|
);
|
||||||
|
|
||||||
|
this.viewMatrix = Matrix4.lookAt( this.eye, this.target, this.up );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewMatrix() {
|
||||||
|
|
||||||
|
return this.viewMatrix;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate( deltaYaw, deltaPitch ) {
|
||||||
|
|
||||||
|
this.yaw += deltaYaw;
|
||||||
|
this.pitch -= deltaPitch;
|
||||||
|
|
||||||
|
const maxPitch = Math.PI / 2 - 0.01;
|
||||||
|
|
||||||
|
if ( this.pitch > maxPitch ) this.pitch = maxPitch;
|
||||||
|
if ( this.pitch < -maxPitch ) this.pitch = -maxPitch;
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom( delta ) {
|
||||||
|
|
||||||
|
this.distance += delta * 1;
|
||||||
|
|
||||||
|
if ( this.distance < 0.1 ) this.distance = 0.1;
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setTarget( target ) {
|
||||||
|
|
||||||
|
this.target = new Vector3( ...target );
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
125
framework/Matrix4.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import Vector3 from "./Vector3.js";
|
||||||
|
|
||||||
|
export default class Matrix4 {
|
||||||
|
|
||||||
|
static lookAt( eye, target, up ) {
|
||||||
|
|
||||||
|
const zAxis = Vector3.normalize( Vector3.subtract( eye, target ) );
|
||||||
|
|
||||||
|
const xAxis = Vector3.normalize( Vector3.cross( up, zAxis ) );
|
||||||
|
|
||||||
|
const yAxis = Vector3.cross( zAxis, xAxis );
|
||||||
|
|
||||||
|
return new Float32Array([
|
||||||
|
xAxis.x, yAxis.x, zAxis.x, 0,
|
||||||
|
xAxis.y, yAxis.y, zAxis.y, 0,
|
||||||
|
xAxis.z, yAxis.z, zAxis.z, 0,
|
||||||
|
-Vector3.dot( xAxis, eye ), -Vector3.dot( yAxis, eye ), -Vector3.dot( zAxis, eye ), 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getColumn( matrix, index ) {
|
||||||
|
const i = index * 4;
|
||||||
|
|
||||||
|
return new Vector3(
|
||||||
|
matrix[ i + 0 ],
|
||||||
|
matrix[ i + 1 ],
|
||||||
|
matrix[ i + 2 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static createProjectionMatrix( camera, canvas ) {
|
||||||
|
return Matrix4.perspective(
|
||||||
|
camera.fovRadians,
|
||||||
|
canvas.width / canvas.height,
|
||||||
|
camera.near,
|
||||||
|
camera.far
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static invert( m ) {
|
||||||
|
const out = new Float32Array(16);
|
||||||
|
|
||||||
|
const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3];
|
||||||
|
const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7];
|
||||||
|
const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11];
|
||||||
|
const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15];
|
||||||
|
|
||||||
|
const a0 = m00 * m11 - m01 * m10;
|
||||||
|
const a1 = m00 * m12 - m02 * m10;
|
||||||
|
const a2 = m00 * m13 - m03 * m10;
|
||||||
|
const a3 = m01 * m12 - m02 * m11;
|
||||||
|
const a4 = m01 * m13 - m03 * m11;
|
||||||
|
const a5 = m02 * m13 - m03 * m12;
|
||||||
|
|
||||||
|
const b0 = m20 * m31 - m21 * m30;
|
||||||
|
const b1 = m20 * m32 - m22 * m30;
|
||||||
|
const b2 = m20 * m33 - m23 * m30;
|
||||||
|
const b3 = m21 * m32 - m22 * m31;
|
||||||
|
const b4 = m21 * m33 - m23 * m31;
|
||||||
|
const b5 = m22 * m33 - m23 * m32;
|
||||||
|
|
||||||
|
const det = a0 * b5 - a1 * b4 + a2 * b3 + a3 * b2 - a4 * b1 + a5 * b0;
|
||||||
|
|
||||||
|
if (det === 0) return null;
|
||||||
|
|
||||||
|
const invDet = 1 / det;
|
||||||
|
|
||||||
|
out[0] = ( m11 * b5 - m12 * b4 + m13 * b3) * invDet;
|
||||||
|
out[1] = (-m01 * b5 + m02 * b4 - m03 * b3) * invDet;
|
||||||
|
out[2] = ( m31 * a5 - m32 * a4 + m33 * a3) * invDet;
|
||||||
|
out[3] = (-m21 * a5 + m22 * a4 - m23 * a3) * invDet;
|
||||||
|
|
||||||
|
out[4] = (-m10 * b5 + m12 * b2 - m13 * b1) * invDet;
|
||||||
|
out[5] = ( m00 * b5 - m02 * b2 + m03 * b1) * invDet;
|
||||||
|
out[6] = (-m30 * a5 + m32 * a2 - m33 * a1) * invDet;
|
||||||
|
out[7] = ( m20 * a5 - m22 * a2 + m23 * a1) * invDet;
|
||||||
|
|
||||||
|
out[8] = ( m10 * b4 - m11 * b2 + m13 * b0) * invDet;
|
||||||
|
out[9] = (-m00 * b4 + m01 * b2 - m03 * b0) * invDet;
|
||||||
|
out[10] = ( m30 * a4 - m31 * a2 + m33 * a0) * invDet;
|
||||||
|
out[11] = (-m20 * a4 + m21 * a2 - m23 * a0) * invDet;
|
||||||
|
|
||||||
|
out[12] = (-m10 * b3 + m11 * b1 - m12 * b0) * invDet;
|
||||||
|
out[13] = ( m00 * b3 - m01 * b1 + m02 * b0) * invDet;
|
||||||
|
out[14] = (-m30 * a3 + m31 * a1 - m32 * a0) * invDet;
|
||||||
|
out[15] = ( m20 * a3 - m21 * a1 + m22 * a0) * invDet;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static perspective( fovRadians, aspect, near, far ) {
|
||||||
|
|
||||||
|
const f = 1.0 / Math.tan( fovRadians / 2 );
|
||||||
|
|
||||||
|
const nf = 1 / ( near - far );
|
||||||
|
|
||||||
|
return new Float32Array([
|
||||||
|
f / aspect, 0, 0, 0,
|
||||||
|
0, f, 0, 0,
|
||||||
|
0, 0, (far + near) * nf, -1,
|
||||||
|
0, 0, (2 * far * near) * nf, 0,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static multiply( a, b ) {
|
||||||
|
const out = new Float32Array(16);
|
||||||
|
|
||||||
|
for ( let col = 0; col < 4; col++ ) {
|
||||||
|
for ( let row = 0; row < 4; row++ ) {
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
for ( let k = 0; k < 4; k++ ) {
|
||||||
|
// a is column-major: element at col k, row row => a[k*4 + row]
|
||||||
|
// b is column-major: element at col col, row k => b[col*4 + k]
|
||||||
|
sum += a[k * 4 + row] * b[col * 4 + k];
|
||||||
|
}
|
||||||
|
|
||||||
|
out[col * 4 + row] = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
framework/Measure.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
export default class Measure {
|
||||||
|
|
||||||
|
startTimes = {};
|
||||||
|
|
||||||
|
endTimes = {};
|
||||||
|
|
||||||
|
writeToPage = false;
|
||||||
|
|
||||||
|
element = false;
|
||||||
|
|
||||||
|
start ( label ) {
|
||||||
|
|
||||||
|
this.startTimes[ label ] = performance.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
end ( label ) {
|
||||||
|
|
||||||
|
this.endTimes[ label ] = performance.now();
|
||||||
|
|
||||||
|
this.log( label );
|
||||||
|
}
|
||||||
|
|
||||||
|
getElapsed ( label ) {
|
||||||
|
|
||||||
|
if ( this.startTimes[ label ] === undefined || this.endTimes[ label ] === undefined ) {
|
||||||
|
|
||||||
|
throw new Error( "Start or end time missing for label: " + label );
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.endTimes[ label ] - this.startTimes[ label ];
|
||||||
|
}
|
||||||
|
|
||||||
|
log ( label ) {
|
||||||
|
|
||||||
|
const elapsed = this.getElapsed( label );
|
||||||
|
|
||||||
|
if( this.writeToPage ) {
|
||||||
|
|
||||||
|
var p = document.createElement("p")
|
||||||
|
|
||||||
|
p.innerText = label + " took " + elapsed.toFixed(3) + " ms";
|
||||||
|
|
||||||
|
this.element.appendChild( p );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log( label + " took " + elapsed.toFixed(3) + " ms" );
|
||||||
|
}
|
||||||
|
}
|
||||||
11
framework/Request.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export class Request {
|
||||||
|
|
||||||
|
constructor( method, payload = {} ) {
|
||||||
|
|
||||||
|
this.method = method; // method name to call on Controller, e.g. "Ping"
|
||||||
|
|
||||||
|
this.payload = payload; // any data for the method
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
67
framework/ShaderInpector.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
class shaderDebugger{
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
|
||||||
|
var shaders = document.shaders;
|
||||||
|
|
||||||
|
var select = document.querySelector(".selectDebugShader");
|
||||||
|
|
||||||
|
for (var i = 0; i < shaders.length; i++) {
|
||||||
|
|
||||||
|
var currentShader = shaders[i];
|
||||||
|
|
||||||
|
var option = document.createElement("option");
|
||||||
|
|
||||||
|
option.innerText = currentShader.path;
|
||||||
|
|
||||||
|
option.id = i;
|
||||||
|
|
||||||
|
select.appendChild( option );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector( "#showBuffers" ).addEventListener( "click", async function() {
|
||||||
|
|
||||||
|
var select = document.querySelector(".selectDebugShader");
|
||||||
|
|
||||||
|
var selectedIndex = select.selectedIndex;
|
||||||
|
|
||||||
|
var selectedShader = document.shaders[ selectedIndex ]
|
||||||
|
|
||||||
|
const keysArray = Array.from( selectedShader.buffers );
|
||||||
|
|
||||||
|
console.log("\n\n\n\n -------------------- Debugging Shader --------------- \n\n\n\n");
|
||||||
|
|
||||||
|
console.log( "Shader Path: ", selectedShader.path );
|
||||||
|
|
||||||
|
console.log( selectedShader );
|
||||||
|
|
||||||
|
for (var i = 0; i < keysArray.length; i++) {
|
||||||
|
|
||||||
|
const bindingInfo = selectedShader.bindings.find( b => b.varName === keysArray[i][0] );
|
||||||
|
|
||||||
|
|
||||||
|
if( bindingInfo ) {
|
||||||
|
|
||||||
|
if( bindingInfo.type == "storage" ) {
|
||||||
|
|
||||||
|
await selectedShader.debugBuffer( keysArray[i][0] );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
console.log("this is a Uniform", keysArray, selectedShader.bindings);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default shaderDebugger;
|
||||||
60
framework/Vector3.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
export default class Vector3 {
|
||||||
|
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
z = 0;
|
||||||
|
|
||||||
|
constructor( x = 0, y = 0, z = 0 ) {
|
||||||
|
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static subtract( a, b ) {
|
||||||
|
|
||||||
|
return new Vector3( a.x - b.x, a.y - b.y, a.z - b.z );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
length() {
|
||||||
|
|
||||||
|
return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static cross( a, b ) {
|
||||||
|
|
||||||
|
return new Vector3(
|
||||||
|
|
||||||
|
a.y * b.z - a.z * b.y,
|
||||||
|
a.z * b.x - a.x * b.z,
|
||||||
|
a.x * b.y - a.y * b.x
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static dot( a, b ) {
|
||||||
|
|
||||||
|
return a.x * b.x + a.y * b.y + a.z * b.z;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static normalize( v ) {
|
||||||
|
|
||||||
|
const length = Math.sqrt( v.x * v.x + v.y * v.y + v.z * v.z );
|
||||||
|
|
||||||
|
if ( length > 0.00001 ) {
|
||||||
|
|
||||||
|
return new Vector3( v.x / length, v.y / length, v.z / length );
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return new Vector3( 0, 0, 0 );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
1893
framework/WebGpu.js
Normal file
113
framework/eventManager.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// eventManager.js
|
||||||
|
|
||||||
|
export default class EventManager {
|
||||||
|
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
lastX = 0;
|
||||||
|
|
||||||
|
lastY = 0;
|
||||||
|
|
||||||
|
camera;
|
||||||
|
|
||||||
|
canvas;
|
||||||
|
|
||||||
|
setCanvas( canvas ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
|
||||||
|
//this.registerEventListeners();
|
||||||
|
|
||||||
|
//this.handleResize();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setup( canvas, camera ) {
|
||||||
|
|
||||||
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
this.camera = camera;
|
||||||
|
|
||||||
|
//this.registerEventListeners();
|
||||||
|
|
||||||
|
//this.handleResize();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEventListeners() {
|
||||||
|
|
||||||
|
this.canvas.addEventListener( "mousedown", this.onMouseDown.bind(this) );
|
||||||
|
|
||||||
|
this.canvas.addEventListener( "mouseup", this.onMouseUp.bind(this) );
|
||||||
|
|
||||||
|
this.canvas.addEventListener( "mouseleave", this.onMouseLeave.bind(this) );
|
||||||
|
|
||||||
|
this.canvas.addEventListener( "mousemove", this.onMouseMove.bind(this) );
|
||||||
|
|
||||||
|
this.canvas.addEventListener( "wheel", this.onWheel.bind(this), { passive: false } );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resize( event ) {
|
||||||
|
|
||||||
|
this.canvas.width = event.width;
|
||||||
|
|
||||||
|
this.canvas.height = event.height;
|
||||||
|
|
||||||
|
//this.canvas.width = window.innerWidth;
|
||||||
|
|
||||||
|
//this.canvas.height = window.innerHeight;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousedown( event ) {
|
||||||
|
|
||||||
|
console.log("mouseDownHandler");
|
||||||
|
|
||||||
|
this.isDragging = true;
|
||||||
|
|
||||||
|
this.lastX = event.clientX;
|
||||||
|
|
||||||
|
this.lastY = event.clientY;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseup( event ) {
|
||||||
|
|
||||||
|
this.isDragging = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseleave( event ) {
|
||||||
|
|
||||||
|
this.isDragging = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
mousemove( event ) {
|
||||||
|
|
||||||
|
if ( !this.isDragging ) return;
|
||||||
|
|
||||||
|
const deltaX = ( event.clientX - this.lastX ) * 0.005;
|
||||||
|
|
||||||
|
const deltaY = ( event.clientY - this.lastY ) * 0.005;
|
||||||
|
|
||||||
|
this.camera.rotate( deltaX, -deltaY );
|
||||||
|
|
||||||
|
this.lastX = event.clientX;
|
||||||
|
|
||||||
|
this.lastY = event.clientY;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel( event ) {
|
||||||
|
|
||||||
|
|
||||||
|
const delta = event.deltaY * 0.01;
|
||||||
|
|
||||||
|
this.camera.zoom( delta );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
6
framework/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
1
package.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"type": "module" }
|
||||||
166
server.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import http from "http";
|
||||||
|
|
||||||
|
import { readdir } from "fs/promises";
|
||||||
|
import { stat } from "fs/promises";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
|
||||||
|
class App
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
constructor( )
|
||||||
|
{
|
||||||
|
|
||||||
|
const selfPath = fileURLToPath( import.meta.url );
|
||||||
|
|
||||||
|
this.rootPath = dirname( selfPath );
|
||||||
|
this.httpServer = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async start( )
|
||||||
|
{
|
||||||
|
|
||||||
|
this.httpServer = http.createServer( this.handleRequest.bind( this ) );
|
||||||
|
|
||||||
|
this.httpServer.listen( 3000 );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async handleRequest( req, res )
|
||||||
|
{
|
||||||
|
|
||||||
|
const requestedPath = decodeURI( req.url );
|
||||||
|
const fullPath = join( this.rootPath, requestedPath );
|
||||||
|
|
||||||
|
const exists = await this.checkFileExists( fullPath );
|
||||||
|
|
||||||
|
if ( !exists )
|
||||||
|
{
|
||||||
|
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end( "Not Found" );
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await stat( fullPath );
|
||||||
|
|
||||||
|
if ( stats.isDirectory( ) )
|
||||||
|
{
|
||||||
|
|
||||||
|
const indexPath = join( fullPath, "index.html" );
|
||||||
|
const indexExists = await this.checkFileExists( indexPath );
|
||||||
|
|
||||||
|
if ( indexExists )
|
||||||
|
{
|
||||||
|
|
||||||
|
await this.sendFile( indexPath, res );
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sendDirectoryListing( fullPath, requestedPath, res );
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sendFile( fullPath, res );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async sendFile( path, res )
|
||||||
|
{
|
||||||
|
|
||||||
|
const contentType = this.getContentType( path );
|
||||||
|
const fileData = await readFile( path );
|
||||||
|
|
||||||
|
res.setHeader( "Content-Type", contentType );
|
||||||
|
res.statusCode = 200;
|
||||||
|
|
||||||
|
res.end( fileData );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async sendDirectoryListing( dirPath, urlPath, res )
|
||||||
|
{
|
||||||
|
|
||||||
|
const entries = await readdir( dirPath, { withFileTypes : true } );
|
||||||
|
|
||||||
|
let html = "<html><body><h1>Index of " + urlPath + "</h1><ul>";
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while ( i < entries.length )
|
||||||
|
{
|
||||||
|
|
||||||
|
const e = entries[ i ].name;
|
||||||
|
const link = urlPath.endsWith( "/" )
|
||||||
|
? urlPath + e
|
||||||
|
: urlPath + "/" + e;
|
||||||
|
|
||||||
|
html = html + "<li><a href=\"" + link + "\">" + e + "</a></li>";
|
||||||
|
|
||||||
|
i = i + 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
html = html + "</ul></body></html>";
|
||||||
|
|
||||||
|
res.setHeader( "Content-Type", "text/html" );
|
||||||
|
res.statusCode = 200;
|
||||||
|
|
||||||
|
res.end( html );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async checkFileExists( path )
|
||||||
|
{
|
||||||
|
|
||||||
|
const exists = await stat( path )
|
||||||
|
.then( function( ) { return true; } )
|
||||||
|
.catch( function( ) { return false; } );
|
||||||
|
|
||||||
|
return exists;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getContentType( path )
|
||||||
|
{
|
||||||
|
|
||||||
|
const lower = path.toLowerCase( );
|
||||||
|
|
||||||
|
if ( lower.endsWith( ".html" ) ) return "text/html";
|
||||||
|
if ( lower.endsWith( ".css" ) ) return "text/css";
|
||||||
|
if ( lower.endsWith( ".js" ) ) return "text/javascript";
|
||||||
|
if ( lower.endsWith( ".json" ) ) return "application/json";
|
||||||
|
if ( lower.endsWith( ".wasm" ) ) return "application/wasm";
|
||||||
|
if ( lower.endsWith( ".png" ) ) return "image/png";
|
||||||
|
if ( lower.endsWith( ".jpg" ) ) return "image/jpeg";
|
||||||
|
if ( lower.endsWith( ".jpeg" ) ) return "image/jpeg";
|
||||||
|
if ( lower.endsWith( ".gif" ) ) return "image/gif";
|
||||||
|
if ( lower.endsWith( ".svg" ) ) return "image/svg+xml";
|
||||||
|
if ( lower.endsWith( ".wgsl" ) ) return "text/plain";
|
||||||
|
if ( lower.endsWith( ".txt" ) ) return "text/plain";
|
||||||
|
|
||||||
|
return "application/octet-stream";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const app = new App( );
|
||||||
|
|
||||||
|
await app.start( );
|
||||||
64
shaders/bitonicSort.wgsl
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> compare: array<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> k: u32; // current stage size (power of two)
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<uniform> j: u32; // current subsequence size
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
|
||||||
|
|
||||||
|
let ixj = idx ^ j;
|
||||||
|
|
||||||
|
|
||||||
|
if (idx >= totalCount || ixj >= totalCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (ixj > idx && ixj < totalCount) {
|
||||||
|
let ascending = (idx & k) == 0u;
|
||||||
|
|
||||||
|
let dist_idx = compare[idx];
|
||||||
|
let dist_ixj = compare[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
if (dist_idx < dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dist_idx > dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
|
||||||
|
let tempDist = compare[idx];
|
||||||
|
let tempDist2 = compare[ixj];
|
||||||
|
|
||||||
|
let tempIndex = indices[idx];
|
||||||
|
let tempIndex2 = indices[ixj];
|
||||||
|
|
||||||
|
compare[idx] = tempDist2;
|
||||||
|
compare[ixj] = tempDist;
|
||||||
|
|
||||||
|
indices[idx] = tempIndex2;
|
||||||
|
indices[ixj] = tempIndex;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
shaders/bitonicSortUInt.wgsl
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> compare: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> k: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<uniform> j: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
@compute @workgroup_size(256)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
||||||
|
let idx = global_id.x;
|
||||||
|
|
||||||
|
let ixj = idx ^ j;
|
||||||
|
|
||||||
|
if (idx >= totalCount || ixj <= idx || ixj >= totalCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ixj > idx) {
|
||||||
|
let ascending = (idx & k) == 0u;
|
||||||
|
|
||||||
|
let dist_idx = compare[idx];
|
||||||
|
let dist_ixj = compare[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
if (dist_idx > dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dist_idx < dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
let tempDist = compare[idx];
|
||||||
|
let tempIndex = indices[idx];
|
||||||
|
|
||||||
|
compare[idx] = compare[ixj];
|
||||||
|
compare[ixj] = tempDist;
|
||||||
|
|
||||||
|
indices[idx] = indices[ixj];
|
||||||
|
indices[ixj] = tempIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
shaders/bitonicSortUIntMultiPass.wgsl
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> gridHashes: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> threadPassIndices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage, read_write> kArray: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<storage, read_write> jArray: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(5)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
@compute @workgroup_size(256)
|
||||||
|
fn main(@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
|
||||||
|
let idx = global_id.x;
|
||||||
|
|
||||||
|
let threadPassIndex = threadPassIndices[idx];
|
||||||
|
|
||||||
|
threadPassIndices[idx] = threadPassIndices[idx] + 1u;
|
||||||
|
|
||||||
|
let j = jArray[threadPassIndex];
|
||||||
|
|
||||||
|
let k = kArray[threadPassIndex];
|
||||||
|
|
||||||
|
let ixj = idx ^ j;
|
||||||
|
|
||||||
|
if (ixj <= idx || ixj >= totalCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ixj > idx) {
|
||||||
|
let ascending = (idx & k) == 0u;
|
||||||
|
|
||||||
|
let dist_idx = gridHashes[idx];
|
||||||
|
let dist_ixj = gridHashes[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
|
||||||
|
if (ascending) {
|
||||||
|
if (dist_idx > dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dist_idx < dist_ixj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
let tempDist = gridHashes[idx];
|
||||||
|
let tempIndex = indices[idx];
|
||||||
|
|
||||||
|
gridHashes[idx] = gridHashes[ixj];
|
||||||
|
gridHashes[ixj] = tempDist;
|
||||||
|
|
||||||
|
indices[idx] = indices[ixj];
|
||||||
|
indices[ixj] = tempIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
shaders/collisionDetection.wgsl
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> positions: array<vec3<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> velocities: array<vec3<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage, read_write> gridHashes: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<storage, read_write> hashSortedIndices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> cellCount: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(5)
|
||||||
|
var<uniform> gridMin: vec3<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(6)
|
||||||
|
var<storage, read> startIndices: array<u32>; // start index of particles in each cell
|
||||||
|
|
||||||
|
@group(0) @binding(7)
|
||||||
|
var<storage, read> endIndices: array<u32>; // end index (exclusive) of particles in each cell
|
||||||
|
|
||||||
|
@group(0) @binding(8)
|
||||||
|
var<uniform> collisionRadius: f32;
|
||||||
|
|
||||||
|
@group(0) @binding(9)
|
||||||
|
var<uniform> deltaTimeSeconds: f32;
|
||||||
|
|
||||||
|
@group(0) @binding(10) var<uniform> gridMax: vec3<f32>;
|
||||||
|
|
||||||
|
// particleIndex = hashSortedIndices[ startIndices[ i ] ]
|
||||||
|
|
||||||
|
fn getHash(gridCoordinate: vec3<i32>, cellCount: u32) -> u32 {
|
||||||
|
|
||||||
|
let maxIndex = i32(cellCount) - 1;
|
||||||
|
|
||||||
|
let x = max(0, min(gridCoordinate.x, maxIndex));
|
||||||
|
let y = max(0, min(gridCoordinate.y, maxIndex));
|
||||||
|
let z = max(0, min(gridCoordinate.z, maxIndex));
|
||||||
|
|
||||||
|
return u32(x + y * i32(cellCount) + z * i32(cellCount) * i32(cellCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@compute @workgroup_size(256)
|
||||||
|
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
|
||||||
|
|
||||||
|
let index = globalInvocationId.x;
|
||||||
|
|
||||||
|
let particleIndex = hashSortedIndices[ index ];
|
||||||
|
|
||||||
|
if ( particleIndex >= arrayLength(&positions) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPosition = positions[ particleIndex ];
|
||||||
|
|
||||||
|
let cellSize = (gridMax - gridMin) / f32(cellCount); // 2.0 / 16 = 0.125
|
||||||
|
|
||||||
|
let relativePos = currentPosition - gridMin; // currentPosition + 1
|
||||||
|
|
||||||
|
let gridCoord = vec3<i32>(floor(relativePos / cellSize)); // relativePos divided by cellSize, then floored
|
||||||
|
|
||||||
|
let hash = getHash(gridCoord, cellCount);
|
||||||
|
|
||||||
|
|
||||||
|
var currentVelocity = velocities[particleIndex];
|
||||||
|
|
||||||
|
var push = vec3<f32>(0.0);
|
||||||
|
|
||||||
|
var count = 0u;
|
||||||
|
|
||||||
|
let collisionRadiusSquared = collisionRadius * collisionRadius;
|
||||||
|
|
||||||
|
for (var dz = -1; dz <= 1; dz = dz + 1) {
|
||||||
|
for (var dy = -1; dy <= 1; dy = dy + 1) {
|
||||||
|
for (var dx = -1; dx <= 1; dx = dx + 1) {
|
||||||
|
|
||||||
|
|
||||||
|
let neighborCell = gridCoord + vec3<i32>(dx, dy, dz);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Compute hash of neighbor cell, with clamping to valid range inside getHash()
|
||||||
|
let neighborHash = getHash(neighborCell, cellCount);
|
||||||
|
|
||||||
|
let startIndex = startIndices[neighborHash];
|
||||||
|
let endIndex = endIndices[neighborHash];
|
||||||
|
|
||||||
|
for (var i = startIndex; i < endIndex; i = i + 1u) {
|
||||||
|
let otherIndex = hashSortedIndices[i];
|
||||||
|
if (otherIndex == particleIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let otherPosition = positions[otherIndex];
|
||||||
|
let offset = currentPosition - otherPosition;
|
||||||
|
let distSquared = dot(offset, offset);
|
||||||
|
|
||||||
|
if (distSquared < collisionRadiusSquared && distSquared > 0.00001) {
|
||||||
|
let distance = sqrt(distSquared);
|
||||||
|
let direction = offset / distance;
|
||||||
|
let overlap = collisionRadius - distance;
|
||||||
|
|
||||||
|
push += direction * overlap;
|
||||||
|
count += 1u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count > 0u ) {
|
||||||
|
|
||||||
|
let averagePush = push / f32(count);
|
||||||
|
|
||||||
|
currentPosition += averagePush * .9;
|
||||||
|
|
||||||
|
currentVelocity += averagePush * 3.0;
|
||||||
|
|
||||||
|
let pushDir = normalize(averagePush);
|
||||||
|
|
||||||
|
// Project current velocity onto push direction
|
||||||
|
let velAlongPush = dot(currentVelocity, pushDir);
|
||||||
|
|
||||||
|
// Damping factor (energy loss on collision)
|
||||||
|
let dampingFactor = 0.25;
|
||||||
|
|
||||||
|
// Reduce velocity along push direction
|
||||||
|
let velAlongPushDamped = velAlongPush * dampingFactor;
|
||||||
|
|
||||||
|
// Velocity perpendicular to push direction remains unchanged
|
||||||
|
let velPerp = currentVelocity - velAlongPush * pushDir;
|
||||||
|
|
||||||
|
// Combine damped velocity components
|
||||||
|
currentVelocity = velPerp + velAlongPushDamped * pushDir;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let deltaTimeClamped = min( deltaTimeSeconds, 0.01 );
|
||||||
|
|
||||||
|
|
||||||
|
let gridExtent = vec3<f32>(f32(cellCount)) * cellSize;
|
||||||
|
//let gridMax = gridMin + gridExtent;
|
||||||
|
|
||||||
|
// Enforce hardcoded bounding box from -1 to +1 on all axes
|
||||||
|
if (currentPosition.x < gridMin.x) {
|
||||||
|
currentPosition.x = gridMin.x;
|
||||||
|
currentVelocity.x = abs(currentVelocity.x) * 0.2;
|
||||||
|
} else if (currentPosition.x > gridMax.x) {
|
||||||
|
currentPosition.x = gridMax.x;
|
||||||
|
currentVelocity.x = -abs(currentVelocity.x) * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPosition.y < gridMin.y) {
|
||||||
|
currentPosition.y = gridMin.y;
|
||||||
|
currentVelocity.y = abs(currentVelocity.y) * 0.2;
|
||||||
|
} else if (currentPosition.y > gridMax.y) {
|
||||||
|
currentPosition.y = gridMax.y;
|
||||||
|
currentVelocity.y = -abs(currentVelocity.y) * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPosition.z < gridMin.z) {
|
||||||
|
currentPosition.z = gridMin.z;
|
||||||
|
currentVelocity.z = abs(currentVelocity.z) * 0.2;
|
||||||
|
} else if (currentPosition.z > gridMax.z) {
|
||||||
|
currentPosition.z = gridMax.z;
|
||||||
|
currentVelocity.z = -abs(currentVelocity.z) * 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (currentPosition.y < -1.0) {
|
||||||
|
currentPosition.y = -1.0;
|
||||||
|
currentVelocity.y *= -0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
currentPosition += currentVelocity * deltaTimeClamped;
|
||||||
|
|
||||||
|
positions[ particleIndex ] = currentPosition;
|
||||||
|
|
||||||
|
velocities[ particleIndex ] = currentVelocity;
|
||||||
|
|
||||||
|
}
|
||||||
14
shaders/copyBuffer.wgsl
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> sortedIndices: array<u32>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
|
||||||
|
|
||||||
|
let particleIndex = globalInvocationId.x;
|
||||||
|
|
||||||
|
sortedIndices[particleIndex] = indices[particleIndex];
|
||||||
|
|
||||||
|
}
|
||||||
49
shaders/findGridHash.wgsl
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
@group(0) @binding(0) var<storage, read_write> positions: array<vec3<f32>>;
|
||||||
|
|
||||||
|
|
||||||
|
@group(0) @binding(1) var<storage, read_write> gridHashes: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2) var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
|
||||||
|
@group(0) @binding(3) var<uniform> cellCount: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(4) var<uniform> gridMin: vec3<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(5) var<uniform> gridMax: vec3<f32>;
|
||||||
|
|
||||||
|
fn getHash(gridCoordinate: vec3<i32>, cellCount: u32) -> u32 {
|
||||||
|
|
||||||
|
let maxIndex = i32(cellCount) - 1;
|
||||||
|
|
||||||
|
let x = max(0, min(gridCoordinate.x, maxIndex));
|
||||||
|
let y = max(0, min(gridCoordinate.y, maxIndex));
|
||||||
|
let z = max(0, min(gridCoordinate.z, maxIndex));
|
||||||
|
|
||||||
|
return u32(x + y * i32(cellCount) + z * i32(cellCount) * i32(cellCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
@compute @workgroup_size(256)
|
||||||
|
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
|
||||||
|
|
||||||
|
let particleIndex = globalInvocationId.x;
|
||||||
|
|
||||||
|
if ( particleIndex >= arrayLength(&positions) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentPosition = positions[particleIndex];
|
||||||
|
|
||||||
|
let cellSize = (gridMax - gridMin) / f32(cellCount); // 2.0 / 16 = 0.125
|
||||||
|
|
||||||
|
let relativePos = currentPosition - gridMin; // currentPosition + 1
|
||||||
|
|
||||||
|
let gridCoord = vec3<i32>(floor(relativePos / cellSize)); // relativePos divided by cellSize, then floored
|
||||||
|
|
||||||
|
let hash = getHash(gridCoord, cellCount);
|
||||||
|
|
||||||
|
gridHashes[ particleIndex ] = hash;
|
||||||
|
|
||||||
|
indices[ particleIndex ] = particleIndex;
|
||||||
|
|
||||||
|
}
|
||||||
40
shaders/findGridHashRanges.wgsl
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read> gridHashes: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage, read_write> startIndices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<storage, read_write> endIndices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
@compute @workgroup_size(256)
|
||||||
|
fn findStartEndIndices(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||||
|
|
||||||
|
let i = globalId.x;
|
||||||
|
|
||||||
|
if (i >= totalCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentHash = gridHashes[i];
|
||||||
|
|
||||||
|
if (i == 0u || gridHashes[i - 1u] != currentHash) {
|
||||||
|
|
||||||
|
startIndices[currentHash] = i;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (i == totalCount - 1u || gridHashes[i + 1u] != currentHash) {
|
||||||
|
|
||||||
|
endIndices[currentHash] = i;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
69
shaders/gravity.wgsl
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> positions: array<vec3<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> velocities: array<vec3<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage, read_write> distances: array<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> deltaTimeSeconds: f32;
|
||||||
|
|
||||||
|
@group(0) @binding(5)
|
||||||
|
var<uniform> cameraPosition: vec3<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(6)
|
||||||
|
var<uniform> updateDistancesAndIndices: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(7)
|
||||||
|
var<uniform> cellCount: u32;
|
||||||
|
|
||||||
|
@group(0) @binding(8)
|
||||||
|
var<uniform> gravity: f32;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn computeMain(@builtin(global_invocation_id) globalInvocationId: vec3<u32>) {
|
||||||
|
let particleIndex = globalInvocationId.x;
|
||||||
|
|
||||||
|
if (particleIndex >= arrayLength(&positions)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let gravityAcceleration = vec3<f32>(0.0, gravity, 0.0);
|
||||||
|
|
||||||
|
var currentPosition = positions[particleIndex];
|
||||||
|
var currentVelocity = velocities[particleIndex];
|
||||||
|
|
||||||
|
let deltaTimeClamped = min(deltaTimeSeconds, 0.01);
|
||||||
|
|
||||||
|
currentVelocity += gravityAcceleration * deltaTimeClamped;
|
||||||
|
|
||||||
|
currentPosition += currentVelocity * deltaTimeClamped;
|
||||||
|
|
||||||
|
let friction = 0.98;
|
||||||
|
|
||||||
|
currentVelocity *= friction;
|
||||||
|
|
||||||
|
|
||||||
|
positions[particleIndex] = currentPosition;
|
||||||
|
|
||||||
|
velocities[particleIndex] = currentVelocity;
|
||||||
|
|
||||||
|
if ( updateDistancesAndIndices == 1u ) {
|
||||||
|
|
||||||
|
let diff = currentPosition - cameraPosition;
|
||||||
|
|
||||||
|
let dist = length(diff);
|
||||||
|
|
||||||
|
distances[ particleIndex ] = dist;
|
||||||
|
|
||||||
|
indices[ particleIndex ] = particleIndex;
|
||||||
|
|
||||||
|
positions[ particleIndex ] = currentPosition;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
18
shaders/initiateParticles.wgsl
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read> initiationPositions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<storage, read_write> velocities : array<vec2<f32>>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn initialize(@builtin(global_invocation_id) id : vec3<u32>) {
|
||||||
|
|
||||||
|
let i = id.x;
|
||||||
|
|
||||||
|
positions[i] = initiationPositions[i];
|
||||||
|
|
||||||
|
velocities[i] = vec2<f32>(0.0, 0.0);
|
||||||
|
}
|
||||||
88
shaders/localSort.wgsl
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> gridHashes: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> indices: array<u32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> totalCount: u32;
|
||||||
|
|
||||||
|
var<workgroup> sharedData: array<u32, 256>;
|
||||||
|
var<workgroup> sharedIndices: array<u32, 256>;
|
||||||
|
|
||||||
|
@compute @workgroup_size(256)
|
||||||
|
fn main(@builtin(local_invocation_id) local_id : vec3<u32>,
|
||||||
|
@builtin(global_invocation_id) global_id : vec3<u32>) {
|
||||||
|
|
||||||
|
let localIndex = local_id.x;
|
||||||
|
let globalIndex = global_id.x;
|
||||||
|
|
||||||
|
// Load element from global memory into shared memory if in range
|
||||||
|
if (globalIndex < totalCount) {
|
||||||
|
sharedData[localIndex] = gridHashes[globalIndex];
|
||||||
|
sharedIndices[localIndex] = indices[globalIndex];
|
||||||
|
} else {
|
||||||
|
sharedData[localIndex] = 0xffffffffu; // Max uint to push invalid values to the end
|
||||||
|
sharedIndices[localIndex] = 0xffffffffu; // or some invalid index
|
||||||
|
}
|
||||||
|
|
||||||
|
workgroupBarrier();
|
||||||
|
|
||||||
|
// Bitonic sort in shared memory on 256 elements
|
||||||
|
var size = 2u;
|
||||||
|
while (size <= 256u) {
|
||||||
|
var stride = size >> 1u;
|
||||||
|
|
||||||
|
var j = stride;
|
||||||
|
while (j > 0u) {
|
||||||
|
|
||||||
|
let ixj = localIndex ^ j;
|
||||||
|
|
||||||
|
if (ixj > localIndex) {
|
||||||
|
|
||||||
|
let ascending = ((localIndex & size) == 0u);
|
||||||
|
|
||||||
|
let valLocal = sharedData[localIndex];
|
||||||
|
let valIxj = sharedData[ixj];
|
||||||
|
|
||||||
|
var swap = false;
|
||||||
|
|
||||||
|
if ( ascending ) {
|
||||||
|
if ( valLocal > valIxj ) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (valLocal < valIxj) {
|
||||||
|
swap = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap) {
|
||||||
|
// Swap values
|
||||||
|
sharedData[localIndex] = valIxj;
|
||||||
|
sharedData[ixj] = valLocal;
|
||||||
|
|
||||||
|
// Swap indices as well
|
||||||
|
let idxLocal = sharedIndices[localIndex];
|
||||||
|
let idxIxj = sharedIndices[ixj];
|
||||||
|
|
||||||
|
sharedIndices[localIndex] = idxIxj;
|
||||||
|
sharedIndices[ixj] = idxLocal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
workgroupBarrier();
|
||||||
|
j = j >> 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
size = size << 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write sorted results back to global memory
|
||||||
|
if (globalIndex < totalCount) {
|
||||||
|
|
||||||
|
gridHashes[globalIndex] = sharedData[localIndex];
|
||||||
|
indices[globalIndex] = sharedIndices[localIndex];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
60
shaders/particle-header.wgsl
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read> positions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read> colors : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> viewProjectionMatrix : mat4x4<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<uniform> aspectRatio : f32;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> mousePos : vec2<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(5)
|
||||||
|
var<uniform> hoverRadius : f32;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) Position : vec4<f32>,
|
||||||
|
@location(0) color : vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn smoothStep(edge0: f32, edge1: f32, x: f32) -> f32 {
|
||||||
|
let t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
|
||||||
|
return t * t * (3.0 - 2.0 * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertex_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput {
|
||||||
|
var output : VertexOutput;
|
||||||
|
|
||||||
|
let pos = positions[vertexIndex];
|
||||||
|
var color = colors[vertexIndex];
|
||||||
|
|
||||||
|
let correctedPosition = vec4<f32>(
|
||||||
|
pos.x * aspectRatio,
|
||||||
|
pos.y,
|
||||||
|
pos.z,
|
||||||
|
pos.w
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change color if near mousePos:
|
||||||
|
// mousePos is passed as uniform vec2<f32> in clip space (-aspectRatio..aspectRatio, -1..1)
|
||||||
|
let dist = distance(vec2<f32>(pos.x* aspectRatio, pos.y), mousePos);
|
||||||
|
if (dist < hoverRadius) {
|
||||||
|
color = vec4<f32>(1.0, 0.0, 0.0, 1.0); // Red highlight
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Position = viewProjectionMatrix * correctedPosition;
|
||||||
|
|
||||||
|
output.color = color;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment_main(input : VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
return input.color;
|
||||||
|
}
|
||||||
73
shaders/points.wgsl
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
struct Point {
|
||||||
|
pos: vec3<f32>,
|
||||||
|
_pad: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BillboardAxis {
|
||||||
|
vector : vec3<f32>,
|
||||||
|
_pad : f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VSOut {
|
||||||
|
@builtin(position) Position : vec4<f32>,
|
||||||
|
@location(0) uv : vec2<f32>,
|
||||||
|
@location(1) color : vec3<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<storage, read> positions: array<Point>;
|
||||||
|
|
||||||
|
@group(0) @binding(1) var<storage, read> sortedIndices: array<u32>; // New binding for sorted indices
|
||||||
|
|
||||||
|
@group(0) @binding(2) var<uniform> viewProjectionMatrix: mat4x4<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3) var<uniform> cameraRight : BillboardAxis;
|
||||||
|
|
||||||
|
@group(0) @binding(4) var<uniform> cameraUp : BillboardAxis;
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertexEntryPoint(
|
||||||
|
@builtin(vertex_index) vertexIndex: u32,
|
||||||
|
@builtin(instance_index) instanceIndex: u32,
|
||||||
|
@location(0) quadOffset: vec2<f32>
|
||||||
|
) -> VSOut {
|
||||||
|
|
||||||
|
var output: VSOut;
|
||||||
|
|
||||||
|
// Use the sorted index to get the actual particle index
|
||||||
|
let actualIndex = sortedIndices[instanceIndex];
|
||||||
|
|
||||||
|
let point = positions[actualIndex];
|
||||||
|
let center = point.pos;
|
||||||
|
let radius = 0.03;
|
||||||
|
|
||||||
|
let rightOffset = cameraRight.vector * quadOffset.x * radius;
|
||||||
|
let upOffset = cameraUp.vector * quadOffset.y * radius;
|
||||||
|
|
||||||
|
let worldPos = vec4<f32>(center + rightOffset + upOffset, 1.0);
|
||||||
|
|
||||||
|
output.Position = viewProjectionMatrix * worldPos;
|
||||||
|
output.uv = quadOffset;
|
||||||
|
output.color = (center + vec3<f32>(1.0, 1.0, 1.0)) * 0.5;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragmentEntryPoint(
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
@location(1) color: vec3<f32>
|
||||||
|
) -> @location(0) vec4<f32> {
|
||||||
|
|
||||||
|
let dist = length(uv);
|
||||||
|
if (dist > 1.0) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
let z = sqrt(1.0 - dist * dist);
|
||||||
|
let normal = normalize(vec3<f32>(uv.x, uv.y, z));
|
||||||
|
let light = normalize(vec3<f32>(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
let diffuse = max(dot(normal, light), 0.0);
|
||||||
|
|
||||||
|
return vec4<f32>(color * diffuse, 1.0);
|
||||||
|
}
|
||||||
72
shaders/simpleGravity.wgsl
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<storage, read_write> positions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read_write> velocities : array<vec4<f32>>;
|
||||||
|
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> deltaTime : f32;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var<uniform> aspectRatio : f32;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var<uniform> mousePos : vec2<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(5)
|
||||||
|
var<uniform> hoverRadius : f32;
|
||||||
|
|
||||||
|
@group(0) @binding(6)
|
||||||
|
var<storage, read_write> states : array<u32>;
|
||||||
|
|
||||||
|
|
||||||
|
@compute @workgroup_size(64)
|
||||||
|
fn main(@builtin(global_invocation_id) id : vec3<u32>) {
|
||||||
|
|
||||||
|
let index = id.x;
|
||||||
|
let gravity = vec3<f32>(0.0, -29.61, 0.0);
|
||||||
|
|
||||||
|
var pos = positions[index];
|
||||||
|
var vel = velocities[index];
|
||||||
|
|
||||||
|
// Apply gravity to velocity
|
||||||
|
|
||||||
|
|
||||||
|
// Integrate position
|
||||||
|
let dist = distance(vec2<f32>(pos.x*aspectRatio, pos.y), mousePos);
|
||||||
|
|
||||||
|
var newPos = pos.xyz;
|
||||||
|
|
||||||
|
if (dist < hoverRadius) {
|
||||||
|
|
||||||
|
states[index] = 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( states[index] == 1 ) {
|
||||||
|
|
||||||
|
let newVel = vel.xyz + gravity * deltaTime;
|
||||||
|
|
||||||
|
newPos = pos.xyz + newVel * 0.001;
|
||||||
|
|
||||||
|
velocities[index] = vec4<f32>(newVel, vel.w);
|
||||||
|
|
||||||
|
positions[index] = vec4<f32>(newPos, pos.w);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
let newVel = vel.xyz;
|
||||||
|
|
||||||
|
newPos = pos.xyz + newVel * 0.001;
|
||||||
|
|
||||||
|
velocities[index] = vec4<f32>(newVel, vel.w);
|
||||||
|
|
||||||
|
|
||||||
|
positions[index] = vec4<f32>(newPos, pos.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store updated values
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
95
shaders/triangle-list-texture-array.wgsl
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> viewProjectionMatrix : mat4x4<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read> instancePositions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> cameraPosition : vec3<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var myTextureArray: texture_2d_array<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var mySampler : sampler;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position : vec4<f32>,
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>,
|
||||||
|
@location(2) uv : vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertexEntryPoint(
|
||||||
|
@location(0) position : vec3<f32>,
|
||||||
|
@location(1) normal : vec3<f32>,
|
||||||
|
@location(2) uv : vec2<f32>,
|
||||||
|
@builtin(instance_index) instanceIndex : u32
|
||||||
|
) -> VertexOutput {
|
||||||
|
var output : VertexOutput;
|
||||||
|
|
||||||
|
let instanceOffset = instancePositions[instanceIndex].xyz;
|
||||||
|
let worldPosition = position + instanceOffset;
|
||||||
|
|
||||||
|
output.worldPosition = worldPosition;
|
||||||
|
output.worldNormal = normalize(normal);
|
||||||
|
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
|
||||||
|
output.uv = uv;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fragmentEntryPoint(
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>,
|
||||||
|
@location(2) uv : vec2<f32>
|
||||||
|
) -> @location(0) vec4<f32> {
|
||||||
|
|
||||||
|
// For test: encode UV as color (no texture sampling)
|
||||||
|
//let baseColor = vec3<f32>(uv, 0.0);
|
||||||
|
|
||||||
|
let baseColor = textureSampleLevel(myTextureArray, mySampler, uv, 0, 0).rgb;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let pi = 3.14159265;
|
||||||
|
let invPi = 0.318309886;
|
||||||
|
let N = normalize(worldNormal);
|
||||||
|
let V = normalize(cameraPosition - worldPosition);
|
||||||
|
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
|
||||||
|
let H = normalize(V + L);
|
||||||
|
|
||||||
|
let metallic = 0.2;
|
||||||
|
let roughness = 0.4;
|
||||||
|
let rough2 = roughness * roughness;
|
||||||
|
let lightColor = vec3<f32>(1.0);
|
||||||
|
|
||||||
|
let NdotV = max(dot(N, V), 0.001);
|
||||||
|
let NdotL = max(dot(N, L), 0.001);
|
||||||
|
let NdotH = max(dot(N, H), 0.001);
|
||||||
|
let HdotV = max(dot(H, V), 0.001);
|
||||||
|
|
||||||
|
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
|
||||||
|
let f = pow(1.0 - HdotV, 5.0);
|
||||||
|
let F = F0 + (1.0 - F0) * f;
|
||||||
|
|
||||||
|
let a2 = rough2 * rough2;
|
||||||
|
let NdotH2 = NdotH * NdotH;
|
||||||
|
let denom = NdotH2 * (a2 - 1.0) + 1.0;
|
||||||
|
let NDF = a2 / (pi * denom * denom);
|
||||||
|
|
||||||
|
let k = (roughness + 1.0);
|
||||||
|
let k2 = (k * k) / 8.0;
|
||||||
|
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
|
||||||
|
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
|
||||||
|
let G = Gv * Gl;
|
||||||
|
|
||||||
|
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
|
||||||
|
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
|
||||||
|
let diff = kd * baseColor * invPi;
|
||||||
|
|
||||||
|
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
|
||||||
|
|
||||||
|
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
|
||||||
|
}
|
||||||
127
shaders/triangle-list-texture-normal.wgsl
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> viewProjectionMatrix : mat4x4<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read> instancePositions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> cameraPosition : vec3<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var myTexture : texture_2d<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var mySampler : sampler;
|
||||||
|
|
||||||
|
@group(0) @binding(5)
|
||||||
|
var normalMapTexture : texture_2d<f32>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position : vec4<f32>,
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>,
|
||||||
|
@location(2) worldBitangent : vec3<f32>,
|
||||||
|
@location(3) uv : vec2<f32>,
|
||||||
|
@location(4) meshIndex : f32
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertexEntryPoint(
|
||||||
|
@location(0) position : vec3<f32>,
|
||||||
|
@location(1) normal : vec3<f32>,
|
||||||
|
@location(2) bitangent : vec3<f32>,
|
||||||
|
@location(3) uv : vec2<f32>,
|
||||||
|
@builtin(instance_index) instanceIndex : u32
|
||||||
|
) -> VertexOutput {
|
||||||
|
|
||||||
|
var output : VertexOutput;
|
||||||
|
|
||||||
|
let instanceOffset = instancePositions[instanceIndex].xyz;
|
||||||
|
let worldPosition = position + instanceOffset;
|
||||||
|
|
||||||
|
output.worldPosition = worldPosition;
|
||||||
|
output.worldNormal = normalize(normal);
|
||||||
|
output.worldBitangent = normalize(bitangent);
|
||||||
|
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
|
||||||
|
output.uv = uv;
|
||||||
|
|
||||||
|
output.meshIndex = f32( instanceIndex );
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fragmentEntryPoint(
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>,
|
||||||
|
@location(2) worldBitangent : vec3<f32>,
|
||||||
|
@location(3) uv : vec2<f32>,
|
||||||
|
@location(4) meshIndex : f32
|
||||||
|
) -> @location(0) vec4<f32> {
|
||||||
|
|
||||||
|
// For test: encode UV as color (no texture sampling)
|
||||||
|
//let baseColor = vec3<f32>(uv, 0.0);
|
||||||
|
|
||||||
|
let baseColor = textureSample(myTexture, mySampler, uv).rgb;
|
||||||
|
|
||||||
|
|
||||||
|
// Sample normal map and decode
|
||||||
|
let normalMapSample = textureSample(normalMapTexture, mySampler, uv).rgb;
|
||||||
|
let tangentSpaceNormal = normalMapSample * 2.0 - 1.0; // Convert [0,1] to [-1,1]
|
||||||
|
|
||||||
|
// Construct TBN matrix
|
||||||
|
let n = normalize(worldNormal);
|
||||||
|
let B = normalize(worldBitangent);
|
||||||
|
let T = normalize(cross(B, n));
|
||||||
|
let TBN = mat3x3<f32>(T, B, n);
|
||||||
|
|
||||||
|
// Transform normal to world space
|
||||||
|
let mappedNormal = normalize(TBN * tangentSpaceNormal);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let pi = 3.14159265;
|
||||||
|
let invPi = 0.318309886;
|
||||||
|
let N = normalize(mappedNormal);
|
||||||
|
let V = normalize(cameraPosition - worldPosition);
|
||||||
|
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
|
||||||
|
let H = normalize(V + L);
|
||||||
|
|
||||||
|
|
||||||
|
let roughnessIndex = meshIndex / 30.0 % 1.0;
|
||||||
|
|
||||||
|
let metallic = 0.2;
|
||||||
|
let roughness = roughnessIndex;
|
||||||
|
let rough2 = roughness * roughness;
|
||||||
|
let lightColor = vec3<f32>(1.0);
|
||||||
|
|
||||||
|
let NdotV = max(dot(N, V), 0.001);
|
||||||
|
let NdotL = max(dot(N, L), 0.001);
|
||||||
|
let NdotH = max(dot(N, H), 0.001);
|
||||||
|
let HdotV = max(dot(H, V), 0.001);
|
||||||
|
|
||||||
|
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
|
||||||
|
let f = pow(1.0 - HdotV, 5.0);
|
||||||
|
let F = F0 + (1.0 - F0) * f;
|
||||||
|
|
||||||
|
let a2 = rough2 * rough2;
|
||||||
|
let NdotH2 = NdotH * NdotH;
|
||||||
|
let denom = NdotH2 * (a2 - 1.0) + 1.0;
|
||||||
|
let NDF = a2 / (pi * denom * denom);
|
||||||
|
|
||||||
|
let k = (roughness + 1.0);
|
||||||
|
let k2 = (k * k) / 8.0;
|
||||||
|
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
|
||||||
|
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
|
||||||
|
let G = Gv * Gl;
|
||||||
|
|
||||||
|
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
|
||||||
|
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
|
||||||
|
let diff = kd * baseColor * invPi;
|
||||||
|
|
||||||
|
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
|
||||||
|
|
||||||
|
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
|
||||||
|
}
|
||||||
93
shaders/triangle-list-texture.wgsl
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> viewProjectionMatrix : mat4x4<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read> instancePositions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> cameraPosition : vec3<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(3)
|
||||||
|
var myTexture : texture_2d<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(4)
|
||||||
|
var mySampler : sampler;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position : vec4<f32>,
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>,
|
||||||
|
@location(2) uv : vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertexEntryPoint(
|
||||||
|
@location(0) position : vec3<f32>,
|
||||||
|
@location(1) normal : vec3<f32>,
|
||||||
|
@location(2) uv : vec2<f32>,
|
||||||
|
@builtin(instance_index) instanceIndex : u32
|
||||||
|
) -> VertexOutput {
|
||||||
|
var output : VertexOutput;
|
||||||
|
|
||||||
|
let instanceOffset = instancePositions[instanceIndex].xyz;
|
||||||
|
let worldPosition = position + instanceOffset;
|
||||||
|
|
||||||
|
output.worldPosition = worldPosition;
|
||||||
|
output.worldNormal = normalize(normal);
|
||||||
|
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
|
||||||
|
output.uv = uv;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fragmentEntryPoint(
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>,
|
||||||
|
@location(2) uv : vec2<f32>
|
||||||
|
) -> @location(0) vec4<f32> {
|
||||||
|
|
||||||
|
// For test: encode UV as color (no texture sampling)
|
||||||
|
//let baseColor = vec3<f32>(uv, 0.0);
|
||||||
|
|
||||||
|
let baseColor = textureSample(myTexture, mySampler, uv).rgb;
|
||||||
|
|
||||||
|
let pi = 3.14159265;
|
||||||
|
let invPi = 0.318309886;
|
||||||
|
let N = normalize(worldNormal);
|
||||||
|
let V = normalize(cameraPosition - worldPosition);
|
||||||
|
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
|
||||||
|
let H = normalize(V + L);
|
||||||
|
|
||||||
|
let metallic = 0.2;
|
||||||
|
let roughness = 0.4;
|
||||||
|
let rough2 = roughness * roughness;
|
||||||
|
let lightColor = vec3<f32>(1.0);
|
||||||
|
|
||||||
|
let NdotV = max(dot(N, V), 0.001);
|
||||||
|
let NdotL = max(dot(N, L), 0.001);
|
||||||
|
let NdotH = max(dot(N, H), 0.001);
|
||||||
|
let HdotV = max(dot(H, V), 0.001);
|
||||||
|
|
||||||
|
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
|
||||||
|
let f = pow(1.0 - HdotV, 5.0);
|
||||||
|
let F = F0 + (1.0 - F0) * f;
|
||||||
|
|
||||||
|
let a2 = rough2 * rough2;
|
||||||
|
let NdotH2 = NdotH * NdotH;
|
||||||
|
let denom = NdotH2 * (a2 - 1.0) + 1.0;
|
||||||
|
let NDF = a2 / (pi * denom * denom);
|
||||||
|
|
||||||
|
let k = (roughness + 1.0);
|
||||||
|
let k2 = (k * k) / 8.0;
|
||||||
|
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
|
||||||
|
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
|
||||||
|
let G = Gv * Gl;
|
||||||
|
|
||||||
|
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
|
||||||
|
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
|
||||||
|
let diff = kd * baseColor * invPi;
|
||||||
|
|
||||||
|
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
|
||||||
|
|
||||||
|
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
|
||||||
|
}
|
||||||
78
shaders/triangle-list.wgsl
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> viewProjectionMatrix : mat4x4<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var<storage, read> instancePositions : array<vec4<f32>>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var<uniform> cameraPosition : vec3<f32>;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position : vec4<f32>,
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertexEntryPoint(
|
||||||
|
@location(0) position : vec3<f32>,
|
||||||
|
@location(1) normal : vec3<f32>,
|
||||||
|
@builtin(instance_index) instanceIndex : u32
|
||||||
|
) -> VertexOutput {
|
||||||
|
var output : VertexOutput;
|
||||||
|
|
||||||
|
let instanceOffset = instancePositions[instanceIndex].xyz;
|
||||||
|
let worldPosition = position + instanceOffset;
|
||||||
|
|
||||||
|
output.worldPosition = worldPosition;
|
||||||
|
output.worldNormal = normalize(normal);
|
||||||
|
output.position = viewProjectionMatrix * vec4<f32>(worldPosition, 1.0);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fragmentEntryPoint(
|
||||||
|
@location(0) worldPosition : vec3<f32>,
|
||||||
|
@location(1) worldNormal : vec3<f32>
|
||||||
|
) -> @location(0) vec4<f32> {
|
||||||
|
let pi = 3.14159265;
|
||||||
|
let invPi = 0.318309886;
|
||||||
|
let N = normalize(worldNormal);
|
||||||
|
let V = normalize(cameraPosition - worldPosition);
|
||||||
|
let L = normalize(vec3<f32>(0.5, 1.0, 0.3));
|
||||||
|
let H = normalize(V + L);
|
||||||
|
|
||||||
|
let baseColor = vec3<f32>(1.0);
|
||||||
|
let metallic = 0.2;
|
||||||
|
let roughness = 0.4;
|
||||||
|
let rough2 = roughness * roughness;
|
||||||
|
let lightColor = vec3<f32>(1.0);
|
||||||
|
|
||||||
|
let NdotV = max(dot(N, V), 0.001);
|
||||||
|
let NdotL = max(dot(N, L), 0.001);
|
||||||
|
let NdotH = max(dot(N, H), 0.001);
|
||||||
|
let HdotV = max(dot(H, V), 0.001);
|
||||||
|
|
||||||
|
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
|
||||||
|
let f = pow(1.0 - HdotV, 5.0);
|
||||||
|
let F = F0 + (1.0 - F0) * f;
|
||||||
|
|
||||||
|
let a2 = rough2 * rough2;
|
||||||
|
let NdotH2 = NdotH * NdotH;
|
||||||
|
let denom = NdotH2 * (a2 - 1.0) + 1.0;
|
||||||
|
let NDF = a2 / (pi * denom * denom);
|
||||||
|
|
||||||
|
let k = (roughness + 1.0);
|
||||||
|
let k2 = (k * k) / 8.0;
|
||||||
|
let Gv = NdotV / (NdotV * (1.0 - k2) + k2);
|
||||||
|
let Gl = NdotL / (NdotL * (1.0 - k2) + k2);
|
||||||
|
let G = Gv * Gl;
|
||||||
|
|
||||||
|
let spec = (NDF * G * F) / (4.0 * NdotV * NdotL + 0.001);
|
||||||
|
let kd = (vec3<f32>(1.0) - F) * (1.0 - metallic);
|
||||||
|
let diff = kd * baseColor * invPi;
|
||||||
|
|
||||||
|
let color = (diff + spec) * lightColor * NdotL + vec3<f32>(0.03) * baseColor;
|
||||||
|
|
||||||
|
return vec4<f32>(pow(color, vec3<f32>(1.0 / 2.2)), 1.0);
|
||||||
|
}
|
||||||