//
// Core pxlNav Engine
const pxlNavVersion = "0.0.27";
// Written by Kevin Edzenga 2020;2024-2025
// -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
//
// When importing this module,
// Navigating the nested classes can be a bit tricky.
// To help with that, use `pxlNav.trigger` in this file to send events to your custom pxlNav rooms.
// Used like - `pxlNav.trigger('toRoom','changeRoom','SaltFlatsEnvironment')`
// This way you can trigger room events or custom code in your rooms
// Use the `onMessage( *type*, *value* )` function in your room to catch the events you send.
//
// The only Callback for subscription is "booted" at the moment, more to come!
// Usage - `pxlNav.subscribe('booted', yourCallbackFunc)`
// This will trigger when the engine has fully booted and is ready to be addressed.
// ALL rooms, assets, files, and initial functions have completed by this point.
// ::the progress bar fades out::
//
// Until I find time to make it easier to connect to `pxlDevice` events.
// For things like mouse position, clicks, mobile phone pose/orientation, etc.
// You'll need to stick your nose into `./js/pxlNav/core/Device.js`
// For all your mouse drag, mouse velocity, key states, etc.
//
// -- -- --
//
// *Note to pxlNav modifiers* -
// `//%=` `//%` and `//&=` `//&`
// Are used to mark code that is removed during webpack version building
// The sandwiched code is for the developmental & moderator specific code & builds.
// ( The initial need for pxlNav was as a virtual event space after all. )
// Treat them the same as `/*` & `*/` for commenting out code
// So if you remove a `//%` and not the before `//%=`, it will likely break your build
// -- -- -- -- -- -- -- -- -- -- -- -- -- --
// -- -- -- -- -- -- -- -- -- -- -- -- -- --
// -- -- -- -- -- -- -- -- -- -- -- -- -- --
import {
WebGLRenderer,
Scene,
Color,
WebGLRenderTarget,
RGBAFormat,
LinearFilter,
RepeatWrapping,
DepthTexture,
DepthFormat,
UnsignedIntType,
UnsignedShortType,
BasicShadowMap,
PCFSoftShadowMap,
PerspectiveCamera,
AmbientLight,
DirectionalLight,
Object3D,
FloatType,
NearestFilter,
HalfFloatType,
SRGBColorSpace,
LinearSRGBColorSpace,
ColorManagement
} from "./libs/three/three.module.min.js";
import { pxlBase } from './pxlNav/pxlBase.js';
import { pxlEnums } from './pxlNav/core/Enums.js';
import { pxlOptions, pxlUserSettings } from './pxlNav/core/Options.js';
import { pxlShaders } from './pxlNav/shaders/shaders.js';
import { pxlEffects } from './pxlNav/effects/effects.js';
import { RoomEnvironment } from './pxlNav/RoomClass.js';
const pxlCore = "pxlNav-coreCanvas"; // Name of DIV in Index
var mapW,mapH;
let sW = window.innerWidth;
let sH = window.innerHeight;
// -- -- -- -- -- -- -- -- -- -- -- -- -- --
// -- -- -- -- -- -- -- -- -- -- -- -- -- --
// -- -- -- -- -- -- -- -- -- -- -- -- -- --
/**
* pxlNav - Core Engine
* The primary entry point for the pxlNav engine.
* Initialize
* @alias pxlNav
* @module pxlNav
* @description Primary pxlNav engine module
* @param {object} options - The options object for the pxlNav environment
* @param {string} projectTitle - The title of the project
* @param {string} startingRoom - The initial room to load
* @param {string[]} roomBootList - A list of rooms to load
* @example
* // Example of a pxlNav environment setup
* // This is how you would initialize the pxlNav environment for your project
* import { pxlNav, pxlEnums, pxlUserSettings, pxlOptions } from './pxlNav.js';
*
* // Verbose Level
* // NONE = 0, ERROR = 1, WARN = 2, INFO = 3, DEBUG = 4
* const verboseLevel = pxlEnums.VERBOSE_LEVEL.INFO || 3;
*
* // Project name
* const projectTitle = "Your Project Title";
*
* // Booting rooms
* const startingRoom = "YourEnvironment";
* const bootRoomList = [startingRoom];
*
* // pxlRoom folder path, available to change folder names or locations if desired
* const pxlRoomRootPath = "../pxlRooms";
* // User settings for the default/initial pxlNav environment
* // These can be adjusted from your `pxlRoom` but easily set defaults here
* const userSettings = Object.assign({}, pxlUserSettings);
* userSettings['height']['standing'] = 1.75; // Standing height in units; any camera in your room's FBX will override this height once loaded
* userSettings['height']['stepSize'] = 5; // Max step height in units
*
* // Target FPS (Frames Per Second)
* // Default is - PC = 60 -&- Mobile = 30
* const targetFPS = {
* 'pc' : 45,
* 'mobile' : 30
* };
*
* // Copy the default options
* let pxlNavOptions = Object.assign({},pxlOptions);
* pxlNavOptions.verbose = verboseLevel;
* pxlNavOptions.fps = targetFPS;
* pxlNavOptions.userSettings = userSettings;
* pxlNavOptions.pxlRoomRoot = pxlRoomRootPath;
*
* // Initialize the pxlNav environment
* const pxlNavEnv = new pxlNav( pxlNavOptions, projectTitle, startingRoom, bootRoomList );
* pxlNavEnv.init();
* @returns {pxlNav} - The pxlNav environment object
* @example
* // Subscribe to events emitted from pxlNav for callback handling
* // Non loop - pxlNavObj.subscribe("pxlNavEventNameHere", procPages.functionName.bind(procPages));
* const printEvent = ( eventType, eventValue )=>{
* console.log( eventType, eventValue );
* };
* const myClassObj = new MyCustomClass();
*
* // Subscribe a single function to an event -
* pxlNavEnv.subscribe( "booted", printEvent );
*
* // Or multiple event subscriptions -
* const pageListenEvents = [ "booted", "shaderEditorVis", "roomChange-Start", "roomChange-Middle", "roomChange-End", "fromRoom", "pingPong" ];
* pageListenEvents.forEach( ( eventType )=>{
* // Subscribe functions to events -
* pxlNavEnv.subscribe( eventType, printEvent );
* // Make sure to bind the class object if you are using class methods
* pxlNavEnv.subscribe( eventType, myClassObj.eventHandler.bind(myClassObj) );
* });
* @example
* // Trigger events within pxlNav
* // Possible triggers -
* // "camera" - Change the camera position, either 'roam' or 'static'
* // "warptoroom" - Change the room to a new room
* // "roommessage" - Send a message to the current pxlRoom, add a `onMessage( eventType, eventValue)` method in your pxlRoom to get outside messages.
* // "ping" - Send a "pong" event
* pxlNavEnv.trigger( "warptoroom", "changeRoom", "SaltFlatsEnvironment" );
* pxlNavEnv.trigger( "camera", "roam" );
*
* // For testing purposes, trigger a `pingPong` event
* // Subscribe to the `ping` event
* const pongPrint = ( eventType, eventValue )=>{
* console.log( eventType, eventValue );
* };
* pxlNavEnv.subscribe( "pingPong", pongPrint );
*
* // Trigger the `pingPong` event
* pxlNavEnv.trigger( "ping" );
* @example
* // pxlNav Event List
* // "booted" : Emitted after the engine has fully booted and is ready to be addressed.
* // "shaderEditorVis" : Returns a [bool]; Emitted when the shader editor is toggled on or off.
* // "roomChange-Start" : Emitted when the room change transition begins.
* // "roomChange-Middle" : Emitted when the room change process occurs mid transition.
* // "roomChange-End" : Returns a [bool]; Emitted when the room change transition ends.
* // "fromRoom" : Returns a custom object; Emitted from your Room code you choose to emit during run time.
* // "device-keydown" : Returns an [int]; The just pressed key.
* // "device-keyup" : Returns an [int]; The just released key.
* // "device-resize" : Returns an [{height:#,width:#}]; Height Width object of the new window size.
* // "camera-move" : Returns a {'pos':vec3(),'dist':float}; Emitted when the camera moves.
* // "camera-rotate" : Returns a [quaternion]; Emitted when the camera rotates.
* // "camera-jump" : Returns a [bool]; Emitted when the camera jumps to a new position.
* // "camera-fall" : Returns a [bool]; Emitted when the camera starts to free-fall / gravity.
* // "camera-landed" : Returns a [bool]; Emitted when the camera lands from a jump / fall.
* // "camera-collision" : Returns a [bool]; Emitted when the camera collides with an object.
* // "pxlNavEventNameHere" : Never emitted; You did some copy'pasta.
* // "help" : Hello! I'm here to help you!
* // "pingPong" : Send 'ping', Get 'pong'! - pxlNav.trigger('ping');
* //
* // ** NOTE : callbacks get an event object shaped - **
* // ** callbackFn( eventType, eventValue ) **
* // ** eventValue = { 'type' : *eventName*, 'value' : *data* } **
*
* // Listen to when pxlNav has finished booting
* const printBoot = ( eventType, eventValue )=>{
* console.log( "pxlNav has booted!" );
* console.log( eventType, eventValue );
* };
* pxlNavEnv.subscribe( "booted", printBoot );
*/
class pxlNav{
constructor( options, projectTitle, startingRoom, roomBootList ){
this._active = false;
// -- -- --
let pxlRoomRoot = "./pxlRooms";
if( options.hasOwnProperty("pxlRoomRoot") ){
pxlRoomRoot = options["pxlRoomRoot"];
}
// Enums object
this.pxlEnums = pxlEnums;
// Option Checks & Defaults
this.assetsToLoadDict = {
"Cloud3d" : true,
"SoftNoise" : true,
"SmoothNoise" : true,
"ChromaticAberration" : false,
"WarpAnimTexture" : false,
"MathFuncs" : false
};
this.knownPostProcessPasses = {
"mapComposerWarpPass" : [ 'cloud3dTexture' ],
"chromaticAberrationPass" : [ 'ChromaticAberration'],
"lizardKingPass" : [ 'cloud3dTexture' ],
"starFieldPass" : [ 'cloud3dTexture' ],
"crystallinePass" : [ 'cloud3dTexture' ]
};
// -- -- --
this.pxlOptions = Object.assign( {}, options );
let optionKeys=Object.keys( this.pxlOptions );
let defaultKeys=Object.keys( pxlOptions );
defaultKeys.forEach( (k)=>{
if( !optionKeys.includes( k ) ){
this.pxlOptions[k]=pxlOptions[k];
}
});
// Should there not be a default `userSettings` object, build one
// Update these values from `pxlRoom.pxlCamera` set methods
if( !this.pxlOptions.hasOwnProperty("userSettings") ){
this.pxlOptions["userSettings"] = Object.assign({}, pxlUserSettings);
}
// -- -- --
this.verbose = this.pxlOptions["verbose"];
this.projectTitle = projectTitle;
this.startingRoom = startingRoom;
if( !roomBootList.includes( startingRoom ) ){
roomBootList.push( startingRoom );
}
this.roomBootList = roomBootList
this.callbacks = {};
this.uriHashParms = this.findHashParms();
this.mobile = this.isMobile();
this.pxlOptions["mobile"] = this.mobile;
this.autoCam = this.getHashParm("autoCam", false);
this.loadPercent=0.0;
this.folderDict = {
"assetRoot" : this.pxlOptions.pxlAssetRoot + "/",
"guiRoot" : this.pxlOptions.pxlAssetRoot + "/GUI/",
"roomRoot" : this.pxlOptions.pxlAssetRoot + "/rooms/",
"vidRoot" : this.pxlOptions.pxlAssetRoot + "/screenContent/"
};
this.validEvents = {
"booted" : "Emitted after the engine has fully booted and is ready to be addressed.",
"shaderEditorVis" : "Returns a [bool]; Emitted when the shader editor is toggled on or off.",
"roomChange-Start" : "Emitted when the room change transition begins.",
"roomChange-Middle" : "Emitted when the room change process occurs mid transition.",
"roomChange-End" : "Returns a [bool]; Emitted when the room change transition ends.",
"fromRoom" : "Returns a custom object; Emitted from your Room code you choose to emit during run time.",
"device-keydown" : "Returns an [int]; The just pressed key.",
"device-keyup" : "Returns an [int]; The just released key.",
"device-resize" : "Returns an [{height:#,width:#}]; Height Width object of the new window size.",
"camera-move" : "Returns a {'pos':vec3(),'dist':float}; Emitted when the camera moves.",
"camera-rotate" : "Returns a [quaternion]; Emitted when the camera rotates.",
"camera-jump" : "Returns a [bool]; Emitted when the camera jumps to a new position.",
"camera-fall" : "Returns a [bool]; Emitted when the camera starts to free-fall / gravity.",
"camera-landed" : "Returns a [bool]; Emitted when the camera lands from a jump / fall.",
"camera-collision" : "Returns a [bool]; Emitted when the camera collides with an object.",
"pxlNavEventNameHere" : "Never emitted; You did some copy'pasta.",
"" : "** NOTE : callbacks get an event object shaped - **",
"" : "** callbackFn( eventType, eventValue ) **",
"" : "** eventValue = { 'type' : *eventName*, 'value' : *data* } **",
"" : "",
"help" : "Hello! I'm here to help you!",
"pingPong" : "Send 'ping', Get 'pong'! - pxlNav.trigger('ping');",
};
this.validEventsKeys = Object.keys( this.validEvents );
// Bool to load the environment asset fbx file;
// This is the included file with test pick-ups / assets
// ./Source/assets/EnvironmentAssets.fbx
// For further information of each item & object,
// See https://github.com/ProcStack/pxlNav/tree/main/docs
// TODO : Turning this off breaks loading, fix that
this.loadEnvAssetFile = true; // this.pxlOptions['LoadEnvAssetFile'];
this.pxlTimer = new pxlBase.Timer();
this.pxlShaders = pxlShaders;
this.pxlCookie = new pxlBase.CookieManager( this.verbose, projectTitle, "/" );
if( this.pxlCookie.hasCookie("forceMobile") ){
this.mobile = pxlCookie.parseCookie("forceMobile");
}
this.pxlQuality = new pxlBase.QualityController( this.verbose, this.mobile, this.uriHashParms );
this.pxlUtils = new pxlBase.Utils( this.folderDict["assetRoot"], this.mobile );
this.pxlFile = new pxlBase.FileIO( this.folderDict );
this.pxlExtensions = new pxlBase.Extensions();
this.pxlAudio = new pxlBase.Audio( this.pxlTimer );
this.pxlAutoCam = new pxlBase.AutoCamera( this.autoCam, this.mobile );
this.pxlAutoCam.active = false;
this.pxlUser = new pxlBase.User();
this.pxlEnv = new pxlBase.Environment( this.pxlOptions, this.startingRoom, this.mobile );
this.pxlDevice = new pxlBase.Device( projectTitle, pxlCore, this.mobile, this.autoCam );
// Default Grid Size 50, Collider Bounds as reference 1000.0
// The reference bounds are used to scale down the grid size for smaller bbox colliders,
// Helping with higher poly counts for performance
this.pxlColliders = new pxlBase.Colliders( this.verbose, this.pxlOptions["collisionScale"]["gridSize"], this.pxlOptions["collisionScale"]["gridReference"] );
this.pxlCamera = new pxlBase.Camera();
// Disable Free-Roam camera mode if static camera is enabled
if( this.pxlOptions["staticCamera"] ){
this.pxlCamera.toggleMovement( false );
}
// TODO : This will be moved to `User.js` in the future
this.pxlCamera.setUserSettings( this.pxlOptions["userSettings"] );
this.pxlAnim = new pxlBase.Animation( this.folderDict["assetRoot"], this.pxlTimer );
this.pxlGuiDraws = new pxlBase.GUI( this.verbose, projectTitle, this.folderDict["assetRoot"], this.folderDict["guiRoot"] );
this.pxlHUD = new pxlBase.HUD();
// TODO : These should really be requested via pxlEnv methods, but for now...
// Too many dependencies still to refactor, but it's cleaner than its ever been, so I'll take it!
this.pxlQuality.setDependencies( this );
this.pxlUtils.setDependencies( this );
this.pxlFile.setDependencies( this );
this.pxlAudio.setDependencies( this );
this.pxlAutoCam.setDependencies( this );
this.pxlUser.setDependencies( this );
this.pxlEnv.setDependencies( this );
this.pxlAnim.setDependencies( this );
this.pxlDevice.setDependencies( this );
this.pxlColliders.setDependencies( this );
this.pxlCamera.setDependencies( this );
this.pxlGuiDraws.setDependencies( this );
this.pxlHUD.setDependencies( this );
this.pxlDevice.init();
this.pxlGuiDraws.prepLoader();
if( !this.pxlOptions.hasOwnProperty("loaderPhrases") ){
this.pxlOptions["loaderPhrases"] = ['...loading the pixels...'];
}
this.pxlGuiDraws.setLoaderPhrases( this.pxlOptions["loaderPhrases"] );
if( this.verbose >= pxlEnums.VERBOSE_LEVEL.INFO ){
console.log("pxlNav v" + pxlNavVersion +" set to Verbose Info Mode");
console.log(" With Three.js v171");
console.log("Booting pxlNav...");
}
this.pxlQuality.init() // Load cookies and update settings
}
set active(val=null){
if( val == null ){
val = !this.pxlTimer.active;
}
if( val == true ){ // Non-strict Truthy
this.pxlTimer.play();
this.step( false );
}else{ // Non-strict Falsy
this.pxlTimer.stop();
}
}
get active(){
return this.pxlTimer.active;
}
start(){
this.active = true;
}
stop(){
this.active = false;
}
// -- -- --
init(){
// Boot timer
if(!this.pxlTimer.active){
this.bootTimer();
}
// Start the pxlNav environment
this.pxlEnv.boot(); // Environment Asset Prep
// Initialize a base quality level
this.pxlQuality.startBenchmark(); // Start benchmark timer
this.checkPxlOptions();
this.buildGui()
.then( ()=>{
this.tickLoader();
this.bootEnvironment();
})
.then( ()=>{
this.tickLoader();
this.bootEngine();
})
.then( ()=>{
this.tickLoader();
// Create post-process passes
this.pxlEnv.buildComposers();
// Prep the HUD
// Post room boot to allow for room specific HUD elements
this.pxlHUD.init();
//this.pxlDevice.resizeRenderResolution();
this.cameraRunAnimatorMobile( this );
this.pxlGuiDraws.stepLoader("postBoot"); // --
this.pxlEnv.mapRender();
this.pxlDevice.setCursor("grab");
})
.catch( (err)=>{
if( this.verbose > pxlEnums.VERBOSE_LEVEL.NONE ){
console.error("Error in pxlNavCore.init(); Load level - ", err);
console.error(err);
}
})
.finally( ()=>{
if( this.verbose >= pxlEnums.VERBOSE_LEVEL.DEBUG ){
console.log("'pxlNavCore' Room Build Promise-Chain Completed; ", this.loadPercent);
console.log("-- Starting pxlNav in Room `"+this.pxlEnv.bootRoom+"`");
}
this.start();
});
}
// -- -- --
// Check if any post process passes are enabled & toggle load status of their pxlAssets
checkPxlOptions(){
if( this.pxlOptions.hasOwnProperty("postProcessPasses") ){
let postProcessKeys = Object.keys( this.pxlOptions["postProcessPasses"] );
let knownPassKeys = Object.keys( this.knownPostProcessPasses );
postProcessKeys.forEach( (key)=>{
if( knownPassKeys.includes( key ) && this.pxlOptions["postProcessPasses"][key] ){
let passAssets = this.knownPostProcessPasses[key];
passAssets.forEach( (assetKey)=>{
this.assetsToLoadDict[assetKey] = true;
});
}
});
}
}
// -- -- --
buildGui(){
return new Promise( (resolve, reject)=>{
this.pxlGuiDraws.booted();
this.pxlGuiDraws.pxlNavCanvas=document.getElementById(pxlCore);
this.pxlGuiDraws.pxlNavCanvas.width= window.innerWidth * this.pxlQuality.screenResPerc;
this.pxlGuiDraws.pxlNavCanvas.height= window.innerHeight * this.pxlQuality.screenResPerc;
if(this.pxlDevice.canCursorLock){
this.pxlGuiDraws.pxlNavCanvas.requestPointerLock=
this.pxlGuiDraws.pxlNavCanvas.requestPointerLock ||
this.pxlGuiDraws.pxlNavCanvas.mozRequestPointerLock ||
this.pxlGuiDraws.pxlNavCanvas.webkitRequestPointerLock;
document.requestPointerLock=
this.pxlGuiDraws.pxlNavCanvas.exitPointerLock ||
this.pxlGuiDraws.pxlNavCanvas.mozExitPointerLock ||
this.pxlGuiDraws.pxlNavCanvas.webkitExitPointerLock;
}
this.pxlGuiDraws.mapPrepPrompts(); // Prep GUI & Hud DOM Elements
resolve( true );
});
}
////////////////////////////////////////////////////
// -- ENGINE PREP & SCENE -- -- -- -- -- -- -- //
//////////////////////////////////////////////////
isMobile(){
var mobile = false;
mobile = (/\b(BlackBerry|webOS|iPhone|IEMobile|Android|Windows Phone|iPad|iPod)\b/i.test(navigator.userAgent));
mobile = this.getHashParm("m", mobile);
return mobile;
}
findHashParms(){
var hashParms={};
var hashStr=window.location.hash;
if(hashStr.length>1){
hashStr=hashStr.substring(1);
var hashList=hashStr.split("&");
for(var i=0; i<hashList.length; i++){
var curParm=hashList[i].split("=");
hashParms[curParm[0]]=curParm[1];
}
}
return hashParms;
}
getHashParm(parm, def){
if( Object.keys(this.uriHashParms).includes(parm) ){
return this.uriHashParms[parm];
}else{
return def;
}
}
tickLoader(){
this.loadPercent+=1.0;
//this.pxlGuiDraws.stepLoader("tick");
}
bootEngine(){
return new Promise( (resolve, reject)=>{
let promiseList=[];
for( let x=0; x<this.roomBootList.length; ++x ){
promiseList.push( this.pxlEnv.loadRoom( this.roomBootList[x] ) );
}
Promise.all( promiseList ).then( ()=>{
resolve( true );
});
});
}
bootEnvironment(){
return new Promise( (resolve, reject)=>{
// Rederer
this.pxlEnv.engine=new WebGLRenderer({
canvas: this.pxlGuiDraws.pxlNavCanvas,
powerPreference : "low-power",
alpha:true,
antialias: false,
sortObjects:true,
depth:true,
//logarithmicDepthBuffer:true,
});
var options = {
format : RGBAFormat,
antialias: false,
sortObjects:true,
alpha:true,
type : /(iPad|iPhone|iPod)/g.test(navigator.userAgent) ? HalfFloatType : FloatType
};
this.pxlEnv.engine.autoClear=true;
ColorManagement.enabled = false;
this.pxlEnv.engine.outputColorSpace = SRGBColorSpace;
//this.pxlEnv.engine.outputColorSpace = LinearSRGBColorSpace;
this.pxlEnv.engine.debug.checkShaderErrors=false;
//%= Dev
this.pxlEnv.engine.debug.checkShaderErrors=true;
//%
let bgCd=0x000000;
let bgCdHex="#000000";
this.pxlEnv.engine.setClearColor(this.pxlEnv.fogColor, 0);
//this.pxlEnv.engine.setPixelRatio(window.devicePixelRatio);
this.pxlEnv.engine.setSize(mapW/this.pxlQuality.screenResPerc, mapH/this.pxlQuality.screenResPerc);
this.pxlEnv.engine.setSize( window.innerWidth, window.innerHeight );
this.pxlEnv.engine.setPixelRatio(1);
if(this.pxlOptions.shadowMapBiasing == pxlEnums.SHADOW_MAP.OFF){
this.pxlEnv.engine.shadowMap.enabled=false;
}else{
this.pxlEnv.engine.shadowMap.enabled=true;
this.pxlEnv.engine.shadowMap.type=BasicShadowMap;
}
// Build render targets for depth and world space reference
this.pxlEnv.scene=new Scene();
this.pxlEnv.scene.fog=this.pxlEnv.fog;
//this.pxlEnv.scene.background = new Color(bgCdHex);
this.pxlEnv.scene.background = new Color(bgCdHex);//this.pxlEnv.fogColor;
this.pxlEnv.scene.renderTarget=new WebGLRenderTarget(sW*this.pxlQuality.screenResPerc,sH*this.pxlQuality.screenResPerc,options);
this.pxlEnv.scene.renderTarget.texture.format=RGBAFormat;
this.pxlEnv.scene.renderTarget.texture.minFilter=LinearFilter;
this.pxlEnv.scene.renderTarget.texture.magFilter=LinearFilter;
this.pxlEnv.scene.renderTarget.texture.generateMipmaps=false;
//this.pxlEnv.scene.renderTarget.texture.type=FloatType;
this.pxlEnv.scene.renderTarget.depthBuffer=true;
this.pxlEnv.scene.renderTarget.depthTexture = new DepthTexture();
this.pxlEnv.scene.renderTarget.depthTexture.format=DepthFormat;
this.pxlEnv.scene.renderTarget.depthTexture.type=UnsignedIntType;
//this.pxlEnv.scene.renderTarget.depthTexture.type=UnsignedShortType;
this.pxlEnv.scene.renderWorldPos=new WebGLRenderTarget(sW*this.pxlQuality.screenResPerc,sH*this.pxlQuality.screenResPerc,options);
this.pxlEnv.scene.renderWorldPos.texture.format=RGBAFormat;
this.pxlEnv.scene.renderWorldPos.texture.minFilter=NearestFilter;
this.pxlEnv.scene.renderWorldPos.texture.magFilter=NearestFilter;
this.pxlEnv.scene.renderWorldPos.texture.generateMipmaps=false;
options.alpha=true;
this.pxlEnv.scene.renderGlowTarget=new WebGLRenderTarget( parseInt(sW*this.pxlQuality.screenResPerc), parseInt(sH*this.pxlQuality.screenResPerc),options);
this.pxlEnv.scene.renderGlowTarget.texture.format=RGBAFormat;
this.pxlEnv.scene.renderGlowTarget.texture.generateMipmaps=false;
/*this.pxlEnv.warpZoneRenderTarget=new WebGLRenderTarget(1024,1024,options);
this.pxlEnv.warpZoneRenderTarget.texture.format=RGBFormat;
this.pxlEnv.warpZoneRenderTarget.texture.minFilter=LinearFilter;
this.pxlEnv.warpZoneRenderTarget.texture.magFilter=LinearFilter;
this.pxlEnv.warpZoneRenderTarget.texture.generateMipmaps=false;*/
// TODO : Aspect Ratio is a bit off, need to fix this
//var aspectRatio=this.pxlGuiDraws.pxlNavCanvas.width/this.pxlGuiDraws.pxlNavCanvas.height;
// To change the near and far, see Environment .init()
let curFOV=this.pxlEnv.pxlCamFOV[ this.pxlDevice.mobile ? 'MOBILE' : 'PC' ];
this.pxlCamera.camera=new PerspectiveCamera( curFOV, 1, this.pxlEnv.camNear, this.pxlEnv.camFar);
this.pxlAutoCam.camera=this.pxlCamera.camera;
//this.pxlEnv.listener = new AudioListener();
//this.pxlCamera.camera.add( this.pxlEnv.listener );
//this.pxlCamera.camera.position.set(-20,0,15);
this.pxlCamera.cameraAimTarget=new Object3D();
this.pxlEnv.scene.add(this.pxlCamera.cameraAimTarget);
this.pxlCamera.camera.target=this.pxlCamera.cameraAimTarget;
//this.pxlEnv.roomSceneList[this.pxlEnv.mainRoom]=this.pxlEnv;
//this.pxlCamera.camera.layers.enable(0);
this.pxlCamera.camera.layers.enable(1);
this.pxlCamera.camera.layers.enable(2);
this.pxlEnv.scene.add( this.pxlEnv.userAvatarGroup );
///////////////////////////////////////////////////
// -- FILE I/O & Shared Assets -- -- -- -- -- -- //
///////////////////////////////////////////////////
if( this.assetsToLoadDict["Cloud3d"] ){
this.pxlEnv.cloud3dTexture=this.pxlUtils.loadTexture( this.folderDict["assetRoot"]+"Noise_Cloud3d.jpg", null, {"encoding":LinearSRGBColorSpace});
this.pxlEnv.cloud3dTexture.wrapS=RepeatWrapping;
this.pxlEnv.cloud3dTexture.wrapT=RepeatWrapping;
}
if( this.assetsToLoadDict["SoftNoise"] ){
this.pxlEnv.softNoiseTexture=this.pxlUtils.loadTexture( this.folderDict["assetRoot"]+"Noise_Soft3d.jpg" );
this.pxlEnv.softNoiseTexture.wrapS = RepeatWrapping;
this.pxlEnv.softNoiseTexture.wrapT = RepeatWrapping;
}
if( this.assetsToLoadDict["SmoothNoise"] ){
this.pxlEnv.detailNoiseTexture=this.pxlUtils.loadTexture( this.folderDict["assetRoot"]+"Noise_UniformSmooth.jpg" );
this.pxlEnv.detailNoiseTexture.wrapS = RepeatWrapping;
this.pxlEnv.detailNoiseTexture.wrapT = RepeatWrapping;
}
if( this.assetsToLoadDict["ChromaticAberration"] ){
let chroAberUVTexture = this.pxlUtils.loadTexture( this.folderDict["assetRoot"]+"uv_ChromaticAberration_rgb.jpg");
chroAberUVTexture.minFilter=LinearFilter;
chroAberUVTexture.magFilter=LinearFilter;
this.pxlEnv.chroAberUVTexture=chroAberUVTexture;
let chroAberUVAlpha = this.pxlUtils.loadTexture( this.folderDict["assetRoot"]+"uv_ChromaticAberration_alpha.jpg");
chroAberUVAlpha.minFilter=LinearFilter;
chroAberUVAlpha.magFilter=LinearFilter;
this.pxlEnv.chroAberUVAlpha=chroAberUVAlpha;
}
if( this.assetsToLoadDict["WarpAnimTexture"] ){
this.pxlEnv.blockAnimTexture=this.pxlUtils.loadTexture( this.folderDict["assetRoot"]+"uv_blockPortalWarp.jpg");
this.pxlEnv.blockAnimTexture.minFilter=LinearFilter;
this.pxlEnv.blockAnimTexture.magFilter=LinearFilter;
}
///////////////////////////////////////////////////
// -- GEOMETRY -- -- -- -- -- -- -- -- -- -- -- //
///////////////////////////////////////////////////
var materialList, transformList;
// deskCandleHolder
var tListIdent=(parentObj=null)=>{
var ident={
"t":[0,0,0],
"r":[0,0,0],
"s":[1,1,1]
};
if(parentObj != null){
ident[parentObj[0]]=parentObj[1];
}
return ident;
}
transformList=tListIdent();
let mobileSuffix="";
if( this.mobile ){
mobileSuffix="_mobile";
}
if( this.loadEnvAssetFile ){
//let sceneFile=this.folderDict["assetRoot"]+"EnvironmentAssets"+mobileSuffix+".fbx";
let sceneFile=this.folderDict["assetRoot"]+"EnvironmentAssets.fbx";
// This is a separate fbx loaded specifically for the Environment Asset FBX
// It opens up the found scene objects to easier global access
this.pxlFile.loadSceneFBX(sceneFile, materialList, transformList, this.verbose,'EnvironmentAssets',[this.pxlEnv.scene]);
}
///////////////////////////////////////////////////
// -- LIGHTS -- -- -- -- -- -- -- -- -- -- -- -- //
///////////////////////////////////////////////////
//Shadow Maps-
if(this.pxlOptions.shadowMapBiasing == pxlEnums.SHADOW_MAP.OFF){
this.pxlEnv.engine.shadowMap.enabled=false;
}else{
this.pxlEnv.engine.shadowMap.enabled=true;
if(this.pxlOptions.shadowMapBiasing == pxlEnums.SHADOW_MAP.BASIC || this.mobile){
this.pxlEnv.engine.shadowMap.type=BasicShadowMap;
}else if(this.pxlOptions.shadowMapBiasing == pxlEnums.SHADOW_MAP.SOFT){
this.pxlEnv.engine.shadowMap.type=PCFSoftShadowMap;
//this.pxlEnv.engine.shadowMap.type=VSMShadowMap;
}
}
// Every light is another frag level dot to matrix calculation
// Add at your own risk
var ambLight=new AmbientLight(0xffffff,.05);
this.pxlEnv.lightList.push(ambLight);
this.pxlEnv.scene.add(ambLight);
var dirLight=new DirectionalLight(0xffffff,.1);
dirLight.position.set(1000,550,1200);
this.pxlEnv.lightList.push(dirLight);
this.pxlEnv.scene.add(dirLight);
/*var dirLightB=new DirectionalLight(0xffffff,.05);
dirLightB.position.set(-500,750,1500);
this.pxlEnv.lightList.push(dirLightB);
this.pxlEnv.scene.add(dirLightB); */
resolve( true );
});
}
cameraRunAnimatorMobile( tmpThis ){
let stillLoadingCheck=false;
let keys=Object.keys(tmpThis.pxlEnv.geoLoadList);
for(let x=0; x<keys.length; ++x){ // Check if any objects are still loading
stillLoadingCheck=tmpThis.pxlEnv.geoLoadList[keys[x]]==0;
stillLoadingCheck = stillLoadingCheck && !tmpThis.pxlEnv.roomSceneList[x]?.booted;
if(stillLoadingCheck){ // If entry isn't 1, means not fully loaded
break;
}
}
if(stillLoadingCheck ){ // Files are still loading
setTimeout(()=>{
tmpThis.cameraRunAnimatorMobile( tmpThis );
},300);
}else{
tmpThis.initRoomList( tmpThis );
}
}
initRoomList( tmpThis){
tmpThis.pxlGuiDraws.stepLoader("camAnim"); // --
tmpThis.pxlCamera.setTransform( tmpThis.pxlEnv.camInitPos, tmpThis.pxlEnv.camInitLookAt );
tmpThis.pxlCamera.updateCamera();
// Append start up functions
tmpThis.pxlEnv.buildRoomEnvironments();
tmpThis.monitorRoomLoad( tmpThis );
}
monitorRoomLoad( tmpThis, loop=0 ){
let stillLoadingCheck=false;
let keys=Object.keys(tmpThis.pxlEnv.geoLoadList);
for(let x=0; x<keys.length; ++x){ // Check if any objects are still loading
stillLoadingCheck=tmpThis.pxlEnv.geoLoadList[keys[x]]==0;
stillLoadingCheck = stillLoadingCheck && !tmpThis.pxlEnv.roomSceneList[x]?.booted;
if(stillLoadingCheck){ // If entry isn't 1, means not fully loaded
break;
}
}
if( stillLoadingCheck ){ // Files are still loading
setTimeout(()=>{
tmpThis.monitorRoomLoad( tmpThis );
},300);
}else{
tmpThis.pxlQuality.mapAutoQualityUpdate(1,true);
//let snapShotCommands=tmpThis.pxlEnv.warpPortalQueue();
//let camStats={ fov:pxlCamera.camera.fov, zoom:pxlCamera.camera.zoom, aspect:pxlCamera.camera.aspect };
//runStartFunctions( camStats, 0, true, snapShotCommands);
tmpThis.runPrepDrawScenes( 0, true, []);//snapShotCommands );
}
}
/**
* Internal Function
* Warp player to each room to build materials on gpu
* This compiles the materials in each scene, at least from the perspective of the camera
* This causes a delay during the first warp to the new room in runtime,
* So this runs that hiccup before the user can feel it
* `cmdList` currently unused, was used to take screenshots of each room initially.
* But this is a stage where custom commands can be injected during environment prep.
* @param {*} runner
* @param {*} jumpCam
* @param {*} cmdList
*/
runPrepDrawScenes(runner=0, jumpCam=true, cmdList=[]){
runner++;
if( cmdList.length > 0 ){
if(jumpCam){
jumpCam=false;
let curRoom=cmdList[cmdList.length-1];
this.pxlCamera.warpToRoom( curRoom );
}
this.pxlCamera.colliderPrevObjHit=null;
// Erroring here means shader failure in scene
this.pxlEnv.mapRender( false );
if(runner%10==0){
let exitingRoom=cmdList.pop();
// Snapshots / Env Map Gen
//pxlEnv.getSceneSnapshot(exitingRoom);
jumpCam=true;
this.pxlGuiDraws.stepLoader(exitingRoom); // --
}
requestAnimationFrame( ()=>{ this.runPrepDrawScenes( runner, jumpCam, cmdList ); });
}else{
this.pxlGuiDraws.stepLoader("Post Room Prep"); // --
this.pxlNavPrepSettings();
}
}
/**
* Internal Function
* All booting has completed
* Clean-up device benchmarking, restart the main room, move player to starting position & look at
* Final prep; settings and gui values based on cookies or benchmarking
* Then release the kraken!
*/
pxlNavPrepSettings(){
this.pxlCamera.warpToRoom( this.pxlEnv.bootRoom, true );
this.pxlQuality.endBenchmark(); // All the heavy lifting as completed
this.pxlGuiDraws.stepLoader( "Nav Settings" ); // --
this.startPxlNav();
}
/**
* Internal function
* Start the pxlNav engine
*/
startPxlNav(){
this.pxlTimer.init();
this.pxlTimer.play();
this.pxlGuiDraws.applyCookies();
if(this.pxlGuiDraws.pxlNavCanvas){ this.pxlGuiDraws.pxlNavCanvas.focus(); }
let curRoom=this.pxlEnv.roomSceneList[this.pxlEnv.currentRoom];
curRoom.active=true;
curRoom.startTime=this.pxlTimer.msRunner.x;
// ## Must be a reason I'm not doing a pop() here
this.pxlEnv.roomPostGuiCalls.forEach( (e)=>{ e.postGuiEvent(); });
this.pxlEnv.roomPostGuiCalls=[];
if( this.pxlAutoCam.enabled ){
this.pxlGuiDraws.guiToggleVisibility( false );
}
this.pxlAutoCam.init();
//this.pxlVideo.boot();
this.pxlDevice.resizeRenderResolution();
this.pxlEnv.mapRender();
this.pxlQuality.setDependentSettings();
this.pxlGuiDraws.stepLoader( "Starting..." );
let tmpThis = this;
setTimeout( ()=>{
tmpThis.pxlEnv.postBoot();
if(tmpThis.pxlGuiDraws.mapPrompt) tmpThis.pxlGuiDraws.promptFader(tmpThis.pxlGuiDraws.mapPrompt, false,null,true);
if(tmpThis.pxlGuiDraws.mapPromptBG) tmpThis.pxlGuiDraws.promptFader(tmpThis.pxlGuiDraws.mapPromptBG, false,null,false);
this.pxlDevice.hideAddressBar();
tmpThis.emit("booted", true);
}, 200);
}
// -- -- -- -- -- -- -- -- -- -- --
// -- -- -- -- -- -- -- -- -- -- --
// -- -- -- -- -- -- -- -- -- -- --
/**
* Initialize pxlNav clock for synced external runtimes
* Don't run `setp(true)` if you are using pxlNav to run the render loop, this will cause two loops to run in tandem.
* This function should only be used if you are using pxlNav for it's utilities and not pxlRoom / Render Stack management.
* @param {boolean} anim If the clock should loop or not
*/
step(anim=false){
if(this.pxlTimer.active){
this.pxlTimer.step();
if( anim ){
requestAnimationFrame( ()=>{ this.step(); });
}
}
}
// -- -- --
/**
* Initialize pxlNav's runtime clock
*/
bootTimer(){
this.pxlTimer.init();
this.pxlTimer.play();
}
/**
* Stop pxlNav's runtime clock
* This will pause all animations and events
*/
stopTimer(){
this.pxlTimer.stop();
}
/**
* Pause pxlNav's runtime clock
*/
pauseTimer(){
this.pxlTimer.pause();
}
// -- -- --
/**
* Prep an object with propper css classes to work with {@link promptFader}
* @param {(string|object)} obj Object to set classes to.
* @param {boolean=} startVis If the object starts visible or not. Defaults to not visible, easier for a fade in.
*/
prepPromptFader( obj, startVis=false ){
let curObj=obj;
if( typeof(curObj) === "string" ){
curObj=document.getElementById(curObj);
}
if(!curObj){
return;
}
curObj.classList.add("fader");
curObj.classList.add( (startVis ? "visOn" : "visOff") );
curObj.style.display= startVis ? "inline-block" : "none";
}
/**
* Fade an object in or out of visiblity over time.
* Fade animation uses css for the visuals, but uses clock time for automatic fade out events.
* @param {(string|object)} faderObjObject to fade in or out.
* @param {boolean} vis Visiblity state to fade to.
* @param {number=} fadeOutLimit Length of time in Seconds until the object automatically fades out.
* This occures in the requestAnimationFrame Render function.
* @param {boolean=} deleteOnEnd Option to remove the object from the document once the fade out completes.
* For garbage collection reasons, its advised to set this `true` when possible.
*/
promptFader(faderObj, vis, fadeOutLimit=null, deleteOnEnd=false){
if(typeof(faderObj)=="string"){
faderObj=document.getElementById(faderObj);
}
if(!faderObj){
return;
}
if(faderObj.classList?.value.indexOf("fader")<0){
faderObj.classList.add("fader");
}
if(vis){
faderObj.style.display="inline-block";
setTimeout( ()=>{
if(faderObj.classList.contains("visOff")){
faderObj.classList.remove("visOff");
faderObj.classList.add("visOn");
if(fadeOutLimit!=null){
faderObj.setAttribute("fadeOut", clockTime+fadeOutLimit*1000);
fadeOutList.push(faderObj);
}
}
}, 50);
}else{
faderObj.classList.remove("visOn");
faderObj.classList.add("visOff");
if(deleteOnEnd){
let transEnd=["webkitTransitionEnd", "otransitionend", "oTransitionEnd", "msTransitionEnd", "transitionend"];
transEnd.forEach((end)=>{
faderObj.addEventListener(end,()=>{
let curParent=faderObj.parentNode;
if(curParent){
curParent.removeChild(faderObj);
}
});
});
}else{
setTimeout( ()=>{
if(faderObj.classList.contains("visOff")){
faderObj.style.display="none";
}
}, 1000);
}
}
}
// -- -- --
addRooms( roomList ){
for( let x=0; x<roomList.length; ++x ){
if( !this.pxlEnv.roomNameList.includes( roomList[x] ) ){
if(this.booted){
}else{
this.roomBootList.push( roomList[x] );
}
}
}
}
setBootRoom( bootRoom, bootLocation ){
this.pxlEnv.bootRoom = bootRoom;
this.pxlEnv.bootLocation = bootLocation;
}
// -- -- --
setLoaderPhrases( phraseList ){
this.pxlGuiDraws.setLoaderPhrases( phraseList );
}
// -- -- --
// pxlNav Extension Functions
initExtension( extName, onFinishFn, onErrorFn ){
this.pxlExtensions.init( extName, onFinishFn, onErrorFn );
}
startExtension( extName, onFinishFn, onErrorFn ){
this.pxlExtensions.start( extName, onFinishFn, onErrorFn );
}
stopExtension( extName, onFinishFn, onErrorFn ){
this.pxlExtensions.stop( extName, onFinishFn, onErrorFn );
}
extensionStatus( extName ){
return this.pxlExtensions.status( extName );
}
// -- -- --
// Messaging Into (triggers) and Out-Of (subscriptions) the pxlNav Engine
/**
*
* @param {*} eventType
* @param {*} eventValue
* @param {*} eventObj
* @example
* pxlNav.trigger( "warptoroom", "roomName" );
* pxlNav.trigger( "camera", "roam" );
* pxlNav.trigger( "ping" );
* pxlNav.trigger( "roomMessage", *roomName*, { "type" : "event", "value" : "eventValue" } );
*/
trigger( eventType, eventValue=null, eventObj=null ){
eventType = eventType.toLowerCase();
switch( eventType ){
case "warptoroom":
this.pxlCamera.warpToRoom( eventValue, true, eventObj );
break;
case "camera":
let curEventVal = eventValue.toLowerCase();
if( curEventVal == "roam" ){
this.pxlCamera.toggleMovement( true ); // Enable camera movement from user inputs
}else if( curEventVal == "static" ){
this.pxlCamera.toggleMovement( false ); // Prevent camera movement from user inputs
}
break;
case "ping":
this.emit("pingPong", "pong");
break;
case "roommessage":
let roomEventType = eventObj["type"];
let roomEventValue = eventObj["value"];
if(eventValue==null){
eventValue=this.pxlEnv.currentRoom;
}
this.pxlEnv.sendRoomMessage( eventValue, roomEventType, roomEventValue );
default:
break;
}
}
/**
*
* @param {*} eventType
* @param {*} callbackFunc
*/
subscribe( eventType, callbackFunc ){
let triggerHelp = false;
if( this.validEventsKeys.includes( eventType ) ){
if( eventType == "test" ){
console.log("Test Event : `pxlNav.subscribe( 'test', ... )` was used; subscription list -");
}else if( eventType == "pxlNavEventNameHere" || eventType == "help" ){
if( eventType == "pxlNavEventNameHere" ){
console.warn("Warning : `pxlNav.subscribe( 'pxlNavEventNameHere', ... )` was used; need some help?");
}else if( eventType == "help" ){
console.log("Help Requested : `pxlNav.subscribe( 'help', ... )` was used; Subscription items--");
}
// developer triggered the emit help event
// Dump all the events and info!
console.log("Valid Event Types : ");
this.validEventsKeys.forEach( (e)=>{
console.log( " - ", e, " : ", this.validEvents[e] );
});
}else{
let eventSplit = eventType.split("-");
if( eventSplit[0] == "device" ){
this.pxlDevice.subscribe( eventSplit[1], callbackFunc );
}else if( eventSplit[0] == "camera" ){
let camEventType = pxlEnums.CAMERA_EVENT.MOVE;
// Find the camera event type
switch( eventSplit[1] ){
case "move":
camEventType = pxlEnums.CAMERA_EVENT.MOVE;
break;
case "rotate":
camEventType = pxlEnums.CAMERA_EVENT.ROTATE;
break;
case "jump":
camEventType = pxlEnums.CAMERA_EVENT.JUMP;
break;
case "fall":
camEventType = pxlEnums.CAMERA_EVENT.FALL;
break;
case "landed":
camEventType = pxlEnums.CAMERA_EVENT.LANDED;
break;
case "collision":
camEventType = pxlEnums.CAMERA_EVENT.COLLISION;
break;
default:
console.warn("Warning : `pxlNav.subscribe( 'camera-"+eventSplit[1]+"', ... )` was used; use 'help' for a list of valid events.");
break;
}
this.pxlCamera.subscribe( camEventType, callbackFunc );
}else{
this.callbacks[eventType] = callbackFunc;
}
}
}else{
console.warn("Warning : `pxlNav.subscribe( '"+eventType+"', ... )` was used; use 'help' for a list of valid events.");
}
}
/**
*
* @param {*} eventType
* @param {*} eventValue
* @param {*} statusValue
*/
emit( eventType, eventValue, statusValue=null ){
if( this.callbacks[eventType] ){
let msg = {
"type" : eventType,
"value" : eventValue
};
if( statusValue !== null ){
msg["status"] = statusValue;
}
this.callbacks[eventType]( msg );
}
}
}
export {
pxlNavVersion,
pxlNav,
pxlEnums,
pxlUserSettings,
pxlOptions,
RoomEnvironment,
pxlEffects,
pxlShaders,
pxlBase
};