Compare commits

..

2 Commits

Author SHA1 Message Date
b198b61bb7 particle smoothing function sandbox 2026-07-31 16:08:21 -05:00
5386b8bee9 flat renderer 2026-07-30 12:12:24 -05:00
5 changed files with 558 additions and 19 deletions

270
flat.js Normal file
View File

@ -0,0 +1,270 @@
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 {
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)
{
const label = "flat";
//////////////////////////////////////////////////////////////////////
// buffer
//////////////////////////////////////////////////////////////////////
const array = new Uint16Array(6);
array[0] = 0;
array[1] = 1;
array[2] = 2;
array[3] = 0;
array[4] = 3;
array[5] = 1;
this.buffer = device.createBuffer({
label: `${label} renderer buffer`,
size: array.byteLength,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(this.buffer, 0, array, 0, array.length);
//////////////////////////////////////////////////////////////////////
// pipeline
//////////////////////////////////////////////////////////////////////
this.bindGroupLayout = {
view: device.createBindGroupLayout({
label: `${label} bind group layout view`,
entries: [{
binding: 0,
visibility: GPUShaderStage.VERTEX,
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({
label: `${label} pipeline layout`,
bindGroupLayouts: [
this.bindGroupLayout.view,
this.bindGroupLayout.particle
],
});
this.renderPipeline = device.createRenderPipeline({
label: `${label} pipeline`,
layout: pipelineLayout,
vertex: {
module: shaderModule,
entryPoint: "vertexMain",
},
fragment: {
module: shaderModule,
entryPoint: "fragmentMain",
targets: [{
format: canvasFormat,
}]
},
primitive: {
topology: "triangle-list",
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: "less",
format: "depth24plus",
},
});
//////////////////////////////////////////////////////////////////////
// particles
//////////////////////////////////////////////////////////////////////
this.createParticles(device);
//////////////////////////////////////////////////////////////////////
// bind group
//////////////////////////////////////////////////////////////////////
this.bindGroups = [
device.createBindGroup({
label: `${label} bind group`,
layout: this.bindGroupLayout.view,
entries: [{
binding: 0,
resource: { buffer: viewUniformBuffer },
}],
}),
];
}
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.setIndexBuffer(this.buffer, "uint16");
renderPass.setBindGroup(0, this.bindGroups[0]);
renderPass.setBindGroup(1, this.particleBindGroups[frameNumber]);
renderPass.drawIndexed(6);
}
}
async function loadFlat(device, canvasFormat, viewUniformBuffer)
{
const flatWgsl = await getPath("flat.wgsl");
const shaderModule = device.createShaderModule({
label: "flag shader",
code: flatWgsl,
});
const renderer = new FlatRenderer(device, canvasFormat, viewUniformBuffer, shaderModule);
return renderer;
}
export { loadFlat };

124
flat.wgsl Normal file
View File

@ -0,0 +1,124 @@
struct Configuration {
viewProj: mat4x4f,
lightViewProj: mat4x4f,
lightPosition: 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(1) @binding(0) var<uniform> particleConfiguration: ParticleConfiguration;
@group(1) @binding(1) var<storage> particle: array<Particle>;
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
};
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) texture: vec2f,
};
const vertices = array(
vec2f(1.0, 0.0),
vec2f(0.0, 1.0),
vec2f(0.0, 0.0),
vec2f(1.0, 1.0),
);
@vertex
fn vertexMain(input: VertexInput) -> VertexOutput
{
let texture = vertices[input.vertex_index];
var output: VertexOutput;
output.position = vec4f(texture * 2.0 - 1.0, 0.0, 1.0);
output.texture = texture;
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
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
{
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 {
font: 0.8rem sans-serif;
}
span {
padding-bottom: 0.1rem;
}
select, option {
font: 0.8rem sans-serif;
}
@ -23,6 +26,12 @@
width: 100%;
height: 100%;
}
input[type="range"] {
height: 0.9rem;
width: 70%;
}
.control-root {
position: fixed;
left: 0;
@ -32,13 +41,13 @@
background: #eee;
}
.control-main {
width: 250px;
width: 310px;
float: right;
margin-right: 15px;
background: #444;
}
.label {
width: calc(40% - 0.3rem);
width: calc(30% - 0.3rem);
float: left;
clear: left;
overflow: hidden;
@ -46,7 +55,7 @@
margin-top: 0.25rem;
}
.value {
width: calc(60% - 0.2rem);
width: calc(70% - 0.2rem);
float: left;
margin-top: 0.1rem;
margin-right: 0.2rem;
@ -57,9 +66,9 @@
.row {
height: 1.5rem;
}
code {
font: 0.95rem monospace;
padding-top: 0.1rem;
.code, code {
font: 0.85rem monospace;
padding-top: 0.15rem;
}
.float3 {
width: 30%;
@ -71,6 +80,9 @@
display: inline-block;
text-align: right;
}
.range-span {
float: right;
}
</style>
<script src="index.js" type="module"></script>
<!--<script src="index2.js" type="module"></script>-->
@ -78,7 +90,7 @@
<body>
<canvas></canvas>
<div class="control-root">
<div class="control-main">
<div class="control-main" id="control-main">
<!--
<div class="row">
<span class="label">model</span>
@ -92,6 +104,7 @@
</select>
</div>
-->
<!--
<div class="row">
<span class="label">eye</span>
<div class="value">
@ -107,6 +120,7 @@
<code class="float2" id="pitch">0.0</code>
</div>
</div>
-->
<!--
<div class="row">
<span class="label">test2</span>
@ -116,6 +130,5 @@
</div>
-->
</div>
</div>
</body>
</html>

147
index.js
View File

@ -1,6 +1,7 @@
import { getPath } from "./common.js";
import { loadGltf } from "./index2.js";
import { loadLight } from "./light.js";
import { loadFlat } from "./flat.js";
if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
@ -64,9 +65,9 @@ const eyeValueZ = document.getElementById("eye-value-z");
const yawValue = document.getElementById("yaw");
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.height = canvas.clientHeight;
@ -95,8 +96,9 @@ const cameraStateAddress = module.instance.exports.mem_alloc(cameraStateSize);
module.instance.exports.camera_init(cameraStateAddress);
const gltfRenderer = await loadGltf(device, canvasFormat, viewUniformBuffer, memory, module);
const lightRenderer = await loadLight(device, canvasFormat, viewUniformBuffer);
//const gltfRenderer = await loadGltf(device, canvasFormat, viewUniformBuffer, memory, module);
//const lightRenderer = await loadLight(device, canvasFormat, viewUniformBuffer);
const flatRenderer = await loadFlat(device, canvasFormat, viewUniformBuffer);
const KEY = {
@ -188,21 +190,140 @@ function updateView()
viewUniformBufferSize);
const cameraStateF32 = new Float32Array(memory.buffer, cameraStateAddress, cameraStateSize);
/*
eyeValueX.innerHTML = cameraStateF32[0].toFixed(1);
eyeValueY.innerHTML = cameraStateF32[1].toFixed(1);
eyeValueZ.innerHTML = cameraStateF32[2].toFixed(1);
yawValue.innerHTML = cameraStateF32[9].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()
{
recreateDepth();
if (canRender === true) {
const configuration = updateSliders();
canRender = false;
recreateDepth(false);
const colorView = context.getCurrentTexture().createView();
updateView();
{
const encoder = device.createCommandEncoder();
const renderPass = encoder.beginRenderPass({
colorAttachments: [{
@ -219,15 +340,25 @@ function render2()
},
});
gltfRenderer.render(renderPass);
lightRenderer.render(renderPass);
//gltfRenderer.render(renderPass);
//lightRenderer.render(renderPass);
flatRenderer.render(device, renderPass, frameNumber, configuration);
renderPass.end();
const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
frameNumber = (frameNumber + 1) % 2;
}
requestAnimationFrame(render2);
}
function setCanRender()
{
canRender = true;
}
setInterval(setCanRender, 66.67); // 15 fps
recreateDepth(true);
requestAnimationFrame(render2);

View File

@ -148,5 +148,6 @@ void camera_view_projection(CameraState * camera_state,
XMStoreFloat4x4(&view_state->viewProj, view_projection);
XMStoreFloat4x4(&view_state->lightViewProj, light_world * view_projection);
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);
}