matrix-engine-wgpu
Version:
Networking implemented - based on kurento openvidu server. fix arcball camera,instanced draws added also effect pipeline blend with instancing option.Normalmap added, Fixed shadows casting vs camera/video texture, webGPU powered pwa application. Crazy fas
631 lines (573 loc) • 18.8 kB
JavaScript
/**
* @author Nikola Lukic zlatnaspirala
* @description
* Importer is adapted for matrix-engine-wgpu.
* Improved - Fix children empty array.
* Access to json raw data.
* @source
* https://github.com/Twinklebear/webgpu-gltf/blob/main/src/glb_import.js
*/
import {mat4} from "gl-matrix";
const GLTFRenderMode = {
POINTS: 0,
LINE: 1,
LINE_LOOP: 2,
LINE_STRIP: 3,
TRIANGLES: 4,
TRIANGLE_STRIP: 5,
// Note: fans are not supported in WebGPU, use should be
// an error or converted into a list/strip
TRIANGLE_FAN: 6,
};
const GLTFComponentType = {
BYTE: 5120,
UNSIGNED_BYTE: 5121,
SHORT: 5122,
UNSIGNED_SHORT: 5123,
INT: 5124,
UNSIGNED_INT: 5125,
FLOAT: 5126,
DOUBLE: 5130,
};
const GLTFTextureFilter = {
NEAREST: 9728,
LINEAR: 9729,
NEAREST_MIPMAP_NEAREST: 9984,
LINEAR_MIPMAP_NEAREST: 9985,
NEAREST_MIPMAP_LINEAR: 9986,
LINEAR_MIPMAP_LINEAR: 9987,
};
const GLTFTextureWrap = {
REPEAT: 10497,
CLAMP_TO_EDGE: 33071,
MIRRORED_REPEAT: 33648,
};
function alignTo(val, align) {
return Math.floor((val + align - 1) / align) * align;
}
function gltfTypeNumComponents(type) {
switch(type) {
case 'SCALAR':
return 1;
case 'VEC2':
return 2;
case 'VEC3':
return 3;
case 'VEC4':
return 4;
default:
alert('Unhandled glTF Type ' + type);
return null;
}
}
function gltfTypeSize(componentType, type) {
var typeSize = 0;
switch(componentType) {
case GLTFComponentType.BYTE:
typeSize = 1;
break;
case GLTFComponentType.UNSIGNED_BYTE:
typeSize = 1;
break;
case GLTFComponentType.SHORT:
typeSize = 2;
break;
case GLTFComponentType.UNSIGNED_SHORT:
typeSize = 2;
break;
case GLTFComponentType.INT:
typeSize = 4;
break;
case GLTFComponentType.UNSIGNED_INT:
typeSize = 4;
break;
case GLTFComponentType.FLOAT:
typeSize = 4;
break;
case GLTFComponentType.DOUBLE:
typeSize = 4;
break;
default:
alert('Unrecognized GLTF Component Type?');
}
return gltfTypeNumComponents(type) * typeSize;
}
export class GLTFBuffer {
constructor(buffer, size, offset) {
this.arrayBuffer = buffer;
this.size = size;
this.byteOffset = offset;
}
}
export class GLTFBufferView {
constructor(buffer, view) {
this.length = view['byteLength'];
this.byteOffset = buffer.byteOffset;
if(view['byteOffset'] !== undefined) {
this.byteOffset += view['byteOffset'];
}
this.byteStride = 0;
if(view['byteStride'] !== undefined) {
this.byteStride = view['byteStride'];
}
this.buffer = new Uint8Array(buffer.arrayBuffer, this.byteOffset, this.length);
this.needsUpload = false;
this.gpuBuffer = null;
this.usage = 0;
}
addUsage(usage) {
this.usage = this.usage | usage;
}
upload(device) {
// Note: must align to 4 byte size when mapped at creation is true
var buf = device.createBuffer({
size: alignTo(this.buffer.byteLength, 4),
usage: this.usage,
mappedAtCreation: true
});
new (this.buffer.constructor)(buf.getMappedRange()).set(this.buffer);
buf.unmap();
this.gpuBuffer = buf;
this.needsUpload = false;
}
}
export class GLTFAccessor {
constructor(view, accessor, weightsAccessIndex) {
this.count = accessor['count'];
this.componentType = accessor['componentType'];
this.gltfType = accessor['type'];
this.numComponents = gltfTypeNumComponents(accessor['type']);
this.numScalars = this.count * this.numComponents;
this.view = view;
this.byteOffset = 0;
if(accessor['byteOffset'] !== undefined) {
this.byteOffset = accessor['byteOffset'];
}
if(weightsAccessIndex) this.weightsAccessIndex = weightsAccessIndex;
}
get byteStride() {
var elementSize = gltfTypeSize(this.componentType, this.gltfType);
return Math.max(elementSize, this.view.byteStride);
}
}
export class GLTFPrimitive {
constructor(indices, positions, normals, texcoords, material, topology, weights, joints, tangents) {
this.indices = indices;
this.positions = positions;
this.normals = normals;
this.texcoords = texcoords;
this.material = material;
this.topology = topology;
this.weights = weights;
this.joints = joints;
this.tangents = tangents;
}
}
export class GLTFMesh {
constructor(name, primitives) {
this.name = name;
this.primitives = primitives;
}
}
export class GLTFNode {
constructor(name, mesh, transform, n) {
this.name = name;
this.mesh = mesh;
this.transform = transform;
this.gpuUniforms = null;
this.bindGroup = null;
this.children = n.children || [];
}
upload(device) {
var buf = device.createBuffer(
{size: 4 * 4 * 4, usage: GPUBufferUsage.UNIFORM, mappedAtCreation: true});
new Float32Array(buf.getMappedRange()).set(this.transform);
buf.unmap();
this.gpuUniforms = buf;
}
}
function readNodeTransform(node) {
if(node['matrix']) {
var m = node['matrix'];
// Both glTF and gl matrix are column major
return mat4.fromValues(m[0],
m[1],
m[2],
m[3],
m[4],
m[5],
m[6],
m[7],
m[8],
m[9],
m[10],
m[11],
m[12],
m[13],
m[14],
m[15]);
} else {
var scale = [1, 1, 1];
var rotation = [0, 0, 0, 1];
var translation = [0, 0, 0];
if(node['scale']) {
scale = node['scale'];
}
if(node['rotation']) {
rotation = node['rotation'];
}
if(node['translation']) {
translation = node['translation'];
}
var m = mat4.create();
return mat4.fromRotationTranslationScale(m, rotation, translation, scale);
}
}
// function flattenGLTFChildren(nodes, node, parent_transform) {
// var tfm = readNodeTransform(node);
// var tfm = mat4.mul(tfm, parent_transform, tfm);
// node['matrix'] = tfm;
// node['scale'] = undefined;
// node['rotation'] = undefined;
// node['translation'] = undefined;
// if(node['children']) {
// for(var i = 0;i < node['children'].length;++i) {
// flattenGLTFChildren(nodes, nodes[node['children'][i]], tfm);
// }
// node['children'] = [];
// }
// }
function flattenGLTFChildren(nodes, node, parent_transform) {
var tfm = readNodeTransform(node);
var tfm = mat4.mul(tfm, parent_transform, tfm);
node['matrix'] = tfm;
node['scale'] = undefined;
node['rotation'] = undefined;
node['translation'] = undefined;
if(node['children']) {
for(var i = 0;i < node['children'].length;++i) {
flattenGLTFChildren(nodes, nodes[node['children'][i]], tfm);
}
// node['children'] = []; // REMOVE THIS LINE
}
}
function makeGLTFSingleLevel(nodes) {
var rootTfm = mat4.create();
for(var i = 0;i < nodes.length;++i) {
flattenGLTFChildren(nodes, nodes[i], rootTfm);
}
return nodes;
}
export class GLTFMaterial {
constructor(material, textures) {
this.baseColorFactor = [1, 1, 1, 1];
this.baseColorTexture = null;
// padded to float4
this.emissiveFactor = [0, 0, 0, 1];
this.metallicFactor = 1.0;
this.roughnessFactor = 1.0;
if(material['pbrMetallicRoughness'] !== undefined) {
var pbr = material['pbrMetallicRoughness'];
if(pbr['baseColorFactor'] !== undefined) {
this.baseColorFactor = pbr['baseColorFactor'];
}
if(pbr['baseColorTexture'] !== undefined) {
// TODO multiple texcoords
this.baseColorTexture = textures[pbr['baseColorTexture']['index']];
}
if(pbr['metallicFactor'] !== undefined) {
this.metallicFactor = pbr['metallicFactor'];
}
if(pbr['roughnessFactor'] !== undefined) {
this.roughnessFactor = pbr['roughnessFactor'];
}
}
if(material['emissiveFactor'] !== undefined) {
this.emissiveFactor[0] = material['emissiveFactor'][0];
this.emissiveFactor[1] = material['emissiveFactor'][1];
this.emissiveFactor[2] = material['emissiveFactor'][2];
}
this.gpuBuffer = null;
this.bindGroupLayout = null;
this.bindGroup = null;
}
upload(device) {
var buf = device.createBuffer(
{size: 3 * 4 * 4, usage: GPUBufferUsage.UNIFORM, mappedAtCreation: true});
var mappingView = new Float32Array(buf.getMappedRange());
mappingView.set(this.baseColorFactor);
mappingView.set(this.emissiveFactor, 4);
mappingView.set([this.metallicFactor, this.roughnessFactor], 8);
buf.unmap();
this.gpuBuffer = buf;
var layoutEntries =
[{binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: {type: 'uniform'}}];
var bindGroupEntries = [{
binding: 0,
resource: {
buffer: this.gpuBuffer,
}
}];
if(this.baseColorTexture) {
// Defaults for sampler and texture are fine, just make the objects
// exist to pick them up
layoutEntries.push({binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {}});
layoutEntries.push({binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {}});
bindGroupEntries.push({
binding: 1,
resource: this.baseColorTexture.sampler,
});
bindGroupEntries.push({
binding: 2,
resource: this.baseColorTexture.imageView,
});
}
this.bindGroupLayout = device.createBindGroupLayout({entries: layoutEntries});
this.bindGroup = device.createBindGroup({
layout: this.bindGroupLayout,
entries: bindGroupEntries,
});
}
}
export class GLTFSampler {
constructor(sampler, device) {
var magFilter = sampler['magFilter'] === undefined ||
sampler['magFilter'] == GLTFTextureFilter.LINEAR
? 'linear'
: 'nearest';
var minFilter = sampler['minFilter'] === undefined ||
sampler['minFilter'] == GLTFTextureFilter.LINEAR
? 'linear'
: 'nearest';
var wrapS = 'repeat';
if(sampler['wrapS'] !== undefined) {
if(sampler['wrapS'] == GLTFTextureFilter.REPEAT) {
wrapS = 'repeat';
} else if(sampler['wrapS'] == GLTFTextureFilter.CLAMP_TO_EDGE) {
wrapS = 'clamp-to-edge';
} else {
wrapS = 'mirror-repeat';
}
}
var wrapT = 'repeat';
if(sampler['wrapT'] !== undefined) {
if(sampler['wrapT'] == GLTFTextureFilter.REPEAT) {
wrapT = 'repeat';
} else if(sampler['wrapT'] == GLTFTextureFilter.CLAMP_TO_EDGE) {
wrapT = 'clamp-to-edge';
} else {
wrapT = 'mirror-repeat';
}
}
this.sampler = device.createSampler({
magFilter: magFilter,
minFilter: minFilter,
addressModeU: wrapS,
addressModeV: wrapT,
});
}
}
export class GLTFTexture {
constructor(sampler, image) {
this.gltfsampler = sampler;
this.sampler = sampler.sampler;
this.image = image;
this.imageView = image.createView();
}
}
export class GLBModel {
constructor(nodes, skins, skinnedMeshNodes, glbJsonData, glbBinaryBuffer,noSkinMeshNodes) {
this.noSkinMeshNodes = noSkinMeshNodes;
this.nodes = nodes;
this.skins = skins;
this.skinnedMeshNodes = skinnedMeshNodes;
this.bvhToGLBMap = null;
this.glbJsonData = glbJsonData;
this.glbBinaryBuffer = glbBinaryBuffer;
}
};
// function getComponentSize(componentType) {
// switch(componentType) {
// case 5126: return 4; // float32
// case 5123: return 2; // uint16
// case 5121: return 1; // uint8
// default: throw new Error("Unknown componentType: " + componentType);
// }
// }
// Upload a GLB model and return it
export async function uploadGLBModel(buffer, device) {
// 1️⃣ Validate header
const header = new Uint32Array(buffer, 0, 5);
if(header[0] !== 0x46546C67) {
alert('This does not appear to be a glb file?');
return;
}
// 2️⃣ JSON chunk
const glbJsonData = JSON.parse(
new TextDecoder('utf-8').decode(new Uint8Array(buffer, 20, header[3]))
);
// 3️⃣ Binary chunk header + buffer
const binaryHeader = new Uint32Array(buffer, 20 + header[3], 2);
const glbBuffer = new GLTFBuffer(buffer, binaryHeader[0], 28 + header[3]);
// 4️⃣ BufferViews
const bufferViews = glbJsonData.bufferViews.map(
v => new GLTFBufferView(glbBuffer, v)
);
const binaryOffset = 28 + header[3];
const binaryLength = binaryHeader[0];
// ✅ raw ArrayBuffer slice of the binary chunk:
const glbBinaryBuffer = buffer.slice(binaryOffset, binaryOffset + binaryLength);
// 5️⃣ Load images
const images = [];
if(glbJsonData.images) {
for(const imgJson of glbJsonData.images) {
const view = new GLTFBufferView(
glbBuffer,
glbJsonData.bufferViews[imgJson.bufferView]
);
const blob = new Blob([view.buffer], {type: imgJson['mime/type']});
const img = await createImageBitmap(blob);
const gpuImg = device.createTexture({
size: [img.width, img.height, 1],
format: 'rgba8unorm-srgb',
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
device.queue.copyExternalImageToTexture(
{source: img},
{texture: gpuImg},
[img.width, img.height, 1]
);
images.push(gpuImg);
}
}
glbJsonData.glbTextures = images;
// console.log('IMAGES FROM GLB: ', images)
// 6️⃣ Samplers, Textures, Materials
const defaultSampler = new GLTFSampler({}, device);
const samplers = (glbJsonData.samplers || []).map(
s => new GLTFSampler(s, device)
);
const textures = (glbJsonData.textures || []).map(tex => {
const sampler =
tex.sampler !== undefined ? samplers[tex.sampler] : defaultSampler;
return new GLTFTexture(sampler, images[tex.source]);
});
const defaultMaterial = new GLTFMaterial({});
const materials = (glbJsonData.materials || []).map(
m => new GLTFMaterial(m, textures)
);
// 7️⃣ Meshes
const meshes = (glbJsonData.meshes || []).map(mesh => {
const primitives = mesh.primitives.map(prim => {
const topology = prim.mode ?? GLTFRenderMode.TRIANGLES;
// console.log('topology ', topology)
// Indices
let indices = null;
if(prim.indices !== undefined) {
const accessor = glbJsonData.accessors[prim.indices];
const viewID = accessor.bufferView;
bufferViews[viewID].needsUpload = true;
bufferViews[viewID].addUsage(GPUBufferUsage.INDEX);
indices = new GLTFAccessor(bufferViews[viewID], accessor);
}
// Vertex attributes
let positions = null,
normals = null,
tangents = null,
texcoords = [];
let weights = null;
let joints = null;
for(const attr in prim.attributes) {
const accessor = glbJsonData.accessors[prim.attributes[attr]];
const viewID = accessor.bufferView;
bufferViews[viewID].needsUpload = true;
bufferViews[viewID].addUsage(GPUBufferUsage.VERTEX);
if(attr === 'POSITION') {
positions = new GLTFAccessor(bufferViews[viewID], accessor);
} else if(attr === 'NORMAL') {
normals = new GLTFAccessor(bufferViews[viewID], accessor);
} else if(attr.startsWith('TEXCOORD')) {
texcoords.push(new GLTFAccessor(bufferViews[viewID], accessor));
} else if(attr === 'WEIGHTS_0') {
weights = new GLTFAccessor(bufferViews[viewID], accessor, prim.attributes['WEIGHTS_0']);
} else if(attr.startsWith('JOINTS')) {
joints = new GLTFAccessor(bufferViews[viewID], accessor);
} else if(attr === 'TANGENT') {
tangents = new GLTFAccessor(bufferViews[viewID], accessor);
} else {
console.log('unknow-attr:', attr)
}
}
const material = prim.material !== undefined ? materials[prim.material] : defaultMaterial;
return new GLTFPrimitive(
indices,
positions,
normals,
texcoords,
material,
topology,
weights,
joints,
tangents
);
});
return new GLTFMesh(mesh.name, primitives);
});
// Upload buffers & materials
for(const bv of bufferViews) if(bv.needsUpload) bv.upload(device);
defaultMaterial.upload(device);
for(const m of materials) m.upload(device);
// 8️⃣ Skins (we only store the index of inverseBindMatrices here)
const skins = (glbJsonData.skins || []).map(skin => ({
name: skin.name,
joints: skin.joints,
inverseBindMatrices: skin.inverseBindMatrices, // accessor index
}));
// 9️⃣ Nodes
const nodes = [];
const gltfNodes = makeGLTFSingleLevel(glbJsonData.nodes);
for(let i = 0;i < gltfNodes.length;i++) {
const n = gltfNodes[i];
const meshObj = n.mesh !== undefined ? meshes[n.mesh] : null;
const node = new GLTFNode(n.name, meshObj, readNodeTransform(n), n);
if(n.skin !== undefined) node.skin = n.skin; // skin index
node.upload(device);
nodes.push(node);
}
// 🟩 Build parent references:
for(let i = 0;i < gltfNodes.length;i++) {
const srcNode = gltfNodes[i];
// srcNode.children is an array of indices
if(srcNode.children) {
for(const childIndex of srcNode.children) {
nodes[childIndex].parent = i; // add .parent to the child node
}
}
}
// Ensure nodes without parent are root nodes
for(const node of nodes) {
if(node.parent === undefined) node.parent = null;
}
const skinnedMeshNodes = nodes.filter(
n => n.mesh && n.skin !== undefined
);
let noSkinMeshNodes = null;
if(skinnedMeshNodes.length === 0) {
console.warn('No skins found — mesh not bound to skeleton');
noSkinMeshNodes = nodes.filter(n => n.mesh);
} else {
skinnedMeshNodes.forEach(n => {
// console.log('Mesh', n.mesh.name, 'uses skin index', n.skin);
// Per-mesh uniform buffer (example)
n.sceneUniformBuffer = device.createBuffer({
size: 44 * 4,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
});
}
let R = new GLBModel(nodes, skins, skinnedMeshNodes, glbJsonData, glbBinaryBuffer, noSkinMeshNodes)
return R;
}