Naloga 3 WIP

This commit is contained in:
Gašper Dobrovoljc
2024-12-28 19:58:17 +01:00
parent 7ad330422b
commit a20a45ebd0
51 changed files with 3327 additions and 28 deletions

View File

@@ -0,0 +1,483 @@
import {
Accessor,
Camera,
Material,
Mesh,
Model,
Node,
Primitive,
Sampler,
Texture,
Transform,
Vertex,
} from '../core.js';
// TODO: GLB support
// TODO: accessors with no buffer views (zero-initialized)
// TODO: image from buffer view
// TODO: mipmaps
// TODO: material texcoord sets
// TODO: material alpha, doubleSided
export class GLTFLoader {
// Loads the GLTF JSON file and all buffers and images that it references.
// It also creates a cache for all future resource loading.
async load(url) {
this.gltfUrl = new URL(url, window.location);
this.gltf = await this.fetchJson(this.gltfUrl);
this.defaultScene = this.gltf.scene ?? 0;
this.cache = new Map();
await Promise.all(this.gltf.buffers?.map(buffer => this.preloadBuffer(buffer)) ?? []);
await Promise.all(this.gltf.images?.map(image => this.preloadImage(image)) ?? []);
}
// Finds an object in list at the given index, or if the 'name'
// property matches the given name.
findByNameOrIndex(list, nameOrIndex) {
if (typeof nameOrIndex === 'number') {
return list[nameOrIndex];
} else {
return list.find(element => element.name === nameOrIndex);
}
}
fetchJson(url) {
return fetch(url)
.then(response => response.json());
}
fetchBuffer(url) {
return fetch(url)
.then(response => response.arrayBuffer());
}
fetchImage(url) {
return fetch(url)
.then(response => response.blob())
.then(blob => createImageBitmap(blob));
}
async preloadImage(gltfSpec) {
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
if (gltfSpec.uri) {
const url = new URL(gltfSpec.uri, this.gltfUrl);
const image = await this.fetchImage(url);
this.cache.set(gltfSpec, image);
return image;
} else {
const bufferView = this.gltf.bufferViews[gltfSpec.bufferView];
const buffer = this.loadBuffer(bufferView.buffer);
const dataView = new DataView(buffer, bufferView.byteOffset ?? 0, bufferView.byteLength);
const blob = new Blob([dataView], { type: gltfSpec.mimeType });
const url = URL.createObjectURL(blob);
const image = await this.fetchImage(url);
URL.revokeObjectURL(url);
this.cache.set(gltfSpec, image);
return image;
}
}
async preloadBuffer(gltfSpec) {
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const url = new URL(gltfSpec.uri, this.gltfUrl);
const buffer = await this.fetchBuffer(url);
this.cache.set(gltfSpec, buffer);
return buffer;
}
loadImage(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.images, nameOrIndex);
if (!gltfSpec) {
return null;
}
return this.cache.get(gltfSpec);
}
loadBuffer(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.buffers, nameOrIndex);
if (!gltfSpec) {
return null;
}
return this.cache.get(gltfSpec);
}
loadSampler(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.samplers, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const minFilter = {
9728: 'nearest',
9729: 'linear',
9984: 'nearest',
9985: 'linear',
9986: 'nearest',
9987: 'linear',
};
const magFilter = {
9728: 'nearest',
9729: 'linear',
};
const mipmapFilter = {
9728: 'nearest',
9729: 'linear',
9984: 'nearest',
9985: 'nearest',
9986: 'linear',
9987: 'linear',
};
const addressMode = {
33071: 'clamp-to-edge',
33648: 'mirror-repeat',
10497: 'repeat',
};
const sampler = new Sampler({
minFilter: minFilter[gltfSpec.minFilter ?? 9729],
magFilter: magFilter[gltfSpec.magFilter ?? 9729],
mipmapFilter: mipmapFilter[gltfSpec.minFilter ?? 9729],
addressModeU: addressMode[gltfSpec.wrapS ?? 10497],
addressModeV: addressMode[gltfSpec.wrapT ?? 10497],
});
this.cache.set(gltfSpec, sampler);
return sampler;
}
loadTexture(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.textures, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const options = {};
if (gltfSpec.source !== undefined) {
options.image = this.loadImage(gltfSpec.source);
}
if (gltfSpec.sampler !== undefined) {
options.sampler = this.loadSampler(gltfSpec.sampler);
} else {
options.sampler = new Sampler();
}
const texture = new Texture(options);
this.cache.set(gltfSpec, texture);
return texture;
}
loadMaterial(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.materials, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const options = {};
const pbr = gltfSpec.pbrMetallicRoughness;
if (pbr) {
if (pbr.baseColorTexture) {
options.baseTexture = this.loadTexture(pbr.baseColorTexture.index);
options.baseTexture.isSRGB = true;
}
if (pbr.metallicRoughnessTexture) {
options.metalnessTexture = this.loadTexture(pbr.metallicRoughnessTexture.index);
options.roughnessTexture = this.loadTexture(pbr.metallicRoughnessTexture.index);
}
options.baseFactor = pbr.baseColorFactor;
options.metalnessFactor = pbr.metallicFactor;
options.roughnessFactor = pbr.roughnessFactor;
}
if (gltfSpec.normalTexture) {
options.normalTexture = this.loadTexture(gltfSpec.normalTexture.index);
options.normalFactor = gltfSpec.normalTexture.scale;
}
if (gltfSpec.emissiveTexture) {
options.emissionTexture = this.loadTexture(gltfSpec.emissiveTexture.index);
options.emissionTexture.isSRGB = true;
options.emissionFactor = gltfSpec.emissiveFactor;
}
if (gltfSpec.occlusionTexture) {
options.occlusionTexture = this.loadTexture(gltfSpec.occlusionTexture.index);
options.occlusionFactor = gltfSpec.occlusionTexture.strength;
}
const material = new Material(options);
this.cache.set(gltfSpec, material);
return material;
}
loadAccessor(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.accessors, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
if (gltfSpec.bufferView === undefined) {
console.warn('Accessor does not reference a buffer view');
return null;
}
const bufferView = this.gltf.bufferViews[gltfSpec.bufferView];
const buffer = this.loadBuffer(bufferView.buffer);
const componentType = {
5120: 'int',
5121: 'int',
5122: 'int',
5123: 'int',
5124: 'int',
5125: 'int',
5126: 'float',
}[gltfSpec.componentType];
const componentSize = {
5120: 1,
5121: 1,
5122: 2,
5123: 2,
5124: 4,
5125: 4,
5126: 4,
}[gltfSpec.componentType];
const componentSigned = {
5120: true,
5121: false,
5122: true,
5123: false,
5124: true,
5125: false,
5126: false,
}[gltfSpec.componentType];
const componentCount = {
SCALAR: 1,
VEC2: 2,
VEC3: 3,
VEC4: 4,
MAT2: 4,
MAT3: 9,
MAT4: 16,
}[gltfSpec.type];
const componentNormalized = gltfSpec.normalized ?? false;
const stride = bufferView.byteStride ?? (componentSize * componentCount);
const offset = gltfSpec.byteOffset ?? 0;
const viewOffset = bufferView.byteOffset ?? 0;
const viewLength = bufferView.byteLength;
const accessor = new Accessor({
buffer,
viewLength,
viewOffset,
offset,
stride,
componentType,
componentCount,
componentSize,
componentSigned,
componentNormalized,
});
this.cache.set(gltfSpec, accessor);
return accessor;
}
createMeshFromPrimitive(spec) {
if (spec.attributes.POSITION === undefined) {
console.warn('No position in mesh');
return new Mesh();
}
if (spec.indices === undefined) {
console.warn('No indices in mesh');
return new Mesh();
}
const accessors = {};
for (const attribute in spec.attributes) {
accessors[attribute] = this.loadAccessor(spec.attributes[attribute]);
}
const position = accessors.POSITION;
const texcoords = accessors.TEXCOORD_0;
const normal = accessors.NORMAL;
const tangent = accessors.TANGENT;
const vertexCount = position.count;
const vertices = [];
for (let i = 0; i < vertexCount; i++) {
const options = {};
if (position) { options.position = position.get(i); }
if (texcoords) { options.texcoords = texcoords.get(i); }
if (normal) { options.normal = normal.get(i); }
if (tangent) { options.tangent = tangent.get(i); }
vertices.push(new Vertex(options));
}
const indices = [];
const indicesAccessor = this.loadAccessor(spec.indices);
const indexCount = indicesAccessor.count;
for (let i = 0; i < indexCount; i++) {
indices.push(indicesAccessor.get(i));
}
return new Mesh({ vertices, indices });
}
loadMesh(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.meshes, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const primitives = [];
for (const primitiveSpec of gltfSpec.primitives) {
if (primitiveSpec.mode !== 4 && primitiveSpec.mode !== undefined) {
console.warn(`GLTFLoader: skipping primitive with mode ${primitiveSpec.mode}`);
continue;
}
const options = {};
options.mesh = this.createMeshFromPrimitive(primitiveSpec);
if (primitiveSpec.material !== undefined) {
options.material = this.loadMaterial(primitiveSpec.material);
}
primitives.push(new Primitive(options));
}
const model = new Model({ primitives });
this.cache.set(gltfSpec, model);
return model;
}
loadCamera(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.cameras, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const options = {};
if (gltfSpec.type === 'perspective') {
const { aspectRatio, yfov, znear, zfar } = gltfSpec.perspective;
Object.assign(options, {
orthographic: 0,
aspect: aspectRatio,
fovy: yfov,
near: znear,
far: zfar,
});
} else if (gltfSpec.type === 'orthographic') {
const { xmag, ymag, znear, zfar } = gltfSpec.orthographic;
Object.assign(options, {
orthographic: 1,
aspect: xmag / ymag,
halfy: ymag,
near: znear,
far: zfar,
});
}
const camera = new Camera(options);
this.cache.set(gltfSpec, camera);
return camera;
}
loadNode(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.nodes, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const node = new Node();
node.addComponent(new Transform(gltfSpec));
if (gltfSpec.children) {
for (const childIndex of gltfSpec.children) {
node.addChild(this.loadNode(childIndex));
}
}
if (gltfSpec.camera !== undefined) {
node.addComponent(this.loadCamera(gltfSpec.camera));
}
if (gltfSpec.mesh !== undefined) {
node.addComponent(this.loadMesh(gltfSpec.mesh));
}
this.cache.set(gltfSpec, node);
return node;
}
loadScene(nameOrIndex) {
const gltfSpec = this.findByNameOrIndex(this.gltf.scenes, nameOrIndex);
if (!gltfSpec) {
return null;
}
if (this.cache.has(gltfSpec)) {
return this.cache.get(gltfSpec);
}
const scene = new Node();
if (gltfSpec.nodes) {
for (const nodeIndex of gltfSpec.nodes) {
scene.addChild(this.loadNode(nodeIndex));
}
}
this.cache.set(gltfSpec, scene);
return scene;
}
}

View File

@@ -0,0 +1,10 @@
export class ImageLoader {
async load(url) {
const response = await fetch(url);
const blob = await response.blob();
const imageBitmap = await createImageBitmap(blob);
return imageBitmap;
}
}

View File

@@ -0,0 +1,24 @@
import { Mesh, Vertex } from '../core.js';
export class JSONLoader {
async loadMesh(url) {
const response = await fetch(url);
const json = await response.json();
const vertices = [];
const vertexCount = json.positions.length / 3;
for (let i = 0; i < vertexCount; i++) {
const position = json?.positions?.slice(i * 3, (i + 1) * 3);
const texcoords = json?.texcoords?.slice(i * 2, (i + 1) * 2);
const normal = json?.normals?.slice(i * 3, (i + 1) * 3);
const tangent = json?.tangents?.slice(i * 3, (i + 1) * 3);
vertices.push(new Vertex({ position, texcoords, normal, tangent }));
}
const indices = json.indices;
return new Mesh({ vertices, indices });
}
}

View File

@@ -0,0 +1,70 @@
import { Mesh, Vertex } from '../core.js';
export class OBJLoader {
async loadMesh(url) {
const response = await fetch(url);
const text = await response.text();
const lines = text.split('\n');
const vRegex = /v\s+(\S+)\s+(\S+)\s+(\S+)\s*/;
const vData = lines
.filter(line => vRegex.test(line))
.map(line => [...line.match(vRegex)].slice(1))
.map(entry => entry.map(entry => Number(entry)));
const vnRegex = /vn\s+(\S+)\s+(\S+)\s+(\S+)\s*/;
const vnData = lines
.filter(line => vnRegex.test(line))
.map(line => [...line.match(vnRegex)].slice(1))
.map(entry => entry.map(entry => Number(entry)));
const vtRegex = /vt\s+(\S+)\s+(\S+)\s*/;
const vtData = lines
.filter(line => vtRegex.test(line))
.map(line => [...line.match(vtRegex)].slice(1))
.map(entry => entry.map(entry => Number(entry)));
function triangulate(list) {
const triangles = [];
for (let i = 2; i < list.length; i++) {
triangles.push(list[0], list[i - 1], list[i]);
}
return triangles;
}
const fRegex = /f\s+(.*)/;
const fData = lines
.filter(line => fRegex.test(line))
.map(line => line.match(fRegex)[1])
.map(line => line.trim().split(/\s+/))
.flatMap(face => triangulate(face));
const vertices = [];
const indices = [];
const cache = {};
let cacheLength = 0;
const indicesRegex = /(\d+)(\/(\d+))?(\/(\d+))?/;
for (const id of fData) {
if (id in cache) {
indices.push(cache[id]);
} else {
cache[id] = cacheLength;
indices.push(cacheLength);
const [,vIndex,,vtIndex,,vnIndex] = [...id.match(indicesRegex)]
.map(entry => Number(entry) - 1);
vertices.push(new Vertex({
position: vData[vIndex],
normal: vnData[vnIndex],
texcoords: vtData[vtIndex],
}));
cacheLength++;
}
}
return new Mesh({ vertices, indices });
}
}