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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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
View 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

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

View File

@ -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
View 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

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

View File

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

Width:  |  Height:  |  Size: 62 KiB

98
naloga_3/shader.wgsl Normal file
View 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

Width:  |  Height:  |  Size: 60 KiB

View File

@ -15,6 +15,35 @@ const depthTexture = device.createTexture({
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
// prettier-ignore
const vertices = new Float32Array([
@ -24,7 +53,7 @@ const vertices = new Float32Array([
-1, 1, -1, 1, 0, 0, 1, 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, 0, 1, 1,
1, 1, 1, 1, 1, 1, 0, 1,
@ -62,12 +91,12 @@ const module = device.createShaderModule({ code });
// Create the pipeline
/** @type {GPUVertexBufferLayout} */
const vertexBufferLayout = {
arrayStride: 24,
arrayStride: 32,
attributes: [
{
shaderLocation: 0,
offset: 0,
format: 'float32x2',
format: 'float32x4',
},
{
shaderLocation: 1,
@ -103,7 +132,11 @@ const uniformBuffer = device.createBuffer({
// Create the bind group
const bindGroup = device.createBindGroup({
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() {

View File

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