UNPKG

@vrspace/babylonjs

Version:

vrspace.org babylonjs client

513 lines (485 loc) 19.3 kB
import { World } from './world.js'; import { VRSPACEUI } from '../ui/vrspace-ui.js'; import { Portal } from '../ui/world/portal.js'; import { ServerFolder } from '../core/server-folder.js'; import { LogoRoom } from '../ui/world/logo-room.js'; import { HumanoidAvatar } from '../avatar/humanoid-avatar.js'; import { VideoAvatar } from '../avatar/video-avatar.js'; import { MeshAvatar } from '../avatar/mesh-avatar.js'; /** * Save and load the scene with this class. * * Addesses existing issues with babylon.js serialization: * 1) technical: gltf serialization doesn't support transparency, json serialization fails with with js errors * 2) generated json files may be huge * 3) intelectual property violation - even with open source models, we must comply with the license * * So we take mixed approach to serialize everything - some custom and some babylon. * Most important is not copying everything, use URL of the model whenever possible. */ export class Sceneshot { /** * Save the world: * - all dynamically loaded assets * - skybox * - ground * - camera(s) * - light(s) * - terrain * - shadow generator * - physics * - portal(s) * TODO: make loading these elements optional. * @return world object */ static async serializeWorld(world = World.lastInstance) { let worldInfo = { name: world.name, baseUrl: world.baseUrl, file: world.file, worldObjects: world.worldObjects, objectsFile: world.objectsFile, physics: { gravityEnabled: world.gravityEnabled, physicsPlugin: world.physicsPlugin?.name }, portals: {}, avatars: { }, videoAvatars: [], meshAvatars: {}, scriptedObjects: [], buttons: [] }; worldInfo.assets = VRSPACEUI.assetLoader.dump(true); // CHECKME include avatars or no? worldInfo.sceneMeshes = []; if (world.skyBox && world.skyBox.skybox) { worldInfo.skyBox = BABYLON.SceneSerializer.SerializeMesh(world.skyBox.skybox); } if (world.room) { worldInfo.room = true; } if (world.ground) { // CHECKME: elseif? worldInfo.ground = BABYLON.SceneSerializer.SerializeMesh(world.ground); } if (world.camera1p) { worldInfo.camera1p = BABYLON.SceneSerializer.SerializeMesh(world.camera1p); } if (world.camera3p) { worldInfo.camera3p = BABYLON.SceneSerializer.SerializeMesh(world.camera3p); } worldInfo.lights = []; for ( let i = 0; i < world.scene.lights.length; i++ ) { let light = world.scene.lights[i]; worldInfo.lights.push(BABYLON.SceneSerializer.SerializeMesh(light)); if ( world.light === light ) { worldInfo.light == i; } } if (world.shadowGenerator) { worldInfo.shadowGenerator = { mapSize: world.shadowGenerator.mapSize, useExponentialShadowMap: world.shadowGenerator.useExponentialShadowMap, transparencyShadow: world.shadowGenerator.transparencyShadow // blur etc? } for ( let i = 0; i < world.scene.lights.length; i++ ) { let light = world.scene.lights[i]; if ( world.shadowGenerator.getLight() === light ) { worldInfo.shadowGenerator.light = i; break; } } } if (world.sceneMeshes) { world.sceneMeshes.forEach(mesh => { if (!mesh.parent) { worldInfo.sceneMeshes.push(BABYLON.SceneSerializer.SerializeMesh(mesh, false, true)); } }); } for ( let node of world.scene.rootNodes ) { try { if (node.isEnabled()) { if (node.name.startsWith('Portal:')) { let portal = node.Portal; //let name = node.name.substring(node.name.indexOf(':')+1); let name = portal.name; worldInfo.portals[name] = { serverFolder: portal.serverFolder, x: node.position.x, y: node.position.y, z: node.position.z, angle: portal.angle, enabled: portal.isEnabled } } else if (node.name.startsWith('ButtonGroup:')) { worldInfo.buttons.push(BABYLON.SceneSerializer.SerializeMesh(node, false, true)); } else if (typeof node.avatar != 'undefined') { let url = node.avatar.getUrl(); console.log("Avatar: " + url); if (node.avatar.video) { let pos = node.avatar.basePosition(); let obj = { name: node.avatar.name, autoStart: node.avatar.autoStart, autoAttach: node.avatar.autoAttach, position: { x: pos.x, y: pos.y, z: pos.z }, displaying: node.avatar.displaying, altText: node.avatar.altText, altImage: node.avatar.altImage }; worldInfo.videoAvatars.push(obj); } else if (node.avatar.humanoid) { if (!worldInfo.avatars[url]) { worldInfo.avatars[url] = { info: VRSPACEUI.assetLoader.containers[url].info, numberOfInstances: VRSPACEUI.assetLoader.containers[url].numberOfInstances, animations: node.avatar.animations, instances: [] }; } let pos = node.avatar.basePosition(); let rot = node.avatar.baseMesh().rotationQuaternion; let scale = node.avatar.baseMesh().getChildren()[0].scaling; let obj = { name: node.avatar.name, position: { x: pos.x, y: pos.y, z: pos.z }, rotationQuaternion: { x: rot.x, y: rot.y, z: rot.z, w: rot.w }, scale: { x: scale.x, y: scale.y, z: scale.z }, turnAround: node.avatar.turnAround, activeAnimation: node.avatar.activeAnimation, userHeight: node.avatar.userHeight }; worldInfo.avatars[url].instances.push(obj); } else { // mesh avatar - TODO not tested if (!worldInfo.meshAvatars[url]) { worldInfo.meshAvatars[url] = { info: VRSPACEUI.assetLoader.containers[url].info, numberOfInstances: VRSPACEUI.assetLoader.containers[url].numberOfInstances, instances: [] }; } let pos = node.avatar.basePosition(); let rot = node.avatar.baseMesh.rotation; let obj = { name: node.avatar.name, position: { x: pos.x, y: pos.y, z: pos.z }, rotation: { x: rot.x, y: rot.y, z: rot.z} }; worldInfo.meshAvatars[url].instances.push(obj); } } else if (typeof node.VRObject != 'undefined' && typeof node.VRObject.script != 'undefined') { // scripts: console.log("Saving script: ", node); worldInfo.scriptedObjects.push(BABYLON.SceneSerializer.SerializeMesh(node, false, true)); } } } catch (exception) { console.log("Error serializing node", node, exception); } } if (world.terrain) { worldInfo.terrain = { mesh: BABYLON.SceneSerializer.SerializeMesh(world.terrain.mesh()) } if (world.terrain.sps) { worldInfo.terrain.sps = BABYLON.SceneSerializer.SerializeMesh(world.terrain.sps.mesh); } } return worldInfo; } /** * Serialize the world as json, and save the file. * Calls serializeWorld, then VRSPACEUI.saveFile. * @param {World} world */ static async saveJson(world) { let worldInfo = await this.serializeWorld(world); VRSPACEUI.saveFile(worldInfo.name + ".json", JSON.stringify(worldInfo)); } /** * Serialize the world as json, generate html that creates the scene and loads json, and save the html file. * Calls serializeWorld, then VRSPACEUI.saveFile. * @param {World} world */ static async saveHtml(world) { let worldInfo = await this.serializeWorld(world); let json = JSON.stringify(worldInfo); let html = ` <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> <meta content="utf-8" http-equiv="encoding"> <title>VRSpace:Sceneshot</title> <style type="text/css"> html, body { width: 100%; height:100%; margin: 0px; padding: 0px; } canvas { width: 100%; height:96%; padding-left: 0; padding-right: 0; margin-left: auto; margin-right: auto; } </style> <script src="https://cdn.babylonjs.com/v7.54.2/babylon.js"></script> <script src="https://cdn.babylonjs.com/v7.54.2/loaders/babylonjs.loaders.min.js"></script> <script src="https://cdn.babylonjs.com/v7.54.2/gui/babylon.gui.min.js"></script> <script src="https://cdn.babylonjs.com/v7.54.2/materialsLibrary/babylonjs.materials.min.js"></script> <script src="https://cdn.babylonjs.com/v7.54.2/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script> </head> <body> <!-- canvas is not focusable by default, tabIndex does that --> <canvas id="renderCanvas" touch-action="none" tabIndex=0></canvas> <script> var canvas = document.getElementById("renderCanvas"); // Get the canvas element // focus canvas so we get keyboard events, otherwise need to click on it first canvas.focus(); var engine = new BABYLON.Engine(canvas, true, { audioEngine: true }); // Generate the BABYLON 3D engine var scene; ` html += 'var json =`'; html += json; html += '`'; let scriptSrc = '/babylon/js/vrspace-min.js'; if ( window.location.href.indexOf('localhost') >= 0 ) { console.warn('This document can not be loaded from filesystem, only from web server'); scriptSrc = 'https://www.vrspace.org/'+ scriptSrc; } else { scriptSrc = window.location.origin + scriptSrc; } html += "\nimport('"+scriptSrc+"').then( (module) =>{"; html += "\n module.VRSPACEUI.contentBase='"+window.location.origin+"';"; html += ` module.Sceneshot.loadString(engine, json).then(world=>scene=world.scene); }); function debugOnOff() { console.log("Debug: "+scene.debugLayer.isVisible()); if ( scene.debugLayer.isVisible() ) { scene.debugLayer.hide(); } else { scene.debugLayer.show(); } } </script> <div style="position:absolute;bottom:10px;right:50%;"> <button onClick="debugOnOff()">Debug</button> </div> </body> </html> ` VRSPACEUI.saveFile(worldInfo.name + ".html", html); } /** * Internal * @private */ static loadComponent(component, scene) { try { if (component) { // skybox is serialized with relative urls // CHECKME this is likely to be the case with all textures // CHECKME better way to find content path? let text = JSON.stringify(component); let replaced = text.replaceAll('"/content/', '"'+VRSPACEUI.contentBase+'/content/'); BABYLON.SceneLoader.Append("", 'data:' + replaced, scene); } } catch (ex) { console.error("Error loading component ", component); } } /** * Internal * @private */ static async loadMesh(url, instance, scene) { var vrObject = { mesh: url, name: instance.name, position: instance.position, rotation: instance.rotation }; let avatar = new MeshAvatar(scene, vrObject); VRSPACEUI.assetLoader.loadObject(vrObject, mesh => { mesh.position = new BABYLON.Vector3(instance.position.x, instance.position.y, instance.position.z); mesh.rotation = new BABYLON.Vector3(instance.rotation.x, instance.rotation.y, instance.rotation.z); avatar.setName(instance.name); }); } /** * Internal * @private */ static async loadAvatar(url, asset, instance, scene, shadowGenerator) { let avatar = await HumanoidAvatar.createFromUrl(scene, url, shadowGenerator); avatar.userHeight = instance.userHeight; avatar.turnAround = instance.turnAround avatar.animations = asset.animations; // load avatar.load(()=>{ avatar.baseMesh().position = new BABYLON.Vector3(instance.position.x, instance.position.y, instance.position.z); avatar.baseMesh().rotationQuaternion = new BABYLON.Quaternion(instance.rotationQuaternion.x, instance.rotationQuaternion.y, instance.rotationQuaternion.z, instance.rotationQuaternion.w); if (instance.scale) { avatar.rootMesh.scaling = new BABYLON.Vector3(instance.scale.x, instance.scale.y, instance.scale.z); } avatar.setName(instance.name); if ( instance.activeAnimation ) { avatar.startAnimation(instance.activeAnimation, true); } }); } /** * Internal * @private */ static loadAsset(url, instance) { var vrObject = { mesh: url, position: instance.position, rotation: instance.rotation, scale: instance.scale }; VRSPACEUI.assetLoader.loadObject(vrObject, mesh => { mesh.position = new BABYLON.Vector3(instance.position.x, instance.position.y, instance.position.z); mesh.rotation = new BABYLON.Vector3(instance.rotation.x, instance.rotation.y, instance.rotation.z); if (instance.scale) { mesh.scaling = new BABYLON.Vector3(instance.scale.x, instance.scale.y, instance.scale.z); } }); } /** * Internal * @private */ static loadAssets(assets, loadFunc) { for (let url in assets) { let asset = assets[url]; let instances = asset.instances; if (url.startsWith("/")) { // relative url, make it relative to world script path url = VRSPACEUI.contentBase + url; } instances.forEach(instance => loadFunc(url, asset, instance)); } } /** * Load a world from a json file: fetch the file and call loadWorld * @param engine babylon.js engine created elsewhere * @param file file name, defaults to scene.json */ static async loadFile(engine, file = "scene.json") { let response = await fetch(file); let worldInfo = await response.json(); let world = await this.loadWorld(engine,worldInfo); return world; } /** * Load a world from a json string: parse the string and call loadWorld * @param engine babylon.js engine created elsewhere * @param text json source */ static async loadString(engine, text) { let worldInfo = JSON.parse(text); let world = await this.loadWorld(engine,worldInfo); return world; } /** * Load the world. * Creates new World object, and the Scene then loads * - sky box * - ground * - cameras * - lights * - shadow generator * - scene meshes * - button groups * - logo room (that's only for avatar selection) * - terrain * - portals * - general VRObjects (e.g. created by world editor) * - humanoid avatars * - mesh avatars * - video avatars (without video obviously) * - scripted objects * So everything except HUD and forms. * TODO: make loading these elements optional. * @param engine babylon.js engine * @param worldInfo serialized world object */ static async loadWorld(engine, worldInfo) { let world = new World(); world.engine = engine; world.scene = new BABYLON.Scene(engine); console.log(worldInfo); world.name = worldInfo.name; world.baseUrl = worldInfo.baseUrl; world.file = worldInfo.file; world.worldObjects = worldInfo.worldObjects; world.objectsFile = worldInfo.objectsFile; world.gravityEnabled = worldInfo.gravityEnabled; this.loadComponent(worldInfo.skyBox, world.scene); this.loadComponent(worldInfo.ground, world.scene); this.loadComponent(worldInfo.camera1p, world.scene); this.loadComponent(worldInfo.camera3p, world.scene); for ( let i = 0; i < worldInfo.lights.length; i++ ) { this.loadComponent(worldInfo.lights[i], world.scene); if ( i == worldInfo.light ) { world.light = world.scene.lights[world.scene.lights.length-1]; } } if ( worldInfo.shadowGenerator ) { world.shadowGenerator = new BABYLON.ShadowGenerator(worldInfo.shadowGenerator.mapSize, world.scene.lights[worldInfo.shadowGenerator.light]); world.shadowGenerator.useExponentialShadowMap = worldInfo.shadowGenerator.useExponentialShadowMap; world.shadowGenerator.transparencyShadow = worldInfo.shadowGenerator.useExponentialShadowMap; } worldInfo.sceneMeshes.forEach(mesh => { this.loadComponent(mesh, world.scene); }); worldInfo.buttons.forEach(mesh => { this.loadComponent(mesh, world.scene); }); world.registerRenderLoop(); VRSPACEUI.init(world.scene).then(() => { world.scene.activeCamera.attachControl(); world.camera = world.scene.activeCamera; if (worldInfo.room) { new LogoRoom(world.scene).load(); } if (worldInfo.terrain) { this.loadComponent(worldInfo.terrain.mesh, world.scene); this.loadComponent(worldInfo.terrain.sps, world.scene); } for (let portalName in worldInfo.portals) { let portalInfo = worldInfo.portals[portalName]; console.log('Portal ' + portalName, portalInfo); // CHECKME: should we rather save this VRSPACEUI.contentBase with each portal url? let serverFolder = new ServerFolder(VRSPACEUI.contentBase+portalInfo.serverFolder.baseUrl, portalInfo.serverFolder.name, portalInfo.serverFolder.related); let portal = new Portal(world.scene, serverFolder); portal.loadAt(portalInfo.x, portalInfo.y, portalInfo.z, portalInfo.angle).then(p => p.enabled(portalInfo.enabled)); } this.loadAssets(worldInfo.assets, (url,asset,instance) => this.loadAsset(url, instance)); this.loadAssets(worldInfo.avatars, (url,avatar,instance) => this.loadAvatar(url, avatar, instance, world.scene, world.shadowGenerator)); this.loadAssets(worldInfo.meshAvatars, (url,asset,instance) => this.loadMesh(url, instance, world.scene)); worldInfo.videoAvatars.forEach( videoAvatar => { let video = new VideoAvatar(world.scene); video.autoStart = videoAvatar.autoStart; video.autoAttach = videoAvatar.autoAttach; video.altText = videoAvatar.altText; video.altImage = videoAvatar.altImage; video.show(); video.mesh.parent = new BABYLON.TransformNode("Root of "+video.mesh.id, world.scene); video.mesh.parent.position = new BABYLON.Vector3(videoAvatar.position.x,videoAvatar.position.y,videoAvatar.position.z); }); worldInfo.scriptedObjects.forEach(o=>this.loadComponent(o, world.scene)); world.initXR(); }); return world; } }