UNPKG

@vrspace/babylonjs

Version:

vrspace.org babylonjs client

909 lines (836 loc) 31.6 kB
import { VRSPACEUI, VRSpaceAPI, World, Buttons, LogoRoom, Portal, WorldManager, HumanoidAvatar, VideoAvatar, AvatarController, OpenViduStreams, ServerFile, LoginForm, DefaultHud, ServerFolder, Skybox } from './js/vrspace-min.js'; export class AvatarSelection extends World { constructor() { super(); /** server to connect to */ this.serverUrl = null; /** content base, defaults to VRSPACEUI.contentBase */ this.contentBase = VRSPACEUI.contentBase; /** background base dir, null defaults to contentBase+"/content/skybox/mp_drakeq/drakeq" (box) or "/content/skybox/eso_milkyway/eso0932a.jpg" (panoramic)*/ this.backgroundPath = null; /** Is backgroundPath a panoramic image? Default false (directory containing 6 images) */ this.backgroundPanorama = false; /** character base dir, null defaults to contentBase+'/content/char/' */ this.characterPath = null; /** character animation folder, null defaults to contentBase+'/content/rpm-anim/' */ this.animationPath = null; /** world base dir, null defaults to contentBase+'/content/worlds' */ this.worldPath = null; /** function to call just before entering a world */ this.beforeEnter = null; /** function to call after entering a world */ this.afterEnter = null; /** function to call after exiting a world */ this.afterExit = null; /** whether to list animations after character loads, default true */ this.showAnimationButtons = true; /** enable oauth2 login form, default true */ this.enableLogin = true; /** wheter to display own video avatar, default true */ this.displayOwnVideo = true; /** custom video avatar options, default null */ this.customOptions = null; /** movement tracking/animation frames per second */ this.fps = 25; /** Enable Oauth2 login */ this.oauth2enabled = true; this.oauth2providerId = null; /** default user height, 1.8 m */ this.userHeight = 1.8; /** is anonymous entry (guest login) allowed */ this.anonymousAllowed = true; /** enable plenty of debug info */ this.debug = false; /** z position of character and animation buttons */ this.buttonsZ = -1; // state variables this.mirror = true; this.authenticated = false; this.customAnimations = []; this.customAvatarFrame = document.getElementById('customAvatarFrame'); this.trackTime = Date.now(); this.trackDelay = 1000 / this.fps; this.api = VRSpaceAPI.getInstance(VRSPACEUI.contentBase); this.tokens = {}; this.serviceWorker = "./serviceworker.js"; this.autoEnter = null; /** @type {HumanoidAvatar} */ this.character = null; /** @type {VideoAvatar} */ this.video = null; } async createSkyBox() { if (this.backgroundPanorama) { var skybox = new BABYLON.PhotoDome("skyDome", this.backgroundDir(), { resolution: 32, size: 1000 }, this.scene ); } else { var skybox = new Skybox(this.scene, this.backgroundDir(), 1); skybox.rotation = new BABYLON.Vector3(0, Math.PI, 0); skybox.create(); } return skybox; } async createCamera() { // Add a camera to the scene and attach it to the canvas this.camera = this.firstPersonCamera(new BABYLON.Vector3(0, 2, -5)); this.camera.setTarget(new BABYLON.Vector3(0, 1.5, 0)); } async createLights() { // Add lights to the scene this.hemisphere = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(1, 1, 0), this.scene); var point = new BABYLON.PointLight("light2", new BABYLON.Vector3(1, 3, -3), this.scene); return point; } async createShadows() { // Shadows this.shadowGenerator = new BABYLON.ShadowGenerator(1024, this.light); this.shadowGenerator.useExponentialShadowMap = true; // slower: //this.shadowGenerator.useBlurExponentialShadowMap = true; //this.shadowGenerator.blurKernel = 32; // hair is usually semi-transparent, this allows it to cast shadow: this.shadowGenerator.transparencyShadow = true; } async createGround() { this.room = await new LogoRoom(this.scene).load(); this.ground = this.room.ground; } async createPhysics() { // 1g makes nasty floor collisions this.scene.gravity = new BABYLON.Vector3(0, -0.1, 0); super.createPhysics(); } async createUI() { this.hud = new DefaultHud(this.scene); this.hud.init(); let providers = await this.api.listOAuthProviders(); this.loginForm = new LoginForm( (text) => this.setMyName(text), () => this.checkValidName(), (providerId, providerName) => { if (this.oauth2enabled) { this.api.oauth2login(providerId, this.userName, this.avatarUrl("")); } }, providers ); // position the form just in front of avatar // make room for virtual keyboard, and resize/mirror buttons if (this.enableLogin) { this.loginForm.position = new BABYLON.Vector3(.25, .8, -1); this.loginForm.init(); // starts speech recognition } // for authenticated user: // fetch user data, set avatar // subscribe to web push notifications this.api.getAuthenticated().then(isAuthenticated => { this.hud.setAuthenticated(isAuthenticated); if (isAuthenticated) { this.authenticated = true; this.api.getUserName().then(name => { this.setMyName(name); }); this.api.getUserObject().then(me => { if (me) { this.oauth2providerId = me.oauth2provider; console.log("user mesh " + me.mesh, me); if (me.mesh) { if (me.mesh == "video") { this.createVideoAvatar(); } else { this.loadCharacterUrl(me.mesh); } } this.loginForm.dispose(); } else { console.log("WARNING: user is logged in but has no avatar"); // TODO // apparently user can be authenticated via Oauth2 but not to vrspace server } }); // this may not work for new clients, as they do not exist in the database yet // they get created only after entering any world for the first time // the only safe place to subscribe is after entering the world //this.webpushSubscribe(); } }); } webpushSubscribe() { this.api.webpushSubscribe(this.serviceWorker); } // CHECKME this is confusing as it enables/disables portals checkValidName() { let ret = true; if (this.authenticated) { this.portalsEnabled(true); } else { console.log('checking name ' + this.userName); if (this.userName) { this.api.verifyName(this.userName).then(validName => { console.log("Valid name: " + validName); if (validName) { this.loginForm.defaultLabel(); canvas.focus(); } else { //this.loginForm.setLabel("INVALID NAME, try another:"); this.loginForm.setLabel("Existing name, log in:"); ret = false; } this.portalsEnabled(validName); }); } else { this.loginForm.defaultLabel(); this.portalsEnabled(this.anonymousAllowed); } } return ret; } backgroundDir() { if (this.backgroundPath) { return this.backgroundPath; } if (this.backgroundPanorama) { return this.contentBase + "/content/skybox/eso_milkyway/eso0932a.jpg"; } return this.contentBase + "/content/skybox/mp_drakeq/drakeq"; //return this.contentBase + "/content/skybox/eso_milkyway/milkyway"; } characterDir() { if (this.characterPath) { return this.characterPath; } return this.contentBase + '/content/char/'; } animationDir() { if (this.animationPath) { return this.animationPath; } return this.contentBase + '/content/rpm-anim/'; } worldDir() { if (this.worldPath) { return this.worldPath; } return this.contentBase + '/content/worlds'; } isSelectableMesh(mesh) { return mesh == this.ground || this.loginForm && this.loginForm.isSelectableMesh(mesh) || mesh.name && (mesh.name.startsWith("Button") || mesh.name.startsWith("PortalEntrance")) || super.isSelectableMesh(mesh); } getFloorMeshes() { return [this.ground]; } load(name, file) { this.loaded(file, null); this.initXR(); } trackXrDevices() { if (this.tracking && this.trackTime + this.trackDelay < Date.now() && this.character && this.character.body && this.character.body.processed && !this.character.activeAnimation ) { this.trackTime = Date.now(); // CHECKME: mirror left-right if (this.xrHelper.controller.left) { if (this.mirror) { var leftPos = this.calcControllerPos(this.character.body.leftArm, 'left'); leftPos.z = -leftPos.z; this.character.reachFor(this.character.body.leftArm, leftPos); } else { var leftPos = this.calcControllerPos(this.character.body.rightArm, 'left'); this.character.reachFor(this.character.body.rightArm, leftPos); } } if (this.xrHelper.controller.right) { if (this.mirror) { var rightPos = this.calcControllerPos(this.character.body.rightArm, 'right'); rightPos.z = -rightPos.z; this.character.reachFor(this.character.body.rightArm, rightPos); } else { var rightPos = this.calcControllerPos(this.character.body.leftArm, 'right'); this.character.reachFor(this.character.body.leftArm, rightPos); } } this.character.lookAt(this.calcCameraTarget()); this.character.trackHeight(this.xrHelper.realWorldHeight()); } } calcControllerPos(arm, side) { this.calcControllerRot(arm, side); return this.xrHelper.handPosition(side); } calcControllerRot(arm, side) { arm.pointerQuat = this.xrHelper.handRotation(side); if (!this.mirror) { // heuristics 1, mirrored arm rotation, works well below shoulder //arm.pointerQuat.y = - arm.pointerQuat.y; // heuristics 2, never point backwards //arm.pointerQuat.z = - arm.pointerQuat.z; arm.pointerQuat = BABYLON.Quaternion.Inverse(arm.pointerQuat); //if ( arm.pointerQuat.z < 0 ) { //arm.pointerQuat.z = 0; //} } } calcCameraTarget() { var cameraQuat = this.xrHelper.camera().rotationQuaternion; var target = new BABYLON.Vector3(0, this.xrHelper.realWorldHeight(), 1); target.rotateByQuaternionAroundPointToRef(cameraQuat, this.character.headPos(), target); if (this.mirror) { target.z = -target.z; } return target; } listAnimations() { VRSPACEUI.listDirectory(this.animationDir(), animations => { this.customAnimations = animations; }); } createSelection(selectionCallback) { if (window.location.search) { var avatarUrl = new URLSearchParams(window.location.search).get("avatarUrl"); if (avatarUrl) { this.loadCharacterUrl(avatarUrl); } } this.selectionCallback = selectionCallback; VRSPACEUI.listMatchingFilesAsync(this.characterDir()).then((folders) => { folders.push({ name: "video" }); if (this.customAvatarFrame) { folders.push({ name: "custom" }); } var buttons = new Buttons(this.scene, "Avatars", folders, (dir) => this.createAvatarSelection(dir), "name"); buttons.setHeight(.5); buttons.group.position = new BABYLON.Vector3(.3, 2.2, this.buttonsZ); buttons.select(0); this.mainButtons = buttons; }); this.listAnimations(); } async createAvatarSelection(folder) { if (this.characterButtons) { this.characterButtons.dispose(); } if (folder.url) { VRSPACEUI.listCharactersAsync(folder.url()).then(avatars => { var buttons = new Buttons(this.scene, folder.name, avatars, (dir) => this.loadCharacter(dir), "name"); buttons.setHeight(0.1 * Math.min(20, avatars.length)); buttons.group.position = new BABYLON.Vector3(1, 2.2, this.buttonsZ); this.characterButtons = buttons; }); } else if (folder.name == "video") { this.createVideoAvatar(); } else if (folder.name == "custom") { this.createCustomAvatar(); } } async createVideoAvatar() { if (this.video) { this.video.show(); return; } // load video avatar and start streaming video this.video = new VideoAvatar( this.scene, () => { if (this.character) { //this.character.dispose(); //delete this.character; this.character.hide(); // do NOT dispose one created by HUD //this.guiManager.dispose(); //delete this.guiManager; this.removeCharacterButtons(); } this.portalsEnabled(true); //this.hud.setAvatar(null); this.hud.toggleWebcam(true, this.video); }, this.customOptions ); await this.video.show(); this.video.setName(this.userName); this.autoEnterPortal(); } removeVideoAvatar() { this.hud.toggleWebcam(false); //this.hud.videoAvatar = null; if (this.video) { this.video.dispose(); //delete this.video; } } async createCustomAvatar() { this.removeVideoAvatar(); // based on example from // https://github.com/readyplayerme/Example-iframe/blob/develop/src/iframe.html if (!this.customAvatarFrame) { return; } this.customAvatarFrame.src = `https://vrspace.readyplayer.me/avatar?frameApi`; this.customAvatarFrame.hidden = false; const subscribe = (event) => { //console.log(event.data); try { var json = JSON.parse(event.data); } catch (error) { return; } if (json?.source !== 'readyplayerme') { return; } // Susbribe to all events sent from Ready Player Me once frame is ready if (json.eventName === 'v1.frame.ready') { this.customAvatarFrame.contentWindow.postMessage( JSON.stringify({ target: 'readyplayerme', type: 'subscribe', eventName: 'v1.**' }), '*' ); } // Get avatar GLB URL if (json.eventName === 'v1.avatar.exported') { var avatarUrl = json.data.url; // something like // https://d1a370nemizbjq.cloudfront.net/a13ab5dc-358d-45e4-a602-446b9c840155.glb console.log("Avatar URL: " + avatarUrl); this.customAvatarFrame.hidden = true; this.loadCharacterUrl(avatarUrl); } // Get user id if (json.eventName === 'v1.user.set') { console.log(`User with id ${json.data.id} set: ${JSON.stringify(json)}`); } } window.addEventListener('message', subscribe); document.addEventListener('message', subscribe); } async loadCharacterUrl(url) { console.log('loading character from ' + url); let file = new ServerFile(url); if ( file.relative ) { // in order to load fixes file, we have to: VRSPACEUI.listCharactersAsync(file.baseUrl).then(avatars => { let localAvatar = avatars.find(folder => folder.name == file.name); if (localAvatar) { // this will load fixes, as ServerFolder contains related fixes file this.loadCharacter(localAvatar); } else { // load without fixes this.loadCharacter(file, file.file); } }); } else { // NOT relative, RPM avatar this.loadCharacter(file, file.file); } } /** * @param {ServerFolder} dir */ loadCharacter(dir, file = "scene.gltf") { this.tracking = false; this.indicator.add(dir); this.indicator.animate(); console.log("Loading character from " + dir.name + " fixes " + dir.related); let loaded = new HumanoidAvatar(this.scene, dir, this.shadowGenerator); loaded.file = file; loaded.animations = this.customAnimations; // resize the character to real-world height if (this.inXR()) { this.userHeight = this.xrHelper.realWorldHeight(); } loaded.userHeight = this.userHeight; loaded.generateAnimations = false; loaded.debug = this.debug; loaded.load((c) => { // on success this.removeVideoAvatar(); this.tracking = true; this.indicator.remove(dir); if (!this.character) { this.addCharacterButtons(); this.portalsEnabled(true); } this.character = loaded.replace(this.character); this.character.setName(this.userName); this.animationButtons(this.character); if (this.selectionCallback) { this.selectionCallback(this.character); } this.checkValidName(); // conditionally enables portals this.hud.setAvatar(this.character); this.hud.toggleWebcam(false); this.autoEnterPortal(); }, // on error (exception) => { console.log("Error loading " + dir.name, exception); this.indicator.remove(dir); } ); } setMyName(name) { this.userName = name; if (this.character) { this.character.setName(this.userName); } else if (this.video) { this.video.setName(this.userName); } } getMyName() { return this.userName; } animationButtons(avatar) { if (!this.showAnimationButtons) { return; } var names = [] var playing; for (var i = 0; i < avatar.getAnimationGroups().length; i++) { var group = avatar.getAnimationGroups()[i]; names.push(group.name); //console.log("Animation group: "+group.name+" "+group.isPlaying); if (group.isPlaying) { playing = i; } avatar.processAnimations(group); } console.log("Animations: " + names); if (this.animationSelection) { this.animationSelection.dispose(); } this.animationSelection = new Buttons(this.scene, "Animations", names, (name) => this.startAnimation(name)); this.animationSelection.turnOff = true; this.animationSelection.setHeight(Math.min(2, names.length / 10)); this.animationSelection.group.position = new BABYLON.Vector3(-1.5, 2.2, this.buttonsZ); } startAnimation(name) { this.character.stopAnimation(name); this.character.startAnimation(name, true); } addCharacterButtons() { //this.guiManager = new BABYLON.GUI.GUI3DManager(this.scene); this.guiManager = VRSPACEUI.hud.guiManager; var resizeButton = new BABYLON.GUI.HolographicButton("resizeButton"); resizeButton.contentResolution = 256; resizeButton.contentScaleRatio = 1; resizeButton.text = "Resize"; this.guiManager.addControl(resizeButton); resizeButton.mesh.isNearPickable = VRSPACEUI.allowHands; this.resizeButton = resizeButton; resizeButton.position = new BABYLON.Vector3(-0.5, 0.2, -0.8); resizeButton.node.scaling = new BABYLON.Vector3(.2, .2, .2); resizeButton.onPointerDownObservable.add(() => { if (this.inXR()) { this.tracking = false; this.userHeight = this.xrHelper.realWorldHeight(); console.log("Resizing to " + this.userHeight); this.character.userHeight = this.userHeight; this.character.standUp(); // CHECKME: move to resize()? this.character.resize(); this.character.maxUserHeight = null; this.tracking = true; } }); var mirrorButton = new BABYLON.GUI.HolographicButton("mirrorButton"); mirrorButton.contentResolution = 256; mirrorButton.contentScaleRatio = 1; mirrorButton.text = "Mirroring"; this.guiManager.addControl(mirrorButton); mirrorButton.mesh.isNearPickable = VRSPACEUI.allowHands; this.mirrorButton = mirrorButton; mirrorButton.position = new BABYLON.Vector3(0.5, 0.2, -0.8); mirrorButton.node.scaling = new BABYLON.Vector3(.2, .2, .2); mirrorButton.onPointerDownObservable.add(() => { if (mirrorButton.text == "Mirroring") { mirrorButton.text = "Copying"; this.mirror = false; this.character.parentMesh.rotationQuaternion = BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y, Math.PI); } else { mirrorButton.text = "Mirroring"; this.mirror = true; this.character.parentMesh.rotationQuaternion = BABYLON.Quaternion.RotationAxis(BABYLON.Axis.Y, 0); } }); } removeCharacterButtons() { if (this.resizeButton) { this.resizeButton.dispose(); this.resizeButton = null; } if (this.mirrorButton) { this.mirrorButton.dispose(); this.mirrorButton = null; } } /** * Show portals, typically called from html. * Sets internal variable this.portals. */ showPortals() { this.portals = {}; if (window.location.search) { // use specified worlds // at the moment, world folder still has to exist on the server const params = new URLSearchParams(window.location.search); const worldToken = params.get("worldToken"); const worldName = params.get("worldName"); const template = params.get("worldThumbnail"); // CHECKME: this ignores baseUrl var serverFolder = new ServerFolder(this.worldDir() + "/", template, template + ".jpg"); var portal = new Portal(this.scene, serverFolder, (p) => this.enterPortal(p)); portal.name = worldName; this.tokens[worldName] = worldToken; this.portals[portal.name] = portal; portal.loadAt(0, 0, this.room.diameter / 2, 0); this.autoEnter = portal; } else { // by default, list worlds from /content/worlds directory this.showContentPortals(); } } /** * Show portals to public worlds avaliable under content/worlds server directory. */ showContentPortals() { var radius = this.room.diameter / 2; var angle = 0; VRSPACEUI.listThumbnails(this.worldDir(), (worlds) => { var angleIncrement = 2 * Math.PI / worlds.length; for (var i = 0; i < worlds.length; i++) { var x = Math.sin(angle) * radius; var z = Math.cos(angle) * radius; // heavy performance impact //new Portal( this.scene, worlds[i], this.enter, this.shadowGenerator).loadAt( x,0,z, angle); var portal = new Portal(this.scene, worlds[i], (p) => this.enterPortal(p)); this.portals[portal.name] = portal; portal.loadAt(x, 0, z, angle); angle += angleIncrement; } this.hud.portals = this.portals; this.showActiveUsers(); }); } // TODO: API client class/library showActiveUsers() { this.api.endpoint.worlds.users().then(worldStats => { if (worldStats) { worldStats.forEach(stat => { //console.log(stat); if (this.portals[stat.worldName]) { if (stat.activeUsers > 0) { // apparently some youtuber said he could not even enter the space due to users 1/1 // not that he tried, but this can be confusing, so //this.portals[stat.worldName].setTitle('Users: ' + stat.activeUsers + '/' + stat.totalUsers); this.portals[stat.worldName].setTitle('Users: ' + stat.activeUsers); } else { this.portals[stat.worldName].setTitle(null); } } }); } }); } portalsEnabled(enable) { if (this.portals) { for (var worldName in this.portals) { this.portals[worldName].enabled(enable && this.hasAvatar()); } } } hasAvatar() { return this.video != null || this.character != null; } removePortals() { if (this.portals) { for (var worldName in this.portals) { console.log("Disposing of portal " + worldName); this.portals[worldName].dispose(); } delete this.portals; } } avatarUrl(defaultUrl = "video") { var url = defaultUrl; if (this.character) { url = this.character.getUrl(); } return url; } autoEnterPortal() { if (this.autoEnter) { // CHECKME // so we enter the portal as soon as the user chooses the avatar // should the user also be authenticated? this.enterPortal(this.autoEnter); } } async enterPortal(portal) { if (this.checkValidName()) { this.enterWorld(portal.worldUrl(), portal.name); } } /** * Enter a world. * TODO quite complex method, order matters. This needs to be moved to a utility method, to allow entering one world from another. */ async enterWorld(worldUrl, worldName, avatarUrl = this.avatarUrl(), worldScript = 'world.js') { console.log("Entering world " + worldUrl + '/' + worldScript + ' as ' + avatarUrl); if (this.video && this.displayOwnVideo) { // CHECKME: dispose or attach? //this.video.dispose(); //delete this.video; this.video.attachToCamera(); } if (this.beforeEnter) { this.beforeEnter(this); } this.loginForm.dispose(); import(worldUrl + '/' + worldScript).then((world) => { world.WORLD.inVR = this.inVR; world.WORLD.inAR = this.inAR; var afterLoad = (world) => { world.serverUrl = this.serverUrl; // TODO refactor this to WorldManager this.worldManager = new WorldManager(world); this.worldManager.tokens = this.tokens; this.worldManager.avatarLoader.customOptions = this.customOptions; this.worldManager.avatarLoader.customAnimations = this.customAnimations; this.worldManager.authenticated = this.authenticated; this.worldManager.oauth2providerId = this.oauth2providerId; this.worldManager.debug = this.debug; // scene debug this.worldManager.VRSPACE.debug = this.debug; // network debug this.worldManager.remoteLogging = false; const mediaStreams = OpenViduStreams.getInstance(this.scene, 'videos'); mediaStreams.debug = false; let avatar = this.video; if (this.character) { // character is null for e.g. video avatar // CHECKME this should be safe to do even earlier, before enter this.character.turnAround = true; avatar = this.character; } // publish video only if currently displayed avatar.video = this.video && this.video.isEnabled() && this.video.displaying == "VIDEO"; this.worldManager.enterAs( avatar ).then(async (welcome) => { world.initXR(this.vrHelper, this.arHelper, this.xrHelper); if (this.inXR()) { console.log("Tracking, " + this.inXR()); this.worldManager.trackCamera(this.xrHelper.camera()); this.xrHelper.startTracking(); this.xrHelper.enableBackground(false); } if ( this.video ) { this.video.altImage = welcome.client.User.picture; } let controller = new AvatarController(this.worldManager, avatar); this.worldManager.addMyChangeListener(changes => controller.processChanges(changes)); // moved to WorldManager.enter() //await this.worldManager.pubSub(welcome.client.User, 'video' === avatarUrl); this.hud.init(); if (this.afterEnter) { this.afterEnter(this, world); } if ( this.authenticated ) { // only authenticated clients can subscribe to web push this.webpushSubscribe(); } }).catch((e) => { console.log("TODO: disconnected", e); if (this.afterExit) { this.afterExit(this); } }); //var recorder = new RecorderUI(world.scene); //recorder.showUI(); } // CHECKME: may be a babylonjs bug, but new camera has null gamepad // TODO: new camera may be of type that doesn't support gamepad var gamepad = this.camera.inputs.attached.gamepad.gamepad; // other components (e.g. AvatarController) may require this this.xrHelper.stopTracking(); world.WORLD.init(this.engine, worldName, this.scene, afterLoad, worldUrl + "/").then((newScene) => { try { this.camera.detachControl(this.canvas); console.log("Loaded ", world); // TODO install world's xr device tracker if (this.inXR()) { // for some reason, this sets Y to 0: this.xrHelper.camera().setTransformationFromNonVRCamera(world.WORLD.camera); this.xrHelper.camera().position.y = world.WORLD.camera.position.y; } else { console.log('New world camera:'); console.log(world.WORLD.camera); // CHECKME: workaround, gamepad stops working // https://github.com/BabylonJS/Babylon.js/blob/master/src/Cameras/Inputs/freeCameraGamepadInput.ts // scene.gamepadManager does not emit event to the new camera if (gamepad) { world.WORLD.camera.inputs.attached.gamepad.gamepad = gamepad; } // CHECKME: why? this.scene.activeCamera = world.WORLD.camera; } this.dispose(); } catch (err) { console.error(err); } }); }); } enableBackground(enabled) { this.room.floorGroup.setEnabled(enabled); } dispose() { super.dispose(); this.hemisphere.dispose(); this.removePortals(); this.room.dispose(); // AKA ground // CHECKME properly dispose of avatar // disposing of own character effectively disables 3rd person view etc if (this.character) { //this.character.dispose(); //VRSPACEUI.assetLoader.unloadAsset(this.character.getUrl()); //this.character = null; // hiding the character to allow for cloning or 3rd person display this.character.hide(true); } if (this.mainButtons) { this.mainButtons.dispose(); } if (this.characterButtons) { this.characterButtons.dispose(); } if (this.animationSelection) { this.animationSelection.dispose(); } if (this.guiManager) { // do NOT dispose one created by HUD //this.guiManager.dispose(); } this.removeCharacterButtons(); // CHECKME: this scene should be cleaned up, but when? //this.scene = null; // next call to render loop stops the current loop } } // NOT exported any longer, as it happens on script import // that happens either before VRSPACEUI constants are set, or complicates setup and startup // so, import, optionally do stuff, then new AvatarSelection() explicitly //export const WORLD = new AvatarSelection();