snake demo

This commit is contained in:
Zack Buhman 2026-08-05 00:38:02 -05:00
parent 38deeabf83
commit 6abcee60fb
15 changed files with 531 additions and 37 deletions

View File

@ -129,10 +129,12 @@
</select>
</div>
-->
<!--
<div class="row">
<span class="label">eye</span>
<input type="color" class="value">
</div>
-->
</div>
</body>
</html>

View File

@ -2,6 +2,8 @@ import { getPath } from "./common.js";
import { loadGltf } from "./index2.js";
import { loadLight } from "./light.js";
import { loadFlat } from "./flat.js";
import { loadSnake } from "./snake.js";
import { loadFont } from "./font.js";
if (!navigator.gpu) {
throw new Error("WebGPU not supported on this browser.");
@ -45,6 +47,7 @@ const module = await WebAssembly.instantiateStreaming(fetch("src/module.wasm"),
env: {
memory: memory,
log: console.log,
logStrInt: console.log,
}
});
module.memory = memory;
@ -82,7 +85,7 @@ function recreateDepth(force)
const matrixSize = 4 * 4 * 4;
const float4Size = 4 * 4;
const float3Size = 4 * 3;
const viewUniformBufferSize = (matrixSize * 2) + (float4Size * 2);
const viewUniformBufferSize = (matrixSize * 2) + (float4Size * 2) + (4 * 4);
const viewUniformBuffer = device.createBuffer({
label: "view uniform buffer",
@ -99,8 +102,9 @@ module.instance.exports.camera_init(cameraStateAddress);
//const gltfRenderer = await loadGltf(device, canvasFormat, viewUniformBuffer, memory, module);
//const lightRenderer = await loadLight(device, canvasFormat, viewUniformBuffer);
const flatRenderer = await loadFlat(device, canvasFormat, viewUniformBuffer, module);
//const flatRenderer = await loadFlat(device, canvasFormat, viewUniformBuffer, module);
//const snakeRenderer = await loadSnake(device, canvasFormat, viewUniformBuffer, module);
const fontRenderer = await loadFont(device, canvasFormat, viewUniformBuffer, module);
const KEY = {
A: 65,
@ -189,15 +193,6 @@ function updateView()
device.queue.writeBuffer(viewUniformBuffer, 0,
memory.buffer, viewStateAddress,
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");
@ -207,7 +202,7 @@ function remap(low1, high1, low2, high2, value)
return low2 + (value - low1) * (high2 - low2) / (high1 - low1);
}
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"};
const sliderDefaults = {"bone length":"32","velocity acc.":"385","velocity damp.":"796","bone count":"40"};
class Slider {
constructor(label, low, high, max = 1024)
@ -246,6 +241,7 @@ class Slider {
} else if (this.label in sliderDefaults) {
this.input.value = sliderDefaults[this.label];
}
this.input.value = sliderDefaults[this.label];
this.update();
}
@ -267,7 +263,8 @@ class Slider {
}
}
const sliders = {
/*
const flatSliders = {
particleSize: new Slider("particle size", 0.0, 0.05),
particleMass1: new Slider("particle mass1", 0.0, 0.05),
particleMass2: new Slider("particle mass2", 0.0, 1.0),
@ -280,27 +277,25 @@ const sliders = {
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]);
function updateSliders()
const snakeSliders = {
boneCount: new Slider("bone count", 0.0, 100.0, 100),
boneLength: new Slider("bone length", 0.0, 0.5),
velocityDamping: new Slider("velocity damp.", 0.0, 1.0),
velocityAcceleration: new Slider("velocity acc.", 0.0, 1.0),
};
function updateSliders(sliders)
{
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(),
lineThickness: sliders.lineThickness.update(),
lineLength: sliders.lineLength.update(),
targetDensity: sliders.targetDensity.update(),
pressure: sliders.pressure.update(),
};
const values = {};
for (let [key, value] of Object.entries(sliders)) {
values[key] = value.update();
}
return values;
}
function onClick(e)
@ -308,14 +303,24 @@ function onClick(e)
mousePosition[0] = e.clientX / canvas.width;
mousePosition[1] = 1.0 - (e.clientY / canvas.height);
}
canvas.addEventListener("click", onClick);
//canvas.addEventListener("click", onClick);
function onMouseMove(e)
{
const aspect = canvas.width / canvas.height;
mousePosition[0] = ((e.clientX / canvas.width) * 2 - 1) * aspect;
mousePosition[1] = (1.0 - (e.clientY / canvas.height)) * 2 - 1;
}
canvas.addEventListener("mousemove", onMouseMove);
var canRender = false;
var frameNumber = 0;
function render2()
{
if (canRender === true) {
const configuration = updateSliders();
//const configuration = updateSliders(flatSliders);
const configuration = updateSliders(snakeSliders);
canRender = false;
recreateDepth(false);
@ -341,7 +346,12 @@ function render2()
//gltfRenderer.render(renderPass);
//lightRenderer.render(renderPass);
flatRenderer.render(device, renderPass, frameNumber, configuration);
//flatRenderer.render(device, renderPass, frameNumber, configuration);
//snakeRenderer.update(device, frameNumber, mousePosition, configuration);
//snakeRenderer.render(renderPass, frameNumber);
fontRenderer.render(renderPass, frameNumber);
renderPass.end();
@ -357,7 +367,7 @@ function setCanRender()
{
canRender = true;
}
setInterval(setCanRender, 66.67); // 15 fps
setInterval(setCanRender, 33.33); // 15 fps
recreateDepth(true);
requestAnimationFrame(render2);

201
snake.js Normal file
View File

@ -0,0 +1,201 @@
import { getPath } from "./common.js";
class SnakeRenderer {
createBuffers(device)
{
const boneCount = 100;
const boneLength = 0.1;
this.boneSystemAddress = this.module.instance.exports.bone_system__create(boneCount, boneLength);
const mem = new DataView(this.module.memory.buffer);
this.bonesAddress = mem.getUint32(this.boneSystemAddress, true);
const configurationOffset = this.module.instance.exports.bone_system__configuration_offset();
this.configurationAddress = this.boneSystemAddress + configurationOffset;
this.bonesBufferSize = this.module.instance.exports.bone_system__bones_size(boneCount);
this.configurationBufferSize = this.module.instance.exports.bone_system__configuration_size();
this.frames = [];
for (let i = 0; i < 2; i++) {
const bonesBuffer = device.createBuffer({
label: `bone buffer ${i}`,
size: this.bonesBufferSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
const configurationBuffer = device.createBuffer({
label: `bone configuration buffer ${i}`,
size: this.configurationBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
label: `bone buffer bind group ${i}`,
layout: this.bindGroupLayout.bone,
entries: [{
binding: 0,
resource: { buffer: bonesBuffer },
}, {
binding: 1,
resource: { buffer: configurationBuffer },
}]
});
this.frames.push({
bonesBuffer: bonesBuffer,
configurationBuffer: configurationBuffer,
bindGroup: bindGroup,
});
}
}
constructor(device, canvasFormat, viewUniformBuffer, wgsl, module)
{
const label = "bone";
this.module = module;
//////////////////////////////////////////////////////////////////////
// shader module
//////////////////////////////////////////////////////////////////////
const shaderModule = device.createShaderModule({
label: `${label} shader`,
code: wgsl,
});
//////////////////////////////////////////////////////////////////////
// index 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" }
}]
}),
bone: device.createBindGroupLayout({
label: `${label} bind group layout particle`,
entries: [{ // bones
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
buffer: { type: "read-only-storage" }
}, { // configuration
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
buffer: { type: "uniform" }
}]
}),
};
const pipelineLayout = device.createPipelineLayout({
label: `${label} pipeline layout`,
bindGroupLayouts: [
this.bindGroupLayout.view,
this.bindGroupLayout.bone
],
});
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",
},
});
//////////////////////////////////////////////////////////////////////
// bones
//////////////////////////////////////////////////////////////////////
this.createBuffers(device);
//////////////////////////////////////////////////////////////////////
// bind group
//////////////////////////////////////////////////////////////////////
this.viewBindGroup = device.createBindGroup({
label: `${label} bind group`,
layout: this.bindGroupLayout.view,
entries: [{
binding: 0,
resource: { buffer: viewUniformBuffer },
}],
});
}
update(device, frameNumber, mousePosition, configuration)
{
this.module.instance.exports.bone_system__update_configuration(this.boneSystemAddress,
configuration.boneCount,
configuration.boneLength,
configuration.velocityDamping,
configuration.velocityAcceleration);
this.module.instance.exports.bone_system__update(this.boneSystemAddress,
mousePosition[0], mousePosition[1], mousePosition[2]);
device.queue.writeBuffer(this.frames[frameNumber].bonesBuffer, 0,
this.module.memory.buffer, this.bonesAddress, this.bonesBufferSize);
device.queue.writeBuffer(this.frames[frameNumber].configurationBuffer, 0,
this.module.memory.buffer, this.configurationAddress, this.configurationBufferSize);
}
render(renderPass, frameNumber)
{
renderPass.setPipeline(this.renderPipeline);
renderPass.setIndexBuffer(this.buffer, "uint16");
renderPass.setBindGroup(0, this.viewBindGroup);
renderPass.setBindGroup(1, this.frames[frameNumber].bindGroup);
renderPass.drawIndexed(6);
}
}
async function loadSnake(device, canvasFormat, viewUniformBuffer, module)
{
const snakeWgsl = await getPath("snake.wgsl");
const renderer = new SnakeRenderer(device, canvasFormat, viewUniformBuffer, snakeWgsl, module);
return renderer;
}
export { loadSnake };

90
snake.wgsl Normal file
View File

@ -0,0 +1,90 @@
struct Configuration {
viewProj: mat4x4f,
lightViewProj: mat4x4f,
lightPosition: vec4f,
eyePosition: vec4f,
aspect: f32,
};
struct Bone {
position: vec4f,
velocity: vec4f,
};
struct BoneConfiguration {
a: vec4f,
};
@group(0) @binding(0) var<uniform> config: Configuration;
@group(1) @binding(0) var<storage> bones: array<Bone>;
@group(1) @binding(1) var<uniform> boneConfig: BoneConfiguration;
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];
let position = texture * 2.0 - 1.0;
var output: VertexOutput;
output.position = vec4f(position, 0.0, 1.0);
output.texture = position * vec2f(config.aspect, 1);
return output;
}
fn sdCircle(center: vec2f, sample: vec2f) -> f32
{
let distance = length(center - sample);
return distance;
}
fn sdSegment(p: vec2f, a: vec2f, b: vec2f) -> f32
{
let ba = b - a;
let pa = p - a;
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - h * ba);
}
@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f
{
var color = vec3f(input.texture.xy, 0);
for (var i: u32 = 0; i < u32(boneConfig.a.x); i++) {
if (sdCircle(input.texture.xy, bones[i].position.xy) < 0.01) {
color = vec3f(1, 1, 1);
}
/*
if (sdSegment(input.texture.xy,
bones[i].position.xy,
bones[i].position.xy + bones[i].velocity.xy * 0.05) < 0.002) {
color = vec3(0, 0, 1);
}
*/
/*
if (i == 1 && sdCircle(input.texture.xy, bones[i].velocity.xy) < 0.05) {
color = vec3(0, 0, 1);
}
*/
}
return vec4f(color, 1);
}

View File

@ -9,6 +9,7 @@ CFLAGS = \
-nostdlib \
-I$(MINIZ) \
-I. \
-I../include \
-Werror \
-Wfatal-errors \
-D_XM_NO_INTRINSICS_
@ -36,6 +37,21 @@ LDFLAGS = \
-Wl,--export=particle_system__update \
-Wl,--export=particle__stride \
-Wl,--export=particle_configuration__stride \
-Wl,--export=bone_system__create \
-Wl,--export=bone_system__bones_size \
-Wl,--export=bone_system__configuration_size \
-Wl,--export=bone_system__configuration_offset \
-Wl,--export=bone_system__update \
-Wl,--export=bone_system__update_configuration \
-Wl,--export=font_layout__create \
-Wl,--export=font_layout__texture_address \
-Wl,--export=font_layout__glyph_buffer_size \
-Wl,--export=font_layout__glyph_buffer_address \
-Wl,--export=font_layout__texture_size \
-Wl,--export=font_layout__texture_address \
-Wl,--export=font_layout__layout_buffer_size \
-Wl,--export=font_layout__layout_buffer_address \
-Wl,--export=font_layout__layout_buffer_index \
-Wl,--import-undefined \
-Wl,--print-map \
-Wl,--import-memory \
@ -56,7 +72,12 @@ PNG_OBJ = \
camera.o \
fluid/particle.o \
fluid/particle_system.o \
fluid/api.o
fluid/api.o \
snake/bone.o \
snake/bone_system.o \
snake/api.o \
font_layout/font_layout.o \
font_layout/api.o
module.wasm: $(PNG_OBJ)
clang++ $(CFLAGS) $(LDFLAGS) -o $@ $^

View File

@ -18,6 +18,7 @@ struct ViewState {
XMFLOAT4X4 lightViewProj;
XMFLOAT4 lightPosition;
XMFLOAT4 eyePosition;
float aspect;
};
static inline XMMATRIX camera_view(CameraState const * state)
@ -150,4 +151,5 @@ void camera_view_projection(CameraState * camera_state,
XMStoreFloat4(&view_state->lightPosition, light_position);
XMVECTOR eye = XMLoadFloat3(&camera_state->eye);
XMStoreFloat4(&view_state->eyePosition, eye);
view_state->aspect = aspect;
}

View File

@ -28,5 +28,4 @@ namespace fluid
velocity.y *= -1;
}
}
}

View File

@ -1,5 +1,7 @@
#pragma once
#include <stddef.h>
extern "C" {
size_t mem_size();
size_t mem_alloc(size_t length);

48
src/snake/api.cpp Normal file
View File

@ -0,0 +1,48 @@
#include "new.h"
#include "bone_system.h"
using namespace snake;
extern "C" {
void logStrInt(char const * s, int a);
BoneSystem * bone_system__create(int boneCount, float boneLength)
{
BoneSystem * system = New<BoneSystem>();
system->init(boneCount, boneLength);
return system;
}
int bone_system__bones_size(int boneCount)
{
return boneCount * (sizeof (Bone));
}
int bone_system__configuration_size()
{
return (sizeof (BoneConfiguration));
}
int bone_system__configuration_offset()
{
return (offsetof (BoneSystem, configuration));
}
void bone_system__update(BoneSystem * bone_system, float x, float y, float z)
{
bone_system->update(x, y, z);
}
void bone_system__update_configuration(BoneSystem * bone_system,
int boneCount,
float boneLength,
float velocityDamping,
float velocityAcceleration)
{
bone_system->configuration.boneCount = boneCount;
bone_system->configuration.boneLength = boneLength;
bone_system->configuration.velocityDamping = velocityDamping;
bone_system->configuration.velocityAcceleration = velocityAcceleration;
}
};

41
src/snake/bone.cpp Normal file
View File

@ -0,0 +1,41 @@
#include "bone.h"
namespace snake {
extern "C" void logStrInt(char const * s, float a);
// assumes ray does intersect with sphere
static inline XMVECTOR intersectRaySphere(XMVECTOR rayPosition, XMVECTOR rayDirection,
XMVECTOR sphereCenter, float sphereRadius)
{
XMVECTOR m = rayPosition - sphereCenter;
float b = XMVectorGetX(XMVector3Dot(m, rayDirection));
float c = XMVectorGetX(XMVector3Dot(m, m)) - sphereRadius * sphereRadius;
float discriminant = b * b - c;
float t = -b - sqrtf(discriminant);
return rayPosition + t * rayDirection;
}
void Bone::update(Bone const * parent, BoneConfiguration const * configuration)
{
XMVECTOR parentPosition = XMLoadFloat4(&parent->position);
XMVECTOR position = XMLoadFloat4(&this->position);
XMVECTOR velocity = XMLoadFloat4(&this->velocity);
position += velocity;
velocity *= configuration->velocityDamping;
XMVECTOR direction = parentPosition - position;
XMVECTOR normal = XMVector3Normalize(direction);
XMVECTOR newPosition = intersectRaySphere(position, normal,
parentPosition, configuration->boneLength);
XMVECTOR force = newPosition - position;
velocity += force * configuration->velocityAcceleration;
XMStoreFloat4(&this->position, newPosition);
XMStoreFloat4(&this->velocity, velocity);
}
}

13
src/snake/bone.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "directxmath/DirectXMath.h"
#include "bone_configuration.h"
namespace snake {
struct Bone {
XMFLOAT4 position;
XMFLOAT4 velocity;
void update(Bone const * parent, BoneConfiguration const * configuration);
};
}

View File

@ -0,0 +1,10 @@
#pragma once
namespace snake {
struct BoneConfiguration {
float boneCount;
float boneLength;
float velocityDamping;
float velocityAcceleration;
};
}

35
src/snake/bone_system.cpp Normal file
View File

@ -0,0 +1,35 @@
#include "new.h"
#include "bone_system.h"
namespace snake {
void BoneSystem::init(int maxBones, float boneLength)
{
this->maxBones = maxBones;
configuration.boneCount = maxBones;
configuration.boneLength = boneLength;
configuration.velocityDamping = 0.0;
configuration.velocityAcceleration = 0.0;
bones = New<Bone>(maxBones);
XMVECTOR direction = XMVectorSet(0, 1, 0, 0);
for (int i = 0; i < maxBones; i++) {
XMStoreFloat4(&bones[i].position, direction * boneLength * i);
XMStoreFloat4(&bones[i].velocity, XMVectorZero());
}
}
void BoneSystem::update(float x, float y, float z)
{
bones[0].position.x = x;
bones[0].position.y = y;
bones[0].position.z = z;
for (int i = 1; i < configuration.boneCount; i++) {
Bone const * parent = &bones[i - 1];
Bone * bone = &bones[i];
bone->update(parent, &configuration);
}
}
}

15
src/snake/bone_system.h Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include "bone.h"
#include "bone_configuration.h"
namespace snake {
struct BoneSystem {
Bone * bones;
int maxBones;
BoneConfiguration configuration;
void init(int maxBones, float boneLength);
void update(float x, float y, float z);
};
}

View File

@ -0,0 +1,5 @@
namespace snake {
struct BoneConfiguration {
float boneLength;
};
}