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
480 lines (446 loc) • 16.3 kB
JavaScript
import {radToDeg} from "../../../src/engine/utils.js";
export default class NavMesh {
constructor(data, options = {}) {
const scale = options.scale ?? [1, 1, 1];
const sx = scale[0], sy = scale[1], sz = scale[2];
// Apply scale to each vertex
this.vertices = data.vertices.map(v => [
v[0] * sx,
v[1] * sy,
v[2] * sz
]);
this.polygons = data.polygons.map(p => ({
indices: p.indices.slice(),
neighbors: (p.neighbors || []).slice()
}));
this._computeCenters();
this._buildEdgeMap();
}
_computeCenters() {
this.centers = this.polygons.map(poly => {
const vs = poly.indices.map(i => this.vertices[i]);
const cx = (vs[0][0] + vs[1][0] + vs[2][0]) / 3;
const cy = (vs[0][1] + vs[1][1] + vs[2][1]) / 3;
const cz = (vs[0][2] + vs[1][2] + vs[2][2]) / 3;
return [cx, cy, cz];
});
}
_edgeKey(a, b) {
return a < b ? `${a}_${b}` : `${b}_${a}`;
}
_buildEdgeMap() {
// map edgeKey -> array of {poly, aIndex, bIndex}
this.edgeMap = new Map();
this.polygons.forEach((poly, pi) => {
const indices = poly.indices;
for(let i = 0;i < indices.length;i++) {
const a = indices[i];
const b = indices[(i + 1) % indices.length];
const key = this._edgeKey(a, b);
if(!this.edgeMap.has(key)) this.edgeMap.set(key, []);
this.edgeMap.get(key).push({poly: pi, a, b});
}
});
}
// Point-in-triangle test in XZ plane using barycentric technique
_pointInTriXZ(pt, v0, v1, v2) {
const x = pt[0], z = pt[2];
const ax = v0[0], az = v0[2];
const bx = v1[0], bz = v1[2];
const cx = v2[0], cz = v2[2];
// vectors
const v0x = cx - ax, v0z = cz - az;
const v1x = bx - ax, v1z = bz - az;
const v2x = x - ax, v2z = z - az;
const dot00 = v0x * v0x + v0z * v0z;
const dot01 = v0x * v1x + v0z * v1z;
const dot02 = v0x * v2x + v0z * v2z;
const dot11 = v1x * v1x + v1z * v1z;
const dot12 = v1x * v2x + v1z * v2z;
const denom = dot00 * dot11 - dot01 * dot01;
if(Math.abs(denom) < 1e-9) return false;
const u = (dot11 * dot02 - dot01 * dot12) / denom;
const v = (dot00 * dot12 - dot01 * dot02) / denom;
return (u >= -1e-6) && (v >= -1e-6) && (u + v <= 1 + 1e-6);
}
findPolygonContainingPoint(point) {
// first try naive linear scan (ok for medium meshes). point = [x,y,z]
for(let i = 0;i < this.polygons.length;i++) {
const poly = this.polygons[i];
const v0 = this.vertices[poly.indices[0]];
const v1 = this.vertices[poly.indices[1]];
const v2 = this.vertices[poly.indices[2]];
if(this._pointInTriXZ(point, v0, v1, v2)) return i;
}
// fallback: return nearest polygon center
let best = 0;
let bestD = Infinity;
for(let i = 0;i < this.centers.length;i++) {
const c = this.centers[i];
const dx = c[0] - point[0];
const dz = c[2] - point[2];
const d = dx * dx + dz * dz;
if(d < bestD) {bestD = d; best = i;}
}
return best;
}
// A* over polygon graph. returns list of polygon indices (inclusive)
_findPolyPath(startPoly, endPoly) {
if(startPoly === endPoly) return [startPoly];
const open = new MinHeap((a, b) => a.f - b.f);
const nodes = new Array(this.polygons.length);
for(let i = 0;i < nodes.length;i++) nodes[i] = {g: Infinity, h: 0, f: Infinity, parent: -1, id: i};
nodes[startPoly].g = 0;
nodes[startPoly].h = this._heuristic(startPoly, endPoly);
nodes[startPoly].f = nodes[startPoly].h;
open.push(nodes[startPoly]);
const closed = new Set();
while(!open.empty()) {
const current = open.pop();
if(current.id === endPoly) {
const path = [];
let cur = current;
while(cur) {
path.push(cur.id);
if(cur.parent === -1) break;
cur = nodes[cur.parent];
}
return path.reverse();
}
closed.add(current.id);
const neighbors = this.polygons[current.id].neighbors || [];
for(const nId of neighbors) {
if(closed.has(nId)) continue;
const tentativeG = current.g + this._edgeCost(current.id, nId);
const neigh = nodes[nId];
if(tentativeG < neigh.g) {
neigh.parent = current.id;
neigh.g = tentativeG;
neigh.h = this._heuristic(nId, endPoly);
neigh.f = neigh.g + neigh.h;
open.push(neigh);
}
}
}
return []; // no path
}
_heuristic(aIdx, bIdx) {
const a = this.centers[aIdx];
const b = this.centers[bIdx];
const dx = a[0] - b[0];
const dz = a[2] - b[2];
return Math.sqrt(dx * dx + dz * dz);
}
_edgeCost(aIdx, bIdx) {
// Euclidean distance between polygon centers
const a = this.centers[aIdx];
const b = this.centers[bIdx];
const dx = a[0] - b[0];
const dz = a[2] - b[2];
return Math.sqrt(dx * dx + dz * dz);
}
// build portal list (pair of points) between the sequence of polygons
_buildPortals(polyPath, startPoint, endPoint) {
// portals: array of {left:[x,y,z], right:[x,y,z]}
const portals = [];
for(let i = 0;i < polyPath.length - 1;i++) {
const aIdx = polyPath[i];
const bIdx = polyPath[i + 1];
// find shared edge between aIdx and bIdx
const pa = this.polygons[aIdx];
const pb = this.polygons[bIdx];
let shared = null;
for(let ia = 0;ia < pa.indices.length;ia++) {
const a0 = pa.indices[ia], a1 = pa.indices[(ia + 1) % pa.indices.length];
const key = this._edgeKey(a0, a1);
const entries = this.edgeMap.get(key) || [];
for(const e of entries) {
if(e.poly === bIdx) {
// shared edge
shared = [this.vertices[a0], this.vertices[a1]];
break;
}
}
if(shared) break;
}
if(!shared) {
// fallback: use centers
const cA = this.centers[aIdx];
const cB = this.centers[bIdx];
portals.push({left: cA.slice(), right: cB.slice()});
} else {
// ensure consistent ordering (left/right) in XZ relative to path direction
portals.push({left: shared[0].slice(), right: shared[1].slice()});
}
}
// prepend start and append end as degenerate portals
portals.unshift({left: startPoint.slice(), right: startPoint.slice()});
portals.push({left: endPoint.slice(), right: endPoint.slice()});
return portals;
}
// Funnel algorithm (returns array of [x,y,z])
_stringPull(portals) {
// classic funnel over XZ plane
const portalLeft = portals.map(p => [p.left[0], p.left[2]]);
const portalRight = portals.map(p => [p.right[0], p.right[2]]);
const points = []; // result XZ
let apexIndex = 0, leftIndex = 0, rightIndex = 0;
let apex = portalLeft[0].slice();
let left = portalLeft[0].slice();
let right = portalRight[0].slice();
points.push([apex[0], apex[1]]); // x,z
function vecCross(a, b) {return a[0] * b[1] - a[1] * b[0];}
function sub(a, b) {return [a[0] - b[0], a[1] - b[1]];}
for(let i = 1;i < portalLeft.length;i++) {
const pLeft = portalLeft[i];
const pRight = portalRight[i];
// update right
const relRight = sub(pRight, apex);
const relRightCur = sub(right, apex);
if(vecCross(relRightCur, relRight) >= 0) {
// new right is more 'right' -> tighten
if(vecCross(sub(left, apex), relRight) > 0) {
// right crosses left -> advance apex to left
points.push([left[0], left[1]]);
apex = left.slice();
// reset indices
apexIndex = leftIndex;
leftIndex = apexIndex;
rightIndex = apexIndex;
left = apex.slice();
right = apex.slice();
i = apexIndex;
continue;
}
right = pRight.slice();
rightIndex = i;
}
// update left
const relLeft = sub(pLeft, apex);
const relLeftCur = sub(left, apex);
if(vecCross(relLeftCur, relLeft) <= 0) {
// new left is more 'left' -> tighten
if(vecCross(sub(right, apex), relLeft) < 0) {
// left crosses right -> advance apex to right
points.push([right[0], right[1]]);
apex = right.slice();
apexIndex = rightIndex;
leftIndex = apexIndex;
rightIndex = apexIndex;
left = apex.slice();
right = apex.slice();
i = apexIndex;
continue;
}
left = pLeft.slice();
leftIndex = i;
}
}
// add goal
const lastPortal = portalLeft[portalLeft.length - 1];
points.push([lastPortal[0], lastPortal[1]]);
// convert back to [x,y,z] with Y taken from mesh average Y (or 0)
const out = points.map(xz => {
const x = xz[0], z = xz[1];
// pick Y from nearest vertex on mesh (cheap approximation)
const y = this._sampleY(x, z);
return [x, y, z];
});
return out;
}
_sampleY(x, z) {
// sample Y using nearest vertex (cheap). If you have heightmap, use that.
let bestD = Infinity, bestY = 0;
for(let i = 0;i < this.vertices.length;i++) {
const v = this.vertices[i];
const dx = v[0] - x, dz = v[2] - z;
const d = dx * dx + dz * dz;
if(d < bestD) {bestD = d; bestY = v[1];}
}
return bestY;
}
// Public API: returns an array of [x,y,z] waypoints or [] if unreachable
findPath(startPoint, endPoint) {
// startPoint and endPoint are [x,y,z]
const startPoly = this.findPolygonContainingPoint(startPoint);
const endPoly = this.findPolygonContainingPoint(endPoint);
if(startPoly === null || endPoly === null) return [];
const polyPath = this._findPolyPath(startPoly, endPoly);
if(!polyPath || polyPath.length === 0) return [];
// If polyPath is single poly, simply return [start,end]
if(polyPath.length === 1) {
return [[startPoint[0], this._sampleY(startPoint[0], startPoint[2]), startPoint[2]],
[endPoint[0], this._sampleY(endPoint[0], endPoint[2]), endPoint[2]]];
}
const portals = this._buildPortals(polyPath, startPoint, endPoint);
const smooth = this._stringPull(portals);
// ensure first/last are exactly start/end
if(smooth.length > 0) {
smooth[0] = [startPoint[0], this._sampleY(startPoint[0], startPoint[2]), startPoint[2]];
smooth[smooth.length - 1] = [endPoint[0], this._sampleY(endPoint[0], endPoint[2]), endPoint[2]];
}
return smooth;
}
// Optional: clamp point into mesh (closest point on triangles) - simple nearest vertex fallback
closestPointOnMesh(point) {
// naive: return nearest vertex
let bestD = Infinity, best = null;
for(const v of this.vertices) {
const dx = v[0] - point[0], dy = v[1] - point[1], dz = v[2] - point[2];
const d = dx * dx + dy * dy + dz * dz;
if(d < bestD) {bestD = d; best = v;}
}
return best.slice();
}
}
export class MinHeap {
constructor(cmp) {
this.cmp = cmp || ((a, b) => a - b);
this.items = [];
}
push(v) {
this.items.push(v);
this._siftUp(this.items.length - 1);
}
pop() {
if(this.items.length === 0) return null;
const top = this.items[0];
const last = this.items.pop();
if(this.items.length > 0) {
this.items[0] = last;
this._siftDown(0);
}
return top;
}
empty() {return this.items.length === 0;}
_siftUp(i) {
while(i > 0) {
const p = Math.floor((i - 1) / 2);
if(this.cmp(this.items[i], this.items[p]) < 0) {
[this.items[i], this.items[p]] = [this.items[p], this.items[i]];
i = p;
} else break;
}
}
_siftDown(i) {
while(true) {
const l = 2 * i + 1, r = 2 * i + 2;
let m = i;
if(l < this.items.length && this.cmp(this.items[l], this.items[m]) < 0) m = l;
if(r < this.items.length && this.cmp(this.items[r], this.items[m]) < 0) m = r;
if(m !== i) {
[this.items[i], this.items[m]] = [this.items[m], this.items[i]];
i = m;
} else break;
}
}
}
const MIN_DIST = 0.1;
export function followPath(character, path, core) {
if(!path || path.length === 0) return;
let idx = 0;
const pos = character.position;
const rot = character.rotation;
const MIN_DIST = 0.001;
const ROTATION_SPEED = 5; // adjust for smoother/slower rotation
// --- Smoothly rotate toward a target angle ---
function smoothRotate(current, target, deltaTime) {
let diff = target - current;
// Normalize angle difference to [-180, 180]
diff = ((diff + 540) % 360) - 180;
return current + diff * Math.min(1, deltaTime * ROTATION_SPEED);
}
// --- Recursive movement ---
function moveToNext() {
if(idx >= path.length) {
dispatchEvent(new CustomEvent('onTargetPositionReach', {
detail: {
name: character.name,
body: character
}
}));
character.position.onTargetPositionReach = () => {};
return;
}
const target = path[idx];
const dx = target[0] - pos.x;
const dz = target[2] - pos.z;
const dist = Math.sqrt(dx * dx + dz * dz);
// --- Skip points that are too close ---
if(dist < MIN_DIST) {
idx++;
moveToNext();
return;
}
// --- Compute target facing direction (Y rotation) ---
let targetAngleY = Math.atan2(dx, dz);
targetAngleY = (radToDeg(targetAngleY) + 360) % 360;
// --- Smooth rotation (optional deltaTime if you have it) ---
const deltaTime = core?.deltaTime || 0.016; // fallback ~60fps
rot.y = smoothRotate(rot.y, targetAngleY, deltaTime);
// --- Move toward next target ---
pos.translateByXZ(target[0], target[2]);
// When position reaches target:
character.position.onTargetPositionReach = () => {
idx++;
moveToNext();
};
}
// --- Initialize rotation toward the first valid point ---
const firstTarget = path.find((p) => {
const dx = p[0] - pos.x;
const dz = p[2] - pos.z;
return Math.sqrt(dx * dx + dz * dz) >= MIN_DIST;
});
if(firstTarget) {
const dx = firstTarget[0] - pos.x;
const dz = firstTarget[2] - pos.z;
let initialAngleY = Math.atan2(dx, dz);
rot.y = (radToDeg(initialAngleY) + 360) % 360;
}
moveToNext();
}
export function orientHeroToDirection(hero, dir) {
const flatDir = [dir[0], 0, dir[2]];
const len = Math.hypot(flatDir[0], flatDir[2]);
if(len < 0.0001) return;
flatDir[0] /= len; flatDir[2] /= len;
// Compute rotation angle around Y axis
const angle = Math.atan2(flatDir[0], flatDir[2]); // note X/Z order!
// Apply to hero
hero.rotation.y = angle; // in radians
}
export function resolvePairRepulsion(Apos, Bpos, minDistance = 30.0, pushStrength = 0.5) {
// Apos and Bpos are Position instances (with x,z,targetX,targetZ)
const dx = Bpos.x - Apos.x;
const dz = Bpos.z - Apos.z;
const distSq = dx * dx + dz * dz;
const minDistSq = minDistance * minDistance;
if(distSq < minDistSq && distSq > 1e-8) {
const dist = Math.sqrt(distSq);
const overlap = minDistance - dist;
const nx = dx / dist;
const nz = dz / dist;
const totalPush = overlap * pushStrength;
const pushA = totalPush * 0.5;
const pushB = totalPush * 0.5;
Apos.x -= nx * pushA;
Apos.z -= nz * pushA;
Bpos.x += nx * pushB;
Bpos.z += nz * pushB;
// Apos.targetX = Apos.x;
// Apos.targetZ = Apos.z;
// Bpos.targetX = Bpos.x;
// Bpos.targetZ = Bpos.z;
return true;
}
// exact overlap (practically same point) -> small jitter to separate
if(distSq <= 1e-8) {
const jitter = 0.01;
Apos.x += (Math.random() - 0.5) * jitter;
Apos.z += (Math.random() - 0.5) * jitter;
Apos.targetX = Apos.x; Apos.targetZ = Apos.z;
return true;
}
return false;
}