diff --git a/common.js b/common.js new file mode 100644 index 0000000..3154443 --- /dev/null +++ b/common.js @@ -0,0 +1,11 @@ +function getPath(path) +{ + return fetch(path).then((response) => { + if (!response.ok) { + throw new Error(`${path}: ${response.status}`) + } + return response.blob() + }).then((blob) => { return blob.text() }) +} + +export { getPath }; diff --git a/gltf.js b/gltf.js new file mode 100644 index 0000000..3c45dd3 --- /dev/null +++ b/gltf.js @@ -0,0 +1,162 @@ +function jsonChunk(buffer, offset, length) +{ + const decoder = new TextDecoder(); + const array = new Uint8Array(buffer); + const object = JSON.parse(decoder.decode(array.subarray(offset, offset + length))); + //console.log(object); + return object; +} + +function parseGltfChunks(buffer) +{ + const view = new DataView(buffer); + + const magic = view.getUint32(0, true); + const version = view.getUint32(4, true); + const length = view.getUint32(8, true); + console.assert(magic === 0x46546C67); + console.assert(version === 2); + + const gltf = {}; + + var offset = 12; + while ((length - offset) > 0) { + const chunkLength = view.getUint32(offset + 0, true); + const chunkType = view.getUint32(offset + 4, true); + console.log("chunkLength", chunkLength); + console.log("chunkType", chunkType); + offset += 8; + + console.assert(chunkType === 0x4E4F534A || chunkType === 0x004E4942); + if (chunkType === 0x4E4F534A) { + const object = jsonChunk(buffer, offset, chunkLength); + console.assert(!("json" in gltf)); + gltf.json = object; + } + if (chunkType === 0x004E4942) { + console.assert(!("bin" in gltf)); + gltf.bin = {buffer: buffer, offset: offset, length: chunkLength}; + } + + offset += chunkLength; + } + + console.assert(offset == length); + return gltf; +} + +function componentCount(componentType) +{ + switch (componentType) { + case "SCALAR": return 1; + case "VEC2": return 2; + case "VEC3": return 3; + case "VEC4": return 4; + default: + console.assert(false, componentType); + break; + } +} + +function typeStride(type) +{ + switch (type) { + case 5120: // signed byte + return 1; + case 5121: // unsigned byte + return 1; + case 5122: // signed short + return 2; + case 5123: // unsigned short + return 2; + case 5125: // unsigned int + return 4; + case 5126: // float + return 4; + default: + console.assert(false, type); + break; + } +} + +function attributeStride(componentType, type) +{ + const t = typeStride(componentType); + const c = componentCount(type); + return c * t; +} + +function vertexFormat(componentType, type) +{ + switch (componentType) { + case 5120: // signed byte + if (type === "SCALAR") return "sint8"; + else if (type === "VEC2") return "sint8x2"; + else if (type === "VEC4") return "sint8x4"; + else console.assert(false, [componentType, type]); + case 5121: // unsigned byte + if (type === "SCALAR") return "uint8"; + else if (type === "VEC2") return "uint8x2"; + else if (type === "VEC4") return "uint8x4"; + else console.assert(false, [componentType, type]); + case 5122: // signed short + if (type === "SCALAR") return "sint16"; + else if (type === "VEC2") return "sint16x2"; + else if (type === "VEC4") return "sint16x4"; + else console.assert(false, [componentType, type]); + case 5123: // unsigned short + if (type === "SCALAR") return "uint16"; + else if (type === "VEC2") return "uint16x2"; + else if (type === "VEC4") return "uint16x4"; + else console.assert(false, [componentType, type]); + case 5125: // unsigned int + if (type === "SCALAR") return "uint32"; + else if (type === "VEC2") return "uint32x2"; + else if (type === "VEC3") return "uint32x3"; + else if (type === "VEC4") return "uint32x4"; + else console.assert(false, [componentType, type]); + case 5126: // float + if (type === "SCALAR") return "float32"; + else if (type === "VEC2") return "float32x2"; + else if (type === "VEC3") return "float32x3"; + else if (type === "VEC4") return "float32x4"; + else console.assert(false, [componentType, type]); + default: + console.assert(false, [componentType, type]); + break; + } +} + +function primitiveVertexBufferLayouts(gltf, primitive) +{ + const accessors = [ + gltf.json.accessors[primitive.attributes["POSITION"]], + gltf.json.accessors[primitive.attributes["NORMAL"]], + gltf.json.accessors[primitive.attributes["TEXCOORD_0"]], + ]; + + const layouts = []; + + for (let i = 0; i < accessors.length; i++) { + const componentType = accessors[i].componentType; + const type = accessors[i].type; + + const bufferView = gltf.json.bufferViews[accessors[i].bufferView]; + + const arrayStride = "byteStride" in bufferView ? bufferView.byteStride : attributeStride(componentType, type); + const offset = "byteOffset" in accessors[i] ? accessors[i].byteOffset : 0; + const layout = { + arrayStride: arrayStride, + attributes: [{ + format: vertexFormat(componentType, type), + offset: offset, + shaderLocation: i, + }], + stepMode: "vertex", + }; + layouts.push(layout); + } + return layouts; +} + +export { parseGltfChunks, primitiveVertexBufferLayouts }; diff --git a/gltf.wgsl b/gltf.wgsl new file mode 100644 index 0000000..e68ee35 --- /dev/null +++ b/gltf.wgsl @@ -0,0 +1,82 @@ +struct Configuration { + matrix: mat4x4f, + matrixWorld: mat4x4f, +}; + +@group(0) @binding(0) var config: Configuration; + +struct VertexInput { + @location(0) position: vec3f, + @location(1) normal: vec3f, + @location(2) texture: vec2f, +}; + +struct VertexOutput { + @builtin(position) position: vec4f, + @location(0) positionWorld: vec3f, + @location(1) normal: vec3f, +}; + +struct FragmentInput { + @location(0) positionWorld: vec3f, + @location(1) normal: vec3f, +}; + +@vertex +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; + 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 +{ + let intensity = lighting(input.positionWorld, normalize(input.normal)); + return vec4(intensity, 1.0); +} diff --git a/index.html b/index.html index 5f392a8..cb859cc 100644 --- a/index.html +++ b/index.html @@ -59,6 +59,7 @@ } + diff --git a/index.js b/index.js index a9b25e9..3ead499 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,6 @@ +import { getPath } from "./common.js"; +import { loadGltf } from "./index2.js"; + if (!navigator.gpu) { throw new Error("WebGPU not supported on this browser."); } @@ -154,16 +157,6 @@ const vertexBufferLayouts = [ }, ]; -function getPath(path) -{ - return fetch(path).then((response) => { - if (!response.ok) { - throw new Error(`${path}: ${response.status}`) - } - return response.blob() - }).then((blob) => { return blob.text() }) -} - const indexWgsl = getPath("index.wgsl"); const computeWgsl = getPath("compute.wgsl"); @@ -190,7 +183,7 @@ const bindGroupLayouts = [ }] }), device.createBindGroupLayout({ - label: "bind group layout 0", + label: "bind group layout 1", entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, @@ -360,7 +353,7 @@ modelSelect.value = currentModel; const lightingSelect = document.getElementById("lighting-select"); lightingSelect.value = "unlit"; -function render() +function recreateDepth() { if (canvas.clientWidth !== canvas.width || canvas.clientWidth !== canvas.height) { canvas.width = canvas.clientWidth; @@ -371,6 +364,11 @@ function render() } depthTexture = createDepthTexture(); } +} + +function render() +{ + recreateDepth(); const encoder = device.createCommandEncoder(); @@ -432,4 +430,19 @@ function render() requestAnimationFrame(render) } -requestAnimationFrame(render) +//requestAnimationFrame(render); + +const gltfRenderer = await loadGltf(device, canvasFormat); +function render2() +{ + recreateDepth(); + const colorView = context.getCurrentTexture().createView(); + + const aspect = canvas.width / canvas.height; + rotate.instance.exports.rotate(tick * 0.01, aspect, 0); + device.queue.writeBuffer(gltfRenderer.uniformBuffer, 0, uniformArray, 0, gltfRenderer.uniformBufferSize / 4); + + gltfRenderer.render(device, colorView, depthTexture); + requestAnimationFrame(render2); +} +requestAnimationFrame(render2); diff --git a/index2.js b/index2.js new file mode 100644 index 0000000..df6f927 --- /dev/null +++ b/index2.js @@ -0,0 +1,161 @@ +import { getPath } from "./common.js"; +import { parseGltfChunks, primitiveVertexBufferLayouts } from "./gltf.js"; + +class GltfRenderer { + constructor(device, canvasFormat, shaderModule, gltf, primitive, bufferLayouts) + { + this.gltf = gltf; + this.primitive = primitive; + + ////////////////////////////////////////////////////////////////////// + // buffer + ////////////////////////////////////////////////////////////////////// + + console.assert(gltf.json.buffers[0].byteLength == gltf.bin.length); + this.vertexBuffer = device.createBuffer({ + label: "gltf vertex buffer 0", + size: gltf.bin.length, + usage: GPUBufferUsage.INDEX | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }); + device.queue.writeBuffer(this.vertexBuffer, 0, gltf.bin.buffer, gltf.bin.offset, gltf.bin.length); + + ////////////////////////////////////////////////////////////////////// + // pipeline + ////////////////////////////////////////////////////////////////////// + + const bindGroupLayouts = [ + device.createBindGroupLayout({ + label: "gltf bind group layout 0", + entries: [{ + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { type: "uniform" } + }] + }), + ]; + + const pipelineLayout = device.createPipelineLayout({ + label: "gltf pipeline layout", + bindGroupLayouts: bindGroupLayouts, + }); + + this.renderPipeline = device.createRenderPipeline({ + label: "gltf pipeline", + layout: pipelineLayout, + vertex: { + module: shaderModule, + entryPoint: "vertexMain", + buffers: bufferLayouts, + }, + fragment: { + module: shaderModule, + entryPoint: "fragmentMain", + targets: [{ + format: canvasFormat, + }] + }, + primitive: { + topology: 'triangle-list', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus', + }, + }); + + ////////////////////////////////////////////////////////////////////// + // bind group + ////////////////////////////////////////////////////////////////////// + + this.uniformBufferSize = (4 * 4 * 4) * 2; + this.uniformBuffer = device.createBuffer({ + label: "gltf uniform 0", + size: this.uniformBufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + this.bindGroups = [ + device.createBindGroup({ + label: "cell bind group 0", + layout: bindGroupLayouts[0], + entries: [{ + binding: 0, + resource: { buffer: this.uniformBuffer }, + }] + }), + ]; + } + + render(device, colorView, depthTexture) + { + const encoder = device.createCommandEncoder(); + const renderPass = encoder.beginRenderPass({ + colorAttachments: [{ + view: colorView, + 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', + }, + }); + + renderPass.setPipeline(this.renderPipeline); + + const attributes = ["POSITION", "NORMAL", "TEXCOORD_0"]; + for (let i = 0; i < attributes.length; i++) { + const attributeIndex = this.primitive.attributes[attributes[i]]; + const bufferViewIndex = this.gltf.json.accessors[attributeIndex].bufferView; + const bufferView = this.gltf.json.bufferViews[bufferViewIndex]; + + renderPass.setVertexBuffer(i, this.vertexBuffer, bufferView.byteOffset, bufferView.byteLength); + } + + const indicesAccessor = this.gltf.json.accessors[this.primitive.indices]; + const indicesBufferView = this.gltf.json.bufferViews[indicesAccessor.bufferView]; + renderPass.setIndexBuffer(this.vertexBuffer, + "uint16", + indicesBufferView.byteOffset, + indicesBufferView.byteLength); + + renderPass.setBindGroup(0, this.bindGroups[0]); + renderPass.drawIndexed(indicesAccessor.count); + + renderPass.end(); + + const commandBuffer = encoder.finish(); + device.queue.submit([commandBuffer]); + } +} + +async function loadGltf(device, canvasFormat) +{ + const response = await fetch("monkey.glb"); + const buffer = await response.arrayBuffer(); + const gltf = parseGltfChunks(buffer); + console.log(gltf); + + const primitive = gltf.json.meshes[0].primitives[0]; + //console.log(primitive.indices); + //console.log(primitive.attributes); + + const bufferLayouts = primitiveVertexBufferLayouts(gltf, primitive); + console.log(bufferLayouts); + + const code = await getPath("gltf.wgsl"); + const shaderModule = device.createShaderModule({ + label: "gltf module", + code: await code, + }); + + const renderer = new GltfRenderer(device, canvasFormat, shaderModule, gltf, primitive, bufferLayouts); + + return renderer; +} + +export { loadGltf }; diff --git a/monkey.glb b/monkey.glb new file mode 100644 index 0000000..07e4b05 Binary files /dev/null and b/monkey.glb differ diff --git a/rotate.c b/rotate.c index 7a8c233..8d0f604 100644 --- a/rotate.c +++ b/rotate.c @@ -9,7 +9,7 @@ void rotate(float angle, float aspect, float * buffer) //XMMATRIX mat2 = XMMatrixTranslation(0, 0, 1); //XMStoreFloat4x4((XMFLOAT4X4*)buffer, mat * mat2); - float d = 0.9; + float d = 1.9; XMVECTOR eye = XMVectorSet(d, d / 2, d, 0); XMVECTOR at = XMVectorSet(0, 0, 0, 0); XMVECTOR up = XMVectorSet(0, 1, 0, 0); diff --git a/torus.glb b/torus.glb new file mode 100644 index 0000000..083565a Binary files /dev/null and b/torus.glb differ