Skip to content

Commit 34f25db

Browse files
committedDec 19, 2024·
Initial Commit
1 parent 29fd8e0 commit 34f25db

File tree

6 files changed

+303
-0
lines changed

6 files changed

+303
-0
lines changed
 

‎index.html

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>WebGPU Lenia</title>
5+
<style>
6+
body, html {
7+
margin: 0;
8+
padding: 0;
9+
overflow: hidden;
10+
width: 100%;
11+
height: 100%;
12+
}
13+
canvas {
14+
display: block;
15+
width: 100%;
16+
height: 100%;
17+
}
18+
19+
</style>
20+
</head>
21+
<body>
22+
<canvas></canvas>
23+
<script type="module" src="index.js"></script>
24+
</body>
25+
</html>

‎index.js

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { fetchShader } from './src/ShaderUtils.js';
2+
import Material from './src/Material.js';
3+
4+
class Main {
5+
constructor() {
6+
this.canvas = document.querySelector('canvas');
7+
this.scaleValue = 250;
8+
this.offset = { x: 0, y: 0 };
9+
this.moving = false;
10+
this.start = { x: 0, y: 0 };
11+
}
12+
13+
async initialize() {
14+
if (!navigator.gpu) {
15+
console.error("WebGPU not supported on this browser.");
16+
return;
17+
}
18+
19+
const adapter = await navigator.gpu.requestAdapter();
20+
const device = await adapter.requestDevice();
21+
const context = this.canvas.getContext('webgpu');
22+
const format = navigator.gpu.getPreferredCanvasFormat();
23+
24+
context.configure({
25+
device,
26+
format,
27+
alphaMode: 'opaque',
28+
});
29+
30+
this.device = device;
31+
this.context = context;
32+
this.format = format;
33+
34+
await this.setupShaders();
35+
this.createMaterial();
36+
this.setupEventListeners();
37+
this.resize();
38+
}
39+
40+
async setupShaders() {
41+
this.vertexShaderCode = await fetchShader('./src/shader/vertex.wgsl');
42+
this.fragmentShaderCode = await fetchShader('./src/shader/fragment.wgsl');
43+
}
44+
45+
createMaterial() {
46+
this.material = new Material(this.device, this.vertexShaderCode, this.fragmentShaderCode, {
47+
0: { size: 8, usage: GPUBufferUsage.UNIFORM }, // resolution
48+
1: { size: 4, usage: GPUBufferUsage.UNIFORM }, // scale
49+
2: { size: 8, usage: GPUBufferUsage.UNIFORM }, // offset
50+
});
51+
52+
this.material.createPipeline(this.format);
53+
}
54+
55+
setupEventListeners() {
56+
window.addEventListener('resize', () => this.resize());
57+
this.canvas.addEventListener('mousedown', (event) => this.onMouseDown(event));
58+
this.canvas.addEventListener('mousemove', (event) => this.onMouseMove(event));
59+
this.canvas.addEventListener('mouseup', () => this.onMouseUp());
60+
this.canvas.addEventListener('wheel', (event) => this.onWheel(event));
61+
}
62+
63+
resize() {
64+
this.canvas.width = window.innerWidth;
65+
this.canvas.height = window.innerHeight;
66+
this.material.updateUniform('0', new Float32Array([this.canvas.width, this.canvas.height]));
67+
this.material.updateUniform('1', new Float32Array([this.scaleValue]));
68+
this.render();
69+
}
70+
71+
onMouseDown(event) {
72+
this.start.x = event.pageX;
73+
this.start.y = event.pageY;
74+
this.moving = true;
75+
}
76+
77+
onMouseMove(event) {
78+
if (this.moving) {
79+
this.offset.x += event.movementX / this.scaleValue;
80+
this.offset.y += event.movementY / this.scaleValue;
81+
this.material.updateUniform('2', new Float32Array([this.offset.x, this.offset.y]));
82+
this.render();
83+
}
84+
}
85+
86+
onMouseUp() {
87+
this.moving = false;
88+
}
89+
90+
onWheel(event) {
91+
this.scaleValue -= (event.deltaY / 100) * (this.scaleValue / 5);
92+
this.material.updateUniform('1', new Float32Array([this.scaleValue]));
93+
this.resize();
94+
}
95+
96+
render() {
97+
const commandEncoder = this.device.createCommandEncoder();
98+
const textureView = this.context.getCurrentTexture().createView();
99+
this.material.render(commandEncoder, textureView);
100+
this.device.queue.submit([commandEncoder.finish()]);
101+
}
102+
}
103+
104+
const app = new Main();
105+
app.initialize();

‎src/Material.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
class Material {
2+
constructor(device, vertexShaderCode, fragmentShaderCode, inputs = {}, sharedBuffers = {}) {
3+
this.device = device;
4+
this.vertexModule = device.createShaderModule({ code: vertexShaderCode });
5+
this.fragmentModule = device.createShaderModule({ code: fragmentShaderCode });
6+
7+
// Create buffers for all defined inputs
8+
this.uniformBuffers = {};
9+
for (const [name, { size, usage }] of Object.entries(inputs)) {
10+
this.uniformBuffers[name] = device.createBuffer({
11+
size,
12+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
13+
});
14+
}
15+
16+
// Store the shared buffers
17+
this.sharedBuffers = sharedBuffers;
18+
19+
// Predefine the pipeline and bind group layout
20+
this.pipeline = null;
21+
this.bindGroup = null;
22+
}
23+
24+
createPipeline(format) {
25+
this.pipeline = this.device.createRenderPipeline({
26+
layout: 'auto',
27+
vertex: {
28+
module: this.vertexModule,
29+
entryPoint: 'main',
30+
},
31+
fragment: {
32+
module: this.fragmentModule,
33+
entryPoint: 'main',
34+
targets: [{ format }],
35+
},
36+
primitive: {
37+
topology: 'triangle-list',
38+
},
39+
});
40+
41+
// Create the bind group including shared buffers
42+
const entries = Object.entries(this.uniformBuffers).map(([name, buffer], index) => ({
43+
binding: index,
44+
resource: { buffer },
45+
}));
46+
47+
// Add the shared buffers to the bind group
48+
for (const [binding, buffer] of Object.entries(this.sharedBuffers)) {
49+
entries.push({
50+
binding: parseInt(binding),
51+
resource: { buffer },
52+
});
53+
}
54+
55+
this.bindGroup = this.device.createBindGroup({
56+
layout: this.pipeline.getBindGroupLayout(0),
57+
entries,
58+
});
59+
}
60+
61+
updateUniform(name, data) {
62+
if (!this.uniformBuffers[name]) {
63+
throw new Error(`Uniform '${name}' is not defined.`);
64+
}
65+
this.device.queue.writeBuffer(this.uniformBuffers[name], 0, data);
66+
}
67+
68+
render(commandEncoder, textureView) {
69+
if (!this.pipeline || !this.bindGroup) {
70+
throw new Error("Pipeline or bind group not initialized. Call createPipeline().");
71+
}
72+
73+
const renderPassDescriptor = {
74+
colorAttachments: [
75+
{
76+
view: textureView,
77+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
78+
loadOp: 'clear',
79+
storeOp: 'store',
80+
},
81+
],
82+
};
83+
84+
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
85+
passEncoder.setPipeline(this.pipeline);
86+
passEncoder.setBindGroup(0, this.bindGroup);
87+
passEncoder.draw(6);
88+
passEncoder.end();
89+
}
90+
}
91+
92+
export default Material;

‎src/ShaderUtils.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
async function fetchShader(url) {
2+
const response = await fetch(url);
3+
if (!response.ok) {
4+
throw new Error(`Failed to load shader: ${url}`);
5+
}
6+
return await response.text();
7+
}
8+
9+
async function include(shaderPath) {
10+
let shaderCode = await fetchShader(shaderPath);
11+
const basePath = shaderPath.substring(0, shaderPath.lastIndexOf('/'));
12+
const includeRegex = /^#include\s+"(.+?)"/gm;
13+
let match;
14+
15+
while ((match = includeRegex.exec(shaderCode)) !== null) {
16+
const includeFile = match[1];
17+
const includeUrl = `${basePath}/${includeFile}`;
18+
19+
try {
20+
const includeContent = await fetchShader(includeUrl);
21+
shaderCode = shaderCode.replace(match[0], includeContent);
22+
} catch (error) {
23+
throw new Error(`Failed to include file: ${includeFile} - ${error.message}`);
24+
}
25+
}
26+
27+
return shaderCode;
28+
}
29+
30+
export { fetchShader, include };

‎src/shader/fragment.wgsl

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
@group(0) @binding(0) var<uniform> resolution : vec2<f32>;
2+
@group(0) @binding(1) var<uniform> scale : f32;
3+
@group(0) @binding(2) var<uniform> offset : vec2<f32>;
4+
5+
@fragment
6+
fn main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {
7+
// Normalize the coordinates to [0, 1] space
8+
let uv = fragCoord.xy / resolution;
9+
10+
// Adjust the x and y coordinates to account for scale and offset
11+
let x = ((fragCoord.x - resolution.x / 2.0) / scale) - offset.x;
12+
let y = ((fragCoord.y - resolution.y / 2.0) / scale) - offset.y;
13+
14+
// Initialize the complex number z0 = 0 and the complex number c = (x, y)
15+
var z = vec2<f32>(0, 0);
16+
let c = vec2<f32>(x, y);
17+
18+
// Maximum number of iterations to determine if a point is in the Mandelbrot set
19+
let maxIterations = i32(sqrt(scale));
20+
var i: i32 = 0;
21+
for (i = 0; i < maxIterations; i = i + 1) {
22+
let zSquared = vec2<f32>(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y);
23+
z = zSquared + c;
24+
if (dot(z, z) > 4.0) {
25+
break;
26+
}
27+
}
28+
29+
let normalizedIterations = f32(i) / f32(maxIterations);
30+
31+
32+
let color = vec3<f32>(
33+
normalizedIterations, // Red channel: varies from 0 to 1
34+
1.0 - normalizedIterations, // Green channel: varies inversely from 1 to 0
35+
0.5 + 0.5 * sin(normalizedIterations * 3.14159) // Blue channel: oscillates based on the sine function
36+
);
37+
38+
return vec4<f32>(color, 1.0);
39+
}

‎src/shader/vertex.wgsl

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@vertex
2+
fn main(@builtin(vertex_index) vertexIndex : u32) -> @builtin(position) vec4<f32> {
3+
var positions = array<vec2<f32>, 6>(
4+
vec2<f32>(-1.0, -1.0),
5+
vec2<f32>( 1.0, -1.0),
6+
vec2<f32>(-1.0, 1.0),
7+
vec2<f32>(-1.0, 1.0),
8+
vec2<f32>( 1.0, -1.0),
9+
vec2<f32>( 1.0, 1.0)
10+
);
11+
return vec4<f32>(positions[vertexIndex], 0.0, 1.0);
12+
}

0 commit comments

Comments
 (0)
Please sign in to comment.