mirror of
https://github.com/overte-org/community-apps.git
synced 2025-04-05 21:22:00 +02:00
605 lines
22 KiB
JavaScript
605 lines
22 KiB
JavaScript
"use strict";
|
|
//
|
|
// cam360.js
|
|
//
|
|
// Created by Zach Fox on 2018-10-26
|
|
// Copyright 2018 High Fidelity, Inc.
|
|
// Copyright 2022, Overte e.V.
|
|
//
|
|
// Application to take 360 degrees photo by throwing a camera in the air (as in Ready Player One (RPO)) or as a standard positionned camera.
|
|
// version 2.0
|
|
//
|
|
// 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
|
|
|
|
// 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)));
|
|
}
|
|
|
|
// Function Name: rpo360On()
|
|
var CAMERA_NAME = "CAM360 Camera";
|
|
var SETTING_LAST_360_CAPTURE = "overte_app_cam360_last_capture";
|
|
var secondaryCameraConfig = Render.getConfig("SecondaryCamera");
|
|
var camera = false;
|
|
var cameraRotation;
|
|
var cameraPosition;
|
|
var cameraGravity = {x: 0, y: -5, z: 0};
|
|
var velocityLoopInterval = false;
|
|
var isThrowMode = true;
|
|
|
|
function rpo360On() {
|
|
// Rez the camera model, and attach
|
|
// the secondary camera to the rezzed model.
|
|
cameraRotation = MyAvatar.orientation;
|
|
cameraPosition = inFrontOf(1.0, Vec3.sum(MyAvatar.position, { x: 0, y: 0.3, z: 0 }));
|
|
var properties;
|
|
var hostType = "";
|
|
if (isThrowMode) {
|
|
properties = {
|
|
"angularDamping": 0.08,
|
|
"canCastShadow": false,
|
|
"damping": 0.01,
|
|
"collisionMask": 7,
|
|
"modelURL": Script.resolvePath("resources/models/cam360white.fst"),
|
|
"name": CAMERA_NAME,
|
|
"rotation": cameraRotation,
|
|
"position": cameraPosition,
|
|
"shapeType": "simple-compound",
|
|
"type": "Model",
|
|
"grab": {
|
|
"grabbable": true
|
|
},
|
|
"script": Script.resolvePath("grabDetection.js"),
|
|
"userData": "",
|
|
"isVisibleInSecondaryCamera": false,
|
|
"gravity": cameraGravity,
|
|
"dynamic": true
|
|
};
|
|
hostType = "avatar";
|
|
} else {
|
|
properties = {
|
|
"canCastShadow": false,
|
|
"collisionMask": 7,
|
|
"modelURL": Script.resolvePath("resources/models/cam360black.fst"),
|
|
"name": CAMERA_NAME,
|
|
"rotation": cameraRotation,
|
|
"position": cameraPosition,
|
|
"shapeType": "sphere",
|
|
"type": "Model",
|
|
"grab": {
|
|
"grabbable": true
|
|
},
|
|
"userData": "",
|
|
"isVisibleInSecondaryCamera": false
|
|
};
|
|
hostType = "avatar";
|
|
}
|
|
|
|
camera = Entities.addEntity(properties, hostType);
|
|
secondaryCameraConfig.attachedEntityId = camera;
|
|
|
|
// Play a little sound to let the user know we've rezzed the camera
|
|
Audio.playSound(SOUND_CAMERA_ON, {
|
|
"volume": 0.15,
|
|
"position": cameraPosition,
|
|
"localOnly": true
|
|
});
|
|
|
|
// Remove the existing camera model from the domain if one exists.
|
|
// It's easy for this to happen if the user crashes while the RPO360 Camera is on.
|
|
// We do this down here (after the new one is rezzed) so that we don't accidentally delete
|
|
// the newly-rezzed model.
|
|
var entityIDs = Entities.findEntitiesByName(CAMERA_NAME, MyAvatar.position, 100, false);
|
|
entityIDs.forEach(function (currentEntityID) {
|
|
var currentEntityOwner = Entities.getEntityProperties(currentEntityID, ['owningAvatarID']).owningAvatarID;
|
|
if (currentEntityOwner === MyAvatar.sessionUUID && currentEntityID !== camera) {
|
|
Entities.deleteEntity(currentEntityID);
|
|
}
|
|
});
|
|
|
|
setTakePhotoControllerMappingStatus();
|
|
|
|
// Start the velocity loop interval at 70ms
|
|
// This is used to determine when the 360 photo should be snapped
|
|
if (isThrowMode) {
|
|
velocityLoopInterval = Script.setInterval(velocityLoop, 70);
|
|
}
|
|
}
|
|
|
|
// Function Name: velocityLoop()
|
|
var hasBeenThrown = false;
|
|
var snapshotVelocity = false;
|
|
var snapshotAngularVelocity = false;
|
|
var velocityWasPositive = false;
|
|
var cameraReleaseTime = false;
|
|
var MIN_AIRTIME_MS = 500;
|
|
var flash = false;
|
|
var useFlash = false;
|
|
function velocityLoop() {
|
|
// Get the velocity and angular velocity of the camera model
|
|
var properties = Entities.getEntityProperties(camera, ["velocity", "angularVelocity", "userData"]);
|
|
var velocity = properties.velocity;
|
|
var angularVelocity = properties.angularVelocity;
|
|
var releasedState = properties.userData;
|
|
|
|
if (releasedState === "RELEASED" && !hasBeenThrown) {
|
|
hasBeenThrown = true;
|
|
// Record the time at which a user has thrown the camera
|
|
cameraReleaseTime = Date.now();
|
|
}
|
|
|
|
// If we've been thrown UP...
|
|
if (hasBeenThrown && velocity.y > 0) {
|
|
// Set this flag to true
|
|
velocityWasPositive = true;
|
|
}
|
|
|
|
// If we've been thrown UP in the past, but now we're coming DOWN...
|
|
if (hasBeenThrown && velocityWasPositive && velocity.y < 0) {
|
|
// Reset the state machine
|
|
hasBeenThrown = false;
|
|
velocityWasPositive = false;
|
|
// Don't take a snapshot if the camera hasn't been in the air for very long
|
|
if (Date.now() - cameraReleaseTime <= MIN_AIRTIME_MS) {
|
|
Entities.editEntity(camera, {
|
|
"userData": ""
|
|
});
|
|
return;
|
|
}
|
|
// Save these properties so that the camera falls realistically
|
|
// after it's taken the 360 snapshot
|
|
snapshotVelocity = velocity;
|
|
snapshotAngularVelocity = angularVelocity;
|
|
// Freeze the camera model and make it not grabbable
|
|
Entities.editEntity(camera, {
|
|
"velocity": {"x": 0, "y": 0, "z": 0},
|
|
"angularVelocity": {"x": 0, "y": 0, "z": 0},
|
|
"gravity": {"x": 0, "y": 0, "z": 0},
|
|
"grab": {
|
|
"grabbable": false
|
|
},
|
|
"userData": ""
|
|
});
|
|
// Add a "flash" to the camera that illuminates the ground below the camera
|
|
if (useFlash) {
|
|
flash = Entities.addEntity({
|
|
"collidesWith": "",
|
|
"collisionMask": 0,
|
|
"color": {
|
|
"blue": 173,
|
|
"green": 252,
|
|
"red": 255
|
|
},
|
|
"dimensions": {
|
|
"x": 100,
|
|
"y": 100,
|
|
"z": 100
|
|
},
|
|
"dynamic": false,
|
|
"falloffRadius": 10,
|
|
"intensity": 1,
|
|
"isSpotlight": false,
|
|
"localRotation": { w: 1, x: 0, y: 0, z: 0 },
|
|
"name": CAMERA_NAME + "_Flash",
|
|
"type": "Light",
|
|
"parentID": camera
|
|
}, "avatar");
|
|
}
|
|
// Take the snapshot!
|
|
maybeTake360Snapshot();
|
|
}
|
|
}
|
|
|
|
function capture() {
|
|
if (!isThrowMode) {
|
|
if (useFlash) {
|
|
flash = Entities.addEntity({
|
|
"collidesWith": "",
|
|
"collisionMask": 0,
|
|
"color": {
|
|
"blue": 173,
|
|
"green": 252,
|
|
"red": 255
|
|
},
|
|
"dimensions": {
|
|
"x": 100,
|
|
"y": 100,
|
|
"z": 100
|
|
},
|
|
"dynamic": false,
|
|
"falloffRadius": 10,
|
|
"intensity": 1,
|
|
"isSpotlight": false,
|
|
"localRotation": { w: 1, x: 0, y: 0, z: 0 },
|
|
"name": CAMERA_NAME + "_Flash",
|
|
"type": "Light",
|
|
"parentID": camera
|
|
}, "avatar");
|
|
}
|
|
// Take the snapshot!
|
|
maybeTake360Snapshot();
|
|
}
|
|
}
|
|
|
|
// Function Name: rpo360Off()
|
|
var WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS = 1 * 1000;
|
|
function rpo360Off(isChangingDomains) {
|
|
if (velocityLoopInterval) {
|
|
Script.clearInterval(velocityLoopInterval);
|
|
velocityLoopInterval = false;
|
|
}
|
|
|
|
function deleteCamera() {
|
|
if (flash) {
|
|
Entities.deleteEntity(flash);
|
|
flash = false;
|
|
}
|
|
if (camera) {
|
|
Entities.deleteEntity(camera);
|
|
camera = false;
|
|
}
|
|
//buttonActive(ui.isOpen);
|
|
}
|
|
|
|
secondaryCameraConfig.attachedEntityId = false;
|
|
if (camera) {
|
|
// Workaround for Avatar Entities not immediately having properties after
|
|
// the "Window.domainChanged()" signal is emitted.
|
|
// May no longer be necessary; untested...
|
|
if (isChangingDomains) {
|
|
Script.setTimeout(function () {
|
|
deleteCamera();
|
|
rpo360On();
|
|
}, WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS);
|
|
} else {
|
|
deleteCamera();
|
|
}
|
|
}
|
|
setTakePhotoControllerMappingStatus();
|
|
}
|
|
|
|
var isCurrentlyTaking360Snapshot = false;
|
|
var processing360Snapshot = false;
|
|
function maybeTake360Snapshot() {
|
|
// Don't take a snapshot if we're currently in the middle of taking one
|
|
// or if the camera entity doesn't exist
|
|
if (!isCurrentlyTaking360Snapshot && camera) {
|
|
isCurrentlyTaking360Snapshot = true;
|
|
var currentCameraPosition = Entities.getEntityProperties(camera, ["position"]).position;
|
|
// Play a sound at the current camera position
|
|
Audio.playSound(SOUND_SNAPSHOT, {
|
|
"position": { "x": currentCameraPosition.x, "y": currentCameraPosition.y, "z": currentCameraPosition.z },
|
|
"localOnly": false,
|
|
"volume": 0.8
|
|
});
|
|
Window.takeSecondaryCamera360Snapshot(currentCameraPosition);
|
|
used360AppToTakeThisSnapshot = true;
|
|
processing360Snapshot = true;
|
|
|
|
// Let the UI know we're processing a 360 snapshot now
|
|
tablet.emitScriptEvent(JSON.stringify({
|
|
"channel": channel,
|
|
"method": "startedProcessing360Snapshot"
|
|
}));
|
|
}
|
|
}
|
|
|
|
function on360SnapshotTaken(path) {
|
|
isCurrentlyTaking360Snapshot = false;
|
|
// Make the camera fall back to the ground with the same
|
|
// physical properties as when it froze in the air
|
|
if (isThrowMode) {
|
|
Entities.editEntity(camera, {
|
|
"velocity": snapshotVelocity,
|
|
"angularVelocity": snapshotAngularVelocity,
|
|
"gravity": cameraGravity,
|
|
"grab": {
|
|
"grabbable": true
|
|
}
|
|
});
|
|
}
|
|
// Delete the flash entity
|
|
if (flash) {
|
|
Entities.deleteEntity(flash);
|
|
flash = false;
|
|
}
|
|
//console.log('360 Snapshot taken. Path: ' + path);
|
|
|
|
//update UI
|
|
tablet.emitScriptEvent(JSON.stringify({
|
|
"channel": channel,
|
|
"method": "last360ThumbnailURL",
|
|
"last360ThumbnailURL": path
|
|
}));
|
|
last360ThumbnailURL = path;
|
|
Settings.setValue(SETTING_LAST_360_CAPTURE, last360ThumbnailURL);
|
|
processing360Snapshot = false;
|
|
tablet.emitScriptEvent(JSON.stringify({
|
|
"channel": channel,
|
|
"method": "finishedProcessing360Snapshot"
|
|
}));
|
|
}
|
|
|
|
|
|
var last360ThumbnailURL = Settings.getValue(SETTING_LAST_360_CAPTURE, "");
|
|
var used360AppToTakeThisSnapshot = false;
|
|
|
|
function onDomainChanged() {
|
|
rpo360Off(true);
|
|
}
|
|
|
|
// These functions will be called when the script is loaded.
|
|
var SOUND_CAMERA_ON = SoundCache.getSound(Script.resolvePath("resources/sounds/cameraOn.wav"));
|
|
var SOUND_SNAPSHOT = SoundCache.getSound(Script.resolvePath("resources/sounds/snap.wav"));
|
|
|
|
|
|
var jsMainFileName = "cam360.js";
|
|
var ROOT = Script.resolvePath('').split(jsMainFileName)[0];
|
|
|
|
var APP_NAME = "CAM360";
|
|
var APP_URL = ROOT + "cam360.html";
|
|
var APP_ICON_INACTIVE = ROOT + "resources/images/icons/cam360-i.svg";
|
|
var APP_ICON_ACTIVE = ROOT + "resources/images/icons/cam360-a.svg";
|
|
var appStatus = false;
|
|
var channel = "org.overte.applications.cam360";
|
|
|
|
var timestamp = 0;
|
|
var INTERCALL_DELAY = 200; //0.3 sec
|
|
var DEG_TO_RAD = Math.PI/180;
|
|
|
|
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
|
|
|
Window.domainChanged.connect(onDomainChanged);
|
|
Window.snapshot360Taken.connect(on360SnapshotTaken);
|
|
HMD.displayModeChanged.connect(onHMDChanged);
|
|
|
|
camera = false;
|
|
|
|
tablet.screenChanged.connect(onScreenChanged);
|
|
|
|
var button = tablet.addButton({
|
|
text: APP_NAME,
|
|
icon: APP_ICON_INACTIVE,
|
|
activeIcon: APP_ICON_ACTIVE
|
|
});
|
|
|
|
|
|
function clicked(){
|
|
if (appStatus === true) {
|
|
tablet.webEventReceived.disconnect(onAppWebEventReceived);
|
|
tablet.gotoHomeScreen();
|
|
appStatus = false;
|
|
}else{
|
|
tablet.gotoWebScreen(APP_URL);
|
|
tablet.webEventReceived.connect(onAppWebEventReceived);
|
|
appStatus = true;
|
|
}
|
|
|
|
button.editProperties({
|
|
isActive: appStatus || camera
|
|
});
|
|
}
|
|
|
|
button.clicked.connect(clicked);
|
|
|
|
|
|
function onAppWebEventReceived(message){
|
|
var d = new Date();
|
|
var n = d.getTime();
|
|
var messageObj = JSON.parse(message);
|
|
if (messageObj.channel === channel) {
|
|
if (messageObj.method === "rpo360On" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
rpo360On();
|
|
|
|
} else if (messageObj.method === "rpo360Off" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
rpo360Off();
|
|
|
|
} else if (messageObj.method === "openSettings" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false)) || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) {
|
|
Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog");
|
|
} else {
|
|
tablet.pushOntoStack("hifi/tablet/TabletGeneralPreferences.qml");
|
|
}
|
|
|
|
} else if (messageObj.method === "disableFlash" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
useFlash = false;
|
|
|
|
} else if (messageObj.method === "enableFlash" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
useFlash = true;
|
|
|
|
} else if (messageObj.method === "uiReady" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
tablet.emitScriptEvent(JSON.stringify({
|
|
"channel": channel,
|
|
"method": "initializeUI",
|
|
"masterSwitchOn": !!camera,
|
|
"last360ThumbnailURL": last360ThumbnailURL,
|
|
"processing360Snapshot": processing360Snapshot,
|
|
"useFlash": useFlash,
|
|
"isThrowMode": isThrowMode
|
|
}));
|
|
|
|
} else if (messageObj.method === "ThrowMode" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
isThrowMode = true;
|
|
if (camera) {
|
|
rpo360Off();
|
|
rpo360On();
|
|
}
|
|
} else if (messageObj.method === "PositionMode" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
isThrowMode = false;
|
|
if (camera) {
|
|
rpo360Off();
|
|
rpo360On();
|
|
}
|
|
} else if (messageObj.method === "Capture" && (n - timestamp) > INTERCALL_DELAY) {
|
|
d = new Date();
|
|
timestamp = d.getTime();
|
|
if (camera) {
|
|
capture();
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
var udateSignateDisconnected = true;
|
|
function onScreenChanged(type, url) {
|
|
if (type === "Web" && url.indexOf(APP_URL) !== -1) {
|
|
appStatus = true;
|
|
Script.update.connect(myTimer);
|
|
udateSignateDisconnected = false;
|
|
} else {
|
|
appStatus = false;
|
|
if (!udateSignateDisconnected) {
|
|
Script.update.disconnect(myTimer);
|
|
udateSignateDisconnected = true;
|
|
}
|
|
}
|
|
|
|
button.editProperties({
|
|
isActive: appStatus || camera
|
|
});
|
|
}
|
|
|
|
function myTimer(deltaTime) {
|
|
var yaw = 0.0;
|
|
var pitch = 0.0;
|
|
var roll = 0.0;
|
|
var euler;
|
|
if (!HMD.active) {
|
|
//Use cuser camera for destop
|
|
euler = Quat.safeEulerAngles(Camera.orientation);
|
|
yaw = -euler.y;
|
|
pitch = -euler.x;
|
|
roll = -euler.z;
|
|
} else {
|
|
//Use Tablet orientation for HMD
|
|
var tabletRotation = Entities.getEntityProperties(HMD.tabletID, ["rotation"]).rotation;
|
|
var noRoll = Quat.cancelOutRoll(tabletRotation); //Pushing the roll is getting quite complexe
|
|
euler = Quat.safeEulerAngles(noRoll);
|
|
yaw = euler.y - 180;
|
|
if (yaw < -180) { yaw = yaw + 360;}
|
|
yaw = -yaw;
|
|
pitch = euler.x;
|
|
roll = 0;
|
|
}
|
|
|
|
tablet.emitScriptEvent(JSON.stringify({
|
|
"channel": channel,
|
|
"method": "yawPitchRoll",
|
|
"yaw": yaw,
|
|
"pitch": pitch,
|
|
"roll": roll
|
|
}));
|
|
|
|
}
|
|
|
|
function cleanup() {
|
|
|
|
if (appStatus) {
|
|
tablet.gotoHomeScreen();
|
|
tablet.webEventReceived.disconnect(onAppWebEventReceived);
|
|
if (!udateSignateDisconnected) {
|
|
Script.update.disconnect(myTimer);
|
|
udateSignateDisconnected = true;
|
|
}
|
|
}
|
|
|
|
tablet.screenChanged.disconnect(onScreenChanged);
|
|
tablet.removeButton(button);
|
|
|
|
rpo360Off();
|
|
|
|
if (takePhotoControllerMapping) {
|
|
takePhotoControllerMapping.disable();
|
|
}
|
|
|
|
Window.domainChanged.disconnect(onDomainChanged);
|
|
Window.snapshot360Taken.disconnect(on360SnapshotTaken);
|
|
HMD.displayModeChanged.disconnect(onHMDChanged);
|
|
}
|
|
|
|
Script.scriptEnding.connect(cleanup);
|
|
|
|
//controller
|
|
function setTakePhotoControllerMappingStatus() {
|
|
if (!takePhotoControllerMapping) {
|
|
return;
|
|
}
|
|
if (!isThrowMode) {
|
|
takePhotoControllerMapping.enable();
|
|
} else {
|
|
takePhotoControllerMapping.disable();
|
|
}
|
|
}
|
|
|
|
var takePhotoControllerMapping;
|
|
var takePhotoControllerMappingName = 'Overte-cam360-Mapping-Capture';
|
|
function registerTakePhotoControllerMapping() {
|
|
takePhotoControllerMapping = Controller.newMapping(takePhotoControllerMappingName);
|
|
if (controllerType === "OculusTouch") {
|
|
takePhotoControllerMapping.from(Controller.Standard.RS).to(function (value) {
|
|
if (value === 1.0) {
|
|
if (camera) {
|
|
capture();
|
|
}
|
|
}
|
|
return;
|
|
});
|
|
} else if (controllerType === "Vive") {
|
|
takePhotoControllerMapping.from(Controller.Standard.RightPrimaryThumb).to(function (value) {
|
|
if (value === 1.0) {
|
|
if (camera) {
|
|
capture();
|
|
}
|
|
}
|
|
return;
|
|
});
|
|
}
|
|
}
|
|
|
|
var controllerType = "Other";
|
|
function registerButtonMappings() {
|
|
var VRDevices = Controller.getDeviceNames().toString();
|
|
if (VRDevices) {
|
|
if (VRDevices.indexOf("Vive") !== -1) {
|
|
controllerType = "Vive";
|
|
} else if (VRDevices.indexOf("OculusTouch") !== -1) {
|
|
controllerType = "OculusTouch";
|
|
} else {
|
|
return; // Neither Vive nor Touch detected
|
|
}
|
|
}
|
|
|
|
if (!takePhotoControllerMapping) {
|
|
registerTakePhotoControllerMapping();
|
|
}
|
|
}
|
|
|
|
function onHMDChanged(isHMDMode) {
|
|
registerButtonMappings();
|
|
}
|
|
|
|
}());
|