/* eslint-env commonjs */ // ---------------------------------------------------------------------------- // helper module that performs the avatar movement update calculations module.exports = AvatarUpdater; var _utils = require('./modules/_utils.js'), assert = _utils.assert; var movementUtils = require('./modules/movement-utils.js'); function AvatarUpdater(options) { options = options || {}; assert(function assertion() { return typeof options.getCameraMovementSettings === 'function' && typeof options.getMovementState === 'function' && options.globalState; }); 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 var DEG_TO_RAD = Math.PI / 180.0; update.frameCount = 0; update.endTime = update.windowStartTime = _utils.getRuntimeSeconds(); update.windowFrame = update.windowStartFrame= 0; this.update = update; this.options = options; this._resetMyAvatarMotor = _resetMyAvatarMotor; this._applyDirectPitchYaw = _applyDirectPitchYaw; var globalState = options.globalState; var getCameraMovementSettings = options.getCameraMovementSettings; var getMovementState = options.getMovementState; var _debugChannel = options.debugChannel; function update(dt) { update.frameCount++; var startTime = _utils.getRuntimeSeconds(); var settings = getCameraMovementSettings(), EPSILON = settings.epsilon; var independentCamera = Camera.mode === 'independent', headPitch = MyAvatar.headPitch; var actualDeltaTime = startTime - update.endTime, practicalDeltaTime = Math.max(MIN_DELTA_TIME, actualDeltaTime), deltaTime; if (settings.useConstantDeltaTime) { deltaTime = settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? (1 / settings.fps) : (1 / 90); } else if (settings.threadMode === movementUtils.CameraControls.SCRIPT_UPDATE) { deltaTime = dt; } else { deltaTime = practicalDeltaTime; } 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 = getMovementState({ 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.POSITION: { pendingChanges.MyAvatar.position = Vec3.sum(currentPosition, deltaPosition); break; } default: { throw new Error('unknown driveMode: ' + settings.driveMode); } } } 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.jitterTest) { 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; pendingChanges.submit(); if ((endTime - update.windowStartTime) > 3) { update.momentaryFPS = (update.frameCount - update.windowStartFrame) / (endTime - update.windowStartTime); update.windowStartFrame = update.frameCount; update.windowStartTime = endTime; } if (_debugChannel && update.windowStartFrame === update.frameCount) { Messages.sendLocalMessage(_debugChannel, JSON.stringify({ threadFrames: update.threadFrames, frame: update.frameCount, threadMode: settings.threadMode, driveMode: settings.driveMode, orientationProperty: orientationProperty, isGrounded: movementState.isGrounded, targetAnimationFPS: settings.threadMode === movementUtils.CameraControls.ANIMATION_FRAME ? settings.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)); } } 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 _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; } }