broken particle motion

This commit is contained in:
Zack Buhman 2026-08-01 19:54:01 -05:00
parent 1a168698a5
commit 136a752e04
3 changed files with 185 additions and 30 deletions

144
flat.js
View File

@ -27,6 +27,20 @@ function smoothing(radius, distance)
return value * value * value / volume; return value * value * value / volume;
} }
function smoothingSlope(radius, distance)
{
if (distance >= radius)
return 0;
const value = radius * radius - distance * distance;
const scale = -24.0 / (Math.PI * Math.pow(radius, 8));
return scale * distance * value * value;
}
function densityToPressure(density, targetDensity, pressure)
{
return (density - targetDensity) * pressure;
}
class FlatRenderer { class FlatRenderer {
positionProperty(position, circleRadius, particleCount) positionProperty(position, circleRadius, particleCount)
{ {
@ -45,6 +59,69 @@ class FlatRenderer {
return property; return property;
} }
positionGradient(position, circleRadius, particleCount)
{
var property = new Float32Array([0, 0]);
for (var i = 0; i < particleCount; i++) {
const particlePosition = [this.particles[i * this.particleStride + 0],
this.particles[i * this.particleStride + 1]];
const signedDistance = sub(position, particlePosition);
const distance = length(signedDistance);
const direction = [signedDistance[0] / distance, signedDistance[1] / distance];
const particleProperty = this.particles[i * this.particleStride + 4];
const particleDensity = this.particles[i * this.particleStride + 5];
const slope = smoothingSlope(circleRadius, distance);
const weight = particleProperty * slope / particleDensity;
property[0] += direction[0] * weight;
property[1] += direction[1] * weight;
}
return property;
}
positionPressureForce(index, circleRadius, particleCount, targetDensity, pressure, mass)
{
var pressureForce = new Float32Array([0, 0]);
const position = [this.particles[index * this.particleStride + 0],
this.particles[index * this.particleStride + 1]];
for (var i = 0; i < particleCount; i++) {
if (i === index) continue;
const particlePosition = [this.particles[i * this.particleStride + 0],
this.particles[i * this.particleStride + 1]];
const signedDistance = sub(position, particlePosition);
const distance = length(signedDistance);
var direction;
if (distance === 0) {
const x = Math.random();
const y = Math.random();
const l = length([x, y]);
direction = [x / l, y / l];
} else {
direction = [signedDistance[0] / distance, signedDistance[1] / distance];
}
const particleDensity = this.particles[i * this.particleStride + 5];
const slope = smoothingSlope(circleRadius, distance);
const sharedPressure = (
densityToPressure(this.particles[index * this.particleStride * 5], targetDensity, pressure) +
densityToPressure(particleDensity, targetDensity, pressure)
) / 2;
const weight = sharedPressure * slope * mass / particleDensity;
pressureForce[0] += direction[0] * weight;
pressureForce[1] += direction[1] * weight;
}
return pressureForce;
}
particleDensity(aPosition, circleRadius, particleCount) particleDensity(aPosition, circleRadius, particleCount)
{ {
var density = 0.0; var density = 0.0;
@ -62,7 +139,7 @@ class FlatRenderer {
{ {
this.maxDim = 32; this.maxDim = 32;
this.maxParticles = this.maxDim * this.maxDim; this.maxParticles = this.maxDim * this.maxDim;
this.particleStride = 4 * 2; // in elements this.particleStride = 4 * 3; // in elements
this.particles = new Float32Array(this.maxParticles * this.particleStride); this.particles = new Float32Array(this.maxParticles * this.particleStride);
this.maxGradientsDim = 32; this.maxGradientsDim = 32;
@ -74,7 +151,7 @@ class FlatRenderer {
this.gradientBuffers = []; this.gradientBuffers = [];
this.particleBindGroups = []; this.particleBindGroups = [];
this.particleConfiguration = new Float32Array(3 * 4); this.particleConfiguration = new Float32Array(4 * 4);
this.particleConfigurationBuffers = []; this.particleConfigurationBuffers = [];
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
@ -249,20 +326,13 @@ class FlatRenderer {
const position = [(x / this.maxGradientsDim) + 0.5 / this.maxGradientsDim, const position = [(x / this.maxGradientsDim) + 0.5 / this.maxGradientsDim,
(y / this.maxGradientsDim) + 0.5 / this.maxGradientsDim]; (y / this.maxGradientsDim) + 0.5 / this.maxGradientsDim];
const stepSize = 0.001; const gradient = this.positionGradient(position, radius, count);
const px = [position[0] + stepSize, position[1]];
const py = [position[0], position[1] + stepSize];
const property = this.positionProperty(position, radius, count);
const dx = this.positionProperty(px, radius, count) - property;
const dy = this.positionProperty(py, radius, count) - property;
this.gradients[i * this.gradientStride + 0] = position[0]; this.gradients[i * this.gradientStride + 0] = position[0];
this.gradients[i * this.gradientStride + 1] = position[1]; this.gradients[i * this.gradientStride + 1] = position[1];
this.gradients[i * this.gradientStride + 4] = dx / stepSize; this.gradients[i * this.gradientStride + 4] = gradient[0];
this.gradients[i * this.gradientStride + 5] = dy / stepSize; this.gradients[i * this.gradientStride + 5] = gradient[1];
} }
} }
@ -286,11 +356,55 @@ class FlatRenderer {
this.particles, 0, this.particles.length); this.particles, 0, this.particles.length);
} }
updateSimulation(device, frameNumber, configuration)
{
for (var i = 0; i < configuration.particleCount; i++) {
const force = this.positionPressureForce(i,
configuration.circleRadius,
configuration.particleCount,
configuration.targetDensity,
configuration.pressure,
configuration.particleMass1);
if (force[0] == NaN)
throw new Error("force");
const particleDensity = this.particles[i * this.particleStride + 5];
const acceleration = [force[0] / particleDensity, force[1] / particleDensity];
this.particles[i * this.particleStride + 8] += acceleration[0];
this.particles[i * this.particleStride + 9] += acceleration[1] + 0.001;
// collisions
}
for (var i = 0; i < configuration.particleCount; i++) {
this.particles[i * this.particleStride + 0] -= this.particles[i * this.particleStride + 8];
this.particles[i * this.particleStride + 1] -= this.particles[i * this.particleStride + 9];
const position = [this.particles[i * this.particleStride + 0],
this.particles[i * this.particleStride + 1]];
for (var c = 0; c < 2; c++) {
if (position[c] < 0) {
this.particles[i * this.particleStride + c] = 0.01;
this.particles[i * this.particleStride + c + 8] *= -1 * 0.9;
}
if (position[c] > 1) {
this.particles[i * this.particleStride + c] = 0.99;
this.particles[i * this.particleStride + c + 8] *= -1 * 0.9;
}
}
}
}
updateConfiguration(device, frameNumber, configuration) updateConfiguration(device, frameNumber, configuration)
{ {
const PARTICLE = 0; const PARTICLE = 0;
const CIRCLE = 1; const CIRCLE = 1;
const MOUSE = 2; const MOUSE = 2;
const D = 3;
const config = this.particleConfiguration; const config = this.particleConfiguration;
config[PARTICLE * 4 + 0] = configuration.particleCount; config[PARTICLE * 4 + 0] = configuration.particleCount;
@ -308,6 +422,11 @@ class FlatRenderer {
config[MOUSE * 4 + 2] = configuration.lineThickness; config[MOUSE * 4 + 2] = configuration.lineThickness;
config[MOUSE * 4 + 3] = configuration.lineLength; config[MOUSE * 4 + 3] = configuration.lineLength;
config[D * 4 + 0] = configuration.targetDensity;
config[D * 4 + 1] = configuration.pressure;
config[D * 4 + 2] = 0;
config[D * 4 + 3] = 0;
device.queue.writeBuffer(this.particleConfigurationBuffers[frameNumber], 0, device.queue.writeBuffer(this.particleConfigurationBuffers[frameNumber], 0,
this.particleConfiguration, 0, this.particleConfiguration.length) this.particleConfiguration, 0, this.particleConfiguration.length)
} }
@ -317,6 +436,7 @@ class FlatRenderer {
// particle update must be before gradient update // particle update must be before gradient update
this.updateParticles(device, frameNumber, configuration); this.updateParticles(device, frameNumber, configuration);
this.updateGradients(device, frameNumber, configuration); this.updateGradients(device, frameNumber, configuration);
this.updateSimulation(device, frameNumber, configuration);
this.updateConfiguration(device, frameNumber, configuration); this.updateConfiguration(device, frameNumber, configuration);
renderPass.setPipeline(this.renderPipeline); renderPass.setPipeline(this.renderPipeline);

View File

@ -8,12 +8,14 @@ struct Configuration {
struct Particle { struct Particle {
position: vec4f, position: vec4f,
value: vec4f, value: vec4f,
velocity: vec4f,
}; };
struct ParticleConfiguration { struct ParticleConfiguration {
particle: vec4f, particle: vec4f,
circle: vec4f, circle: vec4f,
mouse: vec4f, mouse: vec4f,
d: vec4f,
}; };
struct Gradient { struct Gradient {
@ -88,6 +90,11 @@ fn signedDistanceSegment(p: vec2f, a: vec2f, b: vec2f, r: f32) -> f32
return length(pa - h * ba) - r; return length(pa - h * ba) - r;
} }
fn densityToPressure(density: f32, targetDensity: f32, pressure: f32) -> f32
{
return (density - targetDensity) * pressure;
}
@fragment @fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
{ {
@ -107,12 +114,18 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
let lineThickness = particleConfiguration.mouse.z; let lineThickness = particleConfiguration.mouse.z;
let lineLength = particleConfiguration.mouse.w; let lineLength = particleConfiguration.mouse.w;
let targetDensity = particleConfiguration.d.x;
let pressure = particleConfiguration.d.y;
let particleMass = mix(particleMass1, particleMass2, mass12Mix);
var property: f32 = 0.0; var property: f32 = 0.0;
for (var i: u32 = 0; i < particleCount; i++) { for (var i: u32 = 0; i < particleCount; i++) {
let distance = length(input.texture.xy - particle[i].position.xy); let distance = length(input.texture.xy - particle[i].position.xy);
if (distance < particleSize) { if (distance < particleSize) {
color.x = particle[i].value.x; //color.x = particle[i].value.x;
color.x = 1.0;
} }
//let circleDistance = length(input.texture.xy - circlePosition.xy); //let circleDistance = length(input.texture.xy - circlePosition.xy);
let influence = smoothing(circleRadius, distance); let influence = smoothing(circleRadius, distance);
@ -120,16 +133,21 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
let particleProperty = particle[i].value.x; let particleProperty = particle[i].value.x;
let particleDensity = particle[i].value.y; let particleDensity = particle[i].value.y;
let p1 = particleProperty * influence; //let p1 = particleProperty * influence;
let p1 = influence;
let p2 = p1 / particleDensity; let p2 = p1 / particleDensity;
property += mix(p1, p2, mass12Mix); //property += mix(p1, p2, mass12Mix);
property += influence;
} }
color.z = property * mix(particleMass1, particleMass2, mass12Mix);
let density = property * particleMass;
let fragmentPressure = densityToPressure(density, targetDensity, pressure) * 0.01;
color.z = density;
for (var i: u32 = 0; i < 1024; i++) { for (var i: u32 = 0; i < 1024; i++) {
let a = gradient[i].position.xy; let a = gradient[i].position.xy;
let b = a + gradient[i].delta.xy * lineLength; let b = a + gradient[i].delta.xy * particleMass * lineLength;
let distance = signedDistanceSegment(input.texture.xy, a, b, 0); let distance = signedDistanceSegment(input.texture.xy, a, b, 0);
let headDistance = length(input.texture.xy - b); let headDistance = length(input.texture.xy - b);
if ((distance < lineThickness) || (headDistance < lineThickness * 3.5)) { if ((distance < lineThickness) || (headDistance < lineThickness * 3.5)) {
@ -140,6 +158,27 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
let testColor = test(input.texture.xy); let testColor = test(input.texture.xy);
color.z += testColor * testIntensity; color.z += testColor * testIntensity;
return vec4(color.xyz, 1.0); let r = vec3f(1, 0, 0);
let g = vec3f(0, 1, 0);
let b = vec3f(0, 0, 1);
let n = smoothstep(0, -0.01, fragmentPressure);
let p = smoothstep(0, 0.01, fragmentPressure);
let z = smoothstep(0.01, 0, abs(fragmentPressure));
//return vec4(n, z, p, 1);
let pressureColor = (r * n + g * z + b * p) * density;
return vec4(pressureColor.xyz * (1.0 - color.x) + vec3(1, 1, 1) * color.y, 1.0);
//return vec4(color.xyz, 1.0);
//return vec4(color.x, 0, 0, 1.0); //return vec4(color.x, 0, 0, 1.0);
/*
if (fragmentPressure < -0.1) {
return vec4(1, 0, 0, 1);
} else if (fragmentPressure > 0.1) {
return vec4(0, 1, 0, 1);
} else {
return vec4(0, 0, 1, 1);
}
*/
} }

View File

@ -206,16 +206,7 @@ function remap(low1, high1, low2, high2, value)
return low2 + (value - low1) * (high2 - low2) / (high1 - low1); return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
} }
const sliderDefaults = { const sliderDefaults = {"test intensity":"0","mass12 mix":"0","circle thickness":"171","line thickness":"107","particle mass2":"13","particle count":"19","target density":"954","particle size":"70","pressure":"19","line length":"6","particle mass1":"1024","circle radius":"379"};
"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 { class Slider {
constructor(label, low, high, max = 1024) constructor(label, low, high, max = 1024)
@ -254,6 +245,7 @@ class Slider {
} else if (this.label in sliderDefaults) { } else if (this.label in sliderDefaults) {
this.input.value = sliderDefaults[this.label]; this.input.value = sliderDefaults[this.label];
} }
this.update(); this.update();
} }
@ -276,7 +268,7 @@ class Slider {
const sliders = { const sliders = {
particleSize: new Slider("particle size", 0.0, 0.05), particleSize: new Slider("particle size", 0.0, 0.05),
particleMass1: new Slider("particle mass1", 0.0, 0.01), particleMass1: new Slider("particle mass1", 0.0, 0.05),
particleMass2: new Slider("particle mass2", 0.0, 1.0), particleMass2: new Slider("particle mass2", 0.0, 1.0),
particleCount: new Slider("particle count", 0.0, 1024.0), particleCount: new Slider("particle count", 0.0, 1024.0),
circleRadius: new Slider("circle radius", 0.0, 0.5), circleRadius: new Slider("circle radius", 0.0, 0.5),
@ -284,7 +276,9 @@ const sliders = {
testIntensity: new Slider("test intensity", 0.0, 2.0), testIntensity: new Slider("test intensity", 0.0, 2.0),
mass12Mix: new Slider("mass12 mix", 0.0, 1.0, 1), mass12Mix: new Slider("mass12 mix", 0.0, 1.0, 1),
lineThickness: new Slider("line thickness", 0.0, 0.005), lineThickness: new Slider("line thickness", 0.0, 0.005),
lineLength: new Slider("line length", 0.0, 0.025), lineLength: new Slider("line length", 0.0, 9.0),
targetDensity: new Slider("target density", -10.0, 10.0),
pressure: new Slider("pressure", 0.0, 1.0),
}; };
const mousePosition = new Float32Array([0, 0, 0, 0]); const mousePosition = new Float32Array([0, 0, 0, 0]);
@ -303,6 +297,8 @@ function updateSliders()
mass12Mix: sliders.mass12Mix.update(), mass12Mix: sliders.mass12Mix.update(),
lineThickness: sliders.lineThickness.update(), lineThickness: sliders.lineThickness.update(),
lineLength: sliders.lineLength.update(), lineLength: sliders.lineLength.update(),
targetDensity: sliders.targetDensity.update(),
pressure: sliders.pressure.update(),
}; };
} }