render gltf model

This commit is contained in:
Zack Buhman 2026-07-20 23:30:29 -05:00
parent e5cacffaee
commit 2dfd749180
9 changed files with 444 additions and 14 deletions

11
common.js Normal file
View File

@ -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 };

162
gltf.js Normal file
View File

@ -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 };

82
gltf.wgsl Normal file
View File

@ -0,0 +1,82 @@
struct Configuration {
matrix: mat4x4f,
matrixWorld: mat4x4f,
};
@group(0) @binding(0) var<uniform> 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);
}

View File

@ -59,6 +59,7 @@
}
</style>
<script src="index.js" type="module"></script>
<!--<script src="index2.js" type="module"></script>-->
</head>
<body>
<canvas></canvas>

View File

@ -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);

161
index2.js Normal file
View File

@ -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 };

BIN
monkey.glb Normal file

Binary file not shown.

View File

@ -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);

BIN
torus.glb Normal file

Binary file not shown.