mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-05-08 05:38:20 +02:00
874 lines
34 KiB
JavaScript
874 lines
34 KiB
JavaScript
// app-camera-move.js
|
|
//
|
|
// Created by Timothy Dedischew on 05/05/2017.
|
|
// Copyright 2017 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
|
|
//
|
|
// This Client script sets up the Camera Control Tablet App, which can be used to configure and
|
|
// drive your avatar with easing/smoothing movement constraints for a less jittery filming experience.
|
|
|
|
/* eslint-disable comma-dangle */
|
|
"use strict";
|
|
|
|
var VERSION = '0.0.0d',
|
|
NAMESPACE = 'app-camera-move',
|
|
APP_HTML_URL = Script.resolvePath('app.html'),
|
|
BUTTON_CONFIG = {
|
|
text: '\nCam Drive',
|
|
icon: Script.resolvePath('Eye-Camera.svg'),
|
|
},
|
|
DEFAULT_TOGGLE_KEY = '{ "text": "SPACE" }';
|
|
|
|
var MINIMAL_CURSOR_SCALE = 0.5,
|
|
FILENAME = Script.resolvePath(''),
|
|
WANT_DEBUG = (
|
|
false || (FILENAME.match(/[&#?]debug[=](\w+)/)||[])[1] ||
|
|
Settings.getValue(NAMESPACE + '/debug')
|
|
),
|
|
EPSILON = 1e-6,
|
|
DEG_TO_RAD = Math.PI / 180.0;
|
|
|
|
WANT_DEBUG = 1;
|
|
|
|
function log() {
|
|
print( NAMESPACE + ' | ' + [].slice.call(arguments).join(' ') );
|
|
}
|
|
|
|
var require = Script.require,
|
|
debugPrint = function(){},
|
|
_debugChannel = NAMESPACE + '.stats',
|
|
overlayDebugOutput = function(){};
|
|
|
|
if (WANT_DEBUG) {
|
|
_instrumentDebugValues();
|
|
}
|
|
|
|
var _utils = require('./modules/_utils.js'),
|
|
assert = _utils.assert,
|
|
CustomSettingsApp = require('./modules/custom-settings-app/CustomSettingsApp.js'),
|
|
movementUtils = require('./modules/movement-utils.js?'+ +new Date),
|
|
configUtils = require('./modules/config-utils.js');
|
|
|
|
Object.assign = Object.assign || _utils.assign;
|
|
|
|
var cameraControls, eventMapper, cameraConfig, applicationConfig;
|
|
|
|
var DEFAULTS = {
|
|
'namespace': NAMESPACE,
|
|
'debug': WANT_DEBUG,
|
|
'camera-move-enabled': false,
|
|
'thread-update-mode': movementUtils.CameraControls.SCRIPT_UPDATE,
|
|
'fps': 90,
|
|
'drive-mode': movementUtils.DriveModes.MOTOR,
|
|
'use-head': true,
|
|
'stay-grounded': true,
|
|
'prevent-roll': true,
|
|
'constant-delta-time': false,
|
|
'minimal-cursor': false,
|
|
'normalize-inputs': false,
|
|
'enable-mouse-smooth': true,
|
|
'translation-max-velocity': 5.50,
|
|
'translation-ease-in': 1.25,
|
|
'translation-ease-out': 5.50,
|
|
'rotation-max-velocity': 90.00,
|
|
'rotation-ease-in': 1.00,
|
|
'rotation-ease-out': 5.50,
|
|
'rotation-x-speed': 45,
|
|
'rotation-y-speed': 60,
|
|
'rotation-z-speed': 1,
|
|
'rotation-mouse-multiplier': 1.0,
|
|
'rotation-keyboard-multiplier': 1.0,
|
|
|
|
'ui-enable-tooltips': true,
|
|
'ui-show-advanced-options': false,
|
|
};
|
|
|
|
// map setting names to/from corresponding Menu and API properties
|
|
var APPLICATION_SETTINGS = {
|
|
'Avatar/pitchSpeed': 'pitchSpeed' in MyAvatar && {
|
|
object: [ MyAvatar, 'pitchSpeed' ]
|
|
},
|
|
'Avatar/yawSpeed': 'yawSpeed' in MyAvatar && {
|
|
object: [ MyAvatar, 'yawSpeed' ]
|
|
},
|
|
'Avatar/Enable Avatar Collisions': {
|
|
menu: 'Avatar > Enable Avatar Collisions',
|
|
object: [ MyAvatar, 'collisionsEnabled' ],
|
|
},
|
|
'Avatar/Draw Mesh': {
|
|
menu: 'Developer > Draw Mesh',
|
|
// object: [ MyAvatar, 'shouldRenderLocally' ], // shouldRenderLocally seems to be broken...
|
|
object: [ MyAvatar, 'getEnableMeshVisible', 'setEnableMeshVisible' ],
|
|
},
|
|
'Avatar/useSnapTurn': {
|
|
object: [ MyAvatar, 'getSnapTurn', 'setSnapTurn' ],
|
|
},
|
|
'Avatar/lookAtSnappingEnabled': 'lookAtSnappingEnabled' in MyAvatar && {
|
|
menu: 'Developer > Enable LookAt Snapping',
|
|
object: [ MyAvatar, 'lookAtSnappingEnabled' ]
|
|
},
|
|
'camera-move-enabled': {
|
|
get: function() {
|
|
return cameraControls && cameraControls.enabled;
|
|
},
|
|
set: function(nv) {
|
|
cameraControls.setEnabled(!!nv);
|
|
},
|
|
},
|
|
};
|
|
|
|
|
|
var DEBUG_INFO = {
|
|
// these values are also sent to the tablet app after EventBridge initialization
|
|
appVersion: VERSION,
|
|
utilsVersion: _utils.version,
|
|
movementVersion: movementUtils.version,
|
|
configVersion: configUtils.version,
|
|
clientScript: Script.resolvePath(''),
|
|
MyAvatar: {
|
|
supportsPitchSpeed: 'pitchSpeed' in MyAvatar,
|
|
supportsYawSpeed: 'yawSpeed' in MyAvatar,
|
|
supportsLookAtSnappingEnabled: 'lookAtSnappingEnabled' in MyAvatar,
|
|
supportsDensity: 'density' in MyAvatar,
|
|
},
|
|
Reticle: {
|
|
supportsScale: 'scale' in Reticle,
|
|
},
|
|
protocolVersion: location.protocolVersion,
|
|
};
|
|
|
|
var globalState = {
|
|
// cached values from the last animation frame
|
|
previousValues: {
|
|
reset: function() {
|
|
this.pitchYawRoll = Vec3.ZERO;
|
|
this.thrust = Vec3.ZERO;
|
|
},
|
|
},
|
|
|
|
// batch updates to MyAvatar/Camera properties (submitting together seems to help reduce timeslice jitter)
|
|
pendingChanges: _utils.DeferredUpdater.createGroup({
|
|
Camera: Camera,
|
|
MyAvatar: MyAvatar,
|
|
}, { dedupe: false }),
|
|
|
|
// current input controls' effective velocities
|
|
currentVelocities: new movementUtils.VelocityTracker({
|
|
translation: Vec3.ZERO,
|
|
step_translation: Vec3.ZERO,
|
|
rotation: Vec3.ZERO,
|
|
step_rotation: Vec3.ZERO,
|
|
zoom: Vec3.ZERO,
|
|
}),
|
|
};
|
|
|
|
function main() {
|
|
log('initializing...', VERSION);
|
|
|
|
var tablet = Tablet.getTablet('com.highfidelity.interface.tablet.system'),
|
|
button = tablet.addButton(BUTTON_CONFIG);
|
|
|
|
Script.scriptEnding.connect(function() {
|
|
tablet.removeButton(button);
|
|
button = null;
|
|
});
|
|
|
|
// track both runtime state (applicationConfig) and settings state (cameraConfig)
|
|
// (this is necessary because Interface does not yet consistently keep config Menus, APIs and Settings in sync)
|
|
applicationConfig = new configUtils.ApplicationConfig({
|
|
namespace: DEFAULTS.namespace,
|
|
config: APPLICATION_SETTINGS,
|
|
});
|
|
cameraConfig = new configUtils.SettingsConfig({
|
|
namespace: DEFAULTS.namespace,
|
|
defaultValues: DEFAULTS,
|
|
});
|
|
|
|
var toggleKey = JSON.parse(DEFAULT_TOGGLE_KEY);
|
|
if (cameraConfig.getValue('toggle-key')) {
|
|
try { toggleKey = JSON.parse(cameraConfig.getValue('toggle-key')); } catch(e) {}
|
|
}
|
|
// set up a monitor to observe configuration changes between the two sources
|
|
var MONITOR_INTERVAL_MS = 1000;
|
|
_startConfigationMonitor(applicationConfig, cameraConfig, MONITOR_INTERVAL_MS);
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// set up the tablet webview app
|
|
log('APP_HTML_URL', APP_HTML_URL);
|
|
var settingsApp = new CustomSettingsApp({
|
|
namespace: cameraConfig.namespace,
|
|
uuid: cameraConfig.uuid,
|
|
settingsAPI: cameraConfig,
|
|
url: APP_HTML_URL,
|
|
tablet: tablet,
|
|
extraParams: Object.assign({
|
|
toggleKey: toggleKey,
|
|
}, _utils.getSystemMetadata(), DEBUG_INFO),
|
|
debug: WANT_DEBUG > 1,
|
|
});
|
|
Script.scriptEnding.connect(settingsApp, 'cleanup');
|
|
settingsApp.valueUpdated.connect(function(key, value, oldValue, origin) {
|
|
log('[settingsApp.valueUpdated @ ' + origin + ']', key + ' = ' + JSON.stringify(value) + ' (was: ' + JSON.stringify(oldValue) + ')');
|
|
if (/tablet/i.test(origin)) {
|
|
log('cameraConfig applying immediate setting', key, value);
|
|
// apply relevant settings immediately when changed from the app UI
|
|
applicationConfig.applyValue(key, value, origin);
|
|
}
|
|
});
|
|
|
|
settingsApp.onUnhandledMessage = function(msg) {
|
|
switch(msg.method) {
|
|
case 'window.close': {
|
|
this.toggle(false);
|
|
} break;
|
|
case 'reloadClientScript': {
|
|
log('reloadClientScript...');
|
|
_utils.reloadClientScript(FILENAME);
|
|
} break;
|
|
case 'resetSensors': {
|
|
Menu.triggerOption('Reset Sensors');
|
|
Script.setTimeout(function() {
|
|
MyAvatar.bodyPitch = 0;
|
|
MyAvatar.bodyRoll = 0;
|
|
MyAvatar.orientation = Quat.cancelOutRollAndPitch(MyAvatar.orientation);
|
|
}, 500);
|
|
} break;
|
|
case 'reset': {
|
|
//if (!Window.confirm('Reset all camera move settings to system defaults?')) {
|
|
// return;
|
|
//}
|
|
var novalue = Uuid.generate();
|
|
var resetValues = {};
|
|
Object.keys(DEFAULTS).reduce(function(out, key) {
|
|
var resolved = cameraConfig.resolve(key);
|
|
out[resolved] = resolved in out ? out[resolved] : DEFAULTS[key];
|
|
return out;
|
|
}, resetValues);
|
|
Object.keys(applicationConfig.config).reduce(function(out, key) {
|
|
var resolved = applicationConfig.resolve(key);
|
|
out[resolved] = resolved in out ? out[resolved] : applicationConfig.getValue(key);
|
|
return out;
|
|
}, resetValues);
|
|
log('resetValues', JSON.stringify(resetValues, 0, 2));
|
|
for (var p in resetValues) {
|
|
var value = resetValues[p];
|
|
applicationConfig.applyValue(p, value, 'reset');
|
|
cameraConfig.setValue(p, value);
|
|
}
|
|
} break;
|
|
case 'overlayWebWindow': {
|
|
_overlayWebWindow(msg.options);
|
|
} break;
|
|
default: {
|
|
log('onUnhandledMessage', JSON.stringify(msg,0,2));
|
|
} break;
|
|
}
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// set up the keyboard/mouse/controller/meta input state manager
|
|
eventMapper = new movementUtils.MovementEventMapper({
|
|
namespace: DEFAULTS.namespace,
|
|
mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'),
|
|
xexcludeNames: [ 'Keyboard.C', 'Keyboard.E', 'Actions.TranslateY' ],
|
|
mouseMultiplier: cameraConfig.getValue('rotation-mouse-multiplier'),
|
|
keyboardMultiplier: cameraConfig.getValue('rotation-keyboard-multiplier'),
|
|
eventFilter: function eventFilter(from, event, defaultFilter) {
|
|
var result = defaultFilter(from, event),
|
|
driveKeyName = event.driveKeyName;
|
|
if (!result || !driveKeyName) {
|
|
if (from === 'Keyboard.RightMouseButton') {
|
|
settingsApp.syncValue('Keyboard.RightMouseButton', event.actionValue, 'eventFilter');
|
|
}
|
|
return 0;
|
|
}
|
|
if (cameraConfig.getValue('normalize-inputs')) {
|
|
result = _utils.sign(result);
|
|
}
|
|
if (from === 'Actions.Pitch') {
|
|
result *= cameraConfig.getFloat('rotation-x-speed');
|
|
}
|
|
if (from === 'Actions.Yaw') {
|
|
result *= cameraConfig.getFloat('rotation-y-speed');
|
|
}
|
|
return result;
|
|
},
|
|
});
|
|
Script.scriptEnding.connect(eventMapper, 'disable');
|
|
applicationConfig.register({
|
|
'enable-mouse-smooth': { object: [ eventMapper.options, 'mouseSmooth' ] },
|
|
'rotation-keyboard-multiplier': { object: [ eventMapper.options, 'keyboardMultiplier' ] },
|
|
'rotation-mouse-multiplier': { object: [ eventMapper.options, 'mouseMultiplier' ] },
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// set up the top-level camera controls manager / animator
|
|
cameraControls = new movementUtils.CameraControls({
|
|
namespace: DEFAULTS.namespace,
|
|
update: update,
|
|
threadMode: cameraConfig.getValue('thread-update-mode'),
|
|
fps: cameraConfig.getValue('fps'),
|
|
getRuntimeSeconds: _utils.getRuntimeSeconds,
|
|
});
|
|
Script.scriptEnding.connect(cameraControls, 'disable');
|
|
applicationConfig.register({
|
|
'thread-update-mode': { object: [ cameraControls, 'threadMode' ] },
|
|
'fps': { object: [ cameraControls, 'fps' ] },
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// set up SPACEBAR for toggling camera movement mode
|
|
var spacebar = new _utils.KeyListener(Object.assign(toggleKey, {
|
|
onKeyPressEvent: function(event) {
|
|
cameraControls.setEnabled(!cameraControls.enabled);
|
|
},
|
|
}));
|
|
Script.scriptEnding.connect(spacebar, 'disconnect');
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// set up ESC for reset drive key states
|
|
Script.scriptEnding.connect(new _utils.KeyListener({
|
|
text: 'ESC',
|
|
onKeyPressEvent: function(event) {
|
|
if (cameraControls.enabled) {
|
|
log('ESC pressed -- resetting drive keys values:', JSON.stringify({
|
|
virtualDriveKeys: eventMapper.states,
|
|
movementState: eventMapper.getState(),
|
|
}, 0, 2));
|
|
eventMapper.states.reset();
|
|
}
|
|
},
|
|
}), 'disconnect');
|
|
|
|
// set up the tablet button to toggle the app UI display
|
|
button.clicked.connect(settingsApp, function(enable) {
|
|
Object.assign(this.extraParams, _utils.getSystemMetadata());
|
|
this.toggle(enable);
|
|
});
|
|
|
|
settingsApp.isActiveChanged.connect(function(isActive) {
|
|
updateButtonText();
|
|
});
|
|
|
|
cameraControls.modeChanged.connect(onCameraModeChanged);
|
|
|
|
var fpsTimeout = 0;
|
|
function updateButtonText() {
|
|
var lines = [
|
|
settingsApp.isActive ? '(app open)' : '',
|
|
cameraControls.enabled ? (update.momentaryFPS||0).toFixed(2) + 'fps' : BUTTON_CONFIG.text.trim()
|
|
];
|
|
button && button.editProperties({ text: lines.join('\n') });
|
|
}
|
|
|
|
cameraControls.enabledChanged.connect(function(enabled) {
|
|
log('enabledChanged', enabled);
|
|
button && button.editProperties({ isActive: enabled });
|
|
if (enabled) {
|
|
onCameraControlsEnabled();
|
|
fpsTimeout = Script.setInterval(updateButtonText, 1000);
|
|
} else {
|
|
if (fpsTimeout) {
|
|
Script.clearInterval(fpsTimeout);
|
|
fpsTimeout = 0;
|
|
}
|
|
eventMapper.disable();
|
|
_resetMyAvatarMotor({ MyAvatar: MyAvatar });
|
|
updateButtonText();
|
|
}
|
|
cameraConfig.getValue('debug') && Overlays.editOverlay(overlayDebugOutput.overlayID, { visible: enabled });
|
|
});
|
|
|
|
var resetIfChanged = [
|
|
'minimal-cursor', 'drive-mode', 'fps', 'thread-update-mode',
|
|
'rotation-mouse-multiplier', 'rotation-keyboard-multiplier',
|
|
'enable-mouse-smooth', 'constant-delta-time',
|
|
].filter(Boolean).map(_utils.bind(cameraConfig, 'resolve'));
|
|
|
|
cameraConfig.valueUpdated.connect(function(key, value, oldValue, origin) {
|
|
var triggerReset = !!~resetIfChanged.indexOf(key);
|
|
log('[cameraConfig.valueUpdated @ ' + origin + ']',
|
|
key + ' = ' + JSON.stringify(value), '(was:' + JSON.stringify(oldValue) + ')',
|
|
'triggerReset: ' + triggerReset);
|
|
|
|
if (/tablet/i.test(origin)) {
|
|
log('cameraConfig applying immediate setting', key, value);
|
|
// apply relevant settings immediately when changed from the app UI
|
|
applicationConfig.applyValue(key, value, origin);
|
|
log(JSON.stringify(cameraConfig.getValue(key)));
|
|
}
|
|
0&&debugPrint('//cameraConfig.valueUpdated', JSON.stringify({
|
|
key: key,
|
|
cameraConfig: cameraConfig.getValue(key),
|
|
applicationConfig: applicationConfig.getValue(key),
|
|
value: value,
|
|
triggerReset: triggerReset,
|
|
},0,2));
|
|
|
|
if (triggerReset) {
|
|
log('KEYBOARD multiplier', eventMapper.options.keyboardMultiplier);
|
|
cameraControls.reset();
|
|
}
|
|
});
|
|
|
|
if (cameraConfig.getValue('camera-move-enabled')) {
|
|
cameraControls.enable();
|
|
}
|
|
|
|
log('DEFAULTS', JSON.stringify(DEFAULTS, 0, 2));
|
|
} // main()
|
|
|
|
function onCameraControlsEnabled() {
|
|
log('onCameraControlsEnabled!!!! ----------------------------------------------');
|
|
globalState.previousValues.reset();
|
|
globalState.currentVelocities.reset();
|
|
globalState.pendingChanges.reset();
|
|
eventMapper.enable();
|
|
if (cameraConfig.getValue('minimal-cursor')) {
|
|
Reticle.scale = MINIMAL_CURSOR_SCALE;
|
|
}
|
|
log('cameraConfig', JSON.stringify({
|
|
cameraConfig: getCameraMovementSettings(cameraConfig),
|
|
//DEFAULTS: DEFAULTS
|
|
}));
|
|
}
|
|
|
|
// reset values based on the selected Camera.mode (to help keep the visual display/orientation more reasonable)
|
|
function onCameraModeChanged(mode, oldMode) {
|
|
globalState.pendingChanges.reset();
|
|
globalState.previousValues.reset();
|
|
eventMapper.reset();
|
|
var preventRoll = cameraConfig.getValue('prevent-roll');
|
|
var avatarOrientation = cameraConfig.getValue('use-head') ? MyAvatar.headOrientation : MyAvatar.orientation;
|
|
if (preventRoll) {
|
|
avatarOrientation = Quat.cancelOutRollAndPitch(avatarOrientation);
|
|
}
|
|
switch (Camera.mode) {
|
|
case 'mirror':
|
|
case 'entity':
|
|
case 'independent':
|
|
globalState.currentVelocities.reset();
|
|
break;
|
|
default:
|
|
Camera.position = MyAvatar.position;
|
|
Camera.orientation = avatarOrientation;
|
|
break;
|
|
}
|
|
MyAvatar.orientation = avatarOrientation;
|
|
if (preventRoll) {
|
|
MyAvatar.headPitch = MyAvatar.headRoll = 0;
|
|
}
|
|
}
|
|
|
|
// consolidate and normalize cameraConfig settings
|
|
function getCameraMovementSettings(cameraConfig) {
|
|
return {
|
|
epsilon: EPSILON,
|
|
debug: cameraConfig.getValue('debug'),
|
|
driveMode: cameraConfig.getValue('drive-mode'),
|
|
threadMode: cameraConfig.getValue('thread-update-mode'),
|
|
useHead: cameraConfig.getValue('use-head'),
|
|
stayGrounded: cameraConfig.getValue('stay-grounded'),
|
|
preventRoll: cameraConfig.getValue('prevent-roll'),
|
|
useConstantDeltaTime: cameraConfig.getValue('constant-delta-time'),
|
|
|
|
mouseSmooth: cameraConfig.getValue('enable-mouse-smooth'),
|
|
mouseMultiplier: cameraConfig.getValue('rotation-mouse-multiplier'),
|
|
keyboardMultiplier: cameraConfig.getValue('rotation-keyboard-multiplier'),
|
|
|
|
rotation: _getEasingGroup(cameraConfig, 'rotation'),
|
|
translation: _getEasingGroup(cameraConfig, 'translation'),
|
|
zoom: _getEasingGroup(cameraConfig, 'zoom'),
|
|
};
|
|
|
|
// extract an easing group (translation, rotation, or zoom) from cameraConfig
|
|
function _getEasingGroup(cameraConfig, group) {
|
|
var multiplier = 1.0;
|
|
if (group === 'zoom') {
|
|
// BoomIn / TranslateCameraZ support is only partially plumbed -- for now use scaled translation easings
|
|
group = 'translation';
|
|
multiplier = 0.01;
|
|
} else if (group === 'rotation') {
|
|
// degrees -> radians
|
|
//multiplier = DEG_TO_RAD;
|
|
}
|
|
|
|
return {
|
|
easeIn: cameraConfig.getFloat(group + '-ease-in'),
|
|
easeOut: cameraConfig.getFloat(group + '-ease-out'),
|
|
maxVelocity: multiplier * cameraConfig.getFloat(group + '-max-velocity'),
|
|
speed: Vec3.multiply(multiplier, {
|
|
x: cameraConfig.getFloat(group + '-x-speed'),
|
|
y: cameraConfig.getFloat(group + '-y-speed'),
|
|
z: cameraConfig.getFloat(group + '-z-speed')
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
var DEFAULT_MOTOR_TIMESCALE = 1e6; // a large value that matches Interface's default
|
|
var EASED_MOTOR_TIMESCALE = 0.01; // a small value to make Interface quickly apply MyAvatar.motorVelocity
|
|
var EASED_MOTOR_THRESHOLD = 0.1; // above this speed (m/s) EASED_MOTOR_TIMESCALE is used
|
|
var ACCELERATION_MULTIPLIERS = { translation: 1, rotation: 1, zoom: 1 };
|
|
var STAYGROUNDED_PITCH_THRESHOLD = 45.0; // degrees; ground level is maintained when pitch is within this threshold
|
|
var MIN_DELTA_TIME = 0.0001; // to avoid math overflow, never consider dt less than this value
|
|
|
|
update.frameCount = 0;
|
|
update.endTime = _utils.getRuntimeSeconds();
|
|
|
|
function update(dt) {
|
|
update.frameCount++;
|
|
var startTime = _utils.getRuntimeSeconds();
|
|
var settings = getCameraMovementSettings(cameraConfig);
|
|
|
|
var collisions = applicationConfig.getValue('Avatar/Enable Avatar Collisions'),
|
|
independentCamera = Camera.mode === 'independent',
|
|
headPitch = MyAvatar.headPitch;
|
|
|
|
var actualDeltaTime = Math.max(MIN_DELTA_TIME, (startTime - update.endTime)),
|
|
deltaTime;
|
|
|
|
if (settings.useConstantDeltaTime) {
|
|
deltaTime = settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ?
|
|
(1 / cameraControls.fps) : (1 / 90);
|
|
} else if (settings.threadMode === movementUtils.CameraControls.SCRIPT_UPDATE) {
|
|
deltaTime = dt;
|
|
} else {
|
|
deltaTime = actualDeltaTime;
|
|
}
|
|
|
|
var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation',
|
|
currentOrientation = independentCamera ? Camera.orientation : MyAvatar[orientationProperty],
|
|
currentPosition = MyAvatar.position;
|
|
|
|
var previousValues = globalState.previousValues,
|
|
pendingChanges = globalState.pendingChanges,
|
|
currentVelocities = globalState.currentVelocities;
|
|
|
|
var movementState = eventMapper.getState({ update: deltaTime }),
|
|
targetState = movementUtils.applyEasing(deltaTime, 'easeIn', settings, movementState, ACCELERATION_MULTIPLIERS),
|
|
dragState = movementUtils.applyEasing(deltaTime, 'easeOut', settings, currentVelocities, ACCELERATION_MULTIPLIERS);
|
|
|
|
currentVelocities.integrate(targetState, currentVelocities, dragState, settings);
|
|
|
|
var currentSpeed = Vec3.length(currentVelocities.translation),
|
|
targetSpeed = Vec3.length(movementState.translation),
|
|
verticalHold = movementState.isGrounded && settings.stayGrounded && Math.abs(headPitch) < STAYGROUNDED_PITCH_THRESHOLD;
|
|
|
|
var deltaOrientation = Quat.fromVec3Degrees(Vec3.multiply(deltaTime, currentVelocities.rotation)),
|
|
targetOrientation = Quat.normalize(Quat.multiply(currentOrientation, deltaOrientation));
|
|
|
|
var targetVelocity = Vec3.multiplyQbyV(targetOrientation, currentVelocities.translation);
|
|
|
|
if (verticalHold) {
|
|
targetVelocity.y = 0;
|
|
}
|
|
|
|
var deltaPosition = Vec3.multiply(deltaTime, targetVelocity);
|
|
|
|
_resetMyAvatarMotor(pendingChanges);
|
|
|
|
if (!independentCamera) {
|
|
var DriveModes = movementUtils.DriveModes;
|
|
switch(settings.driveMode) {
|
|
case DriveModes.MOTOR: {
|
|
if (currentSpeed > EPSILON || targetSpeed > EPSILON) {
|
|
var motorTimescale = (currentSpeed > EASED_MOTOR_THRESHOLD ? EASED_MOTOR_TIMESCALE : DEFAULT_MOTOR_TIMESCALE);
|
|
var motorPitch = Quat.fromPitchYawRollDegrees(headPitch, 180, 0),
|
|
motorVelocity = Vec3.multiplyQbyV(motorPitch, currentVelocities.translation);
|
|
if (verticalHold) {
|
|
motorVelocity.y = 0;
|
|
}
|
|
Object.assign(pendingChanges.MyAvatar, {
|
|
motorVelocity: motorVelocity,
|
|
motorTimescale: motorTimescale,
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case DriveModes.THRUST: {
|
|
var thrustVector = currentVelocities.translation,
|
|
maxThrust = settings.translation.maxVelocity,
|
|
thrust;
|
|
if (targetSpeed > EPSILON) {
|
|
thrust = movementUtils.calculateThrust(maxThrust * 5, thrustVector, previousValues.thrust);
|
|
} else if (currentSpeed > 1 && Vec3.length(previousValues.thrust) > 1) {
|
|
thrust = Vec3.multiply(-currentSpeed / 10.0, thrustVector);
|
|
} else {
|
|
thrust = Vec3.ZERO;
|
|
}
|
|
if (thrust) {
|
|
thrust = Vec3.multiplyQbyV(MyAvatar[orientationProperty], thrust);
|
|
if (verticalHold) {
|
|
thrust.y = 0;
|
|
}
|
|
}
|
|
previousValues.thrust = pendingChanges.MyAvatar.setThrust = thrust;
|
|
break;
|
|
}
|
|
case DriveModes.JITTER_TEST:
|
|
case DriveModes.POSITION: {
|
|
pendingChanges.MyAvatar.position = Vec3.sum(currentPosition, deltaPosition);
|
|
break;
|
|
}
|
|
default: {
|
|
throw new Error('unknown driveMode: ' + settings.driveMode);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
var finalOrientation;
|
|
switch (Camera.mode) {
|
|
case 'mirror': // fall through
|
|
case 'independent':
|
|
targetOrientation = settings.preventRoll ? Quat.cancelOutRoll(targetOrientation) : targetOrientation;
|
|
var boomVector = Vec3.multiply(-currentVelocities.zoom.z, Quat.getFront(targetOrientation)),
|
|
deltaCameraPosition = Vec3.sum(boomVector, deltaPosition);
|
|
Object.assign(pendingChanges.Camera, {
|
|
position: Vec3.sum(Camera.position, deltaCameraPosition),
|
|
orientation: targetOrientation,
|
|
});
|
|
break;
|
|
case 'entity':
|
|
finalOrientation = targetOrientation;
|
|
break;
|
|
default: // 'first person', 'third person'
|
|
finalOrientation = targetOrientation;
|
|
break;
|
|
}
|
|
|
|
if (settings.driveMode === movementUtils.DriveModes.JITTER_TEST) {
|
|
finalOrientation = Quat.multiply(MyAvatar[orientationProperty], Quat.fromPitchYawRollDegrees(0, 60 * deltaTime, 0));
|
|
// Quat.fromPitchYawRollDegrees(0, _utils.getRuntimeSeconds() * 60, 0)
|
|
}
|
|
|
|
if (finalOrientation) {
|
|
if (settings.preventRoll) {
|
|
finalOrientation = Quat.cancelOutRoll(finalOrientation);
|
|
}
|
|
previousValues.finalOrientation = pendingChanges.MyAvatar[orientationProperty] = Quat.normalize(finalOrientation);
|
|
}
|
|
|
|
if (!movementState.mouseSmooth && movementState.isRightMouseButton) {
|
|
// directly apply mouse pitch and yaw when mouse smoothing is disabled
|
|
_applyDirectPitchYaw(deltaTime, movementState, settings);
|
|
}
|
|
|
|
var endTime = _utils.getRuntimeSeconds();
|
|
var cycleTime = endTime - update.endTime;
|
|
update.endTime = endTime;
|
|
|
|
var submitted = pendingChanges.submit();
|
|
|
|
update.momentaryFPS = 1 / actualDeltaTime;
|
|
|
|
if (settings.debug && update.frameCount % 120 === 0) {
|
|
Messages.sendLocalMessage(_debugChannel, JSON.stringify({
|
|
threadMode: cameraControls.threadMode,
|
|
driveMode: settings.driveMode,
|
|
orientationProperty: orientationProperty,
|
|
isGrounded: movementState.isGrounded,
|
|
targetAnimationFPS: cameraControls.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? cameraControls.fps : undefined,
|
|
actualFPS: 1 / actualDeltaTime,
|
|
effectiveAnimationFPS: 1 / deltaTime,
|
|
seconds: {
|
|
startTime: startTime,
|
|
endTime: endTime,
|
|
},
|
|
milliseconds: {
|
|
actualDeltaTime: actualDeltaTime * 1000,
|
|
deltaTime: deltaTime * 1000,
|
|
cycleTime: cycleTime * 1000,
|
|
calculationTime: (endTime - startTime) * 1000,
|
|
},
|
|
finalOrientation: finalOrientation,
|
|
thrust: thrust,
|
|
maxVelocity: settings.translation,
|
|
targetVelocity: targetVelocity,
|
|
currentSpeed: currentSpeed,
|
|
targetSpeed: targetSpeed,
|
|
}, 0, 2));
|
|
}
|
|
}
|
|
|
|
if (0) {
|
|
Script.update.connect(gc);
|
|
Script.scriptEnding.connect(function() {
|
|
Script.update.disconnect(gc);
|
|
});
|
|
}
|
|
|
|
function _applyDirectPitchYaw(deltaTime, movementState, settings) {
|
|
var orientationProperty = settings.useHead ? 'headOrientation' : 'orientation',
|
|
rotation = movementState.rotation,
|
|
speed = Vec3.multiply(-DEG_TO_RAD / 2.0, settings.rotation.speed);
|
|
|
|
var previousValues = globalState.previousValues,
|
|
pendingChanges = globalState.pendingChanges,
|
|
currentVelocities = globalState.currentVelocities;
|
|
|
|
var previous = previousValues.pitchYawRoll,
|
|
target = Vec3.multiply(deltaTime, Vec3.multiplyVbyV(rotation, speed)),
|
|
pitchYawRoll = Vec3.mix(previous, target, 0.5),
|
|
orientation = Quat.fromVec3Degrees(pitchYawRoll);
|
|
|
|
previousValues.pitchYawRoll = pitchYawRoll;
|
|
|
|
if (pendingChanges.MyAvatar.headOrientation || pendingChanges.MyAvatar.orientation) {
|
|
var newOrientation = Quat.multiply(MyAvatar[orientationProperty], orientation);
|
|
delete pendingChanges.MyAvatar.headOrientation;
|
|
delete pendingChanges.MyAvatar.orientation;
|
|
if (settings.preventRoll) {
|
|
newOrientation = Quat.cancelOutRoll(newOrientation);
|
|
}
|
|
MyAvatar[orientationProperty] = newOrientation;
|
|
} else if (pendingChanges.Camera.orientation) {
|
|
var cameraOrientation = Quat.multiply(Camera.orientation, orientation);
|
|
if (settings.preventRoll) {
|
|
cameraOrientation = Quat.cancelOutRoll(cameraOrientation);
|
|
}
|
|
Camera.orientation = cameraOrientation;
|
|
}
|
|
currentVelocities.rotation = Vec3.ZERO;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
function _startConfigationMonitor(applicationConfig, cameraConfig, interval) {
|
|
// monitor and sync Application state -> Settings values
|
|
return Script.setInterval(function monitor() {
|
|
var settingNames = Object.keys(applicationConfig.config);
|
|
settingNames.forEach(function(key) {
|
|
applicationConfig.resyncValue(key); // align Menus <=> APIs
|
|
var value = cameraConfig.getValue(key),
|
|
appValue = applicationConfig.getValue(key);
|
|
if (appValue !== undefined && String(appValue) !== String(value)) {
|
|
log('applicationConfig -> cameraConfig',
|
|
key, [typeof appValue, appValue], '(was:'+[typeof value, value]+')');
|
|
cameraConfig.setValue(key, appValue); // align Application <=> Settings
|
|
}
|
|
});
|
|
}, interval);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
function _resetMyAvatarMotor(targetObject) {
|
|
if (MyAvatar.motorTimescale !== DEFAULT_MOTOR_TIMESCALE) {
|
|
targetObject.MyAvatar.motorTimescale = DEFAULT_MOTOR_TIMESCALE;
|
|
}
|
|
if (MyAvatar.motorReferenceFrame !== 'avatar') {
|
|
targetObject.MyAvatar.motorReferenceFrame = 'avatar';
|
|
}
|
|
if (Vec3.length(MyAvatar.motorVelocity)) {
|
|
targetObject.MyAvatar.motorVelocity = Vec3.ZERO;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
function _fixedPrecisionStringifiyFilter(key, value, object) {
|
|
if (typeof value === 'object' && value && 'w' in value) {
|
|
return Quat.safeEulerAngles(value);
|
|
} else if (typeof value === 'number') {
|
|
return value.toFixed(4)*1;
|
|
}
|
|
return value;
|
|
}
|
|
function _createOverlayDebugOutput(options) {
|
|
options = require('./modules/_utils.js').assign({
|
|
x: 0, y: 0, width: 500, height: 800, visible: false
|
|
}, options || {});
|
|
options.lineHeight = options.lineHeight || Math.round(options.height / 36);
|
|
options.font = options.font || { size: Math.floor(options.height / 36) };
|
|
overlayDebugOutput.overlayID = Overlays.addOverlay('text', options);
|
|
|
|
Messages.subscribe(_debugChannel);
|
|
Messages.messageReceived.connect(onMessageReceived);
|
|
|
|
Script.scriptEnding.connect(function() {
|
|
Overlays.deleteOverlay(overlayDebugOutput.overlayID);
|
|
Messages.unsubscribe(debugChannel);
|
|
Messages.messageReceived.disconnect(onMessageReceived);
|
|
});
|
|
function overlayDebugOutput(output) {
|
|
var text = JSON.stringify(output, _fixedPrecisionStringifiyFilter, 2);
|
|
if (text !== overlayDebugOutput.lastText) {
|
|
overlayDebugOutput.lastText = text;
|
|
Overlays.editOverlay(overlayDebugOutput.overlayID, { text: text });
|
|
}
|
|
}
|
|
function onMessageReceived(channel, message, ssend, local) {
|
|
if (!local || channel !== _debugChannel) {
|
|
return;
|
|
}
|
|
overlayDebugOutput(JSON.parse(message));
|
|
}
|
|
|
|
return overlayDebugOutput;
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------------
|
|
_patchCameraModeSetting();
|
|
function _patchCameraModeSetting() {
|
|
// FIXME: looks like the Camera API suffered a regression where setting Camera.mode = 'first person' or 'third person'
|
|
// no longer works; the only reliable way to set it now seems to be jury-rigging the Menu items...
|
|
Camera.$setModeString = Camera.$setModeString || function(mode) {
|
|
// 'independent' => "Independent Mode", 'first person' => 'First Person', etc.
|
|
var cameraMenuItem = (mode+'')
|
|
.replace(/^(independent|entity)$/, '$1 mode')
|
|
.replace(/\b[a-z]/g, function(ch) {
|
|
return ch.toUpperCase();
|
|
});
|
|
|
|
log('working around Camera.mode bug by enabling the menuItem:', cameraMenuItem);
|
|
Menu.setIsOptionChecked(cameraMenuItem, true);
|
|
};
|
|
}
|
|
// ----------------------------------------------------------------------------
|
|
|
|
function _instrumentDebugValues() {
|
|
debugPrint = log;
|
|
var cacheBuster = '?' + new Date().getTime().toString(36);
|
|
require = Script.require(Script.resolvePath('./modules/_utils.js') + cacheBuster).makeDebugRequire(Script.resolvePath('.'));
|
|
APP_HTML_URL += cacheBuster;
|
|
overlayDebugOutput = _createOverlayDebugOutput({
|
|
lineHeight: 10,
|
|
font: { size: 10 },
|
|
width: 250, height: 800 });
|
|
// auto-disable camera move mode when debugging
|
|
Script.scriptEnding.connect(function() {
|
|
cameraConfig && cameraConfig.setValue('camera-move-enabled', false);
|
|
});
|
|
}
|
|
|
|
// Show fatal (unhandled) exceptions in a BSOD popup
|
|
Script.unhandledException.connect(function onUnhandledException(error) {
|
|
log('UNHANDLED EXCEPTION!!', error, error && error.stack);
|
|
try { cameraControls.disable(); } catch(e) {}
|
|
Script.unhandledException.disconnect(onUnhandledException);
|
|
if (WANT_DEBUG) {
|
|
// show blue screen of death with the error details
|
|
_utils.BSOD({
|
|
error: error,
|
|
buttons: [ 'Abort', 'Retry', 'Fail' ],
|
|
debugInfo: DEBUG_INFO,
|
|
}, function(error, button) {
|
|
log('BSOD.result', error, button);
|
|
if (button === 'Abort') {
|
|
Script.stop();
|
|
} else if (button === 'Retry') {
|
|
_utils.reloadClientScript(FILENAME);
|
|
}
|
|
});
|
|
} else {
|
|
// use a simple alert to display just the error message
|
|
Window.alert('app-camera-move error: ' + error.message);
|
|
Script.stop();
|
|
}
|
|
});
|
|
// ----------------------------------------------------------------------------
|
|
|
|
main();
|