diff --git a/cube.js b/cube.js new file mode 100644 index 0000000..57fb8e8 --- /dev/null +++ b/cube.js @@ -0,0 +1,371 @@ +const cubeResponse = await fetch("obj/cube.bin"); +const cubeObj = await cubeResponse.arrayBuffer(); + +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", + size: indexBufferSize + vertexBufferSize, + usage: GPUBufferUsage.INDEX | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, +}); +device.queue.writeBuffer(indexVertexBuffer, 0, cubeObj, indexBufferOffset, indexBufferSize); +device.queue.writeBuffer(indexVertexBuffer, indexBufferSize, cubeObj, vertexBufferOffset, vertexBufferSize); + +////////////////////////////////////////////////////////////////////// +// 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", + }, +]; + +const indexWgsl = getPath("index.wgsl"); +const computeWgsl = getPath("compute.wgsl"); + +const cellShaderModule = device.createShaderModule({ + label: "cell shader", + code: await indexWgsl, +}); + +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 1", + entries: [{ + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: "uniform" } + }] + }), +]; + +const pipelineLayout = device.createPipelineLayout({ + label: "pipeline layout", + bindGroupLayouts: bindGroupLayouts, +}); + +const cellPipeline = device.createRenderPipeline({ + label: "cell pipeline", + layout: pipelineLayout, + vertex: { + module: cellShaderModule, + entryPoint: "vertexMain", + buffers: vertexBufferLayouts, + }, + fragment: { + module: cellShaderModule, + entryPoint: "fragmentMain", + targets: [{ + format: canvasFormat, + }] + }, + primitive: { + topology: 'triangle-list', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus', + }, +}); + +const computeShaderModule = device.createShaderModule({ + label: "compute shader", + code: await computeWgsl, +}); + +const computePipelineLayout = device.createPipelineLayout({ + label: "compute pipeline layout", + bindGroupLayouts: [ bindGroupLayouts[0] ], +}); + +const computePipeline = device.createComputePipeline({ + label: "compute pipeline", + layout: computePipelineLayout, + compute: { + module: computeShaderModule, + entryPoint: "computeMain", + }, +}); + +const gridSize = 32; +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] = 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 } +}); + +const uniformBuffer = device.createBuffer({ + label: "grid uniform", + size: uniformArraySize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, +}); +device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0, uniformArraySize / 4); + +const cellStateArray = new Uint32Array(gridSize * gridSize); + +const cellStateStorage = [ + device.createBuffer({ + label: "cell state 0", + size: cellStateArray.byteLength, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }), + device.createBuffer({ + label: "cell state 1", + size: cellStateArray.byteLength, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + }), +]; + +for (let i = 0; i < cellStateArray.length; i += 1) { + cellStateArray[i] = Math.random() > 0.6 ? 1 : 0; +} +device.queue.writeBuffer(cellStateStorage[0], 0, cellStateArray); +device.queue.writeBuffer(cellStateStorage[1], 0, cellStateArray); + +const bindGroups = [ + device.createBindGroup({ + label: "cell bind group 0", + layout: bindGroupLayouts[0], + entries: [{ + binding: 0, + resource: { buffer: uniformBuffer }, + }, { + binding: 1, + resource: { buffer: cellStateStorage[0] }, + }, { + binding: 2, + resource: { buffer: cellStateStorage[1] }, + }], + }), + device.createBindGroup({ + label: "cell bind group 1", + layout: bindGroupLayouts[0], + entries: [{ + binding: 0, + resource: { buffer: uniformBuffer }, + }, { + binding: 1, + resource: { buffer: cellStateStorage[1] }, + }, { + binding: 2, + resource: { buffer: cellStateStorage[0] }, + }], + }) +]; + +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; + +function render() +{ + recreateDepth(); + + const encoder = device.createCommandEncoder(); + + const computePass = encoder.beginComputePass(); + computePass.setPipeline(computePipeline); + computePass.setBindGroup(0, bindGroups[which]); + const workgroupCount = Math.ceil((gridSize * gridSize) / workGroupSize); + computePass.dispatchWorkgroups(workgroupCount); + computePass.end(); + + const aspect = canvas.width / canvas.height; + rotate.instance.exports.rotate(tick * 0.01, aspect, 4 * 4); + + tick += 1; + if ((tick % 5) == 0) { + which ^= 1; + } + + if (lightingSelect.value === "diffuse") { + uniformArray[0] = 1; + } else { + uniformArray[0] = 0; + } + device.queue.writeBuffer(uniformBuffer, 0, uniformArray, 0, uniformArraySize / 4); + + const renderPass = encoder.beginRenderPass({ + colorAttachments: [{ + view: context.getCurrentTexture().createView(), + loadOp: "clear", + clearValue: { r: 0.2, g: 0.2, b: 0.4, a: 1 }, + storeOp: "store", + }], + depthStencilAttachment: { + view: depthTexture.createView(), + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + }, + }); + + 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); + renderPass.drawIndexed(cubeIndexCount, models[currentModel].voxelCount); + + renderPass.end(); + + const commandBuffer = encoder.finish(); + device.queue.submit([commandBuffer]); + + requestAnimationFrame(render) +} +//requestAnimationFrame(render); + + +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"; diff --git a/gltf.wgsl b/gltf.wgsl index 5bbc840..4738cd1 100644 --- a/gltf.wgsl +++ b/gltf.wgsl @@ -1,6 +1,8 @@ struct Configuration { - matrix: mat4x4f, - matrixWorld: mat4x4f, + viewProj: mat4x4f, + lightViewProj: mat4x4f, + lightPosition: vec4f, + eyePosition: vec4f, }; @group(0) @binding(0) var config: Configuration; @@ -27,12 +29,8 @@ struct VertexOutput { @location(0) positionWorld: vec3f, @location(1) normal: vec3f, @location(2) texture: vec2f, -}; - -struct FragmentInput { - @location(0) positionWorld: vec3f, - @location(1) normal: vec3f, - @location(2) texture: vec2f, + @location(3) lightPosition: vec3f, + @location(4) eyePosition: vec3f, }; @vertex @@ -42,10 +40,12 @@ fn vertexMain(input: VertexInput) -> VertexOutput //let position = vec4f(input.position, 1); let normal = vec4f(input.normal, 0.0f); var output: VertexOutput; - output.position = config.matrix * position; - output.positionWorld = (config.matrixWorld * position).xyz; - output.normal = (config.matrixWorld * normal).xyz; + output.position = config.viewProj * position; + output.positionWorld = (position).xyz; + output.normal = (normal).xyz; output.texture = input.texture; + output.lightPosition = config.lightPosition.xyz; + output.eyePosition = config.eyePosition.xyz; return output; } @@ -75,11 +75,8 @@ fn blinnPhong(ndotl: f32, lightVector: vec3f, normal: vec3f, toEye: vec3f) -> ve return specular; } -fn lighting(position: vec3f, normal: vec3f) -> vec3f +fn lighting(position: vec3f, lightPosition: vec3f, eyePosition: vec3f, normal: vec3f) -> vec3f { - let d = 0.9; - let eyePosition = vec3f(d, d / 2, d); - let lightPosition = vec3f(100, 100, 100); let lightVector = normalize(lightPosition - position); let ndotl = max(dot(lightVector, normal), 0.0); let intensity = (ndotl + 0.3) / 1.3; @@ -90,9 +87,9 @@ fn lighting(position: vec3f, normal: vec3f) -> vec3f } @fragment -fn fragmentMain(input: FragmentInput) -> @location(0) vec4f +fn fragmentMain(input: VertexOutput) -> @location(0) vec4f { - let intensity = lighting(input.positionWorld, normalize(input.normal)); + let intensity = lighting(input.positionWorld, input.lightPosition, input.eyePosition, normalize(input.normal)); let color = textureSample(colorTexture, linearSampler, input.texture); return vec4(color.xyz * intensity, 1.0); diff --git a/index.html b/index.html index cb859cc..c026342 100644 --- a/index.html +++ b/index.html @@ -9,10 +9,10 @@ color-scheme: light dark; } div, span { - font: 0.95em sans-serif; + font: 0.8rem sans-serif; } select, option { - font: 0.95em sans-serif; + font: 0.8rem sans-serif; } canvas { position: absolute; @@ -35,6 +35,7 @@ width: 250px; float: right; margin-right: 15px; + background: #444; } .label { width: calc(40% - 0.3rem); @@ -54,9 +55,22 @@ border-bottom: 1px solid #999; } .row { - background: #444; height: 1.5rem; } + code { + font: 0.95rem monospace; + padding-top: 0.1rem; + } + .float3 { + width: 30%; + display: inline-block; + text-align: right; + } + .float2 { + width: 40%; + display: inline-block; + text-align: right; + } @@ -76,6 +90,21 @@ +
+ eye +
+ 0.0 + 0.0 + 0.0 +
+
+
+ yaw/pitch +
+ 0.0 + 0.0 +
+