blob: 66df36cff7c02f758f48e7101488fd7b55a8d53c [file] [log] [blame]
if (!navigator.gpu || GPUBufferUsage.COPY_SRC === undefined)
document.body.className = "error";
const isChrome = navigator.userAgent.indexOf("Chrome") >= 0;
const numParticles = 2;
const numThreadsInThreadGroup = 2;
const numThreadGroups = numParticles / numThreadsInThreadGroup;
const computeShaderGLSL = `#version 450
struct Particle {
vec2 pos;
vec2 vel;
};
layout(std140, set = 0, binding = 0) uniform SimParams {
float deltaT;
float rule1Distance;
float rule2Distance;
float rule3Distance;
float rule1Scale;
float rule2Scale;
float rule3Scale;
} params;
layout(std140, set = 0, binding = 1) buffer ParticlesA {
Particle particles[${numParticles}];
} particlesA;
layout(std140, set = 0, binding = 2) buffer ParticlesB {
Particle particles[${numParticles}];
} particlesB;
layout(local_size_x = ${numThreadsInThreadGroup}, local_size_y = 1, local_size_z = 1) in;
void main() {
// https://github.com/austinEng/Project6-Vulkan-Flocking/blob/master/data/shaders/computeparticles/particle.comp
uint index = gl_GlobalInvocationID.x;
if (index >= ${numParticles}) {
return;
}
vec2 vPos = particlesA.particles[index].pos;
vec2 vVel = particlesA.particles[index].vel;
vec2 cMass = vec2(0.0, 0.0);
vec2 cVel = vec2(0.0, 0.0);
vec2 colVel = vec2(0.0, 0.0);
int cMassCount = 0;
int cVelCount = 0;
//float rand = ${Math.random()};
float rand = 0;
vec2 pos;
vec2 vel;
for (int i = 0; i < ${numParticles}; ++i) {
if (i == index) { continue; }
pos = particlesA.particles[i].pos.xy;
vel = particlesA.particles[i].vel.xy;
if (distance(pos, vPos) < params.rule1Distance) {
cMass += pos;
cMassCount++;
}
if (distance(pos, vPos) < params.rule2Distance) {
colVel -= (pos - vPos);
}
if (distance(pos, vPos) < params.rule3Distance) {
cVel += vel;
cVelCount++;
}
}
if (cMassCount > 0) {
cMass = cMass / cMassCount - vPos;
}
if (cVelCount > 0) {
cVel = cVel / cVelCount;
}
vVel += cMass * params.rule1Scale + colVel * params.rule2Scale + cVel * params.rule3Scale;
// clamp velocity for a more pleasing simulation.
vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1);
// kinematic update
vPos += vVel * params.deltaT;
// Wrap around boundary
if (vPos.x < -1.0) vPos.x = 1.0;
if (vPos.x > 1.0) vPos.x = -1.0;
if (vPos.y < -1.0) vPos.y = 1.0;
if (vPos.y > 1.0) vPos.y = -1.0;
particlesB.particles[index].pos = vPos;
//particlesB.particles[index].vel = vVel;
particlesB.particles[index].vel = vVel + rand;
}
`;
const computeShaderWHLSL = `
struct Particle {
float2 pos;
float2 vel;
}
struct SimParams {
float deltaT;
float rule1Distance;
float rule2Distance;
float rule3Distance;
float rule1Scale;
float rule2Scale;
float rule3Scale;
}
[numthreads(${numThreadsInThreadGroup}, 1, 1)]
compute void main(device SimParams[] paramsBuffer : register(u0), device Particle[] particlesA : register(u1), device Particle[] particlesB : register(u2), float3 threadID : SV_DispatchThreadID) {
uint index = uint(threadID.x);
SimParams params = paramsBuffer[0];
if (index >= ${numParticles}) {
return;
}
float2 vPos = particlesA[index].pos;
float2 vVel = particlesA[index].vel;
float2 cMass = float2(0.0, 0.0);
float2 cVel = float2(0.0, 0.0);
float2 colVel = float2(0.0, 0.0);
float cMassCount = 0.0;
float cVelCount = 0.0;
//float rand = 25;
float2 pos;
float2 vel;
for (uint i = 0; i < ${numParticles}; ++i) {
if (i == index) { continue; }
pos = particlesA[i].pos.xy;
vel = particlesA[i].vel.xy;
if (distance(pos, vPos) < params.rule1Distance) {
cMass += pos;
cMassCount++;
}
if (distance(pos, vPos) < params.rule2Distance) {
colVel -= (pos - vPos);
}
if (distance(pos, vPos) < params.rule3Distance) {
cVel += vel;
cVelCount++;
}
}
if (cMassCount > 0.0) {
cMass = cMass / cMassCount - vPos;
}
if (cVelCount > 0.0) {
cVel = cVel / cVelCount;
}
vVel += cMass * params.rule1Scale + colVel * params.rule2Scale + cVel * params.rule3Scale;
// clamp velocity for a more pleasing simulation.
vVel = normalize(vVel) * clamp(length(vVel), 0.0, 0.1);
// kinematic update
vPos += vVel * params.deltaT;
// Wrap around boundary
if (vPos.x < -1.0) vPos.x = 1.0;
if (vPos.x > 1.0) vPos.x = -1.0;
if (vPos.y < -1.0) vPos.y = 1.0;
if (vPos.y > 1.0) vPos.y = -1.0;
particlesB[index].pos = vPos;
particlesB[index].vel = vVel;
}
`;
async function setData(buffer, data)
{
if (isChrome) {
buffer.setSubData(0, data);
return;
}
const bufferArrayBuffer = await buffer.mapWriteAsync();
const array = new Float32Array(bufferArrayBuffer);
for (let i = 0; i < array.length; ++i)
array[i] = data[i];
buffer.unmap();
}
function appendMessage(msg) {
let d = document.getElementById("results");
let p = document.createElement('p');
p.innerHTML = msg;
d.append(p);
}
async function init() {
if (!isChrome) {
GPUBufferUsage.COPY_DST = GPUBufferUsage.TRANSFER_DST;
GPUBufferUsage.COPY_SRC = GPUBufferUsage.TRANSFER_SRC;
}
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
await Utils.ready;
const canvas = document.querySelector('canvas');
const context = canvas.getContext('gpupresent');
let computeBindGroupLayout;
if (isChrome) {
computeBindGroupLayout = device.createBindGroupLayout({
bindings: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, type: "uniform-buffer" },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" },
],
});
} else {
computeBindGroupLayout = device.createBindGroupLayout({
bindings: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, type: "storage-buffer" },
],
});
}
const computePipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [computeBindGroupLayout],
});
let computeCode;
if (isChrome)
computeCode = Utils.compile("c", computeShaderGLSL);
else
computeCode = computeShaderWHLSL;
const startTime = Date.now();
const computePipeline = device.createComputePipeline({
layout: computePipelineLayout,
computeStage: {
module: device.createShaderModule({
code: computeCode,
isWHLSL: isChrome ? false : true,
}),
entryPoint: "main",
}
});
const simParamData = new Float32Array([0.04, 0.1, 0.025, 0.025, 0.02, 0.05, 0.005]);
let simParamBuffer
if (isChrome) {
simParamBuffer = device.createBuffer({
size: simParamData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
} else {
simParamBuffer = device.createBuffer({
size: simParamData.byteLength,
usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
}
await setData(simParamBuffer, simParamData);
const initialParticleData = new Float32Array(numParticles * 4);
for (let i = 0; i < numParticles; ++i) {
//initialParticleData[4 * i + 0] = 2 * (Math.random() - 0.5);
//initialParticleData[4 * i + 1] = 2 * (Math.random() - 0.5);
//initialParticleData[4 * i + 2] = 2 * (Math.random() - 0.5) * 0.1;
//initialParticleData[4 * i + 3] = 2 * (Math.random() - 0.5) * 0.1;
initialParticleData[4 * i + 0] = 5;
initialParticleData[4 * i + 1] = 5;
initialParticleData[4 * i + 2] = 5;
initialParticleData[4 * i + 3] = 5;
}
const particleBuffers = new Array(2);
particleBuffers[0] = device.createBuffer({
size: initialParticleData.byteLength,
usage: (!isChrome ? GPUBufferUsage.MAP_WRITE : 0) | GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE,
});
if (isChrome) {
particleBuffers[1] = device.createBuffer({
size: initialParticleData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
} else {
particleBuffers[1] = device.createBuffer({
size: initialParticleData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
}
await setData(particleBuffers[0], initialParticleData);
const bindGroup = device.createBindGroup({
layout: computeBindGroupLayout,
bindings: [{
binding: 0,
resource: {
buffer: simParamBuffer,
offset: 0,
size: simParamData.byteLength
},
}, {
binding: 1,
resource: {
buffer: particleBuffers[0],
offset: 0,
size: initialParticleData.byteLength,
},
}, {
binding: 2,
resource: {
buffer: particleBuffers[1],
offset: 0,
size: initialParticleData.byteLength,
},
}],
});
async function foo() {
let result = device.createBuffer({
size: initialParticleData.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const commandEncoder = device.createCommandEncoder({});
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatch(numThreadGroups, 1, 1);
passEncoder.endPass();
if (isChrome)
commandEncoder.copyBufferToBuffer(particleBuffers[1], 0, result, 0, initialParticleData.byteLength);
device.getQueue().submit([commandEncoder.finish()]);
let resultsArrayBuffer;
if (isChrome) {
resultsArrayBuffer = await result.mapReadAsync();
} else {
resultsArrayBuffer = await particleBuffers[1].mapReadAsync();
}
appendMessage(`total time: ${Date.now() - startTime} ms`);
const resultsArray = new Float32Array(resultsArrayBuffer);
//{
// try {
// const error = await device.popErrorScope();
// if (error)
// appendMessage("Error: " + error);
// } catch(e) {
// console.log(e);
// }
//}
try {
if (resultsArray.length !== numParticles*4)
throw new Error("Bad length");
for (let i = 0; i < resultsArray.length; i += 4) {
if (resultsArray[i] !== -1)
throw new Error("not -1, is: " + resultsArray[i]);
if (resultsArray[i + 1] !== -1)
throw new Error("not -1, is: " + resultsArray[i + 1]);
if (resultsArray[i + 2] !== 0.0707106813788414)
throw new Error("not fraction");
if (resultsArray[i + 3] !== 0.0707106813788414)
throw new Error("not fraction");
}
appendMessage("Success.");
} catch (e) {
appendMessage(` error: ${e}`);
}
}
foo();
}
window.onload = init;