diff --git a/flat.js b/flat.js index 11f6926..b20f985 100644 --- a/flat.js +++ b/flat.js @@ -1,6 +1,91 @@ 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"; @@ -29,20 +114,35 @@ class FlatRenderer { // pipeline ////////////////////////////////////////////////////////////////////// - const bindGroupLayouts = [ - device.createBindGroupLayout({ - label: `${label} view matrix bind group layout`, + 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: bindGroupLayouts, + bindGroupLayouts: [ + this.bindGroupLayout.view, + this.bindGroupLayout.particle + ], }); this.renderPipeline = device.createRenderPipeline({ @@ -69,6 +169,12 @@ class FlatRenderer { }, }); + ////////////////////////////////////////////////////////////////////// + // particles + ////////////////////////////////////////////////////////////////////// + + this.createParticles(device); + ////////////////////////////////////////////////////////////////////// // bind group ////////////////////////////////////////////////////////////////////// @@ -76,7 +182,7 @@ class FlatRenderer { this.bindGroups = [ device.createBindGroup({ label: `${label} bind group`, - layout: bindGroupLayouts[0], + layout: this.bindGroupLayout.view, entries: [{ binding: 0, 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.setIndexBuffer(this.buffer, "uint16"); renderPass.setBindGroup(0, this.bindGroups[0]); + renderPass.setBindGroup(1, this.particleBindGroups[frameNumber]); renderPass.drawIndexed(6); } } diff --git a/flat.wgsl b/flat.wgsl index a9bc02e..9865fed 100644 --- a/flat.wgsl +++ b/flat.wgsl @@ -5,7 +5,20 @@ struct Configuration { eyePosition: vec4f, }; +struct Particle { + position: vec4f, + value: vec4f, +}; + +struct ParticleConfiguration { + particle: vec4f, + circle: vec4f, + mouse: vec4f, +}; + @group(0) @binding(0) var config: Configuration; +@group(1) @binding(0) var particleConfiguration: ParticleConfiguration; +@group(1) @binding(1) var particle: array; struct VertexInput { @builtin(vertex_index) vertex_index: u32, @@ -27,16 +40,85 @@ const vertices = array( fn vertexMain(input: VertexInput) -> VertexOutput { let texture = vertices[input.vertex_index]; - let position = vec4f(texture * 2.0 - 1.0, 0.0, 1.0); var output: VertexOutput; - output.position = position; + 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 { - 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); } diff --git a/index.html b/index.html index 8f75151..ff2534e 100644 --- a/index.html +++ b/index.html @@ -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; + } @@ -78,7 +90,7 @@
-
+
+ -
diff --git a/index.js b/index.js index f89c2a1..1dffee1 100644 --- a/index.js +++ b/index.js @@ -65,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; @@ -190,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(); - const colorView = context.getCurrentTexture().createView(); + if (canRender === true) { + const configuration = updateSliders(); - updateView(); + canRender = false; + recreateDepth(false); + const colorView = context.getCurrentTexture().createView(); + + updateView(); - { const encoder = device.createCommandEncoder(); const renderPass = encoder.beginRenderPass({ colorAttachments: [{ @@ -223,14 +342,23 @@ function render2() //gltfRenderer.render(renderPass); //lightRenderer.render(renderPass); - flatRenderer.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); diff --git a/src/camera.cpp b/src/camera.cpp index e237a3c..9d072ea 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -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); }