UNPKG

matrix-engine-wgpu

Version:

Networking implemented - based on kurento openvidu server. fix arcball camera,instanced draws added also effect pipeline blend with instancing option.Normalmap added, Fixed shadows casting vs camera/video texture, webGPU powered pwa application. Crazy fas

556 lines (470 loc) 18 kB
// Note: The code in this file does not use the 'dst' output parameter of functions in the // 'wgpu-matrix' library, so produces many temporary vectors and matrices. // This is intentional, as this sample prefers readability over performance. import {mat4, vec3} from 'wgpu-matrix'; import {LOG_INFO} from './utils'; // The common functionality between camera implementations class CameraBase { // The camera matrix matrix_ = new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, ]); // The calculated view matrix readonly view_ = mat4.create(); // Aliases to column vectors of the matrix right_ = new Float32Array(this.matrix_.buffer, 4 * 0, 4); up_ = new Float32Array(this.matrix_.buffer, 4 * 4, 4); back_ = new Float32Array(this.matrix_.buffer, 4 * 8, 4); position_ = new Float32Array(this.matrix_.buffer, 4 * 12, 4); // Returns the camera matrix get matrix() { return this.matrix_; } // Assigns `mat` to the camera matrix set matrix(mat) { mat4.copy(mat, this.matrix_); } // setProjection(fov = (2*Math.PI) / 5 , aspect = 1, near = 0.5, far = 1000) { // this.projectionMatrix = mat4.perspective(fov, aspect, near, far); // } // Returns the camera view matrix get view() { return this.view_; } // Assigns `mat` to the camera view set view(mat) { mat4.copy(mat, this.view_); } // Returns column vector 0 of the camera matrix get right() { return this.right_; } // Assigns `vec` to the first 3 elements of column vector 0 of the camera matrix set right(vec) { vec3.copy(vec, this.right_); } // Returns column vector 1 of the camera matrix get up() { return this.up_; } // Assigns `vec` to the first 3 elements of column vector 1 of the camera matrix \ Vec3 set up(vec) { vec3.copy(vec, this.up_); } // Returns column vector 2 of the camera matrix get back() { return this.back_; } // Assigns `vec` to the first 3 elements of column vector 2 of the camera matrix set back(vec) { vec3.copy(vec, this.back_); } // Returns column vector 3 of the camera matrix get position() { return this.position_; } // Assigns `vec` to the first 3 elements of column vector 3 of the camera matrix set position(vec) { vec3.copy(vec, this.position_); } } // WASDCamera is a camera implementation that behaves similar to first-person-shooter PC games. export class WASDCamera extends CameraBase { // The camera absolute pitch angle pitch = 0; // The camera absolute yaw angle yaw = 0; // The movement veloicty readonly velocity_ = vec3.create(); // Speed multiplier for camera movement movementSpeed = 10; // Speed multiplier for camera rotation rotationSpeed = 1; // Movement velocity drag coeffient [0 .. 1] // 0: Continues forever // 1: Instantly stops moving frictionCoefficient = 0.99; // Returns velocity vector get velocity() { return this.velocity_; } // Assigns `vec` to the velocity vector set velocity(vec) { vec3.copy(vec, this.velocity_); } setProjection(fov = (2 * Math.PI) / 5, aspect = 1, near = 1, far = 1000) { this.projectionMatrix = mat4.perspective(fov, aspect, near, far); } constructor(options) { super(); if(options && (options.position || options.target)) { const position = options.position ?? vec3.create(0, 0, 0); const target = options.target ?? vec3.create(0, 0, 0); const forward = vec3.normalize(vec3.sub(target, position)); this.recalculateAngles(forward); this.position = position; this.canvas = options.canvas; this.aspect = options.canvas.width / options.canvas.height; this.setProjection((2 * Math.PI) / 5, this.aspect, 1, 2000); // console.log(`%cCamera constructor : ${position}`, LOG_INFO); } } // Returns the camera matrix get matrix() { return super.matrix; } // Assigns `mat` to the camera matrix, and recalcuates the camera angles set matrix(mat) { super.matrix = mat; this.recalculateAngles(this.back); } update(deltaTime, input) { const sign = (positive, negative) => (positive ? 1 : 0) - (negative ? 1 : 0); // Apply the delta rotation to the pitch and yaw angles this.yaw -= input.analog.x * deltaTime * this.rotationSpeed; this.pitch -= input.analog.y * deltaTime * this.rotationSpeed; // Wrap yaw between [0° .. 360°], just to prevent large accumulation. this.yaw = mod(this.yaw, Math.PI * 2); // Clamp pitch between [-90° .. +90°] to prevent somersaults. this.pitch = clamp(this.pitch, -Math.PI / 2, Math.PI / 2); // Save the current position, as we're about to rebuild the camera matrix. const position = vec3.copy(this.position); // Reconstruct the camera's rotation, and store into the camera matrix. super.matrix = mat4.rotateX(mat4.rotationY(this.yaw), this.pitch); // super.matrix = mat4.rotateX(mat4.rotationY(this.yaw), -this.pitch); // super.matrix = mat4.rotateY(mat4.rotateX(this.pitch), this.yaw); // Calculate the new target velocity const digital = input.digital; const deltaRight = sign(digital.right, digital.left); const deltaUp = sign(digital.up, digital.down); const targetVelocity = vec3.create(); const deltaBack = sign(digital.backward, digital.forward); vec3.addScaled(targetVelocity, this.right, deltaRight, targetVelocity); vec3.addScaled(targetVelocity, this.up, deltaUp, targetVelocity); vec3.addScaled(targetVelocity, this.back, deltaBack, targetVelocity); vec3.normalize(targetVelocity, targetVelocity); vec3.mulScalar(targetVelocity, this.movementSpeed, targetVelocity); // Mix new target velocity this.velocity = lerp( targetVelocity, this.velocity, Math.pow(1 - this.frictionCoefficient, deltaTime) ); // Integrate velocity to calculate new position this.position = vec3.addScaled(position, this.velocity, deltaTime); // Invert the camera matrix to build the view matrix this.view = mat4.invert(this.matrix); return this.view; } // Recalculates the yaw and pitch values from a directional vector recalculateAngles(dir) { this.yaw = Math.atan2(dir[0], dir[2]); this.pitch = -Math.asin(dir[1]); } } // ArcballCamera implements a basic orbiting camera around the world origin export class ArcballCamera extends CameraBase { // The camera distance from the target distance = 0; // The current angular velocity angularVelocity = 0; // The current rotation axis axis_ = vec3.create(); // Returns the rotation axis get axis() { return this.axis_; } // Assigns `vec` to the rotation axis set axis(vec) { vec3.copy(vec, this.axis_); } // Speed multiplier for camera rotation rotationSpeed = 1; // Speed multiplier for camera zoom zoomSpeed = 0.1; // Rotation velocity drag coeffient [0 .. 1] // 0: Spins forever // 1: Instantly stops spinning frictionCoefficient = 0.999; setProjection(fov = (2 * Math.PI) / 5, aspect = 1, near = 1, far = 1000) { this.projectionMatrix = mat4.perspective(fov, aspect, near, far); } // Construtor constructor(options) { super(); if(options && options.position) { this.position = options.position; this.distance = vec3.len(this.position); this.back = vec3.normalize(this.position); this.setProjection((2 * Math.PI) / 5, this.aspect, 1, 2000); this.recalcuateRight(); this.recalcuateUp(); } } // Returns the camera matrix get matrix() { return super.matrix; } // Assigns `mat` to the camera matrix, and recalcuates the distance set matrix(mat) { super.matrix = mat; this.distance = vec3.len(this.position); } update(deltaTime, input) { const epsilon = 0.0000001; if(input.analog.touching) { // Currently being dragged. this.angularVelocity = 0; } else { // Dampen any existing angular velocity this.angularVelocity *= Math.pow(1 - this.frictionCoefficient, deltaTime); } // Calculate the movement vector const movement = vec3.create(); vec3.addScaled(movement, this.right, input.analog.x, movement); vec3.addScaled(movement, this.up, -input.analog.y, movement); // Cross the movement vector with the view direction to calculate the rotation axis x magnitude const crossProduct = vec3.cross(movement, this.back); // Calculate the magnitude of the drag const magnitude = vec3.len(crossProduct); if(magnitude > epsilon) { // Normalize the crossProduct to get the rotation axis this.axis = vec3.scale(crossProduct, 1 / magnitude); // Remember the current angular velocity. This is used when the touch is released for a fling. this.angularVelocity = magnitude * this.rotationSpeed; } // The rotation around this.axis to apply to the camera matrix this update const rotationAngle = this.angularVelocity * deltaTime; if(rotationAngle > epsilon) { // Rotate the matrix around axis // Note: The rotation is not done as a matrix-matrix multiply as the repeated multiplications // will quickly introduce substantial error into the matrix. this.back = vec3.normalize(rotate(this.back, this.axis, rotationAngle)); this.recalcuateRight(); this.recalcuateUp(); } // recalculate `this.position` from `this.back` considering zoom if(input.analog.zoom !== 0) { this.distance *= 1 + input.analog.zoom * this.zoomSpeed; } this.position = vec3.scale(this.back, this.distance); // Invert the camera matrix to build the view matrix this.view = mat4.invert(this.matrix); return this.view; } // Assigns `this.right` with the cross product of `this.up` and `this.back` recalcuateRight() { this.right = vec3.normalize(vec3.cross(this.up, this.back)); } // Assigns `this.up` with the cross product of `this.back` and `this.right` recalcuateUp() { this.up = vec3.normalize(vec3.cross(this.back, this.right)); } } // Returns `x` clamped between [`min` .. `max`] function clamp(x, min, max) { return Math.min(Math.max(x, min), max); } // Returns `x` float-modulo `div` function mod(x, div) { return x - Math.floor(Math.abs(x) / div) * div * Math.sign(x); } // Returns `vec` rotated `angle` radians around `axis` function rotate(vec, axis, angle) { return vec3.transformMat4Upper3x3(vec, mat4.rotation(axis, angle)); } // Returns the linear interpolation between 'a' and 'b' using 's' function lerp(a, b, s) { return vec3.addScaled(a, vec3.sub(b, a), s); } export function createInputHandler(window, canvas) { let digital = { forward: false, backward: false, left: false, right: false, up: false, down: false, }; let analog = { x: 0, y: 0, zoom: 0, }; let mouseDown = false; const setDigital = (e, value) => { switch(e.code) { case 'KeyW': digital.forward = value; break; case 'KeyS': digital.backward = value; break; case 'KeyA': digital.left = value; break; case 'KeyD': digital.right = value; break; case 'Space': digital.up = value; break; case 'ShiftLeft': case 'ControlLeft': case 'KeyC': digital.down = value; break; } // if you wanna dosavle all keyboard input for some reason... // add later like new option feature... // e.preventDefault(); e.stopPropagation(); }; window.addEventListener('keydown', (e) => setDigital(e, true)); window.addEventListener('keyup', (e) => setDigital(e, false)); canvas.style.touchAction = 'pinch-zoom'; canvas.addEventListener('pointerdown', () => {mouseDown = true;}); canvas.addEventListener('pointerup', () => {mouseDown = false;}); canvas.addEventListener('pointermove', (e) => { mouseDown = e.pointerType === 'mouse' ? (e.buttons & 1) !== 0 : true; if(mouseDown) { analog.x += e.movementX / 10; analog.y += e.movementY / 10; } }); canvas.addEventListener('wheel', (e) => { // if((e.buttons & 1) !== 0) { // analog.zoom += Math.sign(e.deltaY); // e.preventDefault(); // e.stopPropagation(); // } }, {passive: false}); return () => { // Guard: prevent zero deltas from breaking camera math const safeX = analog.x || 0.0001; const safeY = analog.y || 0.0001; const out = { digital, analog: { x: safeX, y: safeY, zoom: analog.zoom, touching: mouseDown, }, }; // Reset only the deltas for next frame analog.x = 0; analog.y = 0; analog.zoom = 0; return out; }; } export class RPGCamera extends CameraBase { followMe = null; pitch = 0; yaw = 0; velocity_ = vec3.create(); movementSpeed = 10; rotationSpeed = 1; followMeOffset = 150; // << mobile adaptation needed after all... // Movement velocity drag coeffient [0 .. 1] // 0: Continues forever // 1: Instantly stops moving frictionCoefficient = 0.99; // Returns velocity vector // Inside your camera control init scrollY = 50; minY = 50.5; // minimum camera height maxY = 135.0; // maximum camera height scrollSpeed = 1; get velocity() { return this.velocity_; } // Assigns `vec` to the velocity vector set velocity(vec) { vec3.copy(vec, this.velocity_); } setProjection(fov = (2 * Math.PI) / 5, aspect = 1, near = 1, far = 1000) { this.projectionMatrix = mat4.perspective(fov, aspect, near, far); } constructor(options) { super(); if(options && (options.position || options.target)) { const position = options.position ?? vec3.create(0, 0, 0); const target = options.target ?? vec3.create(0, 0, 0); const forward = vec3.normalize(vec3.sub(target, position)); this.recalculateAngles(forward); this.position = position; this.canvas = options.canvas; this.aspect = options.canvas.width / options.canvas.height; this.setProjection((2 * Math.PI) / 5, this.aspect, 1, 2000); // console.log(`%cCamera constructor : ${position}`, LOG_INFO); this.mousRollInAction = false; addEventListener('wheel', (e) => { // Scroll up = zoom out / higher Y this.mousRollInAction = true; this.scrollY -= e.deltaY * this.scrollSpeed * 0.01; // Clamp to range this.scrollY = Math.max(this.minY, Math.min(this.maxY, this.scrollY)); }); } } get matrix() { return super.matrix; } // Assigns `mat` to the camera matrix, and recalcuates the camera angles set matrix(mat) { super.matrix = mat; this.recalculateAngles(this.back); } update(deltaTime, input) { const sign = (positive, negative) => (positive ? 1 : 0) - (negative ? 1 : 0); // Apply the delta rotation to the pitch and yaw angles this.yaw = 0;//-= input.analog.x * deltaTime * this.rotationSpeed; this.pitch = -0.88;// -= input.analog.y * deltaTime * this.rotationSpeed; // // Wrap yaw between [0° .. 360°], just to prevent large accumulation. this.yaw = mod(this.yaw, Math.PI * 2); // // Clamp pitch between [-90° .. +90°] to prevent somersaults. this.pitch = clamp(this.pitch, -Math.PI / 2, Math.PI / 2); // Save the current position, as we're about to rebuild the camera matrix. if(this.followMe != null && this.followMe.inMove === true || this.mousRollInAction == true ) { // console.log(" follow : " + this.followMe.x) this.followMeOffset = this.scrollY; // if player not move allow mouse explore map this.position[0] = this.followMe.x; this.position[2] = this.followMe.z + this.followMeOffset; app.lightContainer[0].position[0] = this.followMe.x; app.lightContainer[0].position[2] = this.followMe.z; app.lightContainer[0].target[0] = this.followMe.x; app.lightContainer[0].target[2] = this.followMe.z; this.mousRollInAction = false; } const smoothFactor = 0.1; this.position[1] += (this.scrollY - this.position[1]) * smoothFactor; let position = vec3.copy(this.position); // Reconstruct the camera's rotation, and store into the camera matrix. super.matrix = mat4.rotateX(mat4.rotationY(this.yaw), this.pitch); // Calculate the new target velocity const digital = input.digital; const deltaRight = sign(digital.right, digital.left); const deltaUp = sign(digital.up, digital.down); const targetVelocity = vec3.create(); const deltaBack = sign(digital.backward, digital.forward); // older then follow if(deltaBack == -1) { // console.log(deltaBack + " deltaBack ") position[2] += -10; } else if(deltaBack == 1) { position[2] += 10; } position[0] += deltaRight * 10; vec3.addScaled(targetVelocity, this.right, deltaRight, targetVelocity); vec3.addScaled(targetVelocity, this.up, deltaUp, targetVelocity); vec3.normalize(targetVelocity, targetVelocity); vec3.mulScalar(targetVelocity, this.movementSpeed, targetVelocity); this.velocity = lerp( targetVelocity, this.velocity, Math.pow(1 - this.frictionCoefficient, deltaTime) ); this.position = vec3.addScaled(position, this.velocity, deltaTime); this.view = mat4.invert(this.matrix); return this.view; } recalculateAngles(dir) { this.yaw = Math.atan2(dir[0], dir[2]); this.pitch = -Math.asin(dir[1]); } }