diff --git a/index.html b/index.html index 9ad6129..5f392a8 100644 --- a/index.html +++ b/index.html @@ -2,8 +2,18 @@ + WebGPU +
+
+
+ model + +
+
+ lighting + +
+ +
+
diff --git a/index.js b/index.js index 7ebc1f5..a9b25e9 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,6 @@ renderPass.end(); const commandBuffer = encoder.finish(); device.queue.submit([commandBuffer]); - const cubeResponse = await fetch("obj/cube.bin"); const cubeObj = await cubeResponse.arrayBuffer(); @@ -45,8 +44,8 @@ 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}`); +//console.log(`indexBufferOffset ${indexBufferOffset} indexBufferSize ${indexBufferSize}`); +//console.log(`vertexBufferOffset ${vertexBufferOffset} vertexBufferSize ${vertexBufferSize}`); const cubeIndexCount = indexBufferSize / 2; const cubeVerticesStride = 36; @@ -59,27 +58,101 @@ const indexVertexBuffer = device.createBuffer({ device.queue.writeBuffer(indexVertexBuffer, 0, cubeObj, indexBufferOffset, indexBufferSize); device.queue.writeBuffer(indexVertexBuffer, indexBufferSize, cubeObj, vertexBufferOffset, vertexBufferSize); -const vertexBufferLayout = { - arrayStride: cubeVerticesStride, - attributes: [{ - format: "float32x3", - offset: 0, - shaderLocation: 0, - }, { - format: "float32x2", - offset: 12, - shaderLocation: 1, - }, { - format: "float16x4", - offset: 20, - shaderLocation: 2, - }, { - format: "float16x4", - offset: 28, - shaderLocation: 3, - }], - stepMode: "vertex", -}; +////////////////////////////////////////////////////////////////////// +// instance buffer +////////////////////////////////////////////////////////////////////// + +async function loadVoxModel(path) +{ + const voxResponse = await fetch(path); + const vox = await voxResponse.arrayBuffer(); + const voxView = new DataView(vox); + const sizeX = voxView.getUint32(0, true); + const sizeY = voxView.getUint32(4, true); + const sizeZ = voxView.getUint32(8, true); + const voxelCount = voxView.getUint32(12, true); + + const instanceBuffer = device.createBuffer({ + label: `instanceBuffer %{}`, + size: voxelCount * 4, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }); + device.queue.writeBuffer(instanceBuffer, 0, vox, 16, voxelCount * 4); + + const modelConfigurationBuffer = device.createBuffer({ + label: "modelConfigurationBuffer", + size: 4 * 4 + 256 * 4 * 4, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + const paletteOffset = 16 + voxelCount * 4; + + const sizeArray = new Float32Array([sizeX, sizeY, sizeZ, 0]); + device.queue.writeBuffer(modelConfigurationBuffer, 0, sizeArray); + device.queue.writeBuffer(modelConfigurationBuffer, 4 * 4, vox, paletteOffset, 256 * 4 * 4); + + return { + instanceBuffer: instanceBuffer, + modelConfigurationBuffer: modelConfigurationBuffer, + voxelCount: voxelCount, + path: path, + } +} + +const modelPaths = [ + "vox/monu1.bin", + "vox/monu7.bin", + "vox/monu9.bin", + "vox/monu16.bin", + "vox/chr_fox.bin", + "vox/ff3.bin", + "vox/dragon.bin", +]; + +const models = {} +for (const path of modelPaths) { + models[path] = loadVoxModel(path); +} +for (const path of modelPaths) { + models[path] = await models[path]; +} +var currentModel = modelPaths[0]; + +////////////////////////////////////////////////////////////////////// +// vertex layout +////////////////////////////////////////////////////////////////////// + +const vertexBufferLayouts = [ + { + arrayStride: cubeVerticesStride, + attributes: [{ + format: "float32x3", + offset: 0, + shaderLocation: 0, + }, { + format: "float32x2", + offset: 12, + shaderLocation: 1, + }, { + format: "float16x4", + offset: 20, + shaderLocation: 2, + }, { + format: "float16x4", + offset: 28, + shaderLocation: 3, + }], + stepMode: "vertex", + }, + { + arrayStride: 4, + attributes: [{ + format: "uint8x4", + offset: 0, + shaderLocation: 4, + }], + stepMode: "instance", + }, +]; function getPath(path) { @@ -91,31 +164,44 @@ function getPath(path) }).then((blob) => { return blob.text() }) } +const indexWgsl = getPath("index.wgsl"); +const computeWgsl = getPath("compute.wgsl"); + const cellShaderModule = device.createShaderModule({ label: "cell shader", - code: await getPath("index.wgsl") + code: await indexWgsl, }); -const bindGroupLayout = device.createBindGroupLayout({ - label: "bind group layout", - entries: [{ - binding: 0, - visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE, - buffer: { type: "uniform" } - }, { - binding: 1, - visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE, - buffer: { type: "read-only-storage" } - }, { - binding: 2, - visibility: GPUShaderStage.COMPUTE, - buffer: { type: "storage" } - }] -}); +const bindGroupLayouts = [ + device.createBindGroupLayout({ + label: "bind group layout 0", + entries: [{ + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE, + buffer: { type: "uniform" } + }, { + binding: 1, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE, + buffer: { type: "read-only-storage" } + }, { + binding: 2, + visibility: GPUShaderStage.COMPUTE, + buffer: { type: "storage" } + }] + }), + device.createBindGroupLayout({ + label: "bind group layout 0", + entries: [{ + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: "uniform" } + }] + }), +]; const pipelineLayout = device.createPipelineLayout({ label: "pipeline layout", - bindGroupLayouts: [ bindGroupLayout ], + bindGroupLayouts: bindGroupLayouts, }); const cellPipeline = device.createRenderPipeline({ @@ -124,7 +210,7 @@ const cellPipeline = device.createRenderPipeline({ vertex: { module: cellShaderModule, entryPoint: "vertexMain", - buffers: [vertexBufferLayout], + buffers: vertexBufferLayouts, }, fragment: { module: cellShaderModule, @@ -145,12 +231,17 @@ const cellPipeline = device.createRenderPipeline({ const computeShaderModule = device.createShaderModule({ label: "compute shader", - code: await getPath("compute.wgsl") + code: await computeWgsl, +}); + +const computePipelineLayout = device.createPipelineLayout({ + label: "compute pipeline layout", + bindGroupLayouts: [ bindGroupLayouts[0] ], }); const computePipeline = device.createComputePipeline({ label: "compute pipeline", - layout: pipelineLayout, + layout: computePipelineLayout, compute: { module: computeShaderModule, entryPoint: "computeMain", @@ -163,11 +254,11 @@ const workGroupSize = 16; //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; +uniformArray[0] = 1; +uniformArray[1] = 0; +uniformArray[2] = 0; +uniformArray[3] = 0; +const uniformArraySize = 4 * 4 + (4 * 4 * 4) * 2; const rotate = await WebAssembly.instantiateStreaming(fetch("rotate.wasm"), { env: { memory: memory } }); @@ -203,7 +294,7 @@ device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray); const bindGroups = [ device.createBindGroup({ label: "cell bind group 0", - layout: bindGroupLayout, + layout: bindGroupLayouts[0], entries: [{ binding: 0, resource: { buffer: uniformBuffer }, @@ -217,7 +308,7 @@ const bindGroups = [ }), device.createBindGroup({ label: "cell bind group 1", - layout: bindGroupLayout, + layout: bindGroupLayouts[0], entries: [{ binding: 0, resource: { buffer: uniformBuffer }, @@ -231,6 +322,19 @@ const bindGroups = [ }) ]; +const modelBindGroups = {}; +for (const path of modelPaths) { + const bindGroup = device.createBindGroup({ + label: "palette bind group", + layout: bindGroupLayouts[1], + entries: [{ + binding: 0, + resource: { buffer: models[path].modelConfigurationBuffer }, + }] + }); + modelBindGroups[path] = bindGroup; +} + let tick = 0; let which = 0; @@ -244,6 +348,18 @@ function createDepthTexture() { var depthTexture = undefined; +const modelSelect = document.getElementById("model-select"); +for (const path of modelPaths) { + const option = document.createElement("option"); + option.value = path; + option.innerHTML = path; + modelSelect.appendChild(option); +} +modelSelect.value = currentModel; + +const lightingSelect = document.getElementById("lighting-select"); +lightingSelect.value = "unlit"; + function render() { if (canvas.clientWidth !== canvas.width || canvas.clientWidth !== canvas.height) { @@ -273,12 +389,10 @@ function render() which ^= 1; } - if (canvas.width > canvas.height) { - uniformArray[2] = aspect; - uniformArray[3] = 1; + if (lightingSelect.value === "diffuse") { + uniformArray[0] = 1; } else { - uniformArray[2] = 1; - uniformArray[3] = 1 / aspect; + uniformArray[0] = 0; } device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0, uniformArraySize / 4); @@ -297,14 +411,19 @@ function render() }, }); + currentModel = modelSelect.value; + renderPass.setPipeline(cellPipeline); const vertexOffset = indexBufferSize; renderPass.setVertexBuffer(0, indexVertexBuffer, vertexOffset, vertexBufferSize); + renderPass.setVertexBuffer(1, models[currentModel].instanceBuffer); renderPass.setIndexBuffer(indexVertexBuffer, "uint16", 0, indexBufferSize); renderPass.setBindGroup(0, bindGroups[which]); + renderPass.setBindGroup(1, modelBindGroups[currentModel]); //renderPass.draw(cubeVerticesCount, gridSize * gridSize); - renderPass.drawIndexed(cubeIndexCount, gridSize * gridSize); + //renderPass.drawIndexed(cubeIndexCount, gridSize * gridSize); //renderPass.drawIndexed(cubeIndexCount); + renderPass.drawIndexed(cubeIndexCount, models[currentModel].voxelCount); renderPass.end(); diff --git a/index.wgsl b/index.wgsl index dcc6b23..fde84d3 100644 --- a/index.wgsl +++ b/index.wgsl @@ -1,29 +1,41 @@ struct Configuration { - gridSize: vec2f, - aspect: vec2f, - matrix: mat4x4f + lighting: vec4f, + matrix: mat4x4f, + matrixWorld: mat4x4f, }; @group(0) @binding(0) var config: Configuration; @group(0) @binding(1) var state: array; +struct ModelConfiguration { + size: vec4f, + color: array, +}; + +@group(1) @binding(0) var modelConfiguration: ModelConfiguration; + struct VertexInput { @location(0) position: vec3f, @location(1) texture: vec2f, @location(2) normal: vec3f, @location(3) tangent: vec4f, + @location(4) instancePosition: vec4, @builtin(instance_index) instance: u32, }; struct VertexOutput { @builtin(position) position: vec4f, - @location(0) cell: vec2f, - @location(1) normal: vec3f, + @location(0) positionWorld: vec3f, + @interpolate(flat) @location(1) colorIndex: i32, + @location(2) normal: vec3f, + @interpolate(flat) @location(3) lighting: i32, }; struct FragmentInput { - @location(0) cell: vec2f, - @location(1) normal: vec3f, + @location(0) positionWorld: vec3f, + @interpolate(flat) @location(1) colorIndex: i32, + @location(2) normal: vec3f, + @interpolate(flat) @location(3) lighting: i32, }; @vertex @@ -32,23 +44,75 @@ fn vertexMain(input: VertexInput) -> VertexOutput let position = input.position * 0.5 + 0.5; let instance = f32(input.instance); + /* let state = f32(state[input.instance]); let cell = vec2f(instance % config.gridSize.x, floor(instance / config.gridSize.y)); let grid = (position * state + vec3f(cell, 0)) / config.gridSize.xxx; + */ + + let size = max(max(modelConfiguration.size.x, modelConfiguration.size.y), modelConfiguration.size.z); + + let cell = (position * 1 + vec3f(input.instancePosition.xzy) - (modelConfiguration.size.xzy/2)) / size; var output: VertexOutput; - //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.normal = input.normal; + output.position = config.matrix * vec4f(cell, 1); + output.positionWorld = (config.matrixWorld * vec4f(cell, 1.0f)).xyz; + output.colorIndex = i32(input.instancePosition.w); + output.normal = (config.matrixWorld * vec4f(input.normal, 0.0f)).xyz; + output.lighting = i32(config.lighting.x != 0.0); return output; } +fn schlickFresnel(R0: vec3f, normal: vec3f, lightVector: vec3f) -> vec3f +{ + let incidentAngle = saturate(dot(normal, lightVector)); + + let f0 = 1.0 - incidentAngle; + let reflectPercent = R0 + (1.0f - R0) * (f0 * f0 * f0 * f0 * f0); + + return reflectPercent; +} + +fn blinnPhong(ndotl: f32, lightVector: vec3f, normal: vec3f, toEye: vec3f) -> vec3f +{ + let roughness = 0.1; + + let m = roughness * 256.0; + let halfVec = normalize(toEye + lightVector); + let roughnessFactor = (m + 8.0) * pow(ndotl, m) / 8.0f; + + let fresnelR0 = vec3f(0.95f, 0.95f, 0.95f); + let fresnelFactor = schlickFresnel(fresnelR0, normal, lightVector); + + let specular = fresnelFactor * roughnessFactor; + + return specular; +} + +fn lighting(position: vec3f, normal: vec3f) -> vec3f +{ + let d = 0.9; + let eyePosition = vec3f(d, d / 2, d); + let lightPosition = vec3f(1, 1, 1); + let lightVector = normalize(lightPosition - position); + let ndotl = max(dot(lightVector, normal), 0.0); + let intensity = (ndotl + 0.3) / 1.3; + + let toEye = normalize(eyePosition - position); + + return (1.0 + blinnPhong(ndotl, lightVector, normal, toEye)) * intensity; +} + @fragment fn fragmentMain(input: FragmentInput) -> @location(0) vec4f { - return vec4f(input.normal * 0.5 + 0.5, 1); + let baseColor = modelConfiguration.color[input.colorIndex - 1] / 255.0; + + var intensity = vec3f(1.0); + + if (input.lighting == 1) { + intensity = lighting(input.positionWorld, normalize(input.normal)); + } + + return vec4(baseColor.xyz * intensity, baseColor.w); } diff --git a/rotate.c b/rotate.c index ebc04b3..7a8c233 100644 --- a/rotate.c +++ b/rotate.c @@ -9,8 +9,8 @@ void rotate(float angle, float aspect, float * buffer) //XMMATRIX mat2 = XMMatrixTranslation(0, 0, 1); //XMStoreFloat4x4((XMFLOAT4X4*)buffer, mat * mat2); - - XMVECTOR eye = XMVectorSet(3, 3, 3, 0); + float d = 0.9; + XMVECTOR eye = XMVectorSet(d, d / 2, d, 0); XMVECTOR at = XMVectorSet(0, 0, 0, 0); XMVECTOR up = XMVectorSet(0, 1, 0, 0); XMMATRIX view = XMMatrixLookAtRH(eye, at, up); @@ -22,4 +22,5 @@ void rotate(float angle, float aspect, float * buffer) XMMATRIX rot = XMMatrixRotationY(angle); XMStoreFloat4x4((XMFLOAT4X4*)buffer, rot * view * projection); + XMStoreFloat4x4((XMFLOAT4X4*)(buffer + 4 * 4), rot); } diff --git a/tools/vox.py b/tools/vox.py new file mode 100644 index 0000000..06e46c4 --- /dev/null +++ b/tools/vox.py @@ -0,0 +1,140 @@ +import struct +from functools import partial +from dataclasses import dataclass +import sys + +with open(sys.argv[1], "rb") as f: + buf = memoryview(f.read()) + +class Reader: + def __init__(self, buf): + self.index = 0 + self.mem = memoryview(buf) + + def read(self, n, t): + v = struct.unpack(t, self.mem[self.index:self.index+n]) + self.index += n + return v + + def read_char4(self): + v = self.read(4, "