draw 3d cubes

This commit is contained in:
Zack Buhman 2026-07-16 15:21:09 -05:00
parent d55ec71edc
commit 2dd13ddd5a
16 changed files with 28315 additions and 34 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*.wasm
*.pyc
__pycache__

29
build.sh Normal file
View File

@ -0,0 +1,29 @@
clang++ \
-v \
--target=wasm32 \
-O3 \
-flto \
-nostdlib \
-Wl,--no-entry \
-Wl,--export=rotate \
-Wl,--lto-O3 \
-Wl,--print-map \
-Wl,--import-memory \
-D_XM_NO_INTRINSICS_ \
-I. \
-o rotate.wasm \
rotate.c
# clang \
# -O1 \
# --target=wasm32 \
# -emit-llvm \
# -c \
# -S \
# test.c
# llc \
# -march=wasm32 \
# -filetype=obj \
# test.ll

View File

@ -1,6 +1,7 @@
struct Configuration { struct Configuration {
gridSize: vec2f, gridSize: vec2f,
aspect: vec2f aspect: vec2f,
matrix: mat4x4f
}; };
@group(0) @binding(0) var<uniform> config: Configuration; @group(0) @binding(0) var<uniform> config: Configuration;

2247
directxmath/directxmath.h Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

121
index.js
View File

@ -15,7 +15,6 @@ canvas.height = canvas.clientHeight
const context = canvas.getContext("webgpu"); const context = canvas.getContext("webgpu");
const canvasFormat = navigator.gpu.getPreferredCanvasFormat(); const canvasFormat = navigator.gpu.getPreferredCanvasFormat();
//console.log(canvasFormat);
context.configure({ context.configure({
device: device, device: device,
format: canvasFormat, format: canvasFormat,
@ -36,30 +35,48 @@ renderPass.end();
const commandBuffer = encoder.finish(); const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]); device.queue.submit([commandBuffer]);
const vertices = new Float32Array([
// X, Y,
-0.8, -0.8, // Triangle 1 (Blue)
0.8, -0.8,
0.8, 0.8,
-0.8, -0.8, // Triangle 2 (Red) const cubeResponse = await fetch("obj/cube.bin");
0.8, 0.8, const cubeObj = await cubeResponse.arrayBuffer();
-0.8, 0.8,
]);
const vertexBuffer = device.createBuffer({ const cubeView = new DataView(cubeObj);
const indexCount = cubeView.getUint32(0, true);
const indexBufferOffset = cubeView.getUint32(4, true);
const indexBufferSize = cubeView.getUint32(8, true);
const vertexBufferOffset = cubeView.getUint32(12, true);
const vertexBufferSize = cubeView.getUint32(16, true);
console.log(`indexBufferOffset ${indexBufferOffset} indexBufferSize ${indexBufferSize}`);
console.log(`vertexBufferOffset ${vertexBufferOffset} vertexBufferSize ${vertexBufferSize}`);
const cubeIndexCount = indexBufferSize / 2;
const cubeVerticesStride = 36;
const indexVertexBuffer = device.createBuffer({
label: "cell vertices", label: "cell vertices",
size: vertices.byteLength, size: indexBufferSize + vertexBufferSize,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, usage: GPUBufferUsage.INDEX | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
}); });
device.queue.writeBuffer(vertexBuffer, 0, vertices); device.queue.writeBuffer(indexVertexBuffer, 0, cubeObj, indexBufferOffset, indexBufferSize);
device.queue.writeBuffer(indexVertexBuffer, indexBufferSize, cubeObj, vertexBufferOffset, vertexBufferSize);
const vertexBufferLayout = { const vertexBufferLayout = {
arrayStride: 8, arrayStride: cubeVerticesStride,
attributes: [{ attributes: [{
format: "float32x2", format: "float32x3",
offset: 0, offset: 0,
shaderLocation: 0, shaderLocation: 0,
}, {
format: "float32x2",
offset: 12,
shaderLocation: 1,
}, {
format: "float16x4",
offset: 20,
shaderLocation: 2,
}, {
format: "float16x4",
offset: 28,
shaderLocation: 3,
}], }],
stepMode: "vertex", stepMode: "vertex",
}; };
@ -115,7 +132,15 @@ const cellPipeline = device.createRenderPipeline({
targets: [{ targets: [{
format: canvasFormat, format: canvasFormat,
}] }]
} },
primitive: {
topology: 'triangle-list',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
}); });
const computeShaderModule = device.createShaderModule({ const computeShaderModule = device.createShaderModule({
@ -135,13 +160,24 @@ const computePipeline = device.createComputePipeline({
const gridSize = 32; const gridSize = 32;
const workGroupSize = 16; const workGroupSize = 16;
const uniformArray = new Float32Array([gridSize, gridSize, 1, 1]); //const uniformArray2 = new Float32Array([gridSize, gridSize, 1, 1]);
const memory = new WebAssembly.Memory({ initial: 1 });
const uniformArray = new Float32Array(memory.buffer);
uniformArray[0] = gridSize;
uniformArray[1] = gridSize;
uniformArray[2] = 1;
uniformArray[3] = 1;
const uniformArraySize = 4 * 4 + 4 * 4 * 4;
const rotate = await WebAssembly.instantiateStreaming(fetch("rotate.wasm"), {
env: { memory: memory }
});
const uniformBuffer = device.createBuffer({ const uniformBuffer = device.createBuffer({
label: "grid uniform", label: "grid uniform",
size: uniformArray.byteLength, size: uniformArraySize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
}); });
device.queue.writeBuffer(uniformBuffer, 0, uniformArray); device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0, uniformArraySize / 4);
const cellStateArray = new Uint32Array(gridSize * gridSize); const cellStateArray = new Uint32Array(gridSize * gridSize);
@ -198,8 +234,28 @@ const bindGroups = [
let tick = 0; let tick = 0;
let which = 0; let which = 0;
function createDepthTexture() {
return device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
}
var depthTexture = undefined;
function render() function render()
{ {
if (canvas.clientWidth !== canvas.width || canvas.clientWidth !== canvas.height) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
if (depthTexture !== undefined) {
depthTexture.destroy();
}
depthTexture = createDepthTexture();
}
const encoder = device.createCommandEncoder(); const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass(); const computePass = encoder.beginComputePass();
@ -209,15 +265,14 @@ function render()
computePass.dispatchWorkgroups(workgroupCount); computePass.dispatchWorkgroups(workgroupCount);
computePass.end(); computePass.end();
const aspect = canvas.width / canvas.height;
rotate.instance.exports.rotate(tick * 0.01, aspect, 4 * 4);
tick += 1; tick += 1;
if ((tick % 5) == 0) { if ((tick % 5) == 0) {
which ^= 1; which ^= 1;
} }
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
const aspect = canvas.height / canvas.width;
if (canvas.width > canvas.height) { if (canvas.width > canvas.height) {
uniformArray[2] = aspect; uniformArray[2] = aspect;
uniformArray[3] = 1; uniformArray[3] = 1;
@ -225,7 +280,7 @@ function render()
uniformArray[2] = 1; uniformArray[2] = 1;
uniformArray[3] = 1 / aspect; uniformArray[3] = 1 / aspect;
} }
device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0); device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0, uniformArraySize / 4);
const renderPass = encoder.beginRenderPass({ const renderPass = encoder.beginRenderPass({
colorAttachments: [{ colorAttachments: [{
@ -233,13 +288,23 @@ function render()
loadOp: "clear", loadOp: "clear",
clearValue: { r: 0.2, g: 0.2, b: 0.4, a: 1 }, clearValue: { r: 0.2, g: 0.2, b: 0.4, a: 1 },
storeOp: "store", storeOp: "store",
}] }],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
}); });
renderPass.setPipeline(cellPipeline); renderPass.setPipeline(cellPipeline);
renderPass.setVertexBuffer(0, vertexBuffer); const vertexOffset = indexBufferSize;
renderPass.setVertexBuffer(0, indexVertexBuffer, vertexOffset, vertexBufferSize);
renderPass.setIndexBuffer(indexVertexBuffer, "uint16", 0, indexBufferSize);
renderPass.setBindGroup(0, bindGroups[which]); renderPass.setBindGroup(0, bindGroups[which]);
renderPass.draw(vertices.length / 2, gridSize * gridSize); //renderPass.draw(cubeVerticesCount, gridSize * gridSize);
renderPass.drawIndexed(cubeIndexCount, gridSize * gridSize);
//renderPass.drawIndexed(cubeIndexCount);
renderPass.end(); renderPass.end();

View File

@ -1,23 +1,29 @@
struct Configuration { struct Configuration {
gridSize: vec2f, gridSize: vec2f,
aspect: vec2f aspect: vec2f,
matrix: mat4x4f
}; };
@group(0) @binding(0) var<uniform> config: Configuration; @group(0) @binding(0) var<uniform> config: Configuration;
@group(0) @binding(1) var<storage> state: array<u32>; @group(0) @binding(1) var<storage> state: array<u32>;
struct VertexInput { struct VertexInput {
@location(0) position: vec2f, @location(0) position: vec3f,
@location(1) texture: vec2f,
@location(2) normal: vec3f,
@location(3) tangent: vec4f,
@builtin(instance_index) instance: u32, @builtin(instance_index) instance: u32,
}; };
struct VertexOutput { struct VertexOutput {
@builtin(position) position: vec4f, @builtin(position) position: vec4f,
@location(0) cell: vec2f, @location(0) cell: vec2f,
@location(1) normal: vec3f,
}; };
struct FragmentInput { struct FragmentInput {
@location(0) cell: vec2f, @location(0) cell: vec2f,
@location(1) normal: vec3f,
}; };
@vertex @vertex
@ -28,16 +34,21 @@ fn vertexMain(input: VertexInput) -> VertexOutput
let state = f32(state[input.instance]); let state = f32(state[input.instance]);
let cell = vec2f(instance % config.gridSize.x, floor(instance / config.gridSize.y)); let cell = vec2f(instance % config.gridSize.x, floor(instance / config.gridSize.y));
let grid = (position * state + cell) / config.gridSize; let grid = (position * state + vec3f(cell, 0)) / config.gridSize.xxx;
var output: VertexOutput; var output: VertexOutput;
output.position = vec4f((grid * 2 - 1) * config.aspect, 0, 1); //output.position = (config.matrix * vec4f((grid * 2 - 1), 1)) * vec4f(config.aspect, 1, 1);
output.position = config.matrix * vec4f((grid * 2 - 1), 1);
//output.position = config.matrix * vec4f(input.position, 1);
output.cell = cell / config.gridSize; output.cell = cell / config.gridSize;
output.normal = input.normal;
return output; return output;
} }
@fragment @fragment
fn fragmentMain(input: FragmentInput) -> @location(0) vec4f fn fragmentMain(input: FragmentInput) -> @location(0) vec4f
{ {
return vec4f(input.cell, 0, 1); return vec4f(input.normal * 0.5 + 0.5, 1);
} }

BIN
obj/cube.bin Normal file

Binary file not shown.

36
obj/cube.obj Normal file
View File

@ -0,0 +1,36 @@
# Blender 5.2.0 LTS
# www.blender.org
mtllib Untitled.mtl
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vn -0.0000 1.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -1.0000 -0.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vt 0.000000 1.000000
vt 1.000000 0.000000
vt 1.000000 1.000000
vt 0.000000 0.000000
s 0
usemtl Material
f 5/1/1 3/2/1 1/3/1
f 3/3/2 8/4/2 4/2/2
f 7/1/3 6/2/3 8/4/3
f 2/3/4 8/4/4 6/1/4
f 1/3/5 4/4/5 2/2/5
f 5/1/6 2/2/6 6/4/6
f 5/1/1 7/4/1 3/2/1
f 3/3/2 7/1/2 8/4/2
f 7/1/3 5/3/3 6/2/3
f 2/3/4 4/2/4 8/4/4
f 1/3/5 3/1/5 4/4/5
f 5/1/6 1/3/6 2/2/6

25
rotate.c Normal file
View File

@ -0,0 +1,25 @@
#include "directxmath/directxmath.h"
extern "C" void rotate(float angle, float aspect, float * buffer);
void rotate(float angle, float aspect, float * buffer)
{
//XMMATRIX mat = XMMatrixRotationZ(angle);
//XMMATRIX mat2 = XMMatrixRotationX(angle);
//XMMATRIX mat2 = XMMatrixTranslation(0, 0, 1);
//XMStoreFloat4x4((XMFLOAT4X4*)buffer, mat * mat2);
XMVECTOR eye = XMVectorSet(3, 3, 3, 0);
XMVECTOR at = XMVectorSet(0, 0, 0, 0);
XMVECTOR up = XMVectorSet(0, 1, 0, 0);
XMMATRIX view = XMMatrixLookAtRH(eye, at, up);
float fov_angle_y = XMConvertToRadians(45);
float near_z = 0.01;
float far_z = 1000.0;
XMMATRIX projection = XMMatrixPerspectiveFovRH(fov_angle_y, aspect, near_z, far_z);
XMMATRIX rot = XMMatrixRotationY(angle);
XMStoreFloat4x4((XMFLOAT4X4*)buffer, rot * view * projection);
}

2941
sal.h Normal file

File diff suppressed because it is too large Load Diff

128
tools/obj/obj.py Normal file
View File

@ -0,0 +1,128 @@
import sys
import struct
from itertools import chain
from dataclasses import dataclass
from obj import tangent
with open(sys.argv[1], 'r') as f:
lines = f.read().split('\n')
position = []
texture = []
normal = []
faces = []
def parse_vertex(prefix, s):
return s.removeprefix(prefix).strip().split()
def float_vector(l, n):
v = list(map(float, l))
assert len(v) == n
return v
vertex_types = [
("v ", position, 3),
("vn ", normal, 3),
("vt ", texture, 2),
]
ignore_prefixes = ["#", "mtllib ", "usemtl ", "o ", "s "]
def parse_line(line):
if not line.strip():
return
for prefix in ignore_prefixes:
if line.startswith(prefix):
return
for prefix, out, n in vertex_types:
if line.startswith(prefix):
out.append(float_vector(parse_vertex(prefix, line), n))
return
if line.startswith("f "):
vertices = [
tuple(int(i)-1 for i in vertex.split("/"))
for vertex in parse_vertex("f ", line)
]
assert len(vertices) == 3, vertices # is not triangle
faces.append(vertices)
return
assert False, repr(line) # unhandled line prefix
for line in lines:
parse_line(line)
vertex_buffer_keys = dict()
vertex_buffer = []
index_buffer = []
for face in faces:
for vertex in face:
if vertex not in vertex_buffer_keys:
vertex_buffer_keys[vertex] = len(vertex_buffer)
vertex_buffer.append(tuple(chain(
position[vertex[0]],
texture[vertex[1]],
normal[vertex[2]],
)))
index_buffer.append(vertex_buffer_keys[vertex])
def render_vertex(f, vertex, tangent):
# extra padding zero for normal R16G16B16A16
# position texture normal tangent
stride = struct.pack("<" + "fff" + "ff" + "eeee" + "eeee", *vertex, 0, *tangent)
print(vertex, tangent)
#print(len(stride))
f.write(stride)
minv = [0, 0, 0]
maxv = [0, 0, 0]
for v in vertex_buffer:
for i in range(3):
if v[i] > maxv[i]:
maxv[i] = v[i]
if v[i] < minv[i]:
minv[i] = v[i]
with open(sys.argv[2], 'wb') as f:
header_size = 4 * 5 + 4 * 6
index_size = 2 * 1
index_buffer_size = index_size * len(index_buffer)
vertex_size = (4 * 3) + (4 * 2) + (2 * 4) + (2 * 4)
vertex_buffer_size = vertex_size * len(vertex_buffer)
index_count = len(index_buffer)
index_buffer_offset = header_size
vertex_buffer_offset = header_size + index_buffer_size
tangents = tangent.generate_tangents(vertex_buffer, index_buffer)
header = struct.pack("<IIIIIffffff",
index_count,
index_buffer_offset,
index_buffer_size,
vertex_buffer_offset,
vertex_buffer_size,
*minv,
*maxv)
f.write(header)
assert f.tell() == index_buffer_offset
for index in index_buffer:
print(index, vertex_buffer[index])
f.write(struct.pack("<H", index))
assert f.tell() == vertex_buffer_offset
for vertex, tangent in zip(vertex_buffer, tangents):
render_vertex(f, vertex, tangent)
assert f.tell() == vertex_buffer_offset + vertex_buffer_size

81
tools/obj/tangent.py Normal file
View File

@ -0,0 +1,81 @@
import numpy as np
PNT = list[tuple[float, float, float, # position
float, float, # texture
float, float, float, float]] # normal
def get_position(vertex_buffer: list[PNT], index):
return vertex_buffer[index][0:3]
def get_texture(vertex_buffer: list[PNT], index):
return vertex_buffer[index][3:5]
def get_normal(vertex_buffer: list[PNT], index):
return vertex_buffer[index][5:8]
def normalize(v):
return v / np.linalg.norm(v)
def generate_tangents(vertex_buffer: list[PNT],
index_buffer: list[int]):
vertex_count = len(vertex_buffer)
triangle_count = len(index_buffer) // 3
tan_s = np.zeros((vertex_count, 3,))
tan_t = np.zeros((vertex_count, 3,))
for t in range(triangle_count):
i0 = index_buffer[t * 3 + 0]
i1 = index_buffer[t * 3 + 1]
i2 = index_buffer[t * 3 + 2]
p0 = get_position(vertex_buffer, i0)
p1 = get_position(vertex_buffer, i1)
p2 = get_position(vertex_buffer, i2)
uv0 = get_texture(vertex_buffer, i0)
uv1 = get_texture(vertex_buffer, i1)
uv2 = get_texture(vertex_buffer, i2)
x1 = p1[0] - p0[0]
x2 = p2[0] - p0[0]
y1 = p1[1] - p0[1]
y2 = p2[1] - p0[1]
z1 = p1[2] - p0[2]
z2 = p2[2] - p0[2]
s1 = uv1[0] - uv0[0]
s2 = uv2[0] - uv0[0]
t1 = uv1[1] - uv0[1]
t2 = uv2[1] - uv0[1]
d = (s1 * t2 - s2 * t1)
r = 1.0 / d if d != 0 else 0
sdir = np.array([(t2 * x1 - t1 * x2) * r,
(t2 * y1 - t1 * y2) * r,
(t2 * z1 - t1 * z2) * r])
tdir = np.array([(s1 * x2 - s2 * x1) * r,
(s1 * y2 - s2 * y1) * r,
(s1 * z2 - s2 * z1) * r])
tan_s += sdir
tan_t += tdir
tangents = []
for v in range(vertex_count):
n = np.array(get_normal(vertex_buffer, v))
s = tan_s[v]
# orthagonalize
tangent = normalize(s - n * np.dot(n, s))
h = np.dot(np.cross(n, s), tan_t[v])
w = -1.0 if h < 0.0 else 1.0
tangent_w = (tangent[0], tangent[1], tangent[2], w)
#print(tangent_w)
tangents.append(tangent_w)
return tangents