Naloga 3 WIP

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

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?.();
}
}