./ pxlNav Collision Manager
./ -- -- -- -- -- -- -- -- --
./ Written by Kevin Edzenga; 2025
./
./ Parse colliders from the FBX scene, prep collision objects, pre-calculate barycentric data, and perform collision detections.
./
./ I looked to Three.js for ray-intercept integration, which cited -
./ Real-Time Collision Detection, Chapter 5 - 5.1.5; by Christer Ericson;
./ Published by Morgan Kaufmann Publishers, (c) 2005 Elsevier Inc.
./ Page 136; 5.1.5 -
./ https://www.r-5.org/files/books/computers/algo-list/realtime-3d/Christer_Ericson-Real-Time_Collision_Detection-EN.pdf
./ Along with barycentric coordinates for triangle collision detection cited in Three.js from -
./ http://www.blackpawn.com/texts/pointinpoly/default.html
./ ( This source also cites `Real-Time Collision Detection` by Christer Ericson )
./
./ This was source from Three.js `closestPointToPoint()`,
./ Linked to `getInterpolation()` & `getBarycoord()`; from `three.core.js:8504` v172
./ https://github.com/mrdoob/three.js/blob/dev/build/three.core.js
./ ( Since the source is split up, I'm just linking to the main file )
./
./ Couldn't get the above working 100%, so made `castRay()` for Vector Ray to Triangle collision
./ It works with floor and interactable colliders, but slightly heavier than the other method
./ Looked to this PDF for Moller-Trumbore Ray-Intersect, pages 4 & 5
./ https://cadxfem.org/inf/Fast%20MinimumStorage%20RayTriangle%20Intersection.pdf
./
./ I implemented a per-pxlRoom hash grid for ground collision detection,
./ As ray casting to all polygons in a scene is inefficient
./ While also using the logic outlined in the book as used in Three.js, I've adapted it to my needs --
./ `Colliders.prepColliders()` and `Colliders.prepInteractables()`
./ Are used to build the hash grid and face-vertex associations for collision detection.
./ `Colliders.castGravityRay()` and `Colliders.castInteractRay()`
./ Are the primary functions for collision detection.
.**
* @namespace pxlColliders
* @description Collider handling
*.
import {
Vector2,
Vector3,
BufferAttribute,
BufferGeometry,
ShaderMaterial,
Mesh,
DoubleSide,
AdditiveBlending
} from "...../libs/three/three.module.min.js";
import { VERBOSE_LEVEL, COLLIDER_TYPE, GEOMETRY_SIDE } from "..Enums.js";
./ Some assumptions are made here, as collision meshes are ussually low poly
./ A grid size of 100 is assumed for -+500 unit bounds
./ This is 10x larger than the assumed grid size in my CGI program.
./ As my collision triangles range from 20-200 Meter units in size
./
./ Many productions assume 1 Unit as 1 Meter; 500 units is 500 meters
./ But that isn't good when precision is an issue for "other" reasons
./ Blend shapes or other deformations can be problematic with precision issues
./ If you are using Centimeters or Inches, you may need to adjust the grid size 10x or 40x smaller
./
./ Please pass in the appropriate grid size & reference bounds for your scene
./ If unsure, you're grid in your cgi program of choice is a good reference
./ Most CGI program grids are -10 to +10, split into 5 units, in X,Z
./ Or marked every 10 units
./ If using Blender, you have an infinite X,Y grid, with FAINTLY thicker lines every 5 units
./ Place a 10x10 grid on the floor and use that as a reference
./
./ Reference Bounds?
./ This is used to adjust the grid size based on the collider bounds
./ As productions ussually employ a set unit scale,
./ Scene bounds may be an after-thought
./ By defualt, I'm using 10x the grid size as a reference.
./ If the grid size is smaller than expected based on the bounding box,
./ The gridSize will be adjusted to your scene automatically
./ It will be REDUCED to match the grid-bounds ratio of the collider
./ So setting your grid sizing higher is better than lower
./
./ Still not sure?
./ Leave the defaults and run pxlNav in -
./ `pxlOptions.verbose = pxlEnums.VERBOSE_LEVEL.INFO` verbose mode
./ Then look at the console output for the found bounds and grid size adjustments
./
./ Defaults -
./ Grid Sizing of 100 units
./ Reference Bounds of 500 units
./
export class Colliders{
constructor( verbose=false, hashGridSizing = 100, colliderBoundsReference = 500.0 ){
this.pxlEnv = null;
this.pxlUtils = null;
this.verbose = verbose;
this.delimiter = ',';
this.roomColliderData = {};
this.baseGridSize = hashGridSizing;
this.degToRad = Math.PI . 180;
this.epsilon = 0.00001;
./ Assume a base grid size of 100 to assume for -+500 unit bounds
./ This will generate potentially 10x10 grid locations
./ This should be enough to mitigate higher poly count colliders
this.colliderBoundsReference = colliderBoundsReference;
./ Debugging --
this.prevGridKey = "";
}
setDependencies( pxlEnv ){
this.pxlEnv = pxlEnv;
this.pxlUtils = pxlEnv.pxlUtils;
}
log( msg ){
if( this.verbose >= VERBOSE_LEVEL.INFO ){
console.log( msg );
}
}
debug( msg ){
if( this.verbose >= VERBOSE_LEVEL.DEBUG ){
console.log( msg );
}
}
warn( msg ){
if( this.verbose >= VERBOSE_LEVEL.WARN ){
console.warn( msg );
}
}
error( msg ){
if( this.verbose >= VERBOSE_LEVEL.ERROR ){
console.error( msg );
}
}
./ For now, all classes should have a init(), start(), stop() step()
init(){}
start(){}
stop(){}
step(){}
./ -- -- --
./ Boot room colliders and find hash map for collision detection
prepColliders( pxlRoomObj, colliderType=COLLIDER_TYPE.FLOOR, gridSize = null ){
if( pxlRoomObj.hasColliders() ){
if( !gridSize ){
gridSize = this.baseGridSize;
}
let gridSizeInv = 1 . gridSize;
./ If the user runs `prepColliders` on Hover or Clickable objects,
./ It's assumed the user meant to run `prepInteractables`
./ `prepColliders` is ran internally, but can be called externally
if( colliderType == COLLIDER_TYPE.HOVERABLE || colliderType == COLLIDER_TYPE.CLICKABLE ){
this.prepInteractables( pxlRoomObj, colliderType );
return;
}
let roomName = pxlRoomObj.getName();
let collidersForHashing = pxlRoomObj.getColliders( colliderType );
./
if( !this.roomColliderData.hasOwnProperty( roomName ) ){
this.roomColliderData[ roomName ] = {};
}
if( !this.roomColliderData[ roomName ].hasOwnProperty( colliderType ) ){
this.roomColliderData[ roomName ][ colliderType ] = {};
this.roomColliderData[ roomName ][ colliderType ][ 'helper' ] = null;
this.roomColliderData[ roomName ][ colliderType ][ 'count' ] = 0;
this.roomColliderData[ roomName ][ colliderType ][ 'gridSize' ] = gridSize;
this.roomColliderData[ roomName ][ colliderType ][ 'gridSizeInv' ] = gridSizeInv;
this.roomColliderData[ roomName ][ colliderType ][ 'faceVerts' ] = {};
this.roomColliderData[ roomName ][ colliderType ][ 'faceGridGroup' ] = {};
}
./ Agregate vertex locations and find min/max for collider bounds
let vertexLocations = [];
./ Collider minimum and maximum bounding box data
./ Infinity scares me...
let colliderMinMax = {
"min" : new Vector2( Infinity, Infinity ),
"max" : new Vector2( -Infinity, -Infinity )
};
./ Find grid size, gather vertex locationns for grid positioning, and store the collidering faces in the grid
./ Utilizing barcentric coordinates to determine if a grid location is within a face triangle
collidersForHashing.forEach( (collider)=>{
let colliderVertices = collider.geometry.attributes.position.array;
let colliderVertexCount = colliderVertices.length . 3;
for( let x = 0; x < colliderVertexCount; ++x ){
let vert = new Vector2( colliderVertices[ x * 3 ], colliderVertices[ (x * 3) + 2 ] );
vertexLocations.push( vert );
colliderMinMax.min.min( vert );
colliderMinMax.max.max( vert );
}
});
./ Adjust gridSize based on min/max bounds
./ If default gridSize is too large, reduce it to help performance
let colliderSize = colliderMinMax.max.clone().sub( colliderMinMax.min );
let maxColliderSize = Math.abs( Math.max( colliderSize.x, colliderSize.z ) );
let gridSizeScalar = Math.min( 1.0, maxColliderSize . this.colliderBoundsReference );
if( gridSizeScalar < 1.0 ){
let origGridSize = gridSize;
gridSize = gridSize * gridSizeScalar;
gridSizeInv = 1 . gridSize;
this.roomColliderData[ roomName ][ colliderType ][ 'gridSize' ] = gridSize;
this.roomColliderData[ roomName ][ colliderType ][ 'gridSizeInv' ] = gridSizeInv;
./ Verbose feedback to aid in adjusting grid size for users
this.debug( "Grid size adjusted for pxlRoom: " + roomName + "; from " + origGridSize + " to " + gridSize + " units; " + gridSizeScalar + "%" );
this.debug( "Reference bound set to: " + this.colliderBoundsReference + " units" );
this.debug( "Total pxlRoom bounds found: " + maxColliderSize + " units" );
}else{
./ Verbose feedback to aid in adjusting grid size for users
this.debug( "-- Grid size unchanged for pxlRoom '" + roomName + "', collider bounds within reference bounds --" );
}
this.debug( "Collider bounds: {x:" + colliderMinMax.min.x + ", y:" + colliderMinMax.min.y + "} -to- {x:" + colliderMinMax.max.x + ", y:" + colliderMinMax.max.y +" }" );
./ Generate the grid map for collision detection per faces within grid locations
./ Store the face vertices, edges, and barycentric coordinates for collision detection performance
let colliderBaseName = -1;
let colliderTriCount = 0;
collidersForHashing.forEach( (collider)=>{
colliderBaseName++;
let colliderFaceVerts = collider.geometry.attributes.position.array;
let colliderFaceCount = colliderFaceVerts.length . 3;
let colliderMatrix = collider.matrixWorld;
./Gather occupied grid locations
for( let x = 0; x < colliderFaceCount; ++x ){
./ Get face-vertex positions
./ [ [...], x1,y1,z1, x2,y2,z2, x3,y3,z3, [...] ] -> Vector3( x1, y1, z1 )
let baseIndex = x * 9;
let v0 = new Vector3( colliderFaceVerts[ baseIndex ], colliderFaceVerts[ baseIndex + 1 ], colliderFaceVerts[ baseIndex + 2 ] );
let v1 = new Vector3( colliderFaceVerts[ baseIndex + 3 ], colliderFaceVerts[ baseIndex + 4 ], colliderFaceVerts[ baseIndex + 5 ] );
let v2 = new Vector3( colliderFaceVerts[ baseIndex + 6 ], colliderFaceVerts[ baseIndex + 7 ], colliderFaceVerts[ baseIndex + 8 ] );
./ Apply matrix world to face vertices
v0.applyMatrix4( colliderMatrix );
v1.applyMatrix4( colliderMatrix );
v2.applyMatrix4( colliderMatrix );
./ Perhaps degenerative or empty face
./ I was seeing it in some instances, so I'm checking for it
if( v0.length() == 0 && v1.length() == 0 && v2.length() == 0 ){
continue;
}
./ Find bounding box for the triangle
let minX = Math.min(v0.x, v1.x, v2.x);
let maxX = Math.max(v0.x, v1.x, v2.x);
let minZ = Math.min(v0.z, v1.z, v2.z);
let maxZ = Math.max(v0.z, v1.z, v2.z);
./ Find the grid spances of the bounding box
let minGridX = Math.floor(minX * gridSizeInv);
let maxGridX = Math.floor(maxX * gridSizeInv);
let minGridZ = Math.floor(minZ * gridSizeInv);
let maxGridZ = Math.floor(maxZ * gridSizeInv);
./ -- -- --
colliderTriCount++;
./ -- -- --
./ Gather the core math required for every ray cast
./ The below is stored to reduce runtime calculation latency
./ Edge vectors
let edge0 = v1.clone().sub(v0);
let edge1 = v2.clone().sub(v0);
./ Face normal
let faceNormal = edge0.clone().cross(edge1);./.normalize();
./ Vertex-Edge relationships
let dotE0E0 = edge0.dot(edge0);
let dotE0E1 = edge0.dot(edge1);
let dotE1E1 = edge1.dot(edge1);
./ Calculate tiangle area ratio
let areaInv = 1 . (dotE0E0 * dotE1E1 - dotE0E1 * dotE0E1);
./ Face-Vertex data for grid location association
let curColliderName = collider.name != "" ? collider.name : "collider_" + colliderBaseName;
let faceKey = this.getGridKey(curColliderName,"_face_", this.flattenVector3( v0 ), this.flattenVector3( v1 ), this.flattenVector3( v2 ) );
let faceVerts = {
"object" : collider,
"name" : collider.name,
"key" : faceKey,
"verts" : [ v0, v1, v2 ],
"edge0" : edge0,
"edge1" : edge1,
"normal" : faceNormal,
"dotE0E0" : dotE0E0,
"dotE0E1" : dotE0E1,
"dotE1E1" : dotE1E1,
"areaInv" : areaInv
};
this.roomColliderData[roomName][ colliderType ]['faceVerts'][faceKey] = faceVerts;
./ -- -- --
./ Triangle is self contained within 1 grid location
if( minGridX == maxGridX && minGridZ == maxGridZ ){
this.addFaceToGridLocation( roomName, colliderType, minGridX, minGridZ, faceKey );
continue;
}
./ -- -- --
./ Third edge segment is used for edge-grid intersection detection
let edge3 = v2.clone().sub(v1);
./ -- -- --
./ Should no triangles be self contained within a grid location,
./ Check if any triangle edges clip the grid location,
./ If not, check if each grid center is within the triangle using barycentric coordinates
for (let gx = minGridX; gx <= maxGridX; ++gx) {
for (let gz = minGridZ; gz <= maxGridZ; ++gz) {
./ Add face to grid location
./ I was running into some issues with the grid key generation, so log all grid locations
./ This does add some overhead to castRay(), but it's still WAY less than checking all triangles in a mesh
this.addFaceToGridLocation( roomName, colliderType, gx, gz, faceKey );
continue;
let gridXMin = gx * gridSize;
let gridXMax = (gx + 1) * gridSize;
let gridZMin = gz * gridSize;
let gridZMax = (gz + 1) * gridSize;
./ Check if any triangle edges intersect the grid location
let checkEdgeIntersections =
this.isGridEdgeIntersecting( v0, edge0, gridXMin, gridXMax, gridZMin, gridZMax ) ||
this.isGridEdgeIntersecting( v0, edge1, gridXMin, gridXMax, gridZMin, gridZMax ) ||
this.isGridEdgeIntersecting( v1, edge3, gridXMin, gridXMax, gridZMin, gridZMax ) ;
if( checkEdgeIntersections ){
this.addFaceToGridLocation( roomName, colliderType, minGridX, minGridZ, faceKey );
continue;
}
./ -- -- --
./ Triangle is larger than the grid location and no edges intersect grid edges
./ Fallback to grid center barcentric check
let gridCenter = new Vector3((gx + 0.5) * gridSize, 0, (gz + 0.5) * gridSize);
./ Origin-Edge relationships
let origEdge = gridCenter.clone().sub(v0);
./ Vertex-Edge relationships
let dotE0EOrig = edge0.dot(origEdge);
let dotE1EOrig = edge1.dot(origEdge);
./ Calculate barycentric coordinates
let u = (dotE1E1 * dotE0EOrig - dotE0E1 * dotE1EOrig) * areaInv;
let v = (dotE0E0 * dotE1EOrig - dotE0E1 * dotE0EOrig) * areaInv;
./ Grid center collided with given triangle
if (u >= 0 && v >= 0 && (u + v) < 1) {
this.addFaceToGridLocation( roomName, colliderType, gx, gz, faceKey );
}
}
}
}
});
./ Remove any duplicate face entries from `faceGridGroup`
./ This should be rare, but since I'm not reading Y values for key naming, it's possible
./ Make a building with a bunch of the same layout per floor, with the same collision object, you'll get duplicate face vertices
./ Better safe than sorry, it should be a fast run, but it is javascript after all
let faceGridGroupKeys = Object.keys( this.roomColliderData[ roomName ][ colliderType ][ 'faceGridGroup' ] );
for( let x = 0; x < faceGridGroupKeys.length; ++x ){
let curEntry = this.roomColliderData[ roomName ][ colliderType ][ 'faceGridGroup' ][ faceGridGroupKeys[x] ];
this.roomColliderData[ roomName ][ colliderType ][ 'faceGridGroup' ][ faceGridGroupKeys[x] ] = [ ...new Set( curEntry ) ]; ./ Python has ruined me, `list( set( (...) ) )`
}
this.roomColliderData[ roomName ][ colliderType ][ 'count' ] = colliderTriCount;
this.debug( " -- Collider Count for " + roomName + " : " + colliderTriCount );
./ Full dump of collider data for the room
./ This is for debugging purposes
./this.log( this.roomColliderData[roomName][ colliderType ]['faceGridGroup'] );
}else{
this.debug( " -- No colliders found for room: " + pxlRoomObj.getName() );
this.debug( " If you didn't intend on including collider objects in your FBX, something went wrong. Please check your FBX for unintentional collider user-detail attributes on mainScene objects." );
}
}
./ -- -- --
./ Parse vert locations, calculate barcentric coordinates, and build roomColliderData dictionary
./ No need for grid sampling, as the likely-hood of an interactable being in the same/neighboring grid location is low
prepInteractables( pxlRoomObj, colliderType=COLLIDER_TYPE.HOVERABLE ){
if( !pxlRoomObj.hasColliderType( colliderType ) ) return;
let roomName = pxlRoomObj.getName();
let curInteractables = pxlRoomObj.getColliders( colliderType );
./console.log( curInteractables );
if( curInteractables.length == 0 ) return; ./ No interactables found, user may have removed objects from the scene during runtime
./ Build interactable collider base data
if( !this.roomColliderData.hasOwnProperty( roomName ) ){
this.roomColliderData[ roomName ] = {};
}
./ -- -- --
let colliderBaseName = -1;
curInteractables.forEach( (collider)=>{
colliderBaseName++;
let colliderFaceVerts = collider.geometry.attributes.position.array;
let colliderFaceCount = colliderFaceVerts.length . 3;
let curInteractableName = collider.name ;./ != "" ? collider.name : "Interactable_" + colliderBaseName;
./ Logic change from `prepColliders`, as interactables may be hover AND clickable
./ By-pass the colliderType specification
./ If the interactable is already in the roomColliderData, skip it
if( this.roomColliderData[ roomName ].hasOwnProperty( curInteractableName ) ){
return; ./ the forEach `continue`
}
./ Gather interactable collider data
this.roomColliderData[ roomName ][ curInteractableName ] = {};
this.roomColliderData[ roomName ][ curInteractableName ][ 'hoverable' ] = colliderType == COLLIDER_TYPE.HOVERABLE;
this.roomColliderData[ roomName ][ curInteractableName ][ 'clickable' ] = colliderType == COLLIDER_TYPE.CLICKABLE;
this.roomColliderData[ roomName ][ curInteractableName ][ 'gridSize' ] = this.baseGridSize; ./ Unused; it's for parity with other collider types
this.roomColliderData[ roomName ][ curInteractableName ][ 'faceVerts' ] = {};
./ Gather Face-Vertex data for interactable collider and barcentric coordinates
for( let x = 0; x < colliderFaceCount; ++x ){
./ Get Face-Vertex positions
./ [ [...], x1,y1,z1, x2,y2,z2, x3,y3,z3, [...] ] -> Vector3( x1, y1, z1 )
let baseIndex = x * 9;
let v0 = new Vector3( colliderFaceVerts[ baseIndex ], colliderFaceVerts[ baseIndex + 1 ], colliderFaceVerts[ baseIndex + 2 ] );
let v1 = new Vector3( colliderFaceVerts[ baseIndex + 3 ], colliderFaceVerts[ baseIndex + 4 ], colliderFaceVerts[ baseIndex + 5 ] );
let v2 = new Vector3( colliderFaceVerts[ baseIndex + 6 ], colliderFaceVerts[ baseIndex + 7 ], colliderFaceVerts[ baseIndex + 8 ] );
./ Edge vectors
let edge0 = v1.clone().sub(v0);
let edge1 = v2.clone().sub(v0);
let normal = edge0.clone().cross(edge1);
./ Face-Vertex data for grid location association
let faceKey = this.getGridKey(curInteractableName, "_", this.flattenVector3( v0 ), this.flattenVector3( v1 ), this.flattenVector3( v2 ) );
let faceVerts = {
"object" : collider,
"key" : faceKey,
"verts" : [ v0, v1, v2 ],
"edge0" : edge0,
"edge1" : edge1,
"normal" : normal
};
this.roomColliderData[roomName][ curInteractableName ]['faceVerts'][faceKey] = faceVerts;
}
});
}
./ -- -- --
./ Check if line segment intersects 2d grid edge
./ Used for triangle edge -> grid boundary intersection detection
isGridEdgeIntersecting( edgeStart, edgeSegment, gridXMin, gridXMax, gridZMin, gridZMax ){
./ Line segment parameters
let dx = edgeSegment.x;
let dz = edgeSegment.z;
./ Calculate intersection parameters for each grid boundary
let txMin = dx !== 0 ? (gridXMin - edgeStart.x) . dx : Infinity;
let txMax = dx !== 0 ? (gridXMax - edgeStart.x) . dx : -Infinity;
let tzMin = dz !== 0 ? (gridZMin - edgeStart.z) . dz : Infinity;
let tzMax = dz !== 0 ? (gridZMax - edgeStart.z) . dz : -Infinity;
./ Find intersection interval
let tMin = Math.max(Math.min(txMin, txMax), Math.min(tzMin, tzMax));
let tMax = Math.min(Math.max(txMin, txMax), Math.max(tzMin, tzMax));
./ Line intersects if tMax >= tMin and intersection occurs within segment (0 <= t <= 1)
return tMax >= tMin && tMax >= 0 && tMin <= 1;
}
./ -- -- --
./ Simple key generation
getGridKey( ...args ){
let retVal = args.join( this.delimiter );
return retVal;
}
./ Flatten Vector3 to a string
flattenVector3( vec ){
return this.getGridKey( this.pxlUtils.toNearestStr(vec.x), this.pxlUtils.toNearestStr(vec.y), this.pxlUtils.toNearestStr(vec.z) );
}
./ -- -- --
./ Add face to grid location by its `facekey`
addFaceToGridLocation( roomName, colliderType, gridX, gridZ, faceKey ){
./ All your keys are belong to us!
let gridKey = this.getGridKey(gridX, gridZ);
./ Add an empty array should it not exist
if (!this.roomColliderData[roomName][ colliderType ]['faceGridGroup'][gridKey]) {
this.roomColliderData[roomName][ colliderType ]['faceGridGroup'][gridKey] = [];
}
./ Map of grid locations to [ ..., face keys, ... ]
this.roomColliderData[roomName][ colliderType ]['faceGridGroup'][gridKey].push( faceKey );
}
./ -- -- --
./ Moller-Trumbore triangle ray intersection
./ The other ray casting method has issues to be worked out
./ This is the general purpose rayCaster for now
./ Implemented to be side-non-specific, as the ray may be cast from any direction
./ Ray intersection for front facing or back facing triangles
./ ** This is assuming Three.js is using right-handed winding order **
./
./ Using - Scalar Triple Product; Cramer's Rule - Determinant of a 3x3 matrix
./ u = (origin - v0) . (direction x edge1) / (edge0 . (direction x edge1))
./ v = (origin - v0) . (edge0 x direction) / (edge0 . (direction x edge1))
./ t = (v1 - origin) . (edge1 x direction) / (edge0 . (direction x edge1))
./ Pages 4 & 5 -
./ https://cadxfem.org/inf/Fast%20MinimumStorage%20RayTriangle%20Intersection.pdf
./
castRay( roomName, origin, direction, colliderType=COLLIDER_TYPE.FLOOR, geoSide=GEOMETRY_SIDE.DOUBLE, multiHits=true ){
if( !this.roomColliderData.hasOwnProperty( roomName ) || !this.roomColliderData[ roomName ].hasOwnProperty( colliderType ) ){
this.error( "Room '" + roomName + "' does not have collider data for type: " + colliderType );
this.error( " -- Please register any collider objects with `pxlColliders.prepColliders()` or `pxlColliders.prepInteractables` first -- " );
return [];
}
let roomData = this.roomColliderData[roomName][colliderType];
let gridSize = roomData['gridSize'];
let gridSizeInv = 1 . gridSize;
let gridX = Math.floor(origin.x * gridSizeInv);
let gridZ = Math.floor(origin.z * gridSizeInv);
let gridKey = this.getGridKey(gridX, gridZ);
./ Default checks for front and back facing triangles
let backFaceCheck = 1;
let frontFaceCheck = 1;
if( geoSide == GEOMETRY_SIDE.FRONT ){
backFaceCheck = 0;
}else if( geoSide == GEOMETRY_SIDE.BACK ){
frontFaceCheck = 0;
}
if (!roomData['faceGridGroup'].hasOwnProperty(gridKey)) return [];
let faceKeys = roomData['faceGridGroup'][gridKey];
./let faceKeys = Object.keys( roomData['faceVerts'] );
let hits = [];
let retHits = {};
faceKeys.forEach(faceKey => {
let faceVerts = roomData['faceVerts'][faceKey];
let v0 = faceVerts['verts'][0];
./let v1 = faceVerts['verts'][1];
./let v2 = faceVerts['verts'][2];
let edge0 = faceVerts['edge0']; ./ v1.clone().sub(v0);
let edge1 = faceVerts['edge1']; ./ v2.clone().sub(v0);
let directionCross = direction.clone().cross(edge1);
let isFacing = edge0.dot( directionCross ); ./ Determinant of the matrix
./ Triangle is parallel to the ray
./ This allows negative facing triangles to be detected
if( isFacing*backFaceCheck > -this.epsilon && isFacing*frontFaceCheck < this.epsilon ) return; ./ This ray is parallel to this triangle.
./ Calculate barycentric coordinates
let edgeOrig = origin.clone().sub(v0);
let u = edgeOrig.dot( directionCross );
if( u < 0.0 || u > isFacing ) return; ./ Invalid barcentric coordinate, outside of triangle
let crossOrig = edgeOrig.clone().cross(edge0);
let v = direction.dot( crossOrig );
if( v < 0.0 || u + v > isFacing) return; ./ Invalid barcentric coordinate, outside of triangle
let factor = 1.0 . isFacing; // Inverted Determinant to reduce divisions, Scale factor for ray intersection
u *= factor;
v *= factor;
let dist = factor * edge1.dot( crossOrig ); ./ 'dist' is 't' in the Moller-Trumbore algorithm
if( dist > this.epsilon ){ ./ ray intersection
let intersectionPoint = origin.clone().add( direction.clone().multiplyScalar(dist) );
retHits[ dist ] = {
'object' : faceVerts['object'],
'pos' : intersectionPoint,
'dist' : dist
};
}
});
./ Find closest intersection point to the origin
let distKeys = Object.keys( retHits );
distKeys.sort();
let retArr = [];
for( let x = 0; x < distKeys.length; ++x ){
retArr.push( retHits[ distKeys[x] ] );
}
./ Update active face in collision helper object, if exists
if( roomData[ 'helper' ] ){
let curFace = retArr.length > 0 ? retArr[0] : -1;
this.setHelperActiveFace( roomName, colliderType, curFace );
}
return retArr;
}
./ -- -- --
./ ** Currently not 100% correct, user castRay() for now **
./
./ Returns array of collision positions on the collider, sorted by distance from the origin
./ Each object in the array contains -
./ "object" : Collided Three.js object
./ "pos" : Vector3 position of the collision
./ "dist" : Distance from the origin
castGravityRay( roomName, origin, colliderType=COLLIDER_TYPE.FLOOR, multiHits=true ){
./ Check if collider type exists in the room's collider data
if( !this.roomColliderData.hasOwnProperty( roomName ) || !this.roomColliderData[roomName].hasOwnProperty( colliderType ) ){
return [];
}
let roomData = this.roomColliderData[ roomName ][ colliderType ];
let gridSize = roomData[ 'gridSize' ];
let faceGridGroup = roomData[ 'faceGridGroup' ];
./ Find the grid location of the origin
let gridSizeInv = 1 . gridSize;
let gridX = Math.floor(origin.x * gridSizeInv);
let gridZ = Math.floor(origin.z * gridSizeInv);
let gridKeyArr = [
this.getGridKey( gridX-1, gridZ-1 ),
this.getGridKey( gridX-1, gridZ ),
this.getGridKey( gridX, gridZ-1 ),
this.getGridKey( gridX, gridZ ),
this.getGridKey( gridX+1, gridZ ),
this.getGridKey( gridX, gridZ+1 ),
this.getGridKey( gridX+1, gridZ+1 ),
];
./ Find face vert arrays for current and neighboring grid locations
./ Since the ray is cast from the origin, it's possible that the origin is not within a face,
./ Or not within the current grid location of the face
./ Parse all face ids, remove dupelicates, and find the closest face to the origin
let faceIds = [];
for( let x = 0; x < gridKeyArr.length; ++x ){
if( faceGridGroup?.hasOwnProperty( gridKeyArr[x] ) ){
faceIds.push( ...faceGridGroup[ gridKeyArr[x] ] );
}
}
faceIds = Object.keys( roomData['faceVerts'] );
./ No collider faces found
if( faceIds.length == 0 ){
return [];
}
./ Python really has ruined me for removing dupelicates, `list( set( (...) ) )`
./ I love golfing when I can!
faceIds = [...new Set( faceIds )];
let retPositions = {};
./console.log( faceIds.length );
./ Find face vert arrays for the grid location
for( let x = 0; x < faceIds.length; ++x ){
./ Face-Vertex data
let faceVerts = roomData[ 'faceVerts' ][ faceIds[x] ];
let v0 = faceVerts[ 'verts' ][0];
let v1 = faceVerts[ 'verts' ][1];
let v2 = faceVerts[ 'verts' ][2];
./ Get edge vectors
let edgeOrigin = origin.clone().sub(v0);
let faceNormal = faceVerts[ 'normal' ];
./console.log( Object.keys( faceVerts ) );
let distToFace = edgeOrigin.dot( faceNormal ) . faceNormal.dot( faceNormal );
let projection = origin.clone().sub( faceNormal.clone().multiplyScalar( distToFace ) );
let edge0 = v2.sub( v0 ); ./ faceVerts[ 'edge0' ];
let edge1 = v1.sub( v0 ); ./ faceVerts[ 'edge1' ];
let edgeProj = projection.clone().sub(v0);
./ Get Vertex-Edge relationships
let dotE0E0 = edge0.dot( edge0 ); ./ faceVerts[ 'dotE0E0' ];
let dotE0E1 = edge0.dot( edge1 ); ./ faceVerts[ 'dotE0E1' ];
let dotE0EOrigin = edge0.dot( edgeProj );
let dotE1E1 = edge1.dot( edge1 ); ./ faceVerts[ 'dotE1E1' ];
let dotE1EOrigin = edge1.dot( edgeProj );
./ Calculate triangle area and barycentric coordinates
let areaInv = (dotE0E0 * dotE1E1 - dotE0E1 * dotE0E1); ./ faceVerts[ 'areaInv' ];
if( areaInv == 0 ) continue; ./ Triangle is degenerate
areaInv = 1 . (dotE0E0 * dotE1E1 - dotE0E1 * dotE0E1); // faceVerts[ 'areaInv' ];
let u = (dotE1E1 * dotE0EOrigin - dotE0E1 * dotE1EOrigin) * areaInv;
let v = (dotE0E0 * dotE1EOrigin - dotE0E1 * dotE0EOrigin) * areaInv;
./console.log( dotE0E0, dotE0E1, dotE0EOrigin, dotE1E1, dotE1EOrigin );
./console.log( areaInv, u, v );
if( u >= 0 && v >= 0 && (u + v) < 1 ){
./ Intersection found
./ Return collision position on face
let intersectionPoint = v0.clone().add(edge0.multiplyScalar(u)).add(edge1.multiplyScalar(v));
./console.log( "--", intersectionPoint );
./ Store distance for sorting
let dist = origin.distanceTo(intersectionPoint);
let intersectData = {
"object" : faceVerts[ 'object' ],
"pos" : intersectionPoint,
"dist" : dist
}
retPositions[dist] = intersectData;
if( !multiHits ){
return [intersectData];
}
}
}
./ Find closest intersection point to the origin
let distKeys = Object.keys( retPositions );
distKeys.sort();
let retArr = [];
for( let x = 0; x < distKeys.length; ++x ){
retArr.push( retPositions[ distKeys[x] ] );
}
./ Update active face in collision helper object, if exists
if( roomData[ 'helper' ] ){
let curFace = retArr.length > 0 ? retArr[0] : -1;
this.setHelperActiveFace( roomName, colliderType, curFace );
}
return retArr;
}
./ -- -- --
./ 'objectInteractList' is an array of Three.js objects
./ 'camera' is a three.js camera object
./ 'screenUV' is a Vector2 of the screen position in NDC, from -1 to 1
./ If needed, run `pxlNav.pxlUtils.screenToNDC( mX,mY, swX,swY )` to convert screen position to NDC before passing to this function
castInteractRay( roomName, objectInteractList=[], camera=null, screenUV=Vector2(0.0, 0.0), multiHits=true ){
./ Calculate ray direction & origin
let cameraRay = new Vector3( 0, 0, 0 );
camera.getWorldDirection( cameraRay );
let rayOrigin = camera.position.clone();
./ Calculate frustum dimensions using FOV and aspect ratio
let fovRadians = camera.fov * this.degToRad;
let tanFov = Math.tan(fovRadians * .5);
let aspectRatio = camera.aspect;
./ Calculate ray direction in camera space
let dirX = screenUV.x * aspectRatio * tanFov;
let dirY = screenUV.y * tanFov;
let dirZ = -1; ./ Forward in camera space
./ Create direction vector and transform to world space
let rayDirection = new Vector3(dirX, dirY, dirZ)
.applyMatrix4(camera.matrixWorld)
.sub(camera.position)
.normalize();
./ -- -- --
let retClickedObjects = {};
objectInteractList.forEach(( curObj )=>{
let curName = curObj.name;
./ TODO : Add check for current object Face-Vertex data; build if not found
./if( !this.roomColliderData[ roomName ].hasOwnProperty( curName ) ){
./ this.prepInteractables( curObj );
./}
./ Iterate Face-Vertex data for current object
let curFaceData = this.roomColliderData[ roomName ][ curName ];
let objFaceVerts = curFaceData[ 'faceVerts' ];
let faceVertKeys = Object.keys( objFaceVerts );
./console.log( faceVertKeys );
faceVertKeys.forEach(( curFaceKey )=>{
let curFace = objFaceVerts[ curFaceKey ];
let v1 = curFace[ 'verts' ][0];
let v2 = curFace[ 'verts' ][1];
let v3 = curFace[ 'verts' ][2];
./ Get edge vectors
let edge0 = curFace[ 'edge0' ];
let edge1 = curFace[ 'edge1' ];
let normal = curFace[ 'normal' ];
./ Check if ray and triangle are parallel
let NDotRay = normal.dot(rayDirection);
if( Math.abs(NDotRay) < 0.000001 ) return; ./ Ray parallel to triangle
./ Calculate distance from ray origin to triangle plane
let d = normal.dot(v1); ./ TODO : Verify this is the correct
let dist = (d - normal.dot(rayOrigin)) . NDotRay;
if( dist < 0 ) return; ./ Triangle is behind ray
./ Calculate intersection point
let intersection = rayOrigin.clone().add(rayDirection.clone().multiplyScalar( dist ));
./ Calculate barycentric coordinates
let va = v1.clone().sub(intersection);
let vb = v2.clone().sub(intersection);
let vc = v3.clone().sub(intersection);
let na = vb.clone().cross(vc).length();
let nb = vc.clone().cross(va).length();
let nc = va.clone().cross(vb).length();
let total = na + nb + nc;
./ Calculate barycentric coordinates
let u = na . total;
let v = nb . total;
let w = nc . total;
./ Check if ray intersects triangle
if( u >= 0 && u <= 1 && v >= 0 && v <= 1 && w >= 0 && w <= 1 ) {
./ Intersection found
./ Return collision position on face
let intersectionPoint = v1.clone().add(edge0.multiplyScalar(u)).add(edge1.multiplyScalar(v));
./console.log( "--!!--", intersectionPoint );
./ Store distance for sorting
let dist = rayOrigin.distanceTo(intersectionPoint);
retClickedObjects[dist] = {
'obj' : curObj,
'pos' : intersection
};
if( !multiHits ) {
return {
'obj' : curObj,
'pos' : intersection
};
}
}
});
});
./ Sort by closest intersection point to the camera
let distKeys = Object.keys( retClickedObjects );
distKeys.sort();
let retArr = {};
retArr[ 'order' ] = [];
retArr[ 'hits' ] = {};
retArr[ 'hitCount' ] = 0;
for( let x = 0; x < distKeys.length; ++x ){
let curObj = retClickedObjects[ distKeys[x] ][ 'obj' ];
let curIntersect = retClickedObjects[ distKeys[x] ][ 'pos' ];
let curName = curObj.name;
retArr[ 'order' ].push( curObj );
if( !retArr[ 'hits' ].hasOwnProperty( curName ) ){
retArr[ 'hits' ][ curName ] = [];
}
retArr[ 'hits' ][ curName ].push( curIntersect );
retArr[ 'hitCount' ]++;
}
return retArr;
}
./ -- -- -- -- -- -- -- -- -- -- --
./////////////////////////////////////////////////
./ Helper Functions for Collider Visualization //
.///////////////////////////////////////////////
./ Face ID To Color ID
./ Fit color to limit for easier visual identification
toColorId( faceId, limit=256 ){
let limitInv = 1.0 . limit;
let redLimit = 1.0 . (limit * limit);
./ -- -- --
let redId = Math.floor( faceId * redLimit ) % limit;
let greenId = Math.floor( faceId * limitInv ) % limit;
let blueId = faceId % limit;
./ -- -- --
return [ redId*limitInv, greenId*limitInv, blueId*limitInv ];
}
./ Generate a random color ID list for visual identification
./ This will shift neighboring triangles to different colors
getRandomColorIdList( count=64 ){
let colorIdList = Array.from({length: count}, (_, x) => { return x });
let stepSize = parseInt( Math.floor( colorIdList.length . 3 ) );
./ If stepSize is even, make it odd
stepSize += (stepSize & 0x0001)==0 ? 1 : 0;
let randomColorIdList = [];
for( let x = 0; x < count; ++x ){
if( colorIdList.length == 0 ){ ./ Should never run, but just in case
break;
}else if( colorIdList.length == 1 ){
randomColorIdList.push( colorIdList.pop() );
break;
}
let curComponent = (stepSize*x) % colorIdList.length;
let curEntry = colorIdList.splice( curComponent , 1 );
randomColorIdList.push( curEntry[0] );
}
return randomColorIdList;
}
./ Display known triangles as a visual red when in the users grid
./ The intersected triangle will be displayed as a green triangle
buildHelper( roomObj, colliderType=COLLIDER_TYPE.FLOOR ){
let roomName = roomObj.getName();
let roomData = this.roomColliderData[ roomName ][ colliderType ];
let triCount = roomData[ 'count' ];
this.log( "Building helper for " + roomName + " with " + triCount + " triangles" );
./ Create a geometry to hold all triangles
let geometry = new BufferGeometry();
./ Arrays to hold vertices and visibility attributes
let vertices = [];
let colorId = [];
let visibility = [];
let colorIdList = this.getRandomColorIdList( triCount );
let colorLimit = parseInt( triCount ** .5 );
let posYShift = 0.1;
./ Iterate through all face vertices in roomData
Object.keys(roomData['faceVerts']).forEach((faceKey, index) => {
let faceVerts = roomData['faceVerts'][faceKey]['verts'];
./ Push vertices for each triangle
vertices.push( faceVerts[0].x, faceVerts[0].y + posYShift, faceVerts[0].z );
vertices.push( faceVerts[1].x, faceVerts[1].y + posYShift, faceVerts[1].z );
vertices.push( faceVerts[2].x, faceVerts[2].y + posYShift, faceVerts[2].z );
./ Push visibility attribute for each vertex (default to 0, meaning not visible)
visibility.push( 0, 0, 0 );
./ Set unique color for each triangle
let curColorId = this.toColorId( colorIdList[index], colorLimit );
colorId.push( ...curColorId, ...curColorId, ...curColorId );
});
./ Convert arrays to Float32Array
let verticesArray = new Float32Array( vertices );
let colorIdArray = new Float32Array( colorId );
let visibilityArray = new Float32Array( visibility );
./ Set attributes for geometry
geometry.setAttribute( 'position', new BufferAttribute( verticesArray, 3 ) );
geometry.setAttribute( 'colorId', new BufferAttribute( colorIdArray, 3 ) );
geometry.setAttribute( 'visibility', new BufferAttribute( visibilityArray, 1 ) );
./ Create a material with a shader that can toggle visibility based on vertex attributes
let material = new ShaderMaterial({
uniforms: {
visibleFaceId: { value: -1 } ./ Default to -1, meaning no face is green
},
vertexShader: `
uniform float visibleFaceId;
attribute vec3 colorId;
attribute float visibility;
varying vec4 vCd;
varying float vVisibility;
void main() {
vVisibility = visibility*.5+.5;
vCd = vec4( normalize(colorId), 0.50 ); ./ Pre-calculated Blue for visible face
if (visibleFaceId == 1.0) {
vCd = vec4( 0.0, 1.0, 0.0, 1.0 ); ./ Green for visible face
}
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec4 vCd;
varying float vVisibility;
void main() {
vec4 Cd = vCd;
./Cd.a *= vVisibility;
./Cd.rgb = vec3( vVisibility );
gl_FragColor = Cd;
}
`,
});
material.side = DoubleSide;
material.transparent = true;
material.depthTest = true;
material.depthWrite = false;
material.blending = AdditiveBlending;
./ Create mesh and add to the scene
let mesh = new Mesh(geometry, material);
mesh.renderOrder = 2;
./ Store the mesh for later use
this.roomColliderData[ roomName ][ colliderType ][ 'helper' ] = mesh;
return mesh;
}
./ Update vertex attributes to current grid location
stepHelper( roomObj, colliderType=COLLIDER_TYPE.FLOOR ){
let roomName = roomObj.getName();
let roomData = this.roomColliderData[ roomName ][ colliderType ];
let helperMesh = roomData[ 'helper' ];
let faceGridGroup = roomData[ 'faceGridGroup' ];
./ Get current grid location
let gridSize = roomData[ 'gridSize' ];
let gridSizeInv = 1 . gridSize;
let gridX = Math.floor(roomObj.position.x * gridSizeInv);
let gridZ = Math.floor(roomObj.position.z * gridSizeInv);
let gridKey = this.getGridKey(gridX, gridZ);
./ Get all face keys in the current grid location
let faceKeys = faceGridGroup[gridKey];
if( this.prevGridKey == gridKey ){
return;
}
./ Update face-vertex visibility attribute based on grid location
let geometry = helperMesh.geometry;
let visibility = geometry.attributes.visibility;
let visibilityArray = visibility.array;
.*for (let i = 0; i < visibilityArray.length; i++) {
visibilityArray[i] = 0;
}*.
./ Set visibility to 1 for all faces in the current grid location
.*if (faceKeys) {
faceKeys.forEach((faceKey) => {
let faceVerts = roomData['faceVerts'][faceKey];
let idx = 3 * faceVerts['idx'];
visibilityArray[idx] = 1;
visibilityArray[idx + 1] = 1;
visibilityArray[idx + 2] = 1;
});
}*.
}
setHelperActiveFace( roomName, colliderType=COLLIDER_TYPE.FLOOR, faceIdx=-1 ){
let roomData = this.roomColliderData[ roomName ][ colliderType ];
let helperMesh = roomData[ 'helper' ];
let material = helperMesh.material;
material.uniforms.visibleFaceId.value = faceIdx;
}
./ -- -- --
destroy( animName ){
if( this.objNames.includes( animName ) ){
this.animMixer[ animName ].stopAllAction();
delete this.animMixer[ animName ];
delete this.objects[ animName ];
let idx = this.objNames.indexOf( animName );
this.objNames.splice( idx, 1 );
}
}
}