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
161 lines (136 loc) • 5.74 kB
JavaScript
/**
* MatrixEngine Raycaster (improved)
* Author: Nikola Lukić
* Version: 2.0
*/
import {mat4, vec3, vec4} from "wgpu-matrix";
export let touchCoordinate = {
enabled: false,
x: 0,
y: 0,
stopOnFirstDetectedHit: false,
};
function multiplyMatrixVector(matrix, vector) {
return vec4.transformMat4(vector, matrix);
}
export function getRayFromMouse(event, canvas, camera) {
const rect = canvas.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = -(((event.clientY - rect.top) / rect.height) * 2 - 1); // flip Y (WebGPU NDC)
// Use precomputed projection if available
const invProjection = mat4.inverse(camera.projectionMatrix);
const invView = mat4.inverse(camera.view);
const clip = [x, y, 1, 1];
let eye = vec4.transformMat4(clip, invProjection);
eye = [eye[0], eye[1], -1, 0];
const worldDir4 = vec4.transformMat4(eye, invView);
const rayDirection = vec3.normalize([worldDir4[0], worldDir4[1], worldDir4[2]]);
const rayOrigin = [...camera.position];
return {rayOrigin, rayDirection, screen: {x, y}};
}
// Backward compatibility alias
export const getRayFromMouse2 = getRayFromMouse;
export function rayIntersectsSphere(rayOrigin, rayDirection, sphereCenter, sphereRadius) {
const center = [sphereCenter.x, sphereCenter.y, sphereCenter.z];
const oc = vec3.subtract(rayOrigin, center);
const a = vec3.dot(rayDirection, rayDirection);
const b = 2.0 * vec3.dot(oc, rayDirection);
const c = vec3.dot(oc, oc) - sphereRadius * sphereRadius;
const discriminant = b * b - 4 * a * c;
if(discriminant < 0) return null;
const t = (-b - Math.sqrt(discriminant)) / (2.0 * a);
if(t < 0) return null;
const hitPoint = vec3.add(rayOrigin, vec3.mulScalar(rayDirection, t));
const hitNormal = vec3.normalize(vec3.subtract(hitPoint, center));
return {t, hitPoint, hitNormal};
}
export function computeAABB(vertices) {
const min = [Infinity, Infinity, Infinity];
const max = [-Infinity, -Infinity, -Infinity];
for(let i = 0;i < vertices.length;i += 3) {
min[0] = Math.min(min[0], vertices[i]);
min[1] = Math.min(min[1], vertices[i + 1]);
min[2] = Math.min(min[2], vertices[i + 2]);
max[0] = Math.max(max[0], vertices[i]);
max[1] = Math.max(max[1], vertices[i + 1]);
max[2] = Math.max(max[2], vertices[i + 2]);
}
return [min, max];
}
// Ray-AABB intersection returning distance (slab method)
export function rayIntersectsAABB(rayOrigin, rayDirection, boxMin, boxMax) {
let tmin = (boxMin[0] - rayOrigin[0]) / rayDirection[0];
let tmax = (boxMax[0] - rayOrigin[0]) / rayDirection[0];
if(tmin > tmax) [tmin, tmax] = [tmax, tmin];
let tymin = (boxMin[1] - rayOrigin[1]) / rayDirection[1];
let tymax = (boxMax[1] - rayOrigin[1]) / rayDirection[1];
if(tymin > tymax) [tymin, tymax] = [tymax, tymin];
if(tmin > tymax || tymin > tmax) return null;
if(tymin > tmin) tmin = tymin;
if(tymax < tmax) tmax = tymax;
let tzmin = (boxMin[2] - rayOrigin[2]) / rayDirection[2];
let tzmax = (boxMax[2] - rayOrigin[2]) / rayDirection[2];
if(tzmin > tzmax) [tzmin, tzmax] = [tzmax, tzmin];
if(tmin > tzmax || tzmin > tmax) return null;
const t = Math.max(tmin, 0.0);
const hitPoint = vec3.add(rayOrigin, vec3.mulScalar(rayDirection, t));
return {t, hitPoint};
}
export function computeWorldVertsAndAABB(object) {
const modelMatrix = object.getModelMatrix(object.position);
const worldVerts = [];
for(let i = 0;i < object.mesh.vertices.length;i += 3) {
const local = [object.mesh.vertices[i], object.mesh.vertices[i + 1], object.mesh.vertices[i + 2]];
const world = vec3.transformMat4(local, modelMatrix);
worldVerts.push(...world);
}
const [boxMin, boxMax] = computeAABB(worldVerts);
return {modelMatrix, worldVerts, boxMin, boxMax};
}
// 🧠 Dispatch rich event
function dispatchRayHitEvent(canvas, data) {
if (data.eventName == 'click') {
canvas.dispatchEvent(new CustomEvent("ray.hit.event", {detail: data}));
} else {
canvas.dispatchEvent(new CustomEvent("ray.hit.event.mm", {detail: data}));
}
}
export function addRaycastsListener(canvasId = "canvas1", eventName = 'click') {
const canvas = document.getElementById(canvasId);
if(!canvas) {
console.warn(`[Raycaster] Canvas with id '${canvasId}' not found.`);
return;
}
canvas.addEventListener(eventName, (event) => {
const camera = app.cameras[app.mainCameraParams.type];
const {rayOrigin, rayDirection, screen} = getRayFromMouse(event, canvas, camera);
let closestHit = null;
for(const object of app.mainRenderBundle) {
if(!object.raycast?.enabled) continue;
const {boxMin, boxMax} = computeWorldVertsAndAABB(object);
const hitAABB = rayIntersectsAABB(rayOrigin, rayDirection, boxMin, boxMax);
if(!hitAABB) continue;
const sphereHit = rayIntersectsSphere(rayOrigin, rayDirection, object.position, object.raycast.radius);
const hit = sphereHit || hitAABB;
if(hit && (!closestHit || hit.t < closestHit.t)) {
closestHit = {...hit, hitObject: object};
if(touchCoordinate.stopOnFirstDetectedHit) break;
}
}
if(closestHit) {
dispatchRayHitEvent(canvas, {
hitObject: closestHit.hitObject,
hitPoint: closestHit.hitPoint,
hitNormal: closestHit.hitNormal || null,
hitDistance: closestHit.t,
rayOrigin,
rayDirection,
screenCoords: screen,
camera,
timestamp: performance.now(),
button: event.button,
eventName: eventName
});
}
});
}