"use strict"; /*jslint vars:true, plusplus:true, forin:true*/ /*global Tablet, Script, */ /* eslint indent: ["error", 4, { "outerIIFEBody": 1 }] */ // // portal.js // // Created by Zach Fox on 2018-11-07 // Copyright 2018 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // (function () { // BEGIN LOCAL_SCOPE var AppUi = Script.require('appUi'); // CONSTS START const PORTAL_Z_DIMENSION = 0.005; const PORTAL_OVERLAY_OFFSET = PORTAL_Z_DIMENSION * 2 + 0.05; // In meters, the distance between the Portal entity and the Portal overlay const PORTAL_OVERLAY_UPDATE_INTERVAL_MS = 50; const PORTAL_ZONE_Z_DIMENSION = 0.2; const PORTAL_ZONE_TP_OFFSET = PORTAL_ZONE_Z_DIMENSION + 0.05; const SECONDARY_CAMERA_RESOLUTION = 1024; // width/height multiplier, in pixels // CONSTS END var spectatorCameraConfig = Render.getConfig("SecondaryCamera"); var previousPortalOverlayDimensions = { x: 0, y: 0 }; // The previous dimensions of the Portal Overlay var currentPortalWithOverlay = false; var portalOverlay = false; var portalOverlayUpdateTimer = false; function updatePortalOverlayDimensions(forceUpdate) { if (!currentPortalWithOverlay) { console.log("Portal is active, but Portal doesn't have an associated entity!"); return; } if (!currentPortalWithOverlay) { console.log("Portal is active, but it doesn't have an associated overlay!"); return; } var newDimensions = Entities.getEntityProperties(currentPortalWithOverlay, 'dimensions').dimensions; if (forceUpdate === true || (newDimensions.x !== previousPortalOverlayDimensions.x || newDimensions.y !== previousPortalOverlayDimensions.y)) { if (portalCameraRunning) { spectatorCameraConfig.resetSizeSpectatorCamera(newDimensions.x * SECONDARY_CAMERA_RESOLUTION, newDimensions.y * SECONDARY_CAMERA_RESOLUTION); } Overlays.editOverlay(portalOverlay, { dimensions: { x: (newDimensions.y > newDimensions.x ? newDimensions.y : newDimensions.x), y: -(newDimensions.y > newDimensions.x ? newDimensions.y : newDimensions.x), z: 0 } }); } previousPortalOverlayDimensions = newDimensions; } // Function Name: inFrontOf() // // Description: // -Returns the position in front of the given "position" argument, where the forward vector is based off // the "orientation" argument and the amount in front is based off the "distance" argument. function inFrontOf(distance, position, orientation) { return Vec3.sum(position || MyAvatar.position, Vec3.multiply(distance, Quat.getForward(orientation || MyAvatar.orientation))); } var portalAScript = (function () { this.enterEntity = function (entityID) { var portalA = Entities.getEntityProperties(entityID, ['parentID']).parentID; var portalB = Entities.getEntityProperties(portalA, ['userData']).userData; var portalBProperties = Entities.getEntityProperties(portalB, ['position', 'rotation']); // Uses `PORTAL_ZONE_TP_OFFSET` - if you change that const, change this value. MyAvatar.position = Vec3.sum(portalBProperties.position, Vec3.multiply(0.25, Quat.getForward(portalBProperties.rotation))); MyAvatar.orientation = portalBProperties.rotation; }; }); var portalBScript = (function () { this.enterEntity = function (entityID) { var portalB = Entities.getEntityProperties(entityID, ['parentID']).parentID; var portalA = Entities.getEntityProperties(portalB, ['userData']).userData; var portalAProperties = Entities.getEntityProperties(portalA, ['position', 'rotation']); // Uses `PORTAL_ZONE_TP_OFFSET` - if you change that const, change this value. MyAvatar.position = Vec3.sum(portalAProperties.position, Vec3.multiply(0.25, Quat.getForward(portalAProperties.rotation))); MyAvatar.orientation = portalAProperties.rotation; }; }); var portalAEntity = false; var portalAZone = false; function rezPortalA() { setPortalCameraStatus(false); if (portalAEntity) { Entities.deleteEntity(portalAEntity); } portalAEntity = Entities.addEntity({ "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionless": true, "color": { "blue": 239, "green": 180, "red": 0 }, "dimensions": { "x": 2, "y": 2, "z": PORTAL_Z_DIMENSION }, "grab": { "grabbable": true }, "ignoreForCollisions": true, "shape": "Cube", "type": "Box", "position": inFrontOf(2, Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 })), "rotation": Quat.multiply(MyAvatar.orientation, { w: 0, x: 0, y: 1, z: 0 }), "userData": portalBEntity || "" }); portalAZone = Entities.addEntity({ "dimensions": { "x": 2, "y": 2, "z": PORTAL_ZONE_Z_DIMENSION }, "grab": { "grabbable": false }, "shapeType": "box", "type": "Zone", "parentID": portalAEntity, "script": "(" + portalAScript + ")" }); if (portalBEntity) { Entities.editEntity(portalBEntity, { "userData": portalAEntity }); } currentPortalWithOverlay = portalAEntity; if (portalAEntity && portalBEntity) { setPortalCameraStatus(true); rezPortalOverlay(currentPortalWithOverlay); } } var portalBEntity = false; var portalBZone = false; function rezPortalB() { setPortalCameraStatus(false); if (portalBEntity) { Entities.deleteEntity(portalBEntity); portalBEntity = false; } portalBEntity = Entities.addEntity({ "collidesWith": "static,dynamic,kinematic,otherAvatar,", "collisionMask": 23, "collisionless": true, "color": { "blue": 0, "green": 180, "red": 239 }, "dimensions": { "x": 2, "y": 2, "z": PORTAL_Z_DIMENSION }, "grab": { "grabbable": true }, "ignoreForCollisions": true, "shape": "Cube", "type": "Box", "position": inFrontOf(2, Vec3.sum(MyAvatar.position, { x: 0, y: 0.5, z: 0 })), "rotation": Quat.multiply(MyAvatar.orientation, { w: 0, x: 0, y: 1, z: 0 }), "userData": portalAEntity || "" }); portalBZone = Entities.addEntity({ "dimensions": { "x": 2, "y": 2, "z": PORTAL_ZONE_Z_DIMENSION }, "grab": { "grabbable": false }, "shapeType": "box", "type": "Zone", "parentID": portalBEntity, "script": "(" + portalBScript + ")" }); if (portalAEntity) { Entities.editEntity(portalAEntity, {"userData": portalBEntity}); } currentPortalWithOverlay = portalBEntity; if (portalAEntity && portalBEntity) { setPortalCameraStatus(true); rezPortalOverlay(currentPortalWithOverlay); } } function rezPortalOverlay(parentPortalEntity) { if (!parentPortalEntity) { console.log("Tried to rez portal overlay, but no parent portal entity was defined!"); return; } maybeDestroyPortalOverlay(); portalOverlay = Overlays.addOverlay("image3d", { name: "portalOverlay", url: "resource://spectatorCameraFrame", emissive: true, parentID: parentPortalEntity, alpha: 1, localRotation: { w: 0, x: 0, y: 1, z: 0 }, localPosition: { x: 0, y: 0, z: -PORTAL_OVERLAY_OFFSET } }); updatePortalOverlayDimensions(true); if (portalOverlayUpdateTimer) { Script.clearInterval(portalOverlayUpdateTimer); } portalOverlayUpdateTimer = Script.setInterval(updatePortalOverlayDimensions, PORTAL_OVERLAY_UPDATE_INTERVAL_MS); if (portalDistanceInterval) { Script.clearInterval(portalDistanceInterval); } portalDistanceInterval = Script.setInterval(portalDistanceCheck, PORTAL_DISTANCE_INTERVAL_MS); } function setPortalEntranceAndExitCameras() { spectatorCameraConfig.portalEntranceEntityId = null; spectatorCameraConfig.attachedEntityId = null; if (currentPortalWithOverlay === portalAEntity) { spectatorCameraConfig.portalEntranceEntityId = portalAEntity; spectatorCameraConfig.attachedEntityId = portalBEntity; } else { spectatorCameraConfig.portalEntranceEntityId = portalBEntity; spectatorCameraConfig.attachedEntityId = portalAEntity; } } var portalCameraRunning = false; function setPortalCameraStatus(enabled) { if (enabled) { if (!(portalAEntity && portalBEntity)) { console.log("Can't enable Portal camera if either Portal A or Portal B are not rezzed."); return; } if (portalCameraRunning) { console.log("Portal camera already running."); return; } portalCameraRunning = true; spectatorCameraConfig.portalProjection = true; setPortalEntranceAndExitCameras(); Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 0; spectatorCameraConfig.enableSecondaryCameraRenderConfigs(true); rezPortalOverlay(currentPortalWithOverlay); } else { if (!portalCameraRunning) { console.log("User tried to disable the portal camera, but it was already off!"); return; } maybeDestroyPortalOverlay(); spectatorCameraConfig.enableSecondaryCameraRenderConfigs(false); spectatorCameraConfig.portalProjection = false; spectatorCameraConfig.portalEntranceEntityId = null; spectatorCameraConfig.attachedEntityId = null; Render.getConfig("SecondaryCameraJob.ToneMapping").curve = 1; portalCameraRunning = false; } } var portalDistanceInterval = false; const PORTAL_DISTANCE_INTERVAL_MS = 200; function portalDistanceCheck() { if (!(portalAEntity && portalBEntity)) { return; } var otherPortalWithOverlay; if (currentPortalWithOverlay === portalAEntity) { otherPortalWithOverlay = portalBEntity; } else { otherPortalWithOverlay = portalAEntity; } var entitiesInCameraFrustum = Entities.findEntitiesInFrustum(Camera.frustum); // If neither portals are in view, we don't care if (entitiesInCameraFrustum.indexOf(currentPortalWithOverlay) === -1 && entitiesInCameraFrustum.indexOf(otherPortalWithOverlay) === -1) { return; } var currentPortalWithOverlayPosition = Entities.getEntityProperties(currentPortalWithOverlay, ['position']).position; var otherPortalWithOverlayPosition = Entities.getEntityProperties(otherPortalWithOverlay, ['position']).position; var cameraPosition = Camera.position; var distanceToCurrentPortal = Vec3.distance(currentPortalWithOverlayPosition, cameraPosition); var distanceToOtherPortal = Vec3.distance(otherPortalWithOverlayPosition, cameraPosition); var newPortalWithOverlay = currentPortalWithOverlay; if (distanceToOtherPortal < distanceToCurrentPortal || (entitiesInCameraFrustum.indexOf(currentPortalWithOverlay) === -1 && entitiesInCameraFrustum.indexOf(otherPortalWithOverlay) > -1)) { newPortalWithOverlay = otherPortalWithOverlay; } // Make sure the new portal is visible if (currentPortalWithOverlay !== newPortalWithOverlay && entitiesInCameraFrustum.indexOf(newPortalWithOverlay) > -1) { currentPortalWithOverlay = newPortalWithOverlay; rezPortalOverlay(currentPortalWithOverlay); setPortalEntranceAndExitCameras(); } } function disablePortals() { setPortalCameraStatus(false); if (portalAZone) { Entities.editEntity(portalAZone, { "script": "" }); } if (portalBZone) { Entities.editEntity(portalBZone, { "script": "" }); } if (portalDistanceInterval) { Script.clearInterval(portalDistanceInterval); portalDistanceInterval = false; } } function enablePortals() { setPortalCameraStatus(true); if (portalAZone) { Entities.editEntity(portalAZone, { "script": "(" + portalAScript + ")" }); } if (portalBZone) { Entities.editEntity(portalBZone, { "script": "(" + portalBScript + ")" }); } } // Function Name: fromQml() // // Description: // -Called when a message is received from Portal.qml. The "message" argument is what is sent from the Portal QML // in the format "{method, params}", like json-rpc. See also sendToQml(). function fromQml(message) { switch (message.method) { case 'disablePortals': disablePortals(); break; case 'enablePortals': enablePortals(); break; case 'rezPortalA': rezPortalA(); break; case 'rezPortalB': rezPortalB(); break; default: print('Unrecognized message from Portal.qml'); } } function initializeUI() { ui.sendMessage({ method: 'initializeUI', portalsEnabled: portalCameraRunning, portalARezzed: !!portalAEntity, portalBRezzed: !!portalBEntity }); } function maybeDestroyPortalOverlay() { if (portalOverlayUpdateTimer) { Script.clearInterval(portalOverlayUpdateTimer); portalOverlayUpdateTimer = false; } if (portalOverlay) { Overlays.deleteOverlay(portalOverlay); portalOverlay = false; } } function destroyPortals() { disablePortals(); maybeDestroyPortalOverlay(); if (portalAEntity) { Entities.deleteEntity(portalAEntity); portalAEntity = false; } if (portalBEntity) { Entities.deleteEntity(portalBEntity); portalBEntity = false; } } // Function Name: onDomainChanged() // // Description: // -A small utility function used when the Window.domainChanged() signal is fired. function onDomainChanged() { destroyPortals(); } var ui; function startup() { ui = new AppUi({ buttonName: "PORTAL", home: Script.resolvePath("./Portal.qml"), graphicsDirectory: Script.resolvePath("./"), onMessage: fromQml, onOpened: initializeUI }); Window.domainChanged.connect(onDomainChanged); } function shutdown() { destroyPortals(); Window.domainChanged.disconnect(onDomainChanged); } startup(); Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE