Add "Action Camera" app

This adds a new applications  "Action Camera",
(Yes another camera)
This one is linked to the avatar to capture the avatar in action as it move or fly.
This commit is contained in:
Alezia Kurdis 2023-12-14 21:24:48 -05:00 committed by GitHub
parent d24d5db3c4
commit ea8c546b99
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1712 additions and 1 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,702 @@
"use strict";
/*jslint vars:true, plusplus:true, forin:true*/
/*global Tablet, Script, */
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
//
// flyCamera.js
//
// Created by Alezia Kurdis, December 10th, 2023. (based on "Spectator Camera" by by Zach Fox on June 5th, 2017)
// Copyright 2023, Overte e.V.
//
// 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 ROOT = Script.resolvePath('').split("flyCamera.js")[0];
// FUNCTION VAR DECLARATIONS
var sendToQml, addOrRemoveButton, onTabletScreenChanged, fromQml,
onTabletButtonClicked, wireEventBridge, startup, shutdown, registerButtonMappings;
// Function Name: flyCameraOn()
//
// Description:
// -Call this function to set up the fly camera and spawn the camera entity.
var flyCameraConfig = Render.getConfig("SecondaryCamera");
var camera = false;
var cameraRotation;
var cameraPosition;
var cameraDistance = 3; //meters
var cameraHeight = 0; //meters
var cameraHorizontalAngle = 180; //degree
var cameraVerticalAngle = 0; //degree
var cameraTarget = "AVATAR"; //AVATAR | FORWARD | BACKWARD | OUTSIDE
var cameraViewWidth = 0.25;
var cameraViewAspect = 16/9;
var toneCurve = 1;
var cameraName = "Action Camera";
function flyCameraPositioningUpdate() {
if (camera) {
computeCamPosition();
Entities.editEntity(camera, {
"localRotation": cameraRotation,
"localPosition": cameraPosition
});
}
}
function computeCamPosition() {
var antiHorizontalAngle = cameraHorizontalAngle - 180;
if (antiHorizontalAngle < 0) {antiHorizontalAngle = antiHorizontalAngle + 360;}
switch(cameraTarget) {
case "AVATAR":
cameraRotation = Quat.fromVec3Degrees({ "x": -cameraVerticalAngle, "y": antiHorizontalAngle, "z": 0 });
break;
case "FORWARD":
cameraRotation = Quat.fromVec3Degrees({ "x": 0, "y": 0, "z": 0 });
break;
case "BACKWARD":
cameraRotation = Quat.fromVec3Degrees({ "x": 0, "y": 180, "z": 0 });
break;
case "OUTSIDE":
cameraRotation = Quat.fromVec3Degrees({ "x": cameraVerticalAngle, "y": cameraHorizontalAngle, "z": 0 });
break;
}
cameraPosition = Vec3.multiplyQbyV(Quat.fromVec3Degrees({ "x": cameraVerticalAngle, "y": cameraHorizontalAngle, "z": 0 }), { "x": 0, "y": cameraHeight, "z": -cameraDistance });
}
function flyCameraOn() {
cameraDistance = Settings.getValue('flyCamera/cameraDistance', 3); //meters
cameraHeight = Settings.getValue('flyCamera/cameraHeight', 0); //meters
cameraHorizontalAngle = Settings.getValue('flyCamera/cameraHorizontalAngle', 180); //degree
cameraVerticalAngle = Settings.getValue('flyCamera/cameraVerticalAngle', 0); //degree
cameraTarget = Settings.getValue('flyCamera/cameraTarget', "AVATAR"); //AVATAR | FORWARD | BACKWARD | OUTSIDE
Render.getConfig("SecondaryCameraJob.ToneMapping").curve = toneCurve;
// Sets the special texture size based on the window it is displayed in, which doesn't include the menu bar
flyCameraConfig.enableSecondaryCameraRenderConfigs(true);
flyCameraConfig.resetSizeSpectatorCamera(Window.innerWidth, Window.innerHeight);
computeCamPosition();
camera = Entities.addEntity({
"name": cameraName,
"dimensions": {
"x": 0.03,
"y": 0.03,
"z": 0.03
},
"color": {
"red": 0,
"green": 0,
"blue": 0
},
"canCastShadow": false,
"parentID": MyAvatar.SELF_ID,
"localRotation": cameraRotation,
"localPosition": cameraPosition,
"type": "Shape",
"shape": "Cube",
"grab": {
"grabbable": false
},
"isVisibleInSecondaryCamera": false,
"visible": false
}, "local");
flyCameraConfig.attachedEntityId = camera;
if (!HMD.active) {
setMonitorShowsCameraView(false);
} else {
setDisplay(monitorShowsCameraView);
}
// Change button to active when window is first opened OR if the camera is on, false otherwise.
if (button) {
button.editProperties({ isActive: onFlyCameraScreen || camera });
}
Audio.playSound(SOUND_CAMERA_ON, {
volume: 0.25,
position: cameraPosition,
localOnly: true
});
setSwitchViewControllerMappingStatus(true);
setTakeSnapshotControllerMappingStatus(true);
}
// Function Name: flyCameraOff()
//
// Description:
// -Call this function to shut down the fly camera and
// destroy the camera entity. "isChangingDomains" is true when this function is called
// from the "Window.domainChanged()" signal.
var WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS = 1 * 1000;
function flyCameraOff(isChangingDomains) {
function deleteCamera() {
if (flash) {
Entities.deleteEntity(flash);
flash = false;
}
if (camera) {
Entities.deleteEntity(camera);
camera = false;
}
setSwitchViewControllerMappingStatus(false);
setTakeSnapshotControllerMappingStatus(false);
if (button) {
// Change button to active when window is first openend OR if the camera is on, false otherwise.
button.editProperties({ isActive: onFlyCameraScreen || camera });
}
}
flyCameraConfig.attachedEntityId = false;
flyCameraConfig.enableSecondaryCameraRenderConfigs(false);
if (camera) {
if (isChangingDomains) {
Script.setTimeout(function () {
deleteCamera();
flyCameraOn();
}, WAIT_AFTER_DOMAIN_SWITCH_BEFORE_CAMERA_DELETE_MS);
} else {
deleteCamera();
}
}
setDisplay(monitorShowsCameraView);
}
// Function Name: addOrRemoveButton()
//
// Description:
// -Used to add or remove the "FLY-CAM" app button from the HUD/tablet. Set the "isShuttingDown" argument
// to true if you're calling this function upon script shutdown. Set the "isHMDmode" to true if the user is
// in HMD; otherwise set to false.
var button = false;
var buttonName = "FLY-CAM";
function addOrRemoveButton(isShuttingDown) {
if (!tablet) {
print("Warning in addOrRemoveButton(): 'tablet' undefined!");
return;
}
if (!button) {
if (!isShuttingDown) {
button = tablet.addButton({
text: buttonName,
icon: ROOT + "flyCam-i.png",
activeIcon: ROOT + "flyCam-a.png"
});
button.clicked.connect(onTabletButtonClicked);
}
} else if (button) {
if (isShuttingDown) {
button.clicked.disconnect(onTabletButtonClicked);
tablet.removeButton(button);
button = false;
}
} else {
print("ERROR adding/removing FLY-CAM button!");
}
}
// Function Name: startup()
//
// Description:
// -startup() will be called when the script is loaded.
var tablet = null;
function startup() {
tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
addOrRemoveButton(false);
tablet.screenChanged.connect(onTabletScreenChanged);
Window.domainChanged.connect(onDomainChanged);
Controller.keyPressEvent.connect(keyPressEvent);
HMD.displayModeChanged.connect(onHMDChanged);
camera = false;
registerButtonMappings();
}
// Function Name: wireEventBridge()
//
// Description:
// -Used to connect/disconnect the script's response to the tablet's "fromQml" signal. Set the "on" argument to enable or
// disable to event bridge.
var hasEventBridge = false;
function wireEventBridge(on) {
if (!tablet) {
print("Warning in wireEventBridge(): 'tablet' undefined!");
return;
}
if (on) {
if (!hasEventBridge) {
tablet.fromQml.connect(fromQml);
hasEventBridge = true;
}
} else {
if (hasEventBridge) {
tablet.fromQml.disconnect(fromQml);
hasEventBridge = false;
}
}
}
// Function Name: setDisplay()
//
// Description:
// -There are two bool variables that determine what the "url" argument to "setDisplayTexture(url)" should be:
// Camera on/off switch, and the "Monitor Shows" on/off switch.
// This results in four possible cases for the argument. Those four cases are:
// 1. Camera is off; "Monitor Shows" is "HMD Preview": "url" is ""
// 2. Camera is off; "Monitor Shows" is "Camera View": "url" is ""
// 3. Camera is on; "Monitor Shows" is "HMD Preview": "url" is ""
// 4. Camera is on; "Monitor Shows" is "Camera View": "url" is "resource://spectatorCameraFrame"
function setDisplay(showCameraView) {
var url = (camera) ? (showCameraView ? "resource://spectatorCameraFrame" : "resource://hmdPreviewFrame") : "";
// FIXME: temporary hack to avoid setting the display texture to hmdPreviewFrame
// until it is the correct mono.
if (url === "resource://hmdPreviewFrame") {
Window.setDisplayTexture("");
} else {
Window.setDisplayTexture(url);
}
}
const MONITOR_SHOWS_CAMERA_VIEW_DEFAULT = false;
var monitorShowsCameraView = !!Settings.getValue('flyCamera/monitorShowsCameraView', MONITOR_SHOWS_CAMERA_VIEW_DEFAULT);
function setMonitorShowsCameraView(showCameraView) {
setDisplay(showCameraView);
monitorShowsCameraView = showCameraView;
Settings.setValue('flyCamera/monitorShowsCameraView', showCameraView);
}
function setMonitorShowsCameraViewAndSendToQml(showCameraView) {
setMonitorShowsCameraView(showCameraView);
sendToQml({ method: 'updateMonitorShowsSwitch', params: showCameraView });
}
function keyPressEvent(event) {
if ((event.text === "0") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && event.isControl && !event.isAlt) {
setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView);
}
}
function setSwitchViewControllerMappingStatus(status) {
if (!switchViewControllerMapping) {
return;
}
if (status) {
switchViewControllerMapping.enable();
} else {
switchViewControllerMapping.disable();
}
}
function setTakeSnapshotControllerMappingStatus(status) {
if (!takeSnapshotControllerMapping) {
return;
}
if (status) {
takeSnapshotControllerMapping.enable();
} else {
takeSnapshotControllerMapping.disable();
}
}
// Function Name: registerButtonMappings()
//
// Description:
// -Updates controller button mappings for fly Camera.
var switchViewControllerMapping;
var switchViewControllerMappingName = 'Hifi-flyCamera-Mapping-SwitchView';
function registerSwitchViewControllerMapping() {
switchViewControllerMapping = Controller.newMapping(switchViewControllerMappingName);
if (controllerType === "OculusTouch") {
switchViewControllerMapping.from(Controller.Standard.LS).to(function (value) {
if (value === 1.0) {
setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView);
}
return;
});
} else if (controllerType === "Vive") {
switchViewControllerMapping.from(Controller.Standard.LeftPrimaryThumb).to(function (value) {
if (value === 1.0) {
setMonitorShowsCameraViewAndSendToQml(!monitorShowsCameraView);
}
return;
});
}
}
var takeSnapshotControllerMapping;
var takeSnapshotControllerMappingName = 'Hifi-flyCamera-Mapping-TakeSnapshot';
var flash = false;
function setFlashStatus(enabled) {
var cameraPosition = Entities.getEntityProperties(camera, ["positon"]).position;
if (enabled) {
if (camera) {
Audio.playSound(SOUND_FLASH_ON, {
position: cameraPosition,
localOnly: true,
volume: 0.8
});
flash = Entities.addEntity({
"collidesWith": "",
"collisionMask": 0,
"color": {
"blue": 173,
"green": 252,
"red": 255
},
"cutoff": 90,
"dimensions": {
"x": 4,
"y": 4,
"z": 4
},
"dynamic": false,
"falloffRadius": 0.20000000298023224,
"intensity": 37,
"isSpotlight": true,
"localRotation": { w: 1, x: 0, y: 0, z: 0 },
"localPosition": { x: 0, y: -0.005, z: -0.08 },
"name": "Camera Flash",
"type": "Light",
"parentID": camera,
}, true);
}
} else {
if (flash) {
Audio.playSound(SOUND_FLASH_OFF, {
position: cameraPosition,
localOnly: true,
volume: 0.8
});
Entities.deleteEntity(flash);
flash = false;
}
}
}
function onStillSnapshotTaken() {
Render.getConfig("SecondaryCameraJob.ToneMapping").curve = toneCurve;
sendToQml({
method: 'finishedProcessingStillSnapshot'
});
}
function maybeTakeSnapshot() {
if (camera) {
sendToQml({
method: 'startedProcessingStillSnapshot'
});
// Wait a moment before taking the snapshot for the tonemapping curve to update
Script.setTimeout(function () {
Audio.playSound(SOUND_SNAPSHOT, {
position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z },
localOnly: true,
volume: 1.0
});
Window.takeSecondaryCameraSnapshot();
}, 250);
} else {
sendToQml({
method: 'finishedProcessingStillSnapshot'
});
}
}
function on360SnapshotTaken() {
if (monitorShowsCameraView) {
setDisplay(true);
}
sendToQml({
method: 'finishedProcessing360Snapshot'
});
}
function maybeTake360Snapshot() {
if (camera) {
Audio.playSound(SOUND_SNAPSHOT, {
position: { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z },
localOnly: true,
volume: 1.0
});
if (HMD.active && monitorShowsCameraView) {
setDisplay(false);
}
Window.takeSecondaryCamera360Snapshot(Entities.getEntityProperties(camera, ["positon"]).position);
}
}
function registerTakeSnapshotControllerMapping() {
takeSnapshotControllerMapping = Controller.newMapping(takeSnapshotControllerMappingName);
if (controllerType === "OculusTouch") {
takeSnapshotControllerMapping.from(Controller.Standard.RS).to(function (value) {
if (value === 1.0) {
maybeTakeSnapshot();
}
return;
});
} else if (controllerType === "Vive") {
takeSnapshotControllerMapping.from(Controller.Standard.RightPrimaryThumb).to(function (value) {
if (value === 1.0) {
maybeTakeSnapshot();
}
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 {
sendToQml({
method: 'updateControllerMappingCheckbox',
controller: controllerType
});
return; // Neither Vive nor Touch detected
}
}
if (!switchViewControllerMapping) {
registerSwitchViewControllerMapping();
}
setSwitchViewControllerMappingStatus(true);
if (!takeSnapshotControllerMapping) {
registerTakeSnapshotControllerMapping();
}
setTakeSnapshotControllerMappingStatus(true);
sendToQml({
method: 'updateControllerMappingCheckbox',
controller: controllerType
});
}
// Function Name: onTabletButtonClicked()
//
// Description:
// -Fired when the fly Camera app button is pressed.
//
// Relevant Variables:
// -FLY_CAMERA_QML_SOURCE: The path to the flyCamera QML
// -onFlyCameraScreen: true/false depending on whether we're looking at the fly camera app.
var FLY_CAMERA_QML_SOURCE = Script.resolvePath("flyCamera.qml");
var onFlyCameraScreen = false;
function onTabletButtonClicked() {
if (!tablet) {
print("Warning in onTabletButtonClicked(): 'tablet' undefined!");
return;
}
if (onFlyCameraScreen) {
// for toolbar-mode: go back to home screen, this will close the window.
tablet.gotoHomeScreen();
} else {
tablet.loadQMLSource(FLY_CAMERA_QML_SOURCE);
}
}
function updateFlyCameraQML() {
var messageToQML = {
"method": 'initializeUI',
"masterSwitchOn": !!camera,
"flashCheckboxChecked": !!flash,
"monitorShowsCamView": monitorShowsCameraView,
"cameraDistance": cameraDistance,
"cameraTarget": cameraTarget,
"cameraHorizontalAngle": cameraHorizontalAngle,
"cameraVerticalAngle": cameraVerticalAngle
};
sendToQml(messageToQML);
registerButtonMappings();
Menu.setIsOptionChecked("Disable Preview", false);
Menu.setIsOptionChecked("Mono Preview", true);
}
var signalsWired = false;
function wireSignals(shouldWire) {
if (signalsWired === shouldWire) {
return;
}
signalsWired = shouldWire;
if (shouldWire) {
Window.stillSnapshotTaken.connect(onStillSnapshotTaken);
Window.snapshot360Taken.connect(on360SnapshotTaken);
} else {
Window.stillSnapshotTaken.disconnect(onStillSnapshotTaken);
Window.snapshot360Taken.disconnect(on360SnapshotTaken);
}
}
// Function Name: onTabletScreenChanged()
//
// Description:
// -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string
// value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML.
function onTabletScreenChanged(type, url) {
onFlyCameraScreen = (type === "QML" && url === FLY_CAMERA_QML_SOURCE);
wireEventBridge(onFlyCameraScreen);
// Change button to active when window is first openend OR if the camera is on, false otherwise.
if (button) {
button.editProperties({ isActive: onFlyCameraScreen || camera });
}
// In the case of a remote QML app, it takes a bit of time
// for the event bridge to actually connect, so we have to wait...
Script.setTimeout(function () {
if (onFlyCameraScreen) {
updateFlyCameraQML();
}
}, 700);
wireSignals(onFlyCameraScreen);
}
// Function Name: sendToQml()
//
// Description:
// -Use this function to send a message to the QML (i.e. to change appearances). The "message" argument is what is sent to
// flyCamera QML in the format "{method, params}", like json-rpc. See also fromQml().
function sendToQml(message) {
if (onFlyCameraScreen) {
tablet.sendToQml(message);
}
}
// Function Name: fromQml()
//
// Description:
// -Called when a message is received from flyCamera.qml. The "message" argument is what is sent from the flyCamera QML
// in the format "{method, params}", like json-rpc. See also sendToQml().
function fromQml(message) {
switch (message.method) {
case 'flyCameraOn':
flyCameraOn();
break;
case 'flyCameraOff':
flyCameraOff();
break;
case 'setMonitorShowsCameraView':
setMonitorShowsCameraView(message.params);
break;
case 'updateCameravFoV':
flyCameraConfig.vFoV = message.vFoV;
break;
case 'updateToneMap':
toneCurve = message.toneCurve;
Render.getConfig("SecondaryCameraJob.ToneMapping").curve = toneCurve;
break;
case 'setFlashStatus':
setFlashStatus(message.enabled);
break;
case 'updateCameraHorAngle':
cameraHorizontalAngle = message.horAngle;
Settings.setValue('flyCamera/cameraHorizontalAngle', cameraHorizontalAngle);
flyCameraPositioningUpdate();
break;
case 'updateCameraVertAngle':
cameraVerticalAngle = message.vertAngle;
Settings.setValue('flyCamera/cameraVerticalAngle', cameraVerticalAngle);
flyCameraPositioningUpdate();
break;
case 'updateCameraDist':
cameraDistance = Math.floor(message.distance*10)/10;
Settings.setValue('flyCamera/cameraDistance', cameraDistance);
flyCameraPositioningUpdate();
break;
case 'updateCameraTarget':
cameraTarget = message.target;
Settings.setValue('flyCamera/cameraTarget', cameraTarget);
flyCameraPositioningUpdate();
break;
case 'updateCameraHeight':
cameraHeight = Math.floor(message.height*100)/100;
Settings.setValue('flyCamera/cameraHeight', cameraHeight);
flyCameraPositioningUpdate();
break;
case 'takeSecondaryCameraSnapshot':
maybeTakeSnapshot();
break;
case 'takeSecondaryCamera360Snapshot':
maybeTake360Snapshot();
break;
case 'openSettings':
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");
}
break;
default:
print('Unrecognized message from flyCamera.qml:', JSON.stringify(message));
}
}
// Function Name: onHMDChanged()
//
// Description:
// -Called from C++ when HMD mode is changed. The argument "isHMDMode" is true if HMD is on; false otherwise.
function onHMDChanged(isHMDMode) {
registerButtonMappings();
if (!isHMDMode) {
setMonitorShowsCameraView(false);
} else {
setDisplay(monitorShowsCameraView);
}
}
// Function Name: shutdown()
//
// Description:
// -shutdown() will be called when the script ends (i.e. is stopped).
function shutdown() {
flyCameraOff();
Window.domainChanged.disconnect(onDomainChanged);
wireSignals(false);
if (tablet) {
tablet.screenChanged.disconnect(onTabletScreenChanged);
if (onFlyCameraScreen) {
tablet.gotoHomeScreen();
}
}
addOrRemoveButton(true);
HMD.displayModeChanged.disconnect(onHMDChanged);
Controller.keyPressEvent.disconnect(keyPressEvent);
if (switchViewControllerMapping) {
switchViewControllerMapping.disable();
}
if (takeSnapshotControllerMapping) {
takeSnapshotControllerMapping.disable();
}
Script.scriptEnding.disconnect(shutdown);
}
// Function Name: onDomainChanged()
//
// Description:
// -A small utility function used when the Window.domainChanged() signal is fired.
function onDomainChanged() {
flyCameraOff(true);
}
// These functions will be called when the script is loaded.
var SOUND_CAMERA_ON = SoundCache.getSound(Script.resolvePath("cameraOn.wav"));
var SOUND_SNAPSHOT = SoundCache.getSound(Script.resolvePath("snap.wav"));
var SOUND_FLASH_ON = SoundCache.getSound(Script.resolvePath("flashOn.wav"));
var SOUND_FLASH_OFF = SoundCache.getSound(Script.resolvePath("flashOff.wav"));
startup();
Script.scriptEnding.connect(shutdown);
}());

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

View file

@ -269,6 +269,15 @@ var metadata = { "applications":
"jsfile": "inventory-app/dist/inventory.js",
"icon": "inventory-app/dist/inventory-i.svg",
"caption": "INVENTORY"
}
},
{
"isActive": true,
"directory": "flyCam",
"name": "Action Camera",
"description": "The Action Camera is attached to your avatar, allowing you to capture video while moving, walking, or flying. Fully adjustable around your avatar, this camera is invisible and ensures optimal capture. It also includes most of the features found in the Spectator Camera.",
"jsfile": "flyCam/flyCamera.js",
"icon": "flyCam/flyCam-i.png",
"caption": "FLY-CAM"
},
]
};