import * as THREE from 'three' ; import * as CANNON from 'cannon' ; import Swal from 'sweetalert2' ; import * as $ from 'jquery' ; import { CameraOperator } from '../core/CameraOperator' ; import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer' ; import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass' ; import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass' ; import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader' ; import { Detector } from '../../lib/utils/Detector' ; import { Stats } from '../../lib/utils/Stats' ; import * as GUI from '../../lib/utils/dat.gui' ; import { CannonDebugRenderer } from '../../lib/cannon/CannonDebugRenderer' ; import * as _ from 'lodash' ; import { InputManager } from '../core/InputManager' ; import * as Utils from '../core/FunctionLibrary' ; import { LoadingManager } from '../core/LoadingManager' ; import { InfoStack } from '../core/InfoStack' ; import { UIManager } from '../core/UIManager' ; import { IWorldEntity } from '../interfaces/IWorldEntity' ; import { IUpdatable } from '../interfaces/IUpdatable' ; import { Character } from '../characters/Character' ; import { Path } from './Path' ; import { CollisionGroups } from '../enums/CollisionGroups' ; import { BoxCollider } from '../physics/colliders/BoxCollider' ; import { TrimeshCollider } from '../physics/colliders/TrimeshCollider' ; import { Vehicle } from '../vehicles/Vehicle' ; import { Scenario } from './Scenario' ; import { Sky } from './Sky' ; import { Ocean } from './Ocean' ; export class World { public renderer : THREE . WebGLRenderer ; public camera : THREE . PerspectiveCamera ; public composer : any ; public stats : Stats ; public graphicsWorld : THREE . Scene ; public sky : Sky ; public physicsWorld : CANNON . World ; public parallelPairs : any [ ] ; public physicsFrameRate : number ; public physicsFrameTime : number ; public physicsMaxPrediction : number ; public clock : THREE . Clock ; public renderDelta : number ; public logicDelta : number ; public requestDelta : number ; public sinceLastFrame : number ; public justRendered : boolean ; public params : any ; public inputManager : InputManager ; public cameraOperator : CameraOperator ; public timeScaleTarget : number = 1 ; public console : InfoStack ; public cannonDebugRenderer : CannonDebugRenderer ; public scenarios : Scenario [ ] = [ ] ; public characters : Character [ ] = [ ] ; public vehicles : Vehicle [ ] = [ ] ; public paths : Path [ ] = [ ] ; public scenarioGUIFolder : any ; public updatables : IUpdatable [ ] = [ ] ; private lastScenarioID : string ; constructor ( worldScenePath ? ) { const scope = this ; if ( ! Detector . webgl ) { Swal . fire ( { icon : 'warning' , title : 'WebGL compatibility' , text : 'This browser doesn\'t seem to have the required WebGL capabilities. The application may not work correctly.' , footer : '<a href="https://get.webgl.org/" target="_blank">Click here for more information</a>' , showConfirmButton : false , buttonsStyling : false } ) ; } this . renderer = new THREE . WebGLRenderer ( ) ; this . renderer . setPixelRatio ( window . devicePixelRatio ) ; this . renderer . setSize ( window . innerWidth , window . innerHeight ) ; this . renderer . toneMapping = THREE . ACESFilmicToneMapping ; this . renderer . toneMappingExposure = 1.0 ; this . renderer . shadowMap . enabled = true ; this . renderer . shadowMap . type = THREE . PCFSoftShadowMap ; this . generateHTML ( ) ; function onWindowResize ( ) { scope . camera . aspect = window . innerWidth / window . innerHeight ; scope . camera . updateProjectionMatrix ( ) ; scope . renderer . setSize ( window . innerWidth , window . innerHeight ) ; fxaaPass . uniforms [ 'resolution' ] . value . set ( 1 / ( window . innerWidth * pixelRatio ) , 1 / ( window . innerHeight * pixelRatio ) ) ; scope . composer . setSize ( window . innerWidth * pixelRatio , window . innerHeight * pixelRatio ) ; } window . addEventListener ( 'resize' , onWindowResize , false ) ; this . graphicsWorld = new THREE . Scene ( ) ; this . camera = new THREE . PerspectiveCamera ( 80 , window . innerWidth / window . innerHeight , 0.1 , 1010 ) ; let renderPass = new RenderPass ( this . graphicsWorld , this . camera ) ; let fxaaPass = new ShaderPass ( FXAAShader ) ; let pixelRatio = this . renderer . getPixelRatio ( ) ; fxaaPass . material [ 'uniforms' ] . resolution . value . x = 1 / ( window . innerWidth * pixelRatio ) ; fxaaPass . material [ 'uniforms' ] . resolution . value . y = 1 / ( window . innerHeight * pixelRatio ) ; this . composer = new EffectComposer ( this . renderer ) ; this . composer . addPass ( renderPass ) ; this . composer . addPass ( fxaaPass ) ; this . physicsWorld = new CANNON . World ( ) ; this . physicsWorld . gravity . set ( 0 , - 9.81 , 0 ) ; this . physicsWorld . broadphase = new CANNON . SAPBroadphase ( this . physicsWorld ) ; this . physicsWorld . solver . iterations = 10 ; this . physicsWorld . allowSleep = true ; this . parallelPairs = [ ] ; this . physicsFrameRate = 60 ; this . physicsFrameTime = 1 / this . physicsFrameRate ; this . physicsMaxPrediction = this . physicsFrameRate ; this . clock = new THREE . Clock ( ) ; this . renderDelta = 0 ; this . logicDelta = 0 ; this . sinceLastFrame = 0 ; this . justRendered = false ; this . stats = Stats ( ) ; this . createParamsGUI ( scope ) ; this . inputManager = new InputManager ( this , this . renderer . domElement ) ; this . cameraOperator = new CameraOperator ( this , this . camera , this . params . Mouse_Sensitivity ) ; this . sky = new Sky ( this ) ; if ( worldScenePath !== undefined ) { let loadingManager = new LoadingManager ( this ) ; loadingManager . onFinishedCallback = ( ) => { this . update ( 1 , 1 ) ; this . setTimeScale ( 1 ) ; Swal . fire ( { title : 'Welcome to Sketchbook!' , text : 'Feel free to explore the world and interact with available vehicles. There are also various scenarios ready to launch from the right panel.' , footer : '<a href="https://github.com/swift502/Sketchbook" target="_blank">GitHub page</a><a href="https://discord.gg/fGuEqCe" target="_blank">Discord server</a>' , confirmButtonText : 'Okay' , buttonsStyling : false , onClose : ( ) => { UIManager . setUserInterfaceVisible ( true ) ; } } ) ; } ; loadingManager . loadGLTF ( worldScenePath , ( gltf ) => { this . loadScene ( loadingManager , gltf ) ; } ) ; } else { UIManager . setUserInterfaceVisible ( true ) ; UIManager . setLoadingScreenVisible ( false ) ; Swal . fire ( { icon : 'success' , title : 'Hello world!' , text : 'Empty Sketchbook world was succesfully initialized. Enjoy the blueness of the sky.' , buttonsStyling : false } ) ; } this . render ( this ) ; } public update ( timeStep , unscaledTimeStep ) { this . updatePhysics ( timeStep ) ; this . updatables . forEach ( ( entity ) => { entity . update ( timeStep , unscaledTimeStep ) ; } ) ; this . params . Time_Scale = THREE . MathUtils . lerp ( this . params . Time_Scale , this . timeScaleTarget , 0.2 ) ; if ( this . params . Debug_Physics ) this . cannonDebugRenderer . update ( ) ; } public updatePhysics ( timeStep ) { this . physicsWorld . step ( this . physicsFrameTime , timeStep ) ; this . characters . forEach ( ( char ) => { if ( this . isOutOfBounds ( char . characterCapsule . body . position ) ) { this . outOfBoundsRespawn ( char . characterCapsule . body ) ; } } ) ; this . vehicles . forEach ( ( vehicle ) => { if ( this . isOutOfBounds ( vehicle . rayCastVehicle . chassisBody . position ) ) { let worldPos = new THREE . Vector3 ( ) ; vehicle . spawnPoint . getWorldPosition ( worldPos ) ; worldPos . y += 1 ; this . outOfBoundsRespawn ( vehicle . rayCastVehicle . chassisBody , Utils . cannonVector ( worldPos ) ) ; } } ) ; } public isOutOfBounds ( position ) { let inside = position . x > - 211.882 && position . x < 211.882 && position . z > - 169.098 && position . z < 153.232 && position . y > 0.107 ; let belowSeaLevel = position . y < 14.989 ; return ! inside && belowSeaLevel ; } public outOfBoundsRespawn ( body , position ? ) { let newPos = position || new CANNON . Vec3 ( 0 , 16 , 0 ) ; let newQuat = new CANNON . Quaternion ( 0 , 0 , 0 , 1 ) ; body . position . copy ( newPos ) ; body . interpolatedPosition . copy ( newPos ) ; body . quaternion . copy ( newQuat ) ; body . interpolatedQuaternion . copy ( newQuat ) ; body . velocity . setZero ( ) ; body . angularVelocity . setZero ( ) ; } param { World } world public render ( world ) { this . requestDelta = this . clock . getDelta ( ) ; requestAnimationFrame ( ( ) => { world . render ( world ) ; } ) ; let unscaledTimeStep = ( this . requestDelta + this . renderDelta + this . logicDelta ) ; let timeStep = unscaledTimeStep * this . params . Time_Scale ; timeStep = Math . min ( timeStep , 1 / 30 ) ; world . update ( timeStep , unscaledTimeStep ) ; this . logicDelta = this . clock . getDelta ( ) ; let interval = 1 / 60 ; this . sinceLastFrame += this . requestDelta + this . renderDelta + this . logicDelta ; this . sinceLastFrame %= interval ; this . stats . end ( ) ; this . stats . begin ( ) ; if ( this . params . FXAA ) this . composer . render ( ) ; else this . renderer . render ( this . graphicsWorld , this . camera ) ; this . renderDelta = this . clock . getDelta ( ) ; } public setTimeScale ( value ) { this . params . Time_Scale = value ; this . timeScaleTarget = value ; } public add ( worldEntity ) { worldEntity . addToWorld ( this ) ; this . registerUpdatable ( worldEntity ) ; } public registerUpdatable ( registree ) { this . updatables . push ( registree ) ; this . updatables . sort ( ( a , b ) => ( a . updateOrder > b . updateOrder ) ? 1 : - 1 ) ; } public remove ( worldEntity ) { worldEntity . removeFromWorld ( this ) ; this . unregisterUpdatable ( worldEntity ) ; } public unregisterUpdatable ( registree ) { _ . pull ( this . updatables , registree ) ; } public loadScene ( loadingManager , gltf ) { gltf . scene . traverse ( ( child ) => { if ( child . hasOwnProperty ( 'userData' ) ) { if ( child . type === 'Mesh' ) { Utils . setupMeshProperties ( child ) ; this . sky . csm . setupMaterial ( child . material ) ; if ( child . material . name === 'ocean' ) { this . registerUpdatable ( new Ocean ( child , this ) ) ; } } if ( child . userData . hasOwnProperty ( 'data' ) ) { if ( child . userData . data === 'physics' ) { if ( child . userData . hasOwnProperty ( 'type' ) ) { if ( child . userData . type === 'box' ) { let phys = new BoxCollider ( { size : new THREE . Vector3 ( child . scale . x , child . scale . y , child . scale . z ) } ) ; phys . body . position . copy ( Utils . cannonVector ( child . position ) ) ; phys . body . quaternion . copy ( Utils . cannonQuat ( child . quaternion ) ) ; phys . body . computeAABB ( ) ; phys . body . shapes . forEach ( ( shape ) => { shape . collisionFilterMask = ~ CollisionGroups . TrimeshColliders ; } ) ; this . physicsWorld . addBody ( phys . body ) ; } else if ( child . userData . type === 'trimesh' ) { let phys = new TrimeshCollider ( child , { } ) ; this . physicsWorld . addBody ( phys . body ) ; } child . visible = false ; } } if ( child . userData . data === 'path' ) { this . paths . push ( new Path ( child ) ) ; } if ( child . userData . data === 'scenario' ) { this . scenarios . push ( new Scenario ( child , this ) ) ; } } } } ) ; this . graphicsWorld . add ( gltf . scene ) ; let defaultScenarioID ; for ( const scenario of this . scenarios ) { if ( scenario . default ) { defaultScenarioID = scenario . id ; break ; } } if ( defaultScenarioID !== undefined ) this . launchScenario ( defaultScenarioID , loadingManager ) ; } public launchScenario ( scenarioID , loadingManager ? ) { this . lastScenarioID = scenarioID ; this . clearEntities ( ) ; if ( ! loadingManager ) loadingManager = new LoadingManager ( this ) ; for ( const scenario of this . scenarios ) { if ( scenario . id === scenarioID || scenario . spawnAlways ) { scenario . launch ( loadingManager , this ) ; } } } public restartScenario ( ) { if ( this . lastScenarioID !== undefined ) { document . exitPointerLock ( ) ; this . launchScenario ( this . lastScenarioID ) ; } else { console . warn ( 'Can\'t restart scenario. Last scenarioID is undefined.' ) ; } } public clearEntities ( ) { for ( let i = 0 ; i < this . characters . length ; i ++ ) { this . remove ( this . characters [ i ] ) ; i -- ; } for ( let i = 0 ; i < this . vehicles . length ; i ++ ) { this . remove ( this . vehicles [ i ] ) ; i -- ; } } public scrollTheTimeScale ( scrollAmount ) { const timeScaleBottomLimit = 0.003 ; const timeScaleChangeSpeed = 1.3 ; if ( scrollAmount > 0 ) { this . timeScaleTarget /= timeScaleChangeSpeed ; if ( this . timeScaleTarget < timeScaleBottomLimit ) this . timeScaleTarget = 0 ; } else { this . timeScaleTarget *= timeScaleChangeSpeed ; if ( this . timeScaleTarget < timeScaleBottomLimit ) this . timeScaleTarget = timeScaleBottomLimit ; this . timeScaleTarget = Math . min ( this . timeScaleTarget , 1 ) ; } } public updateControls ( controls ) { let html = '' ; html += '<h2 class="controls-title">Controls:</h2>' ; controls . forEach ( ( row ) => { html += '<div class="ctrl-row">' ; row . keys . forEach ( ( key ) => { if ( key === '+' || key === 'and' || key === 'or' || key === '&' ) html += '&nbsp;' + key + '&nbsp;' ; else html += '<span class="ctrl-key">' + key + '</span>' ; } ) ; html += '<span class="ctrl-desc">' + row . desc + '</span></div>' ; } ) ; document . getElementById ( 'controls' ) . innerHTML = html ; } private generateHTML ( ) { $ ( 'head' ) . append ( '<link href="https://fonts.googleapis.com/css2?family=Alfa+Slab+One&display=swap" rel="stylesheet">' ) ; $ ( 'head' ) . append ( '<link href="https://fonts.googleapis.com/css2?family=Solway:wght@400;500;700&display=swap" rel="stylesheet">' ) ; $ ( 'head' ) . append ( '<link href="https://fonts.googleapis.com/css2?family=Cutive+Mono&display=swap" rel="stylesheet">' ) ; $ ( `	<div id="loading-screen">
				<div id="loading-screen-background"></div>
				<h1 id="main-title" class="sb-font">Sketchbook 0.4</h1>
				<div class="cubeWrap">
					<div class="cube">
						<div class="faces1"></div>
						<div class="faces2"></div>     
					</div> 
				</div> 
				<div id="loading-text">Loading...</div>
			</div>
		` ) . appendTo ( 'body' ) ; $ ( `	<div id="ui-container" style="display: none;">
				<div class="github-corner">
					<a href="https://github.com/swift502/Sketchbook" target="_blank" title="Fork me on GitHub">
						<svg viewbox="0 0 100 100" fill="currentColor">
							<title>Fork me on GitHub</title>
							<path d="M0 0v100h100V0H0zm60 70.2h.2c1 2.7.3 4.7 0 5.2 1.4 1.4 2 3 2 5.2 0 7.4-4.4 9-8.7 9.5.7.7 1.3 2
							1.3 3.7V99c0 .5 1.4 1 1.4 1H44s1.2-.5 1.2-1v-3.8c-3.5 1.4-5.2-.8-5.2-.8-1.5-2-3-2-3-2-2-.5-.2-1-.2-1
							2-.7 3.5.8 3.5.8 2 1.7 4 1 5 .3.2-1.2.7-2 1.2-2.4-4.3-.4-8.8-2-8.8-9.4 0-2 .7-4 2-5.2-.2-.5-1-2.5.2-5
							0 0 1.5-.6 5.2 1.8 1.5-.4 3.2-.6 4.8-.6 1.6 0 3.3.2 4.8.7 2.8-2 4.4-2 5-2z"></path>
						</svg>
					</a>
				</div>
				<div class="left-panel">
					<div id="controls" class="panel-segment flex-bottom"></div>
				</div>
			</div>
		` ) . appendTo ( 'body' ) ; document . body . appendChild ( this . renderer . domElement ) ; this . renderer . domElement . id = 'canvas' ; } private createParamsGUI ( scope ) { this . params = { Pointer_Lock : true , Mouse_Sensitivity : 0.3 , Time_Scale : 1 , Shadows : true , FXAA : true , Debug_Physics : false , Debug_FPS : false , Sun_Elevation : 50 , Sun_Rotation : 145 , } ; const gui = new GUI . GUI ( ) ; this . scenarioGUIFolder = gui . addFolder ( 'Scenarios' ) ; this . scenarioGUIFolder . open ( ) ; let worldFolder = gui . addFolder ( 'World' ) ; worldFolder . add ( this . params , 'Time_Scale' , 0 , 1 ) . listen ( ) . onChange ( ( value ) => { scope . timeScaleTarget = value ; } ) ; worldFolder . add ( this . params , 'Sun_Elevation' , 0 , 180 ) . listen ( ) . onChange ( ( value ) => { scope . sky . phi = value ; } ) ; worldFolder . add ( this . params , 'Sun_Rotation' , 0 , 360 ) . listen ( ) . onChange ( ( value ) => { scope . sky . theta = value ; } ) ; let settingsFolder = gui . addFolder ( 'Settings' ) ; settingsFolder . add ( this . params , 'FXAA' ) ; settingsFolder . add ( this . params , 'Shadows' ) . onChange ( ( enabled ) => { if ( enabled ) { this . sky . csm . lights . forEach ( ( light ) => { light . castShadow = true ; } ) ; } else { this . sky . csm . lights . forEach ( ( light ) => { light . castShadow = false ; } ) ; } } ) ; settingsFolder . add ( this . params , 'Pointer_Lock' ) . onChange ( ( enabled ) => { scope . inputManager . setPointerLock ( enabled ) ; } ) ; settingsFolder . add ( this . params , 'Mouse_Sensitivity' , 0 , 1 ) . onChange ( ( value ) => { scope . cameraOperator . setSensitivity ( value , value * 0.8 ) ; } ) ; settingsFolder . add ( this . params , 'Debug_Physics' ) . onChange ( ( enabled ) => { if ( enabled ) { this . cannonDebugRenderer = new CannonDebugRenderer ( this . graphicsWorld , this . physicsWorld ) ; } else { this . cannonDebugRenderer . clearMeshes ( ) ; this . cannonDebugRenderer = undefined ; } scope . characters . forEach ( ( char ) => { char . raycastBox . visible = enabled ; } ) ; } ) ; settingsFolder . add ( this . params , 'Debug_FPS' ) . onChange ( ( enabled ) => { UIManager . setFPSVisible ( enabled ) ; } ) ; gui . open ( ) ; } }