particle smoothing function sandbox

This commit is contained in:
Zack Buhman 2026-07-31 16:08:21 -05:00
parent 5386b8bee9
commit b198b61bb7
5 changed files with 411 additions and 26 deletions

175
flat.js
View File

@ -1,6 +1,91 @@
import { getPath } from "./common.js"; import { getPath } from "./common.js";
function test(position)
{
const x = position[0] * 4 * Math.PI;
const y = position[1] * 4 * Math.PI;
return Math.cos(y - 3 + Math.sin(x)) * 0.5 + 0.5;
}
function length(vector)
{
return Math.sqrt(vector[0] * vector[0] + vector[1] * vector[1]);
}
function sub(a, b)
{
return [
a[0] - b[0],
a[1] - b[1],
];
}
function smoothing(radius, distance)
{
const volume = Math.PI * Math.pow(radius, 8) / 4;
const value = Math.max(0, radius * radius - distance * distance);
return value * value * value / volume;
}
class FlatRenderer { class FlatRenderer {
createParticles(device)
{
this.maxDim = 32;
this.maxParticles = this.maxDim * this.maxDim;
this.particleStride = 4 * 2; // in elements
this.particles = new Float32Array(this.maxParticles * this.particleStride);
this.particleBuffers = [];
this.particleBindGroups = [];
this.particleConfiguration = new Float32Array(3 * 4);
this.particleConfigurationBuffers = [];
for (let i = 0; i < 2; i++) {
const buffer = device.createBuffer({
label: `particle buffer ${i}`,
size: this.particles.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
this.particleBuffers.push(buffer);
const configBuffer = device.createBuffer({
label: `particle configuration buffer ${i}`,
size: this.particleConfiguration.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.particleConfigurationBuffers.push(configBuffer);
const bindGroup = device.createBindGroup({
label: `particle buffer bind group ${i}`,
layout: this.bindGroupLayout.particle,
entries: [{
binding: 0,
resource: { buffer: configBuffer },
}, {
binding: 1,
resource: { buffer: buffer },
}],
});
this.particleBindGroups.push(bindGroup);
}
for (let y = 0; y < this.maxDim; y++) {
for (let x = 0; x < this.maxDim; x++) {
const index = y * this.maxDim + x;
//this.particles[index * this.particleStride + 0] = (x / this.maxDim) + 0.5 / this.maxDim;
//this.particles[index * this.particleStride + 1] = (y / this.maxDim) + 0.5 / this.maxDim;
// position
const position = [Math.random(), Math.random()];
this.particles[index * this.particleStride + 0] = position[0];
this.particles[index * this.particleStride + 1] = position[1];
this.particles[index * this.particleStride + 2] = 0.0;
this.particles[index * this.particleStride + 3] = 1.0;
// value.x
this.particles[index * this.particleStride + 4] = test(position);
}
}
}
constructor(device, canvasFormat, viewUniformBuffer, shaderModule) constructor(device, canvasFormat, viewUniformBuffer, shaderModule)
{ {
const label = "flat"; const label = "flat";
@ -29,20 +114,35 @@ class FlatRenderer {
// pipeline // pipeline
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
const bindGroupLayouts = [ this.bindGroupLayout = {
device.createBindGroupLayout({ view: device.createBindGroupLayout({
label: `${label} view matrix bind group layout`, label: `${label} bind group layout view`,
entries: [{ entries: [{
binding: 0, binding: 0,
visibility: GPUShaderStage.VERTEX, visibility: GPUShaderStage.VERTEX,
buffer: { type: "uniform" } buffer: { type: "uniform" }
}] }]
}), }),
]; particle: device.createBindGroupLayout({
label: `${label} bind group layout particle`,
entries: [{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
buffer: { type: "uniform" }
}, {
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
buffer: { type: "read-only-storage" }
}]
}),
};
const pipelineLayout = device.createPipelineLayout({ const pipelineLayout = device.createPipelineLayout({
label: `${label} pipeline layout`, label: `${label} pipeline layout`,
bindGroupLayouts: bindGroupLayouts, bindGroupLayouts: [
this.bindGroupLayout.view,
this.bindGroupLayout.particle
],
}); });
this.renderPipeline = device.createRenderPipeline({ this.renderPipeline = device.createRenderPipeline({
@ -69,6 +169,12 @@ class FlatRenderer {
}, },
}); });
//////////////////////////////////////////////////////////////////////
// particles
//////////////////////////////////////////////////////////////////////
this.createParticles(device);
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// bind group // bind group
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
@ -76,7 +182,7 @@ class FlatRenderer {
this.bindGroups = [ this.bindGroups = [
device.createBindGroup({ device.createBindGroup({
label: `${label} bind group`, label: `${label} bind group`,
layout: bindGroupLayouts[0], layout: this.bindGroupLayout.view,
entries: [{ entries: [{
binding: 0, binding: 0,
resource: { buffer: viewUniformBuffer }, resource: { buffer: viewUniformBuffer },
@ -85,11 +191,66 @@ class FlatRenderer {
]; ];
} }
render(renderPass) positionDensity(position, circleRadius, particleCount)
{ {
var density = 0.0;
for (var i = 0; i < particleCount; i++) {
const particlePosition = [this.particles[i * this.particleStride + 0],
this.particles[i * this.particleStride + 1]];
const distance = length(sub(position, particlePosition));
const influence = smoothing(circleRadius, distance);
density += influence;
}
return density;
}
updateParticles(device, frameNumber, configuration)
{
for (var i = 0; i < configuration.particleCount; i++) {
const position = [this.particles[i * this.particleStride + 0],
this.particles[i * this.particleStride + 1]];
const density = this.positionDensity(position,
configuration.circleRadius,
configuration.particleCount);
// value.y
this.particles[i * this.particleStride + 5] = density;
}
device.queue.writeBuffer(this.particleBuffers[frameNumber], 0,
this.particles, 0, this.particles.length);
const PARTICLE = 0;
const CIRCLE = 1;
const MOUSE = 2;
const config = this.particleConfiguration;
config[PARTICLE * 4 + 0] = configuration.particleCount;
config[PARTICLE * 4 + 1] = configuration.particleSize;
config[PARTICLE * 4 + 2] = configuration.particleMass1;
config[PARTICLE * 4 + 3] = configuration.particleMass2;
config[CIRCLE * 4 + 0] = configuration.circleRadius;
config[CIRCLE * 4 + 1] = configuration.circleThickness;
config[CIRCLE * 4 + 2] = configuration.testIntensity;
config[CIRCLE * 4 + 3] = configuration.mass12Mix;
config[MOUSE * 4 + 0] = configuration.mousePosition[0];
config[MOUSE * 4 + 1] = configuration.mousePosition[1];
config[MOUSE * 4 + 2] = configuration.mousePosition[2];
config[MOUSE * 4 + 3] = configuration.mousePosition[3];
device.queue.writeBuffer(this.particleConfigurationBuffers[frameNumber], 0,
this.particleConfiguration, 0, this.particleConfiguration.length)
}
render(device, renderPass, frameNumber, configuration)
{
this.updateParticles(device, frameNumber, configuration);
renderPass.setPipeline(this.renderPipeline); renderPass.setPipeline(this.renderPipeline);
renderPass.setIndexBuffer(this.buffer, "uint16"); renderPass.setIndexBuffer(this.buffer, "uint16");
renderPass.setBindGroup(0, this.bindGroups[0]); renderPass.setBindGroup(0, this.bindGroups[0]);
renderPass.setBindGroup(1, this.particleBindGroups[frameNumber]);
renderPass.drawIndexed(6); renderPass.drawIndexed(6);
} }
} }

View File

@ -5,7 +5,20 @@ struct Configuration {
eyePosition: vec4f, eyePosition: vec4f,
}; };
struct Particle {
position: vec4f,
value: vec4f,
};
struct ParticleConfiguration {
particle: vec4f,
circle: vec4f,
mouse: vec4f,
};
@group(0) @binding(0) var<uniform> config: Configuration; @group(0) @binding(0) var<uniform> config: Configuration;
@group(1) @binding(0) var<uniform> particleConfiguration: ParticleConfiguration;
@group(1) @binding(1) var<storage> particle: array<Particle>;
struct VertexInput { struct VertexInput {
@builtin(vertex_index) vertex_index: u32, @builtin(vertex_index) vertex_index: u32,
@ -27,16 +40,85 @@ const vertices = array(
fn vertexMain(input: VertexInput) -> VertexOutput fn vertexMain(input: VertexInput) -> VertexOutput
{ {
let texture = vertices[input.vertex_index]; let texture = vertices[input.vertex_index];
let position = vec4f(texture * 2.0 - 1.0, 0.0, 1.0);
var output: VertexOutput; var output: VertexOutput;
output.position = position; output.position = vec4f(texture * 2.0 - 1.0, 0.0, 1.0);
output.texture = texture; output.texture = texture;
return output; return output;
} }
const pi: f32 = 3.14159274101257324219;
fn smoothing(radius: f32, distance: f32) -> f32
{
let volume = pi * pow(radius, 8) / 4;
let value = max(0, radius * radius - distance * distance);
return value * value * value / volume;
}
fn test(position: vec2f) -> f32
{
let x = position.x * 4 * pi;
let y = position.y * 4 * pi;
return cos(y - 3 + sin(x)) * 0.5 + 0.5;
}
fn positionDensity(position: vec2f, circleRadius: f32, particleCount: u32) -> f32
{
var density: f32 = 0.0;
for (var i: u32 = 0; i < u32(particleCount); i++) {
let distance = length(position - particle[i].position.xy);
let influence = smoothing(circleRadius, distance);
density += influence;
}
return density;
}
@fragment @fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
{ {
return vec4(input.texture, 0.0, 1.0); var color: vec4f = vec4f(0.0, 0.0, 0.0, 0.0);
let particleCount = u32(particleConfiguration.particle.x);
let particleSize = particleConfiguration.particle.y;
let particleMass1 = particleConfiguration.particle.z;
let particleMass2 = particleConfiguration.particle.w;
let circleRadius = particleConfiguration.circle.x;
let circleThickness = particleConfiguration.circle.y;
let testIntensity = particleConfiguration.circle.z;
let mass12Mix = particleConfiguration.circle.w;
let circlePosition = particleConfiguration.mouse;
var property: f32 = 0.0;
for (var i: u32 = 0; i < particleCount; i++) {
let distance = length(input.texture.xy - particle[i].position.xy);
if (distance < particleSize) {
color.x = particle[i].value.x;
}
//let circleDistance = length(input.texture.xy - circlePosition.xy);
let influence = smoothing(circleRadius, distance);
//let particleDensity = positionDensity(particle[i].position.xy, circleRadius, particleCount);
let particleProperty = particle[i].value.x;
let particleDensity = particle[i].value.y;
let p1 = particleProperty * influence;
let p2 = p1 / particleDensity;
property += mix(p1, p2, mass12Mix);
}
color.z = property * mix(particleMass1, particleMass2, mass12Mix);
if (circlePosition.w != 0) {
let thickness = abs(length(input.texture.xy - circlePosition.xy) - circleRadius);
if (thickness < circleThickness) {
//color.y = 1.0;
}
}
let testColor = test(input.texture.xy);
return vec4(color.xz, testColor * testIntensity, 1.0);
//return vec4(color.x, 0, 0, 1.0);
} }

View File

@ -11,6 +11,9 @@
div, span { div, span {
font: 0.8rem sans-serif; font: 0.8rem sans-serif;
} }
span {
padding-bottom: 0.1rem;
}
select, option { select, option {
font: 0.8rem sans-serif; font: 0.8rem sans-serif;
} }
@ -23,6 +26,12 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
input[type="range"] {
height: 0.9rem;
width: 70%;
}
.control-root { .control-root {
position: fixed; position: fixed;
left: 0; left: 0;
@ -32,13 +41,13 @@
background: #eee; background: #eee;
} }
.control-main { .control-main {
width: 250px; width: 310px;
float: right; float: right;
margin-right: 15px; margin-right: 15px;
background: #444; background: #444;
} }
.label { .label {
width: calc(40% - 0.3rem); width: calc(30% - 0.3rem);
float: left; float: left;
clear: left; clear: left;
overflow: hidden; overflow: hidden;
@ -46,7 +55,7 @@
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.value { .value {
width: calc(60% - 0.2rem); width: calc(70% - 0.2rem);
float: left; float: left;
margin-top: 0.1rem; margin-top: 0.1rem;
margin-right: 0.2rem; margin-right: 0.2rem;
@ -57,9 +66,9 @@
.row { .row {
height: 1.5rem; height: 1.5rem;
} }
code { .code, code {
font: 0.95rem monospace; font: 0.85rem monospace;
padding-top: 0.1rem; padding-top: 0.15rem;
} }
.float3 { .float3 {
width: 30%; width: 30%;
@ -71,6 +80,9 @@
display: inline-block; display: inline-block;
text-align: right; text-align: right;
} }
.range-span {
float: right;
}
</style> </style>
<script src="index.js" type="module"></script> <script src="index.js" type="module"></script>
<!--<script src="index2.js" type="module"></script>--> <!--<script src="index2.js" type="module"></script>-->
@ -78,7 +90,7 @@
<body> <body>
<canvas></canvas> <canvas></canvas>
<div class="control-root"> <div class="control-root">
<div class="control-main"> <div class="control-main" id="control-main">
<!-- <!--
<div class="row"> <div class="row">
<span class="label">model</span> <span class="label">model</span>
@ -92,6 +104,7 @@
</select> </select>
</div> </div>
--> -->
<!--
<div class="row"> <div class="row">
<span class="label">eye</span> <span class="label">eye</span>
<div class="value"> <div class="value">
@ -107,6 +120,7 @@
<code class="float2" id="pitch">0.0</code> <code class="float2" id="pitch">0.0</code>
</div> </div>
</div> </div>
-->
<!-- <!--
<div class="row"> <div class="row">
<span class="label">test2</span> <span class="label">test2</span>
@ -115,7 +129,6 @@
</select> </select>
</div> </div>
--> -->
</div>
</div> </div>
</body> </body>
</html> </html>

142
index.js
View File

@ -65,9 +65,9 @@ const eyeValueZ = document.getElementById("eye-value-z");
const yawValue = document.getElementById("yaw"); const yawValue = document.getElementById("yaw");
const pitchValue = document.getElementById("pitch"); const pitchValue = document.getElementById("pitch");
function recreateDepth() function recreateDepth(force)
{ {
if (canvas.clientWidth !== canvas.width || canvas.clientWidth !== canvas.height) { if (canvas.clientWidth !== canvas.width || canvas.clientWidth !== canvas.height || force === true) {
canvas.width = canvas.clientWidth; canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight; canvas.height = canvas.clientHeight;
@ -190,21 +190,140 @@ function updateView()
viewUniformBufferSize); viewUniformBufferSize);
const cameraStateF32 = new Float32Array(memory.buffer, cameraStateAddress, cameraStateSize); const cameraStateF32 = new Float32Array(memory.buffer, cameraStateAddress, cameraStateSize);
/*
eyeValueX.innerHTML = cameraStateF32[0].toFixed(1); eyeValueX.innerHTML = cameraStateF32[0].toFixed(1);
eyeValueY.innerHTML = cameraStateF32[1].toFixed(1); eyeValueY.innerHTML = cameraStateF32[1].toFixed(1);
eyeValueZ.innerHTML = cameraStateF32[2].toFixed(1); eyeValueZ.innerHTML = cameraStateF32[2].toFixed(1);
yawValue.innerHTML = cameraStateF32[9].toFixed(2); yawValue.innerHTML = cameraStateF32[9].toFixed(2);
pitchValue.innerHTML = cameraStateF32[10].toFixed(2); pitchValue.innerHTML = cameraStateF32[10].toFixed(2);
*/
} }
const controlMain = document.getElementById("control-main");
function remap(low1, high1, low2, high2, value)
{
return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
}
const sliderDefaults = {
"circle radius": 385,
"circle thickness": 284,
"mass12 mix": 1,
"particle count": 360,
"particle mass1": 126,
"particle mass2": 417,
"particle size": 51,
"test intensity": 0,
};
class Slider {
constructor(label, low, high, max = 1024)
{
const labelE = document.createElement("span");
labelE.className = "label";
labelE.innerHTML = label;
const valueE = document.createElement("div");
valueE.className = "value";
const inputE = document.createElement("input");
inputE.className = "range";
inputE.min = 0;
inputE.max = max;
inputE.type = "range";
const spanE = document.createElement("span");
spanE.className = "range-span code";
valueE.appendChild(inputE);
valueE.appendChild(spanE);
controlMain.appendChild(labelE);
controlMain.appendChild(valueE);
this.label = label;
this.input = inputE;
this.span = spanE;
this.low = low;
this.high = high;
this.lastValue = undefined;
const storageValue = localStorage.getItem(this.label);
if (storageValue !== null) {
this.input.value = storageValue;
} else if (this.label in sliderDefaults) {
this.input.value = sliderDefaults[this.label];
}
this.update();
}
update()
{
const rawValue = parseInt(this.input.value);
const value = remap(this.input.min, this.input.max, this.low, this.high, this.input.value);
if (rawValue !== this.lastValue) {
this.lastValue = rawValue;
localStorage.setItem(this.label, rawValue);
}
if (this.high > 10) {
this.span.innerHTML = value.toFixed(0);
} else {
this.span.innerHTML = value.toFixed(5);
}
return value;
}
}
const sliders = {
particleSize: new Slider("particle size", 0.0, 0.05),
particleMass1: new Slider("particle mass1", 0.0, 0.01),
particleMass2: new Slider("particle mass2", 0.0, 1.0),
particleCount: new Slider("particle count", 0.0, 1024.0),
circleRadius: new Slider("circle radius", 0.0, 0.5),
circleThickness: new Slider("circle thickness", 0.0, 0.01),
testIntensity: new Slider("test intensity", 0.0, 2.0),
mass12Mix: new Slider("mass12 mix", 0.0, 1.0, 1),
};
const mousePosition = new Float32Array([0, 0, 0, 0]);
function updateSliders()
{
return {
mousePosition: mousePosition,
particleSize: sliders.particleSize.update(),
particleMass1: sliders.particleMass1.update(),
particleMass2: sliders.particleMass2.update(),
particleCount: sliders.particleCount.update(),
circleRadius: sliders.circleRadius.update(),
circleThickness: sliders.circleThickness.update(),
testIntensity: sliders.testIntensity.update(),
mass12Mix: sliders.mass12Mix.update(),
};
}
function onClick(e)
{
mousePosition[0] = e.clientX / canvas.width;
mousePosition[1] = 1.0 - (e.clientY / canvas.height);
mousePosition[2] = 0;
mousePosition[3] = 1;
}
canvas.addEventListener("click", onClick);
var canRender = false;
var frameNumber = 0;
function render2() function render2()
{ {
recreateDepth(); if (canRender === true) {
const colorView = context.getCurrentTexture().createView(); const configuration = updateSliders();
updateView(); canRender = false;
recreateDepth(false);
const colorView = context.getCurrentTexture().createView();
updateView();
{
const encoder = device.createCommandEncoder(); const encoder = device.createCommandEncoder();
const renderPass = encoder.beginRenderPass({ const renderPass = encoder.beginRenderPass({
colorAttachments: [{ colorAttachments: [{
@ -223,14 +342,23 @@ function render2()
//gltfRenderer.render(renderPass); //gltfRenderer.render(renderPass);
//lightRenderer.render(renderPass); //lightRenderer.render(renderPass);
flatRenderer.render(renderPass); flatRenderer.render(device, renderPass, frameNumber, configuration);
renderPass.end(); renderPass.end();
const commandBuffer = encoder.finish(); const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]); device.queue.submit([commandBuffer]);
frameNumber = (frameNumber + 1) % 2;
} }
requestAnimationFrame(render2); requestAnimationFrame(render2);
} }
function setCanRender()
{
canRender = true;
}
setInterval(setCanRender, 66.67); // 15 fps
recreateDepth(true);
requestAnimationFrame(render2); requestAnimationFrame(render2);

View File

@ -148,5 +148,6 @@ void camera_view_projection(CameraState * camera_state,
XMStoreFloat4x4(&view_state->viewProj, view_projection); XMStoreFloat4x4(&view_state->viewProj, view_projection);
XMStoreFloat4x4(&view_state->lightViewProj, light_world * view_projection); XMStoreFloat4x4(&view_state->lightViewProj, light_world * view_projection);
XMStoreFloat4(&view_state->lightPosition, light_position); XMStoreFloat4(&view_state->lightPosition, light_position);
XMStoreFloat4(&view_state->eyePosition, XMLoadFloat3(&camera_state->eye)); XMVECTOR eye = XMLoadFloat3(&camera_state->eye);
XMStoreFloat4(&view_state->eyePosition, eye);
} }