Naloga 3 WIP

This commit is contained in:
Gašper Dobrovoljc 2024-12-28 19:58:17 +01:00
parent 7ad330422b
commit a20a45ebd0
No known key found for this signature in database
GPG Key ID: 0E7E037018CFA5A5
51 changed files with 3327 additions and 28 deletions

7
naloga_3/Light.js Normal file

@ -0,0 +1,7 @@
export class Light {
constructor({ ambient, cutoffAngle, intensity }) {
this.ambient = ambient;
this.cutoffAngle = cutoffAngle;
this.intensity = intensity;
}
}

319
naloga_3/Renderer.js Normal file

@ -0,0 +1,319 @@
import { vec3, mat4 } from 'glm';
import * as WebGPU from 'engine/WebGPU.js';
import { Camera, Transform } from 'engine/core.js';
import {
getLocalModelMatrix,
getGlobalModelMatrix,
getGlobalViewMatrix,
getProjectionMatrix,
getModels,
} from 'engine/core/SceneUtils.js';
import { BaseRenderer } from 'engine/renderers/BaseRenderer.js';
import { Light } from './Light.js';
const vertexBufferLayout = {
arrayStride: 32,
attributes: [
{
name: 'position',
shaderLocation: 0,
offset: 0,
format: 'float32x3',
},
{
name: 'texcoords',
shaderLocation: 1,
offset: 12,
format: 'float32x2',
},
{
name: 'normal',
shaderLocation: 2,
offset: 20,
format: 'float32x3',
},
],
};
export class Renderer extends BaseRenderer {
constructor(canvas) {
super(canvas);
}
async initialize() {
await super.initialize();
const code = await fetch(new URL('shader.wgsl', import.meta.url)).then(
(response) => response.text(),
);
const module = this.device.createShaderModule({ code });
this.pipeline = await this.device.createRenderPipelineAsync({
layout: 'auto',
vertex: {
module,
buffers: [vertexBufferLayout],
},
fragment: {
module,
targets: [{ format: this.format }],
},
depthStencil: {
format: 'depth24plus',
depthWriteEnabled: true,
depthCompare: 'less',
},
});
this.recreateDepthTexture();
}
recreateDepthTexture() {
this.depthTexture?.destroy();
this.depthTexture = this.device.createTexture({
format: 'depth24plus',
size: [this.canvas.width, this.canvas.height],
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
}
prepareNode(node) {
if (this.gpuObjects.has(node)) {
return this.gpuObjects.get(node);
}
const modelUniformBuffer = this.device.createBuffer({
label: 'modelUniformBuffer',
size: 128,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const modelBindGroup = this.device.createBindGroup({
label: 'modelBindGroup',
layout: this.pipeline.getBindGroupLayout(1),
entries: [{ binding: 0, resource: { buffer: modelUniformBuffer } }],
});
const gpuObjects = { modelUniformBuffer, modelBindGroup };
this.gpuObjects.set(node, gpuObjects);
return gpuObjects;
}
prepareCamera(camera) {
if (this.gpuObjects.has(camera)) {
return this.gpuObjects.get(camera);
}
const cameraUniformBuffer = this.device.createBuffer({
label: 'cameraUniformBuffer',
size: 144,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const cameraBindGroup = this.device.createBindGroup({
label: 'cameraBindGroup',
layout: this.pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: cameraUniformBuffer } },
],
});
const gpuObjects = { cameraUniformBuffer, cameraBindGroup };
this.gpuObjects.set(camera, gpuObjects);
return gpuObjects;
}
prepareLight(light) {
if (this.gpuObjects.has(light)) {
return this.gpuObjects.get(light);
}
const lightUniformBuffer = this.device.createBuffer({
label: 'lightUniformBuffer',
size: 48,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const lightBindGroup = this.device.createBindGroup({
label: 'lightBindGroup',
layout: this.pipeline.getBindGroupLayout(3),
entries: [{ binding: 0, resource: { buffer: lightUniformBuffer } }],
});
const gpuObjects = { lightUniformBuffer, lightBindGroup };
this.gpuObjects.set(light, gpuObjects);
return gpuObjects;
}
prepareMaterial(material) {
if (this.gpuObjects.has(material)) {
return this.gpuObjects.get(material);
}
const baseTexture = this.prepareImage(
material.baseTexture.image,
).gpuTexture;
const baseSampler = this.prepareSampler(
material.baseTexture.sampler,
).gpuSampler;
const materialUniformBuffer = this.device.createBuffer({
size: 64,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const materialBindGroup = this.device.createBindGroup({
label: 'materialBindGroup',
layout: this.pipeline.getBindGroupLayout(2),
entries: [
{ binding: 0, resource: { buffer: materialUniformBuffer } },
{ binding: 1, resource: baseTexture.createView() },
{ binding: 2, resource: baseSampler },
],
});
const gpuObjects = { materialUniformBuffer, materialBindGroup };
this.gpuObjects.set(material, gpuObjects);
return gpuObjects;
}
calculateLightDirection(lightMatrix) {
// const lightDirection = mat4.getTranslation(vec3.create(), lightMatrix);
const lightDirection = vec3.fromValues(
lightMatrix[4],
lightMatrix[5],
lightMatrix[6],
);
vec3.negate(lightDirection, lightDirection);
vec3.normalize(lightDirection, lightDirection);
return lightDirection;
}
render(scene, camera) {
if (
this.depthTexture.width !== this.canvas.width ||
this.depthTexture.height !== this.canvas.height
) {
this.recreateDepthTexture();
}
const encoder = this.device.createCommandEncoder();
this.renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view: this.context.getCurrentTexture().createView(),
clearValue: [1, 1, 1, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: this.depthTexture.createView(),
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'discard',
},
});
this.renderPass.setPipeline(this.pipeline);
const cameraComponent = camera.getComponentOfType(Camera);
const cameraMatrix = getGlobalModelMatrix(camera);
const cameraPosition = mat4.getTranslation(vec3.create(), cameraMatrix);
const viewMatrix = getGlobalViewMatrix(camera);
const projectionMatrix = getProjectionMatrix(camera);
const { cameraUniformBuffer, cameraBindGroup } =
this.prepareCamera(cameraComponent);
this.device.queue.writeBuffer(cameraUniformBuffer, 0, viewMatrix);
this.device.queue.writeBuffer(
cameraUniformBuffer,
64,
projectionMatrix,
);
this.device.queue.writeBuffer(cameraUniformBuffer, 128, cameraPosition);
this.renderPass.setBindGroup(0, cameraBindGroup);
const light = scene.find((node) => node.getComponentOfType(Light));
const lightComponent = light.getComponentOfType(Light);
const lightMatrix = getGlobalModelMatrix(light);
const lightPosition = mat4.getTranslation(vec3.create(), lightMatrix);
const lightDirection = this.calculateLightDirection(lightMatrix);
const { lightUniformBuffer, lightBindGroup } =
this.prepareLight(lightComponent);
this.device.queue.writeBuffer(lightUniformBuffer, 0, lightPosition);
this.device.queue.writeBuffer(lightUniformBuffer, 12, lightDirection);
this.device.queue.writeBuffer(
lightUniformBuffer,
24,
new Float32Array([
1.0,
Math.cos(lightComponent.cutoffAngle),
lightComponent.ambient,
lightComponent.intensity,
]),
);
this.renderPass.setBindGroup(3, lightBindGroup);
this.renderNode(scene);
this.renderPass.end();
this.device.queue.submit([encoder.finish()]);
}
renderNode(node, modelMatrix = mat4.create()) {
const localMatrix = getLocalModelMatrix(node);
modelMatrix = mat4.multiply(mat4.create(), modelMatrix, localMatrix);
const { modelUniformBuffer, modelBindGroup } = this.prepareNode(node);
const normalMatrix = mat4.normalFromMat4(mat4.create(), modelMatrix);
this.device.queue.writeBuffer(modelUniformBuffer, 0, modelMatrix);
this.device.queue.writeBuffer(modelUniformBuffer, 64, normalMatrix);
this.renderPass.setBindGroup(1, modelBindGroup);
for (const model of getModels(node)) {
this.renderModel(model);
}
for (const child of node.children) {
this.renderNode(child, modelMatrix);
}
}
renderModel(model) {
for (const primitive of model.primitives) {
this.renderPrimitive(primitive);
}
}
renderPrimitive(primitive) {
const { materialUniformBuffer, materialBindGroup } =
this.prepareMaterial(primitive.material);
this.device.queue.writeBuffer(
materialUniformBuffer,
0,
new Float32Array(primitive.material.baseFactor),
);
this.device.queue.writeBuffer(
materialUniformBuffer,
16,
new Float32Array([1, 1]),
);
this.renderPass.setBindGroup(2, materialBindGroup);
const { vertexBuffer, indexBuffer } = this.prepareMesh(
primitive.mesh,
vertexBufferLayout,
);
this.renderPass.setVertexBuffer(0, vertexBuffer);
this.renderPass.setIndexBuffer(indexBuffer, 'uint32');
this.renderPass.drawIndexed(primitive.mesh.indices.length);
}
}

73
naloga_3/engine/WebGPU.js Normal file

@ -0,0 +1,73 @@
export function createBuffer(device, { data, usage }) {
const buffer = device.createBuffer({
size: Math.ceil(data.byteLength / 4) * 4,
mappedAtCreation: true,
usage,
});
if (ArrayBuffer.isView(data)) {
new data.constructor(buffer.getMappedRange()).set(data);
} else {
new Uint8Array(buffer.getMappedRange()).set(new Uint8Array(data));
}
buffer.unmap();
return buffer;
}
export function createTextureFromSource(device, {
source,
format = 'rgba8unorm',
usage = 0,
mipLevelCount = 1,
flipY = false,
}) {
const size = [source.width, source.height];
const texture = device.createTexture({
format,
size,
mipLevelCount,
usage: usage |
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
device.queue.copyExternalImageToTexture(
{ source, flipY },
{ texture },
size,
);
return texture;
}
export function createTextureFromData(device, {
data,
size,
bytesPerRow,
rowsPerImage,
format = 'rgba8unorm',
dimension = '2d',
usage = 0,
mipLevelCount = 1,
flipY = false,
}) {
const texture = device.createTexture({
format,
size,
mipLevelCount,
usage: usage | GPUTextureUsage.COPY_DST,
});
device.queue.writeTexture(
{ texture },
data,
{ bytesPerRow, rowsPerImage },
size,
);
return texture;
}
export function createTexture(device, options) {
if (options.source) {
return createTextureFromSource(device, options);
} else {
return createTextureFromData(device, options);
}
}

@ -0,0 +1,47 @@
export function swap(f, t, ...args) { return 1 - f(1 - t, ...args); }
export function inout(f, t, ...args) { return t < 0.5 ? f(2 * t, ...args) / 2 : 1 - f(2 * (1 - t), ...args) / 2; }
export function step(t, p = 0) { return t < p ? 0 : 1; }
export function stepEaseIn(...args) { return step(...args); }
export function stepEaseOut(...args) { return swap(step, ...args); }
export function stepEaseInOut(...args) { return inout(step, ...args); }
export function linear(t) { return t; }
export function linearEaseIn(...args) { return linear(...args); }
export function linearEaseOut(...args) { return swap(linear, ...args); }
export function linearEaseInOut(...args) { return inout(linear, ...args); }
export function poly(t, p = 2) { return Math.pow(t, p); }
export function polyEaseIn(...args) { return poly(...args); }
export function polyEaseOut(...args) { return swap(poly, ...args); }
export function polyEaseInOut(...args) { return inout(poly, ...args); }
export function expo(t, p = 5) { return (Math.exp(p * t) - 1) / (Math.exp(p) - 1); }
export function expoEaseIn(...args) { return expo(...args); }
export function expoEaseOut(...args) { return swap(expo, ...args); }
export function expoEaseInOut(...args) { return inout(expo, ...args); }
export function sine(t, n = 1) { return 1 - Math.cos(n * t * Math.PI / 2); }
export function sineEaseIn(...args) { return sine(...args); }
export function sineEaseOut(...args) { return swap(sine, ...args); }
export function sineEaseInOut(...args) { return inout(sine, ...args); }
export function circ(t) { return 1 - Math.sqrt(1 - t * t); }
export function circEaseIn(...args) { return circ(...args); }
export function circEaseOut(...args) { return swap(circ, ...args); }
export function circEaseInOut(...args) { return inout(circ, ...args); }
export function back(t, p = 2) { return t * t * ((p + 1) * t - p); }
export function backEaseIn(...args) { return back(...args); }
export function backEaseOut(...args) { return swap(back, ...args); }
export function backEaseInOut(...args) { return inout(back, ...args); }
export function elastic(t, p = 5, n = 5) { return expo(t, p) * (1 - sine(t, 4 * n)); }
export function elasticEaseIn(...args) { return elastic(...args); }
export function elasticEaseOut(...args) { return swap(elastic, ...args); }
export function elasticEaseInOut(...args) { return inout(elastic, ...args); }
export function bounce(t, p = 2, n = 2) { return Math.abs(poly(t, p) * (1 - sine(t, 4 * n))); }
export function bounceEaseIn(...args) { return bounce(...args); }
export function bounceEaseOut(...args) { return swap(bounce, ...args); }
export function bounceEaseInOut(...args) { return inout(bounce, ...args); }

@ -0,0 +1,54 @@
import { vec3 } from 'glm';
import { Transform } from '../core/Transform.js';
export class LinearAnimator {
constructor(node, {
startPosition = [0, 0, 0],
endPosition = [0, 0, 0],
startTime = 0,
duration = 1,
loop = false,
} = {}) {
this.node = node;
this.startPosition = startPosition;
this.endPosition = endPosition;
this.startTime = startTime;
this.duration = duration;
this.loop = loop;
this.playing = true;
}
play() {
this.playing = true;
}
pause() {
this.playing = false;
}
update(t, dt) {
if (!this.playing) {
return;
}
const linearInterpolation = (t - this.startTime) / this.duration;
const clampedInterpolation = Math.min(Math.max(linearInterpolation, 0), 1);
const loopedInterpolation = ((linearInterpolation % 1) + 1) % 1;
this.updateNode(this.loop ? loopedInterpolation : clampedInterpolation);
}
updateNode(interpolation) {
const transform = this.node.getComponentOfType(Transform);
if (!transform) {
return;
}
vec3.lerp(transform.translation, this.startPosition, this.endPosition, interpolation);
}
}

@ -0,0 +1,54 @@
import { quat, vec3 } from 'glm';
import { Transform } from '../core/Transform.js';
export class RotateAnimator {
constructor(node, {
startRotation = [0, 0, 0, 1],
endRotation = [0, 0, 0, 1],
startTime = 0,
duration = 1,
loop = false,
} = {}) {
this.node = node;
this.startRotation = startRotation;
this.endRotation = endRotation;
this.startTime = startTime;
this.duration = duration;
this.loop = loop;
this.playing = true;
}
play() {
this.playing = true;
}
pause() {
this.playing = false;
}
update(t, dt) {
if (!this.playing) {
return;
}
const linearInterpolation = (t - this.startTime) / this.duration;
const clampedInterpolation = Math.min(Math.max(linearInterpolation, 0), 1);
const loopedInterpolation = ((linearInterpolation % 1) + 1) % 1;
this.updateNode(this.loop ? loopedInterpolation : clampedInterpolation);
}
updateNode(interpolation) {
const transform = this.node.getComponentOfType(Transform);
if (!transform) {
return;
}
quat.slerp(transform.rotation, this.startRotation, this.endRotation, interpolation);
}
}

@ -0,0 +1,131 @@
import { quat, vec3, mat4 } from 'glm';
import { Transform } from '../core/Transform.js';
export class FirstPersonController {
constructor(node, domElement, {
pitch = 0,
yaw = 0,
velocity = [0, 0, 0],
acceleration = 50,
maxSpeed = 5,
decay = 0.99999,
pointerSensitivity = 0.002,
} = {}) {
this.node = node;
this.domElement = domElement;
this.keys = {};
this.pitch = pitch;
this.yaw = yaw;
this.velocity = velocity;
this.acceleration = acceleration;
this.maxSpeed = maxSpeed;
this.decay = decay;
this.pointerSensitivity = pointerSensitivity;
this.initHandlers();
}
initHandlers() {
this.pointermoveHandler = this.pointermoveHandler.bind(this);
this.keydownHandler = this.keydownHandler.bind(this);
this.keyupHandler = this.keyupHandler.bind(this);
const element = this.domElement;
const doc = element.ownerDocument;
doc.addEventListener('keydown', this.keydownHandler);
doc.addEventListener('keyup', this.keyupHandler);
element.addEventListener('click', e => element.requestPointerLock());
doc.addEventListener('pointerlockchange', e => {
if (doc.pointerLockElement === element) {
doc.addEventListener('pointermove', this.pointermoveHandler);
} else {
doc.removeEventListener('pointermove', this.pointermoveHandler);
}
});
}
update(t, dt) {
// Calculate forward and right vectors.
const cos = Math.cos(this.yaw);
const sin = Math.sin(this.yaw);
const forward = [-sin, 0, -cos];
const right = [cos, 0, -sin];
// Map user input to the acceleration vector.
const acc = vec3.create();
if (this.keys['KeyW']) {
vec3.add(acc, acc, forward);
}
if (this.keys['KeyS']) {
vec3.sub(acc, acc, forward);
}
if (this.keys['KeyD']) {
vec3.add(acc, acc, right);
}
if (this.keys['KeyA']) {
vec3.sub(acc, acc, right);
}
// Update velocity based on acceleration.
vec3.scaleAndAdd(this.velocity, this.velocity, acc, dt * this.acceleration);
// If there is no user input, apply decay.
if (!this.keys['KeyW'] &&
!this.keys['KeyS'] &&
!this.keys['KeyD'] &&
!this.keys['KeyA'])
{
const decay = Math.exp(dt * Math.log(1 - this.decay));
vec3.scale(this.velocity, this.velocity, decay);
}
// Limit speed to prevent accelerating to infinity and beyond.
const speed = vec3.length(this.velocity);
if (speed > this.maxSpeed) {
vec3.scale(this.velocity, this.velocity, this.maxSpeed / speed);
}
const transform = this.node.getComponentOfType(Transform);
if (transform) {
// Update translation based on velocity.
vec3.scaleAndAdd(transform.translation,
transform.translation, this.velocity, dt);
// Update rotation based on the Euler angles.
const rotation = quat.create();
quat.rotateY(rotation, rotation, this.yaw);
quat.rotateX(rotation, rotation, this.pitch);
transform.rotation = rotation;
}
}
pointermoveHandler(e) {
const dx = e.movementX;
const dy = e.movementY;
this.pitch -= dy * this.pointerSensitivity;
this.yaw -= dx * this.pointerSensitivity;
const twopi = Math.PI * 2;
const halfpi = Math.PI / 2;
this.pitch = Math.min(Math.max(this.pitch, -halfpi), halfpi);
this.yaw = ((this.yaw % twopi) + twopi) % twopi;
}
keydownHandler(e) {
this.keys[e.code] = true;
}
keyupHandler(e) {
this.keys[e.code] = false;
}
}

@ -0,0 +1,74 @@
import { quat, vec3 } from 'glm';
import { Transform } from '../core/Transform.js';
export class OrbitController {
constructor(node, domElement, {
rotation = [0, 0, 0, 1],
distance = 2,
moveSensitivity = 0.004,
zoomSensitivity = 0.002,
} = {}) {
this.node = node;
this.domElement = domElement;
this.rotation = rotation;
this.distance = distance;
this.moveSensitivity = moveSensitivity;
this.zoomSensitivity = zoomSensitivity;
this.initHandlers();
}
initHandlers() {
this.pointerdownHandler = this.pointerdownHandler.bind(this);
this.pointerupHandler = this.pointerupHandler.bind(this);
this.pointermoveHandler = this.pointermoveHandler.bind(this);
this.wheelHandler = this.wheelHandler.bind(this);
this.domElement.addEventListener('pointerdown', this.pointerdownHandler);
this.domElement.addEventListener('wheel', this.wheelHandler);
}
pointerdownHandler(e) {
this.domElement.setPointerCapture(e.pointerId);
this.domElement.requestPointerLock();
this.domElement.removeEventListener('pointerdown', this.pointerdownHandler);
this.domElement.addEventListener('pointerup', this.pointerupHandler);
this.domElement.addEventListener('pointermove', this.pointermoveHandler);
}
pointerupHandler(e) {
this.domElement.releasePointerCapture(e.pointerId);
this.domElement.ownerDocument.exitPointerLock();
this.domElement.addEventListener('pointerdown', this.pointerdownHandler);
this.domElement.removeEventListener('pointerup', this.pointerupHandler);
this.domElement.removeEventListener('pointermove', this.pointermoveHandler);
}
pointermoveHandler(e) {
const dx = e.movementX;
const dy = e.movementY;
quat.rotateX(this.rotation, this.rotation, -dy * this.moveSensitivity);
quat.rotateY(this.rotation, this.rotation, -dx * this.moveSensitivity);
quat.normalize(this.rotation, this.rotation);
}
wheelHandler(e) {
this.distance *= Math.exp(this.zoomSensitivity * e.deltaY);
}
update() {
const transform = this.node.getComponentOfType(Transform);
if (!transform) {
return;
}
quat.copy(transform.rotation, this.rotation);
vec3.transformQuat(transform.translation, [0, 0, this.distance], this.rotation);
}
}

@ -0,0 +1,143 @@
import { quat, vec2, vec3 } from 'glm';
import { Transform } from 'engine/core/Transform.js';
export class TouchController {
constructor(node, domElement, {
translation = [0, 0, 0],
rotation = [0, 0, 0, 1],
distance = 2,
translateSensitivity = 0.001,
rotateSensitivity = 0.004,
wheelSensitivity = 0.002,
} = {}) {
this.node = node;
this.domElement = domElement;
this.translation = translation;
this.rotation = rotation;
this.distance = distance;
this.translateSensitivity = translateSensitivity;
this.rotateSensitivity = rotateSensitivity;
this.wheelSensitivity = wheelSensitivity;
this.pointers = new Map();
this.initHandlers();
}
initHandlers() {
this.pointerdownHandler = this.pointerdownHandler.bind(this);
this.pointerupHandler = this.pointerupHandler.bind(this);
this.pointermoveHandler = this.pointermoveHandler.bind(this);
this.wheelHandler = this.wheelHandler.bind(this);
this.domElement.addEventListener('pointerdown', this.pointerdownHandler);
this.domElement.addEventListener('pointerup', this.pointerupHandler);
this.domElement.addEventListener('pointercancel', this.pointerupHandler);
this.domElement.addEventListener('pointermove', this.pointermoveHandler);
this.domElement.addEventListener('wheel', this.wheelHandler);
}
pointerdownHandler(e) {
this.pointers.set(e.pointerId, e);
this.domElement.setPointerCapture(e.pointerId);
this.domElement.requestPointerLock();
}
pointerupHandler(e) {
this.pointers.delete(e.pointerId);
this.domElement.releasePointerCapture(e.pointerId);
this.domElement.ownerDocument.exitPointerLock();
}
pointermoveHandler(e) {
if (!this.pointers.has(e.pointerId)) {
return;
}
if (this.pointers.size === 1) {
if (e.shiftKey) {
this.translate(e.movementX, e.movementY);
} else {
this.rotate(e.movementX, e.movementY);
}
this.pointers.set(e.pointerId, e);
} else {
const N = this.pointers.size;
// Points before movement
const A = [...this.pointers.values()].map(e => [e.clientX, e.clientY]);
this.pointers.set(e.pointerId, e);
// Points after movement
const B = [...this.pointers.values()].map(e => [e.clientX, e.clientY]);
const centroidA = A.reduce((a, v) => vec2.scaleAndAdd(a, a, v, 1 / N), [0, 0]);
const centroidB = B.reduce((a, v) => vec2.scaleAndAdd(a, a, v, 1 / N), [0, 0]);
const translation = vec2.subtract(vec2.create(), centroidB, centroidA);
const centeredA = A.map(v => vec2.subtract(vec2.create(), v, centroidA));
const centeredB = B.map(v => vec2.subtract(vec2.create(), v, centroidB));
const scaleA = centeredA.reduce((a, v) => a + vec2.length(v) / N, 0);
const scaleB = centeredB.reduce((a, v) => a + vec2.length(v) / N, 0);
const scale = scaleA / scaleB;
const normalizedA = centeredA.map(v => vec2.normalize(vec2.create(), v));
const normalizedB = centeredB.map(v => vec2.normalize(vec2.create(), v));
let sin = 0;
for (let i = 0; i < A.length; i++) {
const a = normalizedA[i];
const b = normalizedB[i];
sin += (a[0] * b[1] - a[1] * b[0]) / N;
}
const angle = Math.asin(sin);
this.translate(...translation);
this.scale(scale);
this.screw(angle);
}
}
wheelHandler(e) {
this.scale(Math.exp(e.deltaY * this.wheelSensitivity));
}
screw(amount) {
quat.rotateZ(this.rotation, this.rotation, amount);
}
rotate(dx, dy) {
quat.rotateX(this.rotation, this.rotation, -dy * this.rotateSensitivity);
quat.rotateY(this.rotation, this.rotation, -dx * this.rotateSensitivity);
quat.normalize(this.rotation, this.rotation);
}
translate(dx, dy) {
const translation = [-dx, dy, 0];
vec3.transformQuat(translation, translation, this.rotation);
vec3.scaleAndAdd(this.translation, this.translation, translation, this.distance * this.translateSensitivity);
}
scale(amount) {
this.distance *= amount;
}
update() {
const transform = this.node.getComponentOfType(Transform);
if (!transform) {
return;
}
quat.copy(transform.rotation, this.rotation);
vec3.transformQuat(transform.translation, [0, 0, this.distance], this.rotation);
vec3.add(transform.translation, transform.translation, this.translation);
}
}

@ -0,0 +1,88 @@
import { quat, vec3 } from 'glm';
import { Transform } from '../core/Transform.js';
export class TurntableController {
constructor(node, domElement, {
pitch = 0,
yaw = 0,
distance = 1,
moveSensitivity = 0.004,
zoomSensitivity = 0.002,
} = {}) {
this.node = node;
this.domElement = domElement;
this.pitch = pitch;
this.yaw = yaw;
this.distance = distance;
this.moveSensitivity = moveSensitivity;
this.zoomSensitivity = zoomSensitivity;
this.initHandlers();
}
initHandlers() {
this.pointerdownHandler = this.pointerdownHandler.bind(this);
this.pointerupHandler = this.pointerupHandler.bind(this);
this.pointermoveHandler = this.pointermoveHandler.bind(this);
this.wheelHandler = this.wheelHandler.bind(this);
this.domElement.addEventListener('pointerdown', this.pointerdownHandler);
this.domElement.addEventListener('wheel', this.wheelHandler);
}
pointerdownHandler(e) {
this.domElement.setPointerCapture(e.pointerId);
this.domElement.requestPointerLock();
this.domElement.removeEventListener('pointerdown', this.pointerdownHandler);
this.domElement.addEventListener('pointerup', this.pointerupHandler);
this.domElement.addEventListener('pointermove', this.pointermoveHandler);
}
pointerupHandler(e) {
this.domElement.releasePointerCapture(e.pointerId);
this.domElement.ownerDocument.exitPointerLock();
this.domElement.addEventListener('pointerdown', this.pointerdownHandler);
this.domElement.removeEventListener('pointerup', this.pointerupHandler);
this.domElement.removeEventListener('pointermove', this.pointermoveHandler);
}
pointermoveHandler(e) {
const dx = e.movementX;
const dy = e.movementY;
this.pitch -= dy * this.moveSensitivity;
this.yaw -= dx * this.moveSensitivity;
const twopi = Math.PI * 2;
const halfpi = Math.PI / 2;
this.pitch = Math.min(Math.max(this.pitch, -halfpi), halfpi);
this.yaw = ((this.yaw % twopi) + twopi) % twopi;
}
wheelHandler(e) {
this.distance *= Math.exp(this.zoomSensitivity * e.deltaY);
}
update() {
const transform = this.node.getComponentOfType(Transform);
if (!transform) {
return;
}
const rotation = quat.create();
quat.rotateY(rotation, rotation, this.yaw);
quat.rotateX(rotation, rotation, this.pitch);
transform.rotation = rotation;
const translation = [0, 0, this.distance];
vec3.rotateX(translation, translation, [0, 0, 0], this.pitch);
vec3.rotateY(translation, translation, [0, 0, 0], this.yaw);
transform.translation = translation;
}
}

11
naloga_3/engine/core.js Normal file

@ -0,0 +1,11 @@
export * from './core/Accessor.js';
export * from './core/Camera.js';
export * from './core/Material.js';
export * from './core/Mesh.js';
export * from './core/Model.js';
export * from './core/Node.js';
export * from './core/Primitive.js';
export * from './core/Sampler.js';
export * from './core/Texture.js';
export * from './core/Transform.js';
export * from './core/Vertex.js';

@ -0,0 +1,132 @@
export class Accessor {
constructor({
buffer,
viewLength,
viewOffset = 0,
offset = 0,
stride = componentSize,
componentType = 'int',
componentCount = 1,
componentSize = 1,
componentSigned = false,
componentNormalized = false,
} = {}) {
this.buffer = buffer;
this.offset = offset;
this.stride = stride;
this.componentType = componentType;
this.componentCount = componentCount;
this.componentSize = componentSize;
this.componentSigned = componentSigned;
this.componentNormalized = componentNormalized;
const viewType = this.getViewType({
componentType,
componentSize,
componentSigned,
});
if (viewLength !== undefined) {
this.view = new viewType(buffer, viewOffset, viewLength / viewType.BYTES_PER_ELEMENT);
} else {
this.view = new viewType(buffer, viewOffset);
}
this.offsetInElements = offset / viewType.BYTES_PER_ELEMENT;
this.strideInElements = stride / viewType.BYTES_PER_ELEMENT;
this.count = Math.floor((this.view.length - this.offsetInElements) / this.strideInElements);
this.normalize = this.getNormalizer({
componentType,
componentSize,
componentSigned,
componentNormalized,
});
this.denormalize = this.getDenormalizer({
componentType,
componentSize,
componentSigned,
componentNormalized,
});
}
get(index) {
const start = index * this.strideInElements + this.offsetInElements;
const end = start + this.componentCount;
return [...this.view.slice(start, end)].map(this.normalize);
}
set(index, value) {
const start = index * this.strideInElements + this.offsetInElements;
this.view.set(value.map(this.denormalize), start);
}
getNormalizer({
componentType,
componentSize,
componentSigned,
componentNormalized,
}) {
if (!componentNormalized || componentType === 'float') {
return x => x;
}
const multiplier = componentSigned
? 2 ** ((componentSize * 8) - 1) - 1
: 2 ** (componentSize * 8) - 1;
return x => Math.max(x / multiplier, -1);
}
getDenormalizer({
componentType,
componentSize,
componentSigned,
componentNormalized,
}) {
if (!componentNormalized || componentType === 'float') {
return x => x;
}
const multiplier = componentSigned
? 2 ** ((componentSize * 8) - 1) - 1
: 2 ** (componentSize * 8) - 1;
const min = componentSigned ? -1 : 0;
const max = 1;
return x => Math.floor(0.5 + multiplier * Math.min(Math.max(x, min), max));
}
getViewType({
componentType,
componentSize,
componentSigned,
}) {
if (componentType === 'float') {
if (componentSize === 4) {
return Float32Array;
}
} else if (componentType === 'int') {
if (componentSigned) {
switch (componentSize) {
case 1: return Int8Array;
case 2: return Int16Array;
case 4: return Int32Array;
}
} else {
switch (componentSize) {
case 1: return Uint8Array;
case 2: return Uint16Array;
case 4: return Uint32Array;
}
}
}
}
}

@ -0,0 +1,46 @@
import { mat4 } from 'glm';
export class Camera {
constructor({
orthographic = 0,
aspect = 1,
fovy = 1,
halfy = 1,
near = 0.01,
far = 1000,
} = {}) {
this.orthographic = orthographic;
this.aspect = aspect;
this.fovy = fovy;
this.halfy = halfy;
this.near = near;
this.far = far;
}
get projectionMatrix() {
if (this.orthographic === 0) {
return this.perspectiveMatrix;
} else if (this.orthographic === 1) {
return this.orthographicMatrix;
} else {
const a = this.orthographicMatrix;
const b = this.perspectiveMatrix;
return mat4.add(mat4.create(),
mat4.multiplyScalar(a, a, this.orthographic),
mat4.multiplyScalar(b, b, 1 - this.orthographic));
}
}
get orthographicMatrix() {
const { halfy, aspect, near, far } = this;
const halfx = halfy * aspect;
return mat4.orthoZO(mat4.create(), -halfx, halfx, -halfy, halfy, near, far);
}
get perspectiveMatrix() {
const { fovy, aspect, near, far } = this;
return mat4.perspectiveZO(mat4.create(), fovy, aspect, near, far);
}
}

@ -0,0 +1,33 @@
export class Material {
constructor({
baseTexture,
emissionTexture,
normalTexture,
occlusionTexture,
roughnessTexture,
metalnessTexture,
baseFactor = [1, 1, 1, 1],
emissionFactor = [0, 0, 0],
normalFactor = 1,
occlusionFactor = 1,
roughnessFactor = 1,
metalnessFactor = 1,
} = {}) {
this.baseTexture = baseTexture;
this.emissionTexture = emissionTexture;
this.normalTexture = normalTexture;
this.occlusionTexture = occlusionTexture;
this.roughnessTexture = roughnessTexture;
this.metalnessTexture = metalnessTexture;
this.baseFactor = baseFactor;
this.emissionFactor = emissionFactor;
this.normalFactor = normalFactor;
this.occlusionFactor = occlusionFactor;
this.roughnessFactor = roughnessFactor;
this.metalnessFactor = metalnessFactor;
}
}

@ -0,0 +1,11 @@
export class Mesh {
constructor({
vertices = [],
indices = [],
} = {}) {
this.vertices = vertices;
this.indices = indices;
}
}

@ -0,0 +1,43 @@
import { quat, vec3, vec4, mat3, mat4 } from 'glm';
export function transformVertex(vertex, matrix,
normalMatrix = mat3.normalFromMat4(mat3.create(), matrix),
tangentMatrix = mat3.fromMat4(mat3.create(), matrix),
) {
vec3.transformMat4(vertex.position, vertex.position, matrix);
vec3.transformMat3(vertex.normal, vertex.normal, normalMatrix);
vec3.transformMat3(vertex.tangent, vertex.tangent, tangentMatrix);
}
export function transformMesh(mesh, matrix,
normalMatrix = mat3.normalFromMat4(mat3.create(), matrix),
tangentMatrix = mat3.fromMat4(mat3.create(), matrix),
) {
for (const vertex of mesh.vertices) {
transformVertex(vertex, matrix, normalMatrix, tangentMatrix);
}
}
export function calculateAxisAlignedBoundingBox(mesh) {
const initial = {
min: vec3.clone(mesh.vertices[0].position),
max: vec3.clone(mesh.vertices[0].position),
};
return {
min: mesh.vertices.reduce((a, b) => vec3.min(a, a, b.position), initial.min),
max: mesh.vertices.reduce((a, b) => vec3.max(a, a, b.position), initial.max),
};
}
export function mergeAxisAlignedBoundingBoxes(boxes) {
const initial = {
min: vec3.clone(boxes[0].min),
max: vec3.clone(boxes[0].max),
};
return {
min: boxes.reduce(({ min: amin }, { min: bmin }) => vec3.min(amin, amin, bmin), initial),
max: boxes.reduce(({ max: amax }, { max: bmax }) => vec3.max(amax, amax, bmax), initial),
};
}

@ -0,0 +1,9 @@
export class Model {
constructor({
primitives = [],
} = {}) {
this.primitives = primitives;
}
}

@ -0,0 +1,69 @@
export class Node {
constructor() {
this.children = [];
this.parent = null;
this.components = [];
}
addChild(node) {
node.parent?.removeChild(node);
this.children.push(node);
node.parent = this;
}
removeChild(node) {
const index = this.children.indexOf(node);
if (index >= 0) {
this.children.splice(index, 1);
node.parent = null;
}
}
traverse(before, after) {
before?.(this);
for (const child of this.children) {
child.traverse(before, after);
}
after?.(this);
}
linearize() {
const array = [];
this.traverse(node => array.push(node));
return array;
}
filter(predicate) {
return this.linearize().filter(predicate);
}
find(predicate) {
return this.linearize().find(predicate);
}
map(transform) {
return this.linearize().map(transform);
}
addComponent(component) {
this.components.push(component);
}
removeComponent(component) {
this.components = this.components.filter(c => c !== component);
}
removeComponentsOfType(type) {
this.components = this.components.filter(component => !(component instanceof type));
}
getComponentOfType(type) {
return this.components.find(component => component instanceof type);
}
getComponentsOfType(type) {
return this.components.filter(component => component instanceof type);
}
}

@ -0,0 +1,11 @@
export class Primitive {
constructor({
mesh,
material,
} = {}) {
this.mesh = mesh;
this.material = material;
}
}

@ -0,0 +1,21 @@
export class Sampler {
constructor({
minFilter = 'linear',
magFilter = 'linear',
mipmapFilter = 'linear',
addressModeU = 'clamp-to-edge',
addressModeV = 'clamp-to-edge',
addressModeW = 'clamp-to-edge',
maxAnisotropy = 1,
} = {}) {
this.minFilter = minFilter;
this.magFilter = magFilter;
this.mipmapFilter = mipmapFilter;
this.addressModeU = addressModeU;
this.addressModeV = addressModeV;
this.addressModeW = addressModeW;
this.maxAnisotropy = maxAnisotropy;
}
}

@ -0,0 +1,42 @@
import { mat4 } from 'glm';
import { Camera } from './Camera.js';
import { Model } from './Model.js';
import { Transform } from './Transform.js';
export function getLocalModelMatrix(node) {
const matrix = mat4.create();
for (const transform of node.getComponentsOfType(Transform)) {
mat4.mul(matrix, matrix, transform.matrix);
}
return matrix;
}
export function getGlobalModelMatrix(node) {
if (node.parent) {
const parentMatrix = getGlobalModelMatrix(node.parent);
const modelMatrix = getLocalModelMatrix(node);
return mat4.multiply(parentMatrix, parentMatrix, modelMatrix);
} else {
return getLocalModelMatrix(node);
}
}
export function getLocalViewMatrix(node) {
const matrix = getLocalModelMatrix(node);
return mat4.invert(matrix, matrix);
}
export function getGlobalViewMatrix(node) {
const matrix = getGlobalModelMatrix(node);
return mat4.invert(matrix, matrix);
}
export function getProjectionMatrix(node) {
const camera = node.getComponentOfType(Camera);
return camera ? camera.projectionMatrix : mat4.create();
}
export function getModels(node) {
return node.getComponentsOfType(Model);
}

@ -0,0 +1,21 @@
export class Texture {
constructor({
image,
sampler,
isSRGB = false,
} = {}) {
this.image = image;
this.sampler = sampler;
this.isSRGB = isSRGB;
}
get width() {
return this.image.width;
}
get height() {
return this.image.height;
}
}

@ -0,0 +1,30 @@
import { mat4 } from 'glm';
export class Transform {
constructor({
rotation = [0, 0, 0, 1],
translation = [0, 0, 0],
scale = [1, 1, 1],
matrix,
} = {}) {
this.rotation = rotation;
this.translation = translation;
this.scale = scale;
if (matrix) {
this.matrix = matrix;
}
}
get matrix() {
return mat4.fromRotationTranslationScale(mat4.create(),
this.rotation, this.translation, this.scale);
}
set matrix(matrix) {
mat4.getRotation(this.rotation, matrix);
mat4.getTranslation(this.translation, matrix);
mat4.getScaling(this.scale, matrix);
}
}

@ -0,0 +1,15 @@
export class Vertex {
constructor({
position = [0, 0, 0],
texcoords = [0, 0],
normal = [0, 0, 0],
tangent = [0, 0, 0],
} = {}) {
this.position = position;
this.texcoords = texcoords;
this.normal = normal;
this.tangent = tangent;
}
}

@ -0,0 +1,35 @@
import { Accessor } from './Accessor.js';
export function parseFormat(format) {
const regex = /(?<type>float|((?<sign>u|s)(?<norm>int|norm)))(?<bits>\d+)x(?<count>\d+)/;
const groups = format.match(regex).groups;
return {
componentType: groups.type === 'float' ? 'float' : 'int',
componentNormalized: groups.norm === 'norm',
componentSigned: groups.sign === 's',
componentSize: Number(groups.bits) / 8,
componentCount: Number(groups.count),
};
}
export function createVertexBuffer(vertices, layout) {
const buffer = new ArrayBuffer(layout.arrayStride * vertices.length);
const accessors = layout.attributes.map(attribute => new Accessor({
buffer,
stride: layout.arrayStride,
...parseFormat(attribute.format),
...attribute,
}));
for (let i = 0; i < vertices.length; i++) {
const vertex = vertices[i];
for (let j = 0; j < layout.attributes.length; j++) {
const accessor = accessors[j];
const attribute = layout.attributes[j].name;
accessor.set(i, vertex[attribute]);
}
}
return buffer;
}

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

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

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

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

@ -0,0 +1,75 @@
import { mat4 } from 'glm';
import * as WebGPU from '../WebGPU.js';
import { createVertexBuffer } from '../core/VertexUtils.js';
export class BaseRenderer {
constructor(canvas) {
this.canvas = canvas;
this.gpuObjects = new WeakMap();
}
async initialize() {
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = this.canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
this.device = device;
this.context = context;
this.format = format;
}
prepareImage(image, isSRGB = false) {
if (this.gpuObjects.has(image)) {
return this.gpuObjects.get(image);
}
const gpuTexture = WebGPU.createTexture(this.device, {
source: image,
format: isSRGB ? 'rgba8unorm-srgb' : 'rgba8unorm',
});
const gpuObjects = { gpuTexture };
this.gpuObjects.set(image, gpuObjects);
return gpuObjects;
}
prepareSampler(sampler) {
if (this.gpuObjects.has(sampler)) {
return this.gpuObjects.get(sampler);
}
const gpuSampler = this.device.createSampler(sampler);
const gpuObjects = { gpuSampler };
this.gpuObjects.set(sampler, gpuObjects);
return gpuObjects;
}
prepareMesh(mesh, layout) {
if (this.gpuObjects.has(mesh)) {
return this.gpuObjects.get(mesh);
}
const vertexBufferArrayBuffer = createVertexBuffer(mesh.vertices, layout);
const vertexBuffer = WebGPU.createBuffer(this.device, {
data: vertexBufferArrayBuffer,
usage: GPUBufferUsage.VERTEX,
});
const indexBufferArrayBuffer = new Uint32Array(mesh.indices).buffer;
const indexBuffer = WebGPU.createBuffer(this.device, {
data: indexBufferArrayBuffer,
usage: GPUBufferUsage.INDEX,
});
const gpuObjects = { vertexBuffer, indexBuffer };
this.gpuObjects.set(mesh, gpuObjects);
return gpuObjects;
}
}

@ -0,0 +1,236 @@
import { mat4 } from 'glm';
import * as WebGPU from '../WebGPU.js';
import { Camera } from '../core.js';
import {
getLocalModelMatrix,
getGlobalViewMatrix,
getProjectionMatrix,
getModels,
} from '../core/SceneUtils.js';
import { BaseRenderer } from './BaseRenderer.js';
const vertexBufferLayout = {
arrayStride: 20,
attributes: [
{
name: 'position',
shaderLocation: 0,
offset: 0,
format: 'float32x3',
},
{
name: 'texcoords',
shaderLocation: 1,
offset: 12,
format: 'float32x2',
},
],
};
export class UnlitRenderer extends BaseRenderer {
constructor(canvas) {
super(canvas);
}
async initialize() {
await super.initialize();
const code = await fetch(new URL('UnlitRenderer.wgsl', import.meta.url))
.then(response => response.text());
const module = this.device.createShaderModule({ code });
this.pipeline = await this.device.createRenderPipelineAsync({
layout: 'auto',
vertex: {
module,
entryPoint: 'vertex',
buffers: [ vertexBufferLayout ],
},
fragment: {
module,
entryPoint: 'fragment',
targets: [{ format: this.format }],
},
depthStencil: {
format: 'depth24plus',
depthWriteEnabled: true,
depthCompare: 'less',
},
});
this.recreateDepthTexture();
}
recreateDepthTexture() {
this.depthTexture?.destroy();
this.depthTexture = this.device.createTexture({
format: 'depth24plus',
size: [this.canvas.width, this.canvas.height],
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
}
prepareNode(node) {
if (this.gpuObjects.has(node)) {
return this.gpuObjects.get(node);
}
const modelUniformBuffer = this.device.createBuffer({
size: 128,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const modelBindGroup = this.device.createBindGroup({
layout: this.pipeline.getBindGroupLayout(1),
entries: [
{ binding: 0, resource: { buffer: modelUniformBuffer } },
],
});
const gpuObjects = { modelUniformBuffer, modelBindGroup };
this.gpuObjects.set(node, gpuObjects);
return gpuObjects;
}
prepareCamera(camera) {
if (this.gpuObjects.has(camera)) {
return this.gpuObjects.get(camera);
}
const cameraUniformBuffer = this.device.createBuffer({
size: 128,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const cameraBindGroup = this.device.createBindGroup({
layout: this.pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: cameraUniformBuffer } },
],
});
const gpuObjects = { cameraUniformBuffer, cameraBindGroup };
this.gpuObjects.set(camera, gpuObjects);
return gpuObjects;
}
prepareTexture(texture) {
if (this.gpuObjects.has(texture)) {
return this.gpuObjects.get(texture);
}
const { gpuTexture } = this.prepareImage(texture.image); // ignore sRGB
const { gpuSampler } = this.prepareSampler(texture.sampler);
const gpuObjects = { gpuTexture, gpuSampler };
this.gpuObjects.set(texture, gpuObjects);
return gpuObjects;
}
prepareMaterial(material) {
if (this.gpuObjects.has(material)) {
return this.gpuObjects.get(material);
}
const baseTexture = this.prepareTexture(material.baseTexture);
const materialUniformBuffer = this.device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const materialBindGroup = this.device.createBindGroup({
layout: this.pipeline.getBindGroupLayout(2),
entries: [
{ binding: 0, resource: { buffer: materialUniformBuffer } },
{ binding: 1, resource: baseTexture.gpuTexture.createView() },
{ binding: 2, resource: baseTexture.gpuSampler },
],
});
const gpuObjects = { materialUniformBuffer, materialBindGroup };
this.gpuObjects.set(material, gpuObjects);
return gpuObjects;
}
render(scene, camera) {
if (this.depthTexture.width !== this.canvas.width || this.depthTexture.height !== this.canvas.height) {
this.recreateDepthTexture();
}
const encoder = this.device.createCommandEncoder();
this.renderPass = encoder.beginRenderPass({
colorAttachments: [
{
view: this.context.getCurrentTexture().createView(),
clearValue: [1, 1, 1, 1],
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
view: this.depthTexture.createView(),
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'discard',
},
});
this.renderPass.setPipeline(this.pipeline);
const cameraComponent = camera.getComponentOfType(Camera);
const viewMatrix = getGlobalViewMatrix(camera);
const projectionMatrix = getProjectionMatrix(camera);
const { cameraUniformBuffer, cameraBindGroup } = this.prepareCamera(cameraComponent);
this.device.queue.writeBuffer(cameraUniformBuffer, 0, viewMatrix);
this.device.queue.writeBuffer(cameraUniformBuffer, 64, projectionMatrix);
this.renderPass.setBindGroup(0, cameraBindGroup);
this.renderNode(scene);
this.renderPass.end();
this.device.queue.submit([encoder.finish()]);
}
renderNode(node, modelMatrix = mat4.create()) {
const localMatrix = getLocalModelMatrix(node);
modelMatrix = mat4.multiply(mat4.create(), modelMatrix, localMatrix);
const { modelUniformBuffer, modelBindGroup } = this.prepareNode(node);
const normalMatrix = mat4.normalFromMat4(mat4.create(), modelMatrix);
this.device.queue.writeBuffer(modelUniformBuffer, 0, modelMatrix);
this.device.queue.writeBuffer(modelUniformBuffer, 64, normalMatrix);
this.renderPass.setBindGroup(1, modelBindGroup);
for (const model of getModels(node)) {
this.renderModel(model);
}
for (const child of node.children) {
this.renderNode(child, modelMatrix);
}
}
renderModel(model) {
for (const primitive of model.primitives) {
this.renderPrimitive(primitive);
}
}
renderPrimitive(primitive) {
const { materialUniformBuffer, materialBindGroup } = this.prepareMaterial(primitive.material);
this.device.queue.writeBuffer(materialUniformBuffer, 0, new Float32Array(primitive.material.baseFactor));
this.renderPass.setBindGroup(2, materialBindGroup);
const { vertexBuffer, indexBuffer } = this.prepareMesh(primitive.mesh, vertexBufferLayout);
this.renderPass.setVertexBuffer(0, vertexBuffer);
this.renderPass.setIndexBuffer(indexBuffer, 'uint32');
this.renderPass.drawIndexed(primitive.mesh.indices.length);
}
}

@ -0,0 +1,56 @@
struct VertexInput {
@location(0) position: vec3f,
@location(1) texcoords: vec2f,
}
struct VertexOutput {
@builtin(position) position: vec4f,
@location(1) texcoords: vec2f,
}
struct FragmentInput {
@location(1) texcoords: vec2f,
}
struct FragmentOutput {
@location(0) color: vec4f,
}
struct CameraUniforms {
viewMatrix: mat4x4f,
projectionMatrix: mat4x4f,
}
struct ModelUniforms {
modelMatrix: mat4x4f,
normalMatrix: mat3x3f,
}
struct MaterialUniforms {
baseFactor: vec4f,
}
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
@group(2) @binding(1) var baseTexture: texture_2d<f32>;
@group(2) @binding(2) var baseSampler: sampler;
@vertex
fn vertex(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.position = camera.projectionMatrix * camera.viewMatrix * model.modelMatrix * vec4(input.position, 1);
output.texcoords = input.texcoords;
return output;
}
@fragment
fn fragment(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
output.color = textureSample(baseTexture, baseSampler, input.texcoords) * material.baseFactor;
return output;
}

56
naloga_3/engine/style.css Normal file

@ -0,0 +1,56 @@
body, html {
margin: 0;
padding: 0;
}
* {
font-family: sans-serif;
}
.fullscreen {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.fullscreen > * {
width: 100%;
height: 100%;
}
.overlay {
position: fixed;
left: 0;
top: 0;
}
.no-touch {
touch-action: none;
}
.pixelated {
image-rendering: pixelated;
}
.loader-container {
display: flex;
align-items: center;
justify-content: center;
}
.loader {
width: 100px;
height: 100px;
border: 15px solid transparent;
border-radius: 50%;
border-top-color: #999;
border-bottom-color: #999;
animation: spin 2s ease-in-out infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(1800deg); }
}

@ -0,0 +1,84 @@
export class ResizeSystem {
constructor({
canvas,
resize,
resolutionFactor = 1,
minWidth = 1,
minHeight = 1,
maxWidth = Infinity,
maxHeight = Infinity,
} = {}) {
this._resize = this._resize.bind(this);
this.canvas = canvas;
this.resize = resize;
this.resolutionFactor = resolutionFactor;
this.minCanvasSize = {
width: minWidth,
height: minHeight,
};
this.maxCanvasSize = {
width: maxWidth,
height: maxHeight,
};
this.lastSize = {
width: null,
height: null,
};
}
start() {
if (this._resizeFrame) {
return;
}
this._resizeFrame = requestAnimationFrame(this._resize);
}
stop() {
if (!this._resizeFrame) {
return;
}
this._resizeFrame = cancelAnimationFrame(this._resizeFrame);
}
_resize() {
this._resizeFrame = requestAnimationFrame(this._resize);
const displayRect = this.canvas.getBoundingClientRect();
if (displayRect.width === this.lastSize.width && displayRect.height === this.lastSize.height) {
return;
}
this.lastSize = {
width: displayRect.width,
height: displayRect.height,
};
const displaySize = {
width: displayRect.width * devicePixelRatio,
height: displayRect.height * devicePixelRatio,
};
const unclampedSize = {
width: Math.round(displaySize.width * this.resolutionFactor),
height: Math.round(displaySize.height * this.resolutionFactor),
};
const canvasSize = {
width: Math.min(Math.max(unclampedSize.width, this.minCanvasSize.width), this.maxCanvasSize.width),
height: Math.min(Math.max(unclampedSize.height, this.minCanvasSize.height), this.maxCanvasSize.height),
};
if (this.canvas.width !== canvasSize.width || this.canvas.height !== canvasSize.height) {
this.canvas.width = canvasSize.width;
this.canvas.height = canvasSize.height;
}
this.resize?.({ displaySize, canvasSize });
}
}

@ -0,0 +1,49 @@
export class UpdateSystem {
constructor(application) {
this._update = this._update.bind(this);
this._render = this._render.bind(this);
this.application = application;
this.running = false;
}
start() {
if (this.running) {
return;
}
this.application.start?.();
this._time = performance.now() / 1000;
this._updateFrame = setInterval(this._update, 0);
this._renderFrame = requestAnimationFrame(this._render);
}
stop() {
if (!this.running) {
return;
}
this.application.stop?.();
this._updateFrame = clearInterval(this._updateFrame);
this._renderFrame = cancelAnimationFrame(this._render);
}
_update() {
const time = performance.now() / 1000;
const dt = time - this._time;
this._time = time;
this.application.update?.(time, dt);
}
_render() {
this._renderFrame = requestAnimationFrame(this._render);
this.application.render?.();
}
}

23
naloga_3/index.html Normal file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Naloga 3</title>
<script type="importmap">
{
"imports": {
"engine/": "./engine/",
"dat": "./lib/dat.js",
"glm": "./lib/glm.js"
}
}
</script>
<link rel="stylesheet" href="engine/style.css">
<script type="module" src="main.js"></script>
</head>
<body>
<div class="fullscreen no-touch pixelated">
<canvas></canvas>
</div>
</body>
</html>

22
naloga_3/lib/dat.js Normal file

File diff suppressed because one or more lines are too long

1
naloga_3/lib/glm.js Normal file

File diff suppressed because one or more lines are too long

98
naloga_3/main.js Normal file

@ -0,0 +1,98 @@
import { ResizeSystem } from 'engine/systems/ResizeSystem.js';
import { UpdateSystem } from 'engine/systems/UpdateSystem.js';
import { GLTFLoader } from 'engine/loaders/GLTFLoader.js';
import { OrbitController } from 'engine/controllers/OrbitController.js';
import { RotateAnimator } from 'engine/animators/RotateAnimator.js';
import { LinearAnimator } from 'engine/animators/LinearAnimator.js';
import { quat } from './lib/glm.js';
import { Camera, Model, Node, Transform } from 'engine/core.js';
import { Renderer } from './Renderer.js';
import { Light } from './Light.js';
const canvas = document.querySelector('canvas');
const renderer = new Renderer(canvas);
await renderer.initialize();
const gltfLoader = new GLTFLoader();
await gltfLoader.load('./models/monkey/monkey.gltf');
const gltfLoader2 = new GLTFLoader();
await gltfLoader2.load('./models/cone/cone.gltf');
const scene = gltfLoader.loadScene(gltfLoader.defaultScene);
const camera = scene.find((node) => node.getComponentOfType(Camera));
const cameraTransform = camera.getComponentOfType(Transform);
cameraTransform.rotation = [0, 0, 0, 1];
cameraTransform.translation = [0, 0, 10];
camera.addComponent(
new OrbitController(camera, document.body, {
distance: 8,
}),
);
const model = scene.find((node) => node.getComponentOfType(Model));
// model.addComponent(
// new LinearAnimator(model, {
// startPosition: [1, 0, 0],
// endPosition: [-1, 0, 0],
// duration: 5,
// loop: true,
// }),
// );
const light = gltfLoader2.loadScene(gltfLoader.defaultScene);
light.addComponent(
new Transform({
translation: [0, 0, 0],
}),
);
light.addComponent(
new Light({
ambient: 0,
intensity: 0.5,
cutoffAngle: 45 * (Math.PI / 180),
}),
);
light.addComponent({
update(t, dt) {
const transform = light.getComponentOfType(Transform);
const rotation = quat.create();
quat.rotateX(rotation, rotation, (Math.PI / 180) * t * 100);
transform.rotation = rotation;
},
});
// light.addComponent(
// new LinearAnimator(light, {
// startPosition: [0, 0, 0],
// endPosition: [0, 0, 0],
// duration: 5,
// loop: true,
// }),
// );
scene.addChild(light);
function update(time, dt) {
scene.traverse((node) => {
for (const component of node.components) {
component.update?.(time, dt);
}
});
}
function render() {
renderer.render(scene, camera);
}
function resize({ displaySize: { width, height } }) {
camera.getComponentOfType(Camera).aspect = width / height;
}
new ResizeSystem({ canvas, resize }).start();
new UpdateSystem({ update, render }).start();

Binary file not shown.

After

(image error) Size: 60 KiB

Binary file not shown.

@ -0,0 +1,149 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v4.2.60",
"version":"2.0"
},
"extensionsUsed":[
"KHR_materials_unlit"
],
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0
]
}
],
"nodes":[
{
"mesh":0,
"name":"Cone",
"rotation":[
0,
0,
-0.7071068286895752,
0.7071068286895752
]
}
],
"materials":[
{
"doubleSided":true,
"extensions":{
"KHR_materials_unlit":{}
},
"name":"Material",
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.9
}
}
],
"meshes":[
{
"name":"Cone",
"primitives":[
{
"attributes":{
"POSITION":0,
"NORMAL":1,
"TEXCOORD_0":2
},
"indices":3,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
}
],
"images":[
{
"mimeType":"image/png",
"name":"base",
"uri":"base.png"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":128,
"max":[
1,
1,
1
],
"min":[
-1,
-1,
-1
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":128,
"type":"VEC3"
},
{
"bufferView":2,
"componentType":5126,
"count":128,
"type":"VEC2"
},
{
"bufferView":3,
"componentType":5123,
"count":186,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":1536,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":1536,
"byteOffset":1536,
"target":34962
},
{
"buffer":0,
"byteLength":1024,
"byteOffset":3072,
"target":34962
},
{
"buffer":0,
"byteLength":372,
"byteOffset":4096,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":4468,
"uri":"cone.bin"
}
]
}

40
naloga_3/models/cube.obj Normal file

@ -0,0 +1,40 @@
# Blender 4.2.1 LTS
# www.blender.org
mtllib cube.mtl
o Cube
v 1.000000 1.000000 -1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 1.000000 -1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 1.000000
vn -0.0000 1.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -1.0000 -0.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vt 0.625000 0.500000
vt 0.875000 0.500000
vt 0.875000 0.750000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.625000 1.000000
vt 0.375000 1.000000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.125000 0.500000
vt 0.375000 0.500000
vt 0.125000 0.750000
s 0
usemtl Material
f 1/1/1 5/2/1 7/3/1 3/4/1
f 4/5/2 3/4/2 7/6/2 8/7/2
f 8/8/3 7/9/3 5/10/3 6/11/3
f 6/12/4 2/13/4 4/5/4 8/14/4
f 2/13/5 1/1/5 3/4/5 4/5/5
f 6/11/6 5/10/6 1/1/6 2/13/6

Binary file not shown.

After

(image error) Size: 60 KiB

Binary file not shown.

@ -0,0 +1,190 @@
{
"asset":{
"generator":"Khronos glTF Blender I/O v3.6.6",
"version":"2.0"
},
"scene":0,
"scenes":[
{
"name":"Scene",
"nodes":[
0,
1
]
}
],
"nodes":[
{
"camera":0,
"name":"Camera",
"rotation":[
-0.20997299253940582,
0.3857799470424652,
0.09062844514846802,
0.8937962055206299
],
"translation":[
7.358891487121582,
4.958309173583984,
6.925790786743164
]
},
{
"mesh":0,
"name":"Suzanne"
}
],
"cameras":[
{
"name":"Camera",
"perspective":{
"aspectRatio":1.7777777777777777,
"yfov":0.39959652046304894,
"zfar":100,
"znear":0.10000000149011612
},
"type":"perspective"
}
],
"materials":[
{
"doubleSided":true,
"name":"Material",
"normalTexture": {
"index":1
},
"pbrMetallicRoughness":{
"baseColorTexture":{
"index":0
},
"metallicFactor":0,
"roughnessFactor":0.5
}
}
],
"meshes":[
{
"name":"Suzanne",
"primitives":[
{
"attributes":{
"POSITION":0,
"TEXCOORD_0":1,
"NORMAL":2,
"TANGENT":3
},
"indices":4,
"material":0
}
]
}
],
"textures":[
{
"sampler":0,
"source":0
},
{
"sampler":0,
"source":1
}
],
"images":[
{
"mimeType":"image/png",
"name":"base",
"uri":"base.png"
},
{
"mimeType":"image/webp",
"name":"normal",
"uri":"normal.webp"
}
],
"accessors":[
{
"bufferView":0,
"componentType":5126,
"count":556,
"max":[
1.3671875,
0.984375,
0.8515625
],
"min":[
-1.3671875,
-0.984375,
-0.8515625
],
"type":"VEC3"
},
{
"bufferView":1,
"componentType":5126,
"count":556,
"type":"VEC2"
},
{
"bufferView":2,
"componentType":5126,
"count":556,
"type":"VEC3"
},
{
"bufferView":3,
"componentType":5126,
"count":556,
"type":"VEC4"
},
{
"bufferView":4,
"componentType":5123,
"count":2904,
"type":"SCALAR"
}
],
"bufferViews":[
{
"buffer":0,
"byteLength":6672,
"byteOffset":0,
"target":34962
},
{
"buffer":0,
"byteLength":4448,
"byteOffset":6672,
"target":34962
},
{
"buffer":0,
"byteLength":6672,
"byteOffset":11120,
"target":34962
},
{
"buffer":0,
"byteLength":8896,
"byteOffset":17792,
"target":34962
},
{
"buffer":0,
"byteLength":5808,
"byteOffset":26688,
"target":34963
}
],
"samplers":[
{
"magFilter":9729,
"minFilter":9987
}
],
"buffers":[
{
"byteLength":32496,
"uri":"monkey.bin"
}
]
}

Binary file not shown.

After

(image error) Size: 62 KiB

98
naloga_3/shader.wgsl Normal file

@ -0,0 +1,98 @@
struct VertexInput {
@location(0) position: vec3f,
@location(1) texcoords: vec2f,
@location(2) normal: vec3f,
}
struct VertexOutput {
@builtin(position) clipPosition: vec4f,
@location(0) position: vec3f,
@location(1) texcoords: vec2f,
@location(2) normal: vec3f,
}
struct FragmentInput {
@location(0) position: vec3f,
@location(1) texcoords: vec2f,
@location(2) normal: vec3f,
}
struct FragmentOutput {
@location(0) color: vec4f,
}
struct CameraUniforms {
viewMatrix: mat4x4f,
projectionMatrix: mat4x4f,
cameraPosition: vec3f,
}
struct ModelUniforms {
modelMatrix: mat4x4f,
normalMatrix: mat3x3f,
}
struct MaterialUniforms {
baseFactor: vec4f,
specularFactor: f32,
shininess: f32,
}
struct SpotLightUniforms {
position: vec3f,
direction: vec3f,
cutoffAngle: f32,
ambient: f32,
intensity: f32,
}
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
@group(1) @binding(0) var<uniform> model: ModelUniforms;
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
@group(2) @binding(1) var baseTexture: texture_2d<f32>;
@group(2) @binding(2) var baseSampler: sampler;
@group(3) @binding(0) var<uniform> spotLight: SpotLightUniforms;
@vertex
fn vertex(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.clipPosition = camera.projectionMatrix * camera.viewMatrix * model.modelMatrix * vec4(input.position, 1);
output.position = (model.modelMatrix * vec4(input.position, 1)).xyz;
output.texcoords = input.texcoords;
output.normal = model.normalMatrix * input.normal;
return output;
}
@fragment
fn fragment(input: FragmentInput) -> FragmentOutput {
var output: FragmentOutput;
let N = normalize(input.normal);
let L = normalize(spotLight.position - input.position);
let V = normalize(camera.cameraPosition - input.position);
let H = normalize(L + V);
// Spotlight factor based on cutoff angle
let lightDir = normalize(spotLight.direction);
let theta = dot(L, -lightDir);
let spotlightFactor = select(0.0, 1.0, theta > spotLight.cutoffAngle);
// Diffuse and Specular components
let lambert = max(dot(N, L), 0) * spotlightFactor;
let specular = pow(max(dot(N, H), 0), material.shininess) * spotlightFactor;
let materialColor = textureSample(baseTexture, baseSampler, input.texcoords) * material.baseFactor;
let lambertFactor = vec4(vec3(lambert), 1);
let specularFactor = vec4(vec3(specular * material.specularFactor), 1);
let ambientFactor = vec4(vec3(spotLight.ambient), 1);
// Final color
// output.color = materialColor * ((lambertFactor + specularFactor) + specularFactor * spotLight.intensity);
// output.color = spotLight.intensity * (lambertFactor + specularFactor) + ambientFactor;
output.color = spotlightFactor * materialColor;
return output;
}

BIN
vaja_1/base.png Normal file

Binary file not shown.

After

(image error) Size: 60 KiB

@ -15,6 +15,35 @@ const depthTexture = device.createTexture({
format: 'depth24plus', format: 'depth24plus',
}); });
// Prepare color texture
// 1. fetch the texture from the server
const imageBitmap = await fetch('base.png')
.then((response) => response.blob())
.then((blob) => createImageBitmap(blob));
// 2. create a texture
const colorTexture = device.createTexture({
size: [imageBitmap.width, imageBitmap.height],
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.RENDER_ATTACHMENT |
GPUTextureUsage.COPY_DST,
format: 'rgba8unorm',
mipLevelCount: 2,
});
// 3. transfer data
device.queue.copyExternalImageToTexture(
{ source: imageBitmap },
{ texture: colorTexture },
[imageBitmap.width, imageBitmap.height],
);
// 4. create sampler
const colorSampler = device.createSampler({
mipmapFilter: 'linear',
});
// Create vertex buffer // Create vertex buffer
// prettier-ignore // prettier-ignore
const vertices = new Float32Array([ const vertices = new Float32Array([
@ -24,7 +53,7 @@ const vertices = new Float32Array([
-1, 1, -1, 1, 0, 0, 1, 1, -1, 1, -1, 1, 0, 0, 1, 1,
1, 1, -1, 1, 1, 1, 0, 1, 1, 1, -1, 1, 1, 1, 0, 1,
-1, 1, 1, 1, 1, 0, 0, 1, -1, -1, 1, 1, 1, 0, 0, 1,
1, -1, 1, 1, 0, 1, 0, 1, 1, -1, 1, 1, 0, 1, 0, 1,
-1, 1, 1, 1, 0, 0, 1, 1, -1, 1, 1, 1, 0, 0, 1, 1,
1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1,
@ -62,12 +91,12 @@ const module = device.createShaderModule({ code });
// Create the pipeline // Create the pipeline
/** @type {GPUVertexBufferLayout} */ /** @type {GPUVertexBufferLayout} */
const vertexBufferLayout = { const vertexBufferLayout = {
arrayStride: 24, arrayStride: 32,
attributes: [ attributes: [
{ {
shaderLocation: 0, shaderLocation: 0,
offset: 0, offset: 0,
format: 'float32x2', format: 'float32x4',
}, },
{ {
shaderLocation: 1, shaderLocation: 1,
@ -103,7 +132,11 @@ const uniformBuffer = device.createBuffer({
// Create the bind group // Create the bind group
const bindGroup = device.createBindGroup({ const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0), layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }], entries: [
{ binding: 0, resource: { buffer: uniformBuffer } },
{ binding: 1, resource: colorTexture.createView() },
{ binding: 2, resource: colorSampler },
],
}); });
function update() { function update() {

@ -1,38 +1,21 @@
struct VertexInput {
@location(0) position: vec4f,
@location(1) color: vec4f,
}
struct VertexOutput { struct VertexOutput {
@builtin(position) position: vec4f, @builtin(position) position: vec4f,
@location(0) color: vec4f, @location(0) color: vec4f,
} }
struct FragmentInput {
@location(0) color: vec4f,
}
struct FragmentOutput {
@location(0) color: vec4f,
}
@group(0) @binding(0) var<uniform> mtrx: mat4x4f; @group(0) @binding(0) var<uniform> mtrx: mat4x4f;
@group(0) @binding(1) var colorTexture: texture_2d<f32>;
@group(0) @binding(2) var colorSampler: sampler;
@vertex @vertex
fn vertex(input: VertexInput) -> VertexOutput { fn vertex(@location(0) position: vec4f, @location(1) color: vec4f) -> VertexOutput {
var output: VertexOutput; var output: VertexOutput;
output.position = mtrx * position;
output.position = mtrx * input.position; output.color = color;
output.color = input.color;
return output; return output;
} }
@fragment @fragment
fn fragment(input: FragmentInput) -> FragmentOutput { fn fragment(@location(0) color: vec4f) -> @location(0) vec4f {
var output: FragmentOutput; return textureSample(colorTexture, colorSampler, color.xy);
output.color = input.color;
return output;
} }