import { Scene , Color , PerspectiveCamera , WebGLRenderer , sRGBEncoding , Camera , Vector3 , Mesh , CanvasTexture , PlaneBufferGeometry , MeshPhongMaterial , DirectionalLight , AmbientLight , Light , FrontSide , MathUtils , Vector2 , AnimationMixer , Clock , LoopOnce , AnimationClip , Group , LinearFilter , NearestFilter } from 'three' ; import { VRButton } from 'three/examples/jsm/webxr/VRButton.js' ; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' ; import { GLTFLoader , GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js' ; import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory.js' ; import Stats from 'stats.js' ; import { Key } from 'ts-keycode-enum' ; import DosWorkerWrapper from './DosWorkerWrapper' ; enum GamePadAxis { x = 2 , y = 3 } class VRDos { private scene : Scene | null = null ; private camera : PerspectiveCamera | null = null ; private renderer : WebGLRenderer | null = null ; private initialized = false ; private screenMeshName = 'SM_Monitor_Screen_0' ; private gamepads : Gamepad [ ] = [ ] ; private pressThreshold = .5 ; private stats = new Stats ( ) ; private isDev = document . location . port === '1234' ; private dosCanvas = document . createElement ( 'canvas' ) ; private dosTexture = new CanvasTexture ( this . dosCanvas ) ; private dos = new DosWorkerWrapper ( this . dosCanvas , this . dosTexture ) ; private animationMixer : AnimationMixer | null = null ; private animationClips : AnimationClip [ ] = [ ] ; private clock = new Clock ( ) ; private isLoading = true ; private userHeight = 0 ; private keyStatus : { [ code ] : string ; } = { } ; private gamePadButtonMap : { [ vrButtonIndex ] : number [ ] ; } = { 0 : [ Key . Enter ] , 1 : [ Key . Shift ] , 3 : [ Key . Ctrl ] , 4 : [ Key . Space , Key . Shift , Key . Ctrl ] , 5 : [ Key . Ctrl , Key . Q , Key . Escape ] } ; vrUser : Group = new Group ( ) ; get devicePixelRatio ( ) : number { return window . devicePixelRatio ; } get width ( ) : number { return window . innerWidth ; } get height ( ) : number { return window . innerHeight ; } get aspectRatio ( ) : number { return ( this . width / this . height ) ; } private setLoading ( loading , text = 'Loading...' ) { const ctx = this . dosCanvas . getContext ( '2d' ) ; if ( loading ) { if ( ctx && this . dosTexture ) { this . dosCanvas . width = 512 ; this . dosCanvas . height = 512 ; ctx . font = 'bold 15px Verdana' ; ctx . fillStyle = 'green' ; ctx . fillText ( text , 40 , 40 ) ; } this . dosTexture . needsUpdate = true ; this . isLoading = loading ; } else { this . isLoading = loading ; } ; } public emulateKeyEvent ( code , type ) { if ( ! this . keyStatus [ code ] && type === 'keydown' ) { this . dos . sendKey ( code , type ) ; this . keyStatus [ code ] = type ; } else if ( this . keyStatus [ code ] === 'keydown' && type === 'keyup' ) { this . dos . sendKey ( code , type ) ; delete this . keyStatus [ code ] ; } } private processGamepadsInputs ( ) { if ( this . gamepads . length === 0 ) { return ; } const gamepad = this . gamepads [ this . gamepads . length - 1 ] ; for ( let ai = 0 ; ai < gamepad . axes . length ; ai ++ ) { const value = gamepad . axes [ ai ] ; if ( ai === GamePadAxis . x ) { if ( value > this . pressThreshold ) { this . emulateKeyEvent ( Key . RightArrow , 'keydown' ) ; } else if ( value < - this . pressThreshold ) { this . emulateKeyEvent ( Key . LeftArrow , 'keydown' ) ; } else { this . emulateKeyEvent ( Key . RightArrow , 'keyup' ) ; this . emulateKeyEvent ( Key . LeftArrow , 'keyup' ) ; } } if ( ai === GamePadAxis . y ) { if ( value > this . pressThreshold ) { this . emulateKeyEvent ( Key . DownArrow , 'keydown' ) ; } else if ( value < - this . pressThreshold ) { this . emulateKeyEvent ( Key . UpArrow , 'keydown' ) ; } else { this . emulateKeyEvent ( Key . UpArrow , 'keyup' ) ; this . emulateKeyEvent ( Key . DownArrow , 'keyup' ) ; } } } for ( let bi = 0 ; bi < gamepad . buttons . length ; bi ++ ) { const button = gamepad . buttons [ bi ] ; const map = this . gamePadButtonMap [ bi ] ; if ( map ) { map . forEach ( key => { const action = button . pressed ? 'keydown' : 'keyup' ; this . emulateKeyEvent ( key , action ) ; } ) ; } } ; } render ( time , xrFrame ) { if ( this . animationMixer ) { const deltaTime = this . clock . getDelta ( ) ; this . animationMixer . update ( deltaTime ) ; } if ( this . userHeight === 0 && xrFrame ) { const xrPose = xrFrame . getViewerPose ( this . renderer ?. xr . getReferenceSpace ( ) ) ; if ( xrPose ) { this . userHeight = xrPose ?. transform . position . y ; } } if ( ! this . isLoading ) { this . fixTextureSize ( this . dosCanvas , this . dosTexture ) ; this . processGamepadsInputs ( ) ; } this . renderer ?. render ( < Scene > this . scene , < Camera > this . camera ) ; if ( this . isDev ) { this . stats . update ( ) ; } } private createScene ( color = new Color ( 'white' ) ) { const scene = new Scene ( ) ; scene . castShadow = true ; scene . background = color ; return scene ; } private createCamera ( fov , ratio , initialPosition , near = 0.1 , far = 30 ) { const camera = new PerspectiveCamera ( fov , ratio , near , far ) ; camera . position . copy ( initialPosition ) ; return camera ; } private createRenderer ( ) { const renderer = new WebGLRenderer ( { antialias : true , alpha : false , } ) ; renderer . xr . enabled = true ; renderer . setPixelRatio ( devicePixelRatio ) ; renderer . setSize ( window . innerWidth , window . innerHeight ) ; renderer . outputEncoding = sRGBEncoding ; return renderer ; } private createOrbitControls ( camera , container , target ) { const controls = new OrbitControls ( camera , container ) ; controls . target . copy ( target ) ; controls . update ( ) ; return controls ; } private createLights ( ) { var light = new DirectionalLight ( 0xFFFFFF ) ; light . position . set ( 3 , 5 , 3 ) ; const alight = new AmbientLight ( 0xFFFFFF , 0.2 ) ; return [ light , alight ] ; } private handleResize ( ) { if ( this . camera && this . renderer ) { this . camera . aspect = this . aspectRatio ; this . camera . updateProjectionMatrix ( ) ; this . renderer . setSize ( this . width , this . height ) ; } } private setupVRControllers ( ) { const xrManager = this . renderer ?. xr ; const scene = this . scene ; if ( xrManager && scene ) { const controllers = [ xrManager . getController ( 0 ) , xrManager . getController ( 1 ) ] ; controllers . forEach ( controller => { controller . addEventListener ( 'connected' , event => { const controller = < XRInputSource > event . data ; if ( controller . gamepad ) { this . gamepads . push ( controller . gamepad ) ; } } ) ; controller . addEventListener ( 'disconnected' , event => { const controller = < XRInputSource > event . data ; this . gamepads = this . gamepads . filter ( gamepad => controller . gamepad ?. id !== gamepad . id ) ; } ) ; } ) ; const controllerModelFactory = new XRControllerModelFactory ( ) ; const controllerGrips = [ xrManager . getControllerGrip ( 0 ) , xrManager . getControllerGrip ( 1 ) ] ; controllerGrips . forEach ( grip => { grip . add ( controllerModelFactory . createControllerModel ( grip ) ) ; this . vrUser . add ( grip ) ; } ) ; } } async init ( domElement ) { if ( this . initialized ) { return ; } if ( this . isDev ) { this . stats . showPanel ( 0 ) ; document . body . appendChild ( this . stats . dom ) ; } this . renderer = this . createRenderer ( ) ; const computerMonitorHeight = .7 ; this . scene = this . createScene ( ) ; const cameraPosition = new Vector3 ( 0 , computerMonitorHeight , 0 ) ; const fov = 65 ; this . camera = this . createCamera ( fov , this . aspectRatio , cameraPosition ) ; this . vrUser . position . set ( 0 , 0 , 0 ) ; this . vrUser . add ( this . camera ) ; this . scene . add ( this . vrUser ) ; this . createOrbitControls ( this . camera , this . renderer . domElement , new Vector3 ( 0 , computerMonitorHeight , 0 ) ) ; this . renderer . xr . addEventListener ( 'sessionstart' , ( ) => { setTimeout ( ( ) => { const diff = computerMonitorHeight - this . userHeight ; this . vrUser . position . set ( 0 , diff , 0 ) ; } , 100 ) ; } ) ; this . renderer . xr . addEventListener ( 'sessionend' , ( ) => { this . userHeight = 0 ; this . vrUser . position . set ( 0 , 0 , 0 ) ; } ) ; this . createLights ( ) . forEach ( light => this . scene ?. add ( light ) ) ; this . setupVRControllers ( ) ; if ( domElement ) { domElement . appendChild ( this . renderer . domElement ) ; domElement . appendChild ( VRButton . createButton ( this . renderer , { referenceSpaceType : 'local-floor' } ) ) ; } else { throw Error ( 'Missing container dom element' ) ; } window . addEventListener ( 'resize' , this . handleResize . bind ( this ) ) ; const roomGLTF = await this . loadGLTF ( 'room.glb' ) ; const roomMesh = < Mesh > roomGLTF . scene . children [ 0 ] ; this . scene . add ( roomMesh ) ; this . animationClips = roomGLTF . animations ; const roomScreenMesh = < Mesh > this . scene . getObjectByName ( this . screenMeshName ) ; if ( roomScreenMesh ) { await this . attachDosScreen ( roomScreenMesh ) ; } else { throw new Error ( 'Screen mesh not found' + this . screenMeshName ) ; } this . renderer . setAnimationLoop ( this . render . bind ( this ) ) ; this . initialized = true ; } public async run ( archiveUrl , commands = [ ] ) { this . setLoading ( true , `Please wait...` ) ; await this . playIntro ( ) ; this . setLoading ( true , `Booting ${ archiveUrl }...` ) ; await this . dos . run ( archiveUrl , commands ) ; this . setLoading ( false ) ; } private async playIntro ( ) { return new Promise ( ( resolve ) => { if ( this . scene && this . dosCanvas ) { const mixer = this . animationMixer = new AnimationMixer ( this . scene ) ; const introClip = this . animationClips [ 0 ] ; const action = mixer . clipAction ( introClip ) ; action . clampWhenFinished = true ; action . loop = LoopOnce ; mixer . addEventListener ( 'finished' , ( ) => { this . animationMixer = null ; resolve ( ) ; } ) ; action . play ( ) ; } else { resolve ( ) ; } } ) ; } private loadGLTF ( path ) { const promise = new Promise ( ( resolve , reject ) => { const gltfLoader = new GLTFLoader ( ) ; gltfLoader . load ( path , ( gltf ) => resolve ( gltf ) , ( ) => { } , ( msg ) => reject ( msg ) ) ; } ) ; return < Promise < GLTF > > promise ; } private fixTextureSize ( canvas , texture ) { if ( ! MathUtils . isPowerOfTwo ( canvas . width ) ) { canvas . width = MathUtils . ceilPowerOfTwo ( canvas . width ) ; canvas . height = MathUtils . ceilPowerOfTwo ( canvas . height ) ; texture . offset = new Vector2 ( 0 , 0.2 ) ; texture . repeat . set ( 0.63 , 0.8 ) ; } } private attachDosScreen ( dosScreen ) { const material = new MeshPhongMaterial ( { side : FrontSide , shininess : 40 , specular : new Color ( 0xffffff ) , map : this . dosTexture , } ) ; const geo = new PlaneBufferGeometry ( .245 , .23 ) ; const realScreen = new Mesh ( geo , material ) ; realScreen . position . set ( - .25 , .2 , .1223 ) ; realScreen . rotateY ( - Math . PI / 2 ) ; dosScreen . add ( realScreen ) ; } } export default VRDos ;