diff --git a/examples/Recorder.js b/examples/Recorder.js new file mode 100644 index 0000000000..9a4375ab4f --- /dev/null +++ b/examples/Recorder.js @@ -0,0 +1,203 @@ +// +// Recorder.js +// examples +// +// Created by Clément Brisset on 8/20/14. +// Copyright 2014 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 +// + +Script.include("toolBars.js"); + +var recordingFile = "recording.rec"; + +var windowDimensions = Controller.getViewportDimensions(); +var TOOL_ICON_URL = "http://s3-us-west-1.amazonaws.com/highfidelity-public/images/tools/"; +var ALPHA_ON = 1.0; +var ALPHA_OFF = 0.7; +var COLOR_ON = { red: 128, green: 0, blue: 0 }; +var COLOR_OFF = { red: 128, green: 128, blue: 128 }; +Tool.IMAGE_WIDTH *= 0.7; +Tool.IMAGE_HEIGHT *= 0.7; + +var toolBar = null; +var recordIcon; +var playIcon; +var saveIcon; +var loadIcon; +setupToolBar(); + +var timer = null; +setupTimer(); + +function setupToolBar() { + if (toolBar != null) { + print("Multiple calls to Recorder.js:setupToolBar()"); + return; + } + + toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL); + toolBar.setBack(COLOR_OFF, ALPHA_OFF); + + recordIcon = toolBar.addTool({ + imageURL: TOOL_ICON_URL + "record.svg", + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_ON, + visible: true + }, false); + + playIcon = toolBar.addTool({ + imageURL: TOOL_ICON_URL + "play.svg", + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_ON, + visible: true + }, false, false); + + saveIcon = toolBar.addTool({ + imageURL: TOOL_ICON_URL + "save.svg", + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_ON, + visible: true + }, false, false); + + loadIcon = toolBar.addTool({ + imageURL: TOOL_ICON_URL + "load.svg", + width: Tool.IMAGE_WIDTH, + height: Tool.IMAGE_HEIGHT, + alpha: ALPHA_ON, + visible: true + }, false, false); +} + +function setupTimer() { + timer = Overlays.addOverlay("text", { + font: { size: 20 }, + text: (0.00).toFixed(3), + backgroundColor: COLOR_OFF, + x: 0, y: 0, + width: 100, + height: 100, + alpha: 1.0, + visible: true + }); +} + +function updateTimer() { + var text = ""; + if (MyAvatar.isRecording()) { + text = formatTime(MyAvatar.recorderElapsed()) + } else { + text = formatTime(MyAvatar.playerElapsed()) + " / " + + formatTime(MyAvatar.playerLength()); + } + + Overlays.editOverlay(timer, { + text: text + }) +} + +function formatTime(time) { + var MIN_PER_HOUR = 60; + var SEC_PER_MIN = 60; + var MSEC_PER_SEC = 1000; + + var hours = Math.floor(time / (MSEC_PER_SEC * SEC_PER_MIN * MIN_PER_HOUR)); + time -= hours * (MSEC_PER_SEC * SEC_PER_MIN * MIN_PER_HOUR); + + var minutes = Math.floor(time / (MSEC_PER_SEC * SEC_PER_MIN)); + time -= minutes * (MSEC_PER_SEC * SEC_PER_MIN); + + var seconds = Math.floor(time / MSEC_PER_SEC); + seconds = time / MSEC_PER_SEC; + + var text = ""; + text += (hours > 0) ? hours + ":" : + ""; + text += (minutes > 0) ? ((minutes < 10 && text != "") ? "0" : "") + minutes + ":" : + ""; + text += ((seconds < 10 && text != "") ? "0" : "") + seconds.toFixed(3); + return text; +} + +function moveUI() { + var relative = { x: 30, y: 90 }; + toolBar.move(relative.x, + windowDimensions.y - relative.y); + Overlays.editOverlay(timer, { + x: relative.x - 10, + y: windowDimensions.y - relative.y - 35, + width: 0, + height: 0 + }); +} + +function mousePressEvent(event) { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + + if (recordIcon === toolBar.clicked(clickedOverlay)) { + if (!MyAvatar.isRecording()) { + MyAvatar.startRecording(); + toolBar.setBack(COLOR_ON, ALPHA_ON); + } else { + MyAvatar.stopRecording(); + MyAvatar.loadLastRecording(); + toolBar.setBack(COLOR_OFF, ALPHA_OFF); + } + } else if (playIcon === toolBar.clicked(clickedOverlay)) { + if (!MyAvatar.isRecording()) { + if (MyAvatar.isPlaying()) { + MyAvatar.stopPlaying(); + } else { + MyAvatar.startPlaying(); + } + } + } else if (saveIcon === toolBar.clicked(clickedOverlay)) { + if (!MyAvatar.isRecording()) { + recordingFile = Window.save("Save recording to file", ".", "*.rec"); + MyAvatar.saveRecording(recordingFile); + } + } else if (loadIcon === toolBar.clicked(clickedOverlay)) { + if (!MyAvatar.isRecording()) { + recordingFile = Window.browse("Load recorcding from file", ".", "*.rec"); + MyAvatar.loadRecording(recordingFile); + } + } else { + + } +} + +function update() { + var newDimensions = Controller.getViewportDimensions(); + if (windowDimensions.x != newDimensions.x || + windowDimensions.y != newDimensions.y) { + windowDimensions = newDimensions; + moveUI(); + } + + updateTimer(); +} + +function scriptEnding() { + if (MyAvatar.isRecording()) { + MyAvatar.stopRecording(); + } + if (MyAvatar.isPlaying()) { + MyAvatar.stopPlaying(); + } + toolBar.cleanup(); + Overlays.deleteOverlay(timer); +} + +Controller.mousePressEvent.connect(mousePressEvent); +Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); + +// Should be called last to put everything into position +moveUI(); + + diff --git a/examples/frisbee.js b/examples/frisbee.js new file mode 100644 index 0000000000..e893a29309 --- /dev/null +++ b/examples/frisbee.js @@ -0,0 +1,443 @@ +// +// frisbee.js +// examples +// +// Created by Thijs Wenker on 7/5/14. +// Copyright 2014 High Fidelity, Inc. +// +// Requirements: Razer Hydra's +// +// Fun game to throw frisbee's to eachother. Hold the trigger on any of the hydra's to create or catch a frisbee. +// +// Tip: use this together with the squeezeHands.js script to make it look nicer. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +Script.include("toolBars.js"); + +const LEFT_PALM = 0; +const LEFT_TIP = 1; +const LEFT_BUTTON_FWD = 5; +const LEFT_BUTTON_3 = 3; + +const RIGHT_PALM = 2; +const RIGHT_TIP = 3; +const RIGHT_BUTTON_FWD = 11; +const RIGHT_BUTTON_3 = 9; + +const FRISBEE_RADIUS = 0.08; +const GRAVITY_STRENGTH = 0.5; + +const CATCH_RADIUS = 0.5; +const MIN_SIMULATION_SPEED = 0.15; +const THROWN_VELOCITY_SCALING = 1.5; + +const SOUNDS_ENABLED = true; +const FRISBEE_BUTTON_URL = "http://test.thoys.nl/hifi/images/frisbee/frisbee_button_by_Judas.svg"; +const FRISBEE_MODEL_SCALE = 275; +const FRISBEE_MENU = "Toys>Frisbee"; +const FRISBEE_DESIGN_MENU = "Toys>Frisbee>Design"; +const FRISBEE_ENABLED_SETTING = "Frisbee>Enabled"; +const FRISBEE_CREATENEW_SETTING = "Frisbee>CreateNew"; +const FRISBEE_DESIGN_SETTING = "Frisbee>Design"; +const FRISBEE_FORCE_MOUSE_CONTROLS_SETTING = "Frisbee>ForceMouseControls"; + +//Add your own designs in FRISBEE_DESIGNS, be sure to put frisbee in the URL if you want others to be able to catch it without having a copy of your frisbee script. +const FRISBEE_DESIGNS = [ + {"name":"Interface", "model":"http://test.thoys.nl/hifi/models/frisbee/frisbee.fbx"}, + {"name":"Pizza", "model":"http://test.thoys.nl/hifi/models/frisbee/pizza.fbx"}, + {"name":"Swirl", "model":"http://test.thoys.nl/hifi/models/frisbee/swirl.fbx"}, + {"name":"Mayan", "model":"http://test.thoys.nl/hifi/models/frisbee/mayan.fbx"}, + ]; +const FRISBEE_MENU_DESIGN_POSTFIX = " Design"; +const FRISBEE_DESIGN_RANDOM = "Random"; + +const SPIN_MULTIPLIER = 1000; +const FRISBEE_LIFETIME = 300; // 5 minutes + +var windowDimensions = Controller.getViewportDimensions(); +var toolHeight = 50; +var toolWidth = 50; +var frisbeeToggle; +var toolBar; +var frisbeeEnabled = true; +var newfrisbeeEnabled = false; +var forceMouseControls = false; +var hydrasConnected = false; +var selectedDesign = FRISBEE_DESIGN_RANDOM; + +function loadSettings() { + frisbeeEnabled = Settings.getValue(FRISBEE_ENABLED_SETTING, "true") == "true"; + newfrisbeeEnabled = Settings.getValue(FRISBEE_CREATENEW_SETTING, "false") == "true"; + forceMouseControls = Settings.getValue(FRISBEE_FORCE_MOUSE_CONTROLS_SETTING, "false") == "true"; + selectedDesign = Settings.getValue(FRISBEE_DESIGN_SETTING, "Random"); +} + +function saveSettings() { + Settings.setValue(FRISBEE_ENABLED_SETTING, frisbeeEnabled ? "true" : "false"); + Settings.setValue(FRISBEE_CREATENEW_SETTING, newfrisbeeEnabled ? "true" : "false"); + Settings.setValue(FRISBEE_FORCE_MOUSE_CONTROLS_SETTING, forceMouseControls ? "true" : "false"); + Settings.setValue(FRISBEE_DESIGN_SETTING, selectedDesign); +} + +function moveOverlays() { + var newViewPort = Controller.getViewportDimensions(); + if (typeof(toolBar) === 'undefined') { + initToolBar(); + } else if (windowDimensions.x == newViewPort.x && + windowDimensions.y == newViewPort.y) { + return; + } + + windowDimensions = newViewPort; + var toolsX = windowDimensions.x - 8 - toolBar.width; + var toolsY = (windowDimensions.y - toolBar.height) / 2 + 80; + toolBar.move(toolsX, toolsY); +} + +function frisbeeURL() { + return selectedDesign == FRISBEE_DESIGN_RANDOM ? FRISBEE_DESIGNS[Math.floor(Math.random() * FRISBEE_DESIGNS.length)].model : getFrisbee(selectedDesign).model; +} + +//This function checks if the modelURL is inside of our Designs or contains "frisbee" in it. +function validFrisbeeURL(frisbeeURL) { + for (var frisbee in FRISBEE_DESIGNS) { + if (FRISBEE_DESIGNS[frisbee].model == frisbeeURL) { + return true; + } + } + return frisbeeURL.toLowerCase().indexOf("frisbee") !== -1; +} + +function getFrisbee(frisbeeName) { + for (var frisbee in FRISBEE_DESIGNS) { + if (FRISBEE_DESIGNS[frisbee].name == frisbeeName) { + return FRISBEE_DESIGNS[frisbee]; + } + } + return undefined; +} + +function Hand(name, palm, tip, forwardButton, button3, trigger) { + this.name = name; + this.palm = palm; + this.tip = tip; + this.forwardButton = forwardButton; + this.button3 = button3; + this.trigger = trigger; + this.holdingFrisbee = false; + this.particle = false; + this.palmPosition = function() { return Controller.getSpatialControlPosition(this.palm); } + this.grabButtonPressed = function() { + return ( + Controller.isButtonPressed(this.forwardButton) || + Controller.isButtonPressed(this.button3) || + Controller.getTriggerValue(this.trigger) > 0.5 + ) + }; + this.holdPosition = function() { return this.palm == LEFT_PALM ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); }; + this.holdRotation = function() { + var q = Controller.getSpatialControlRawRotation(this.palm); + q = Quat.multiply(MyAvatar.orientation, q); + return {x: q.x, y: q.y, z: q.z, w: q.w}; + }; + this.tipVelocity = function() { return Controller.getSpatialControlVelocity(this.tip); }; +} + +function MouseControl(button) { + this.button = button; +} + +var leftHand = new Hand("LEFT", LEFT_PALM, LEFT_TIP, LEFT_BUTTON_FWD, LEFT_BUTTON_3, 0); +var rightHand = new Hand("RIGHT", RIGHT_PALM, RIGHT_TIP, RIGHT_BUTTON_FWD, RIGHT_BUTTON_3, 1); + +var leftMouseControl = new MouseControl("LEFT"); +var middleMouseControl = new MouseControl("MIDDLE"); +var rightMouseControl = new MouseControl("RIGHT"); +var mouseControls = [leftMouseControl, middleMouseControl, rightMouseControl]; +var currentMouseControl = false; + +var newSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw"); +var catchSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/catch.raw"); +var throwSound = new Sound("http://highfidelity-public.s3-us-west-1.amazonaws.com/sounds/Switches%20and%20sliders/slider%20-%20whoosh1.raw"); + +var simulatedFrisbees = []; + +var wantDebugging = false; +function debugPrint(message) { + if (wantDebugging) { + print(message); + } +} + +function playSound(sound, position) { + if (!SOUNDS_ENABLED) { + return; + } + var options = new AudioInjectionOptions(); + options.position = position; + options.volume = 1.0; + Audio.playSound(sound, options); +} + +function cleanupFrisbees() { + simulatedFrisbees = []; + var particles = Particles.findParticles(MyAvatar.position, 1000); + for (particle in particles) { + Particles.deleteParticle(particles[particle]); + } +} + +function checkControllerSide(hand) { + // If I don't currently have a frisbee in my hand, then try to catch closest one + if (!hand.holdingFrisbee && hand.grabButtonPressed()) { + var closestParticle = Particles.findClosestParticle(hand.palmPosition(), CATCH_RADIUS); + var modelUrl = Particles.getParticleProperties(closestParticle).modelURL; + if (closestParticle.isKnownID && validFrisbeeURL(Particles.getParticleProperties(closestParticle).modelURL)) { + Particles.editParticle(closestParticle, {modelScale: 1, inHand: true, position: hand.holdPosition(), shouldDie: true}); + Particles.deleteParticle(closestParticle); + debugPrint(hand.message + " HAND- CAUGHT SOMETHING!!"); + + var properties = { + position: hand.holdPosition(), + velocity: { x: 0, y: 0, z: 0}, + gravity: { x: 0, y: 0, z: 0}, + inHand: true, + radius: FRISBEE_RADIUS, + damping: 0.999, + modelURL: modelUrl, + modelScale: FRISBEE_MODEL_SCALE, + modelRotation: hand.holdRotation(), + lifetime: FRISBEE_LIFETIME + }; + + newParticle = Particles.addParticle(properties); + + hand.holdingFrisbee = true; + hand.particle = newParticle; + + playSound(catchSound, hand.holdPosition()); + + return; // exit early + } + } + + // If '3' is pressed, and not holding a frisbee, make a new one + if (hand.grabButtonPressed() && !hand.holdingFrisbee && newfrisbeeEnabled) { + var properties = { + position: hand.holdPosition(), + velocity: { x: 0, y: 0, z: 0}, + gravity: { x: 0, y: 0, z: 0}, + inHand: true, + radius: FRISBEE_RADIUS, + damping: 0.999, + modelURL: frisbeeURL(), + modelScale: FRISBEE_MODEL_SCALE, + modelRotation: hand.holdRotation(), + lifetime: FRISBEE_LIFETIME + }; + + newParticle = Particles.addParticle(properties); + hand.holdingFrisbee = true; + hand.particle = newParticle; + + // Play a new frisbee sound + playSound(newSound, hand.holdPosition()); + + return; // exit early + } + + if (hand.holdingFrisbee) { + // If holding the frisbee keep it in the palm + if (hand.grabButtonPressed()) { + debugPrint(">>>>> " + hand.name + "-FRISBEE IN HAND, grabbing, hold and move"); + var properties = { + position: hand.holdPosition(), + modelRotation: hand.holdRotation() + }; + Particles.editParticle(hand.particle, properties); + } else { + debugPrint(">>>>> " + hand.name + "-FRISBEE IN HAND, not grabbing, THROW!!!"); + // If frisbee just released, add velocity to it! + + var properties = { + velocity: Vec3.multiply(hand.tipVelocity(), THROWN_VELOCITY_SCALING), + inHand: false, + lifetime: FRISBEE_LIFETIME, + gravity: { x: 0, y: -GRAVITY_STRENGTH, z: 0}, + modelRotation: hand.holdRotation() + }; + + Particles.editParticle(hand.particle, properties); + + simulatedFrisbees.push(hand.particle); + + hand.holdingFrisbee = false; + hand.particle = false; + + playSound(throwSound, hand.holdPosition()); + } + } +} + +function initToolBar() { + toolBar = new ToolBar(0, 0, ToolBar.VERTICAL); + frisbeeToggle = toolBar.addTool({ + imageURL: FRISBEE_BUTTON_URL, + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + visible: true, + alpha: 0.9 + }, true); + enableNewFrisbee(newfrisbeeEnabled); +} + +function hydraCheck() { + var numberOfButtons = Controller.getNumberOfButtons(); + var numberOfTriggers = Controller.getNumberOfTriggers(); + var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); + var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; + hydrasConnected = (numberOfButtons == 12 && numberOfTriggers == 2 && controllersPerTrigger == 2); + return hydrasConnected; +} + +function checkController(deltaTime) { + moveOverlays(); + if (!frisbeeEnabled) { + return; + } + // this is expected for hydras + if (hydraCheck()) { + checkControllerSide(leftHand); + checkControllerSide(rightHand); + } + if (!hydrasConnected || forceMouseControls) { + //TODO: add mouse cursor control code here. + } +} + +function controlFrisbees(deltaTime) { + var killSimulations = []; + for (frisbee in simulatedFrisbees) { + var properties = Particles.getParticleProperties(simulatedFrisbees[frisbee]); + //get the horizon length from the velocity origin in order to get speed + var speed = Vec3.length({x:properties.velocity.x, y:0, z:properties.velocity.z}); + if (speed < MIN_SIMULATION_SPEED) { + //kill the frisbee simulation when speed is low + killSimulations.push(frisbee); + continue; + } + Particles.editParticle(simulatedFrisbees[frisbee], {modelRotation: Quat.multiply(properties.modelRotation, Quat.fromPitchYawRollDegrees(0, speed * deltaTime * SPIN_MULTIPLIER, 0))}); + + } + for (var i = killSimulations.length - 1; i >= 0; i--) { + simulatedFrisbees.splice(killSimulations[i], 1); + } +} + +//catches interfering calls of hydra-cursors +function withinBounds(coords) { + return coords.x >= 0 && coords.x < windowDimensions.x && coords.y >= 0 && coords.y < windowDimensions.y; +} + +function mouseMoveEvent(event) { + //TODO: mouse controls //print(withinBounds(event)); //print("move"+event.x); +} + +function mousePressEvent(event) { + print(event.x); + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + if (frisbeeToggle == toolBar.clicked(clickedOverlay)) { + newfrisbeeEnabled = !newfrisbeeEnabled; + saveSettings(); + enableNewFrisbee(newfrisbeeEnabled); + } +} + +function enableNewFrisbee(enable) { + if (toolBar.numberOfTools() > 0) { + toolBar.tools[0].select(enable); + } +} + +function mouseReleaseEvent(event) { + //TODO: mouse controls //print(JSON.stringify(event)); +} + +function setupMenus() { + Menu.addMenu(FRISBEE_MENU); + Menu.addMenuItem({ + menuName: FRISBEE_MENU, + menuItemName: "Frisbee Enabled", + isCheckable: true, + isChecked: frisbeeEnabled + }); + Menu.addMenuItem({ + menuName: FRISBEE_MENU, + menuItemName: "Cleanup Frisbees" + }); + Menu.addMenuItem({ + menuName: FRISBEE_MENU, + menuItemName: "Force Mouse Controls", + isCheckable: true, + isChecked: forceMouseControls + }); + Menu.addMenu(FRISBEE_DESIGN_MENU); + Menu.addMenuItem({ + menuName: FRISBEE_DESIGN_MENU, + menuItemName: FRISBEE_DESIGN_RANDOM + FRISBEE_MENU_DESIGN_POSTFIX, + isCheckable: true, + isChecked: selectedDesign == FRISBEE_DESIGN_RANDOM + }); + for (frisbee in FRISBEE_DESIGNS) { + Menu.addMenuItem({ + menuName: FRISBEE_DESIGN_MENU, + menuItemName: FRISBEE_DESIGNS[frisbee].name + FRISBEE_MENU_DESIGN_POSTFIX, + isCheckable: true, + isChecked: selectedDesign == FRISBEE_DESIGNS[frisbee].name + }); + } +} + +//startup calls: +loadSettings(); +setupMenus(); +function scriptEnding() { + toolBar.cleanup(); + Menu.removeMenu(FRISBEE_MENU); +} + +function menuItemEvent(menuItem) { + if (menuItem == "Cleanup Frisbees") { + cleanupFrisbees(); + return; + } else if (menuItem == "Frisbee Enabled") { + frisbeeEnabled = Menu.isOptionChecked(menuItem); + saveSettings(); + return; + } else if (menuItem == "Force Mouse Controls") { + forceMouseControls = Menu.isOptionChecked(menuItem); + saveSettings(); + return; + } + if (menuItem.indexOf(FRISBEE_MENU_DESIGN_POSTFIX, menuItem.length - FRISBEE_MENU_DESIGN_POSTFIX.length) !== -1) { + var item_name = menuItem.substring(0, menuItem.length - FRISBEE_MENU_DESIGN_POSTFIX.length); + if (item_name == FRISBEE_DESIGN_RANDOM || getFrisbee(item_name) != undefined) { + Menu.setIsOptionChecked(selectedDesign + FRISBEE_MENU_DESIGN_POSTFIX, false); + selectedDesign = item_name; + saveSettings(); + Menu.setIsOptionChecked(selectedDesign + FRISBEE_MENU_DESIGN_POSTFIX, true); + } + } +} + +// register the call back so it fires before each data send +Controller.mouseMoveEvent.connect(mouseMoveEvent); +Controller.mousePressEvent.connect(mousePressEvent); +Controller.mouseReleaseEvent.connect(mouseReleaseEvent); +Menu.menuItemEvent.connect(menuItemEvent); +Script.scriptEnding.connect(scriptEnding); +Script.update.connect(checkController); +Script.update.connect(controlFrisbees); \ No newline at end of file diff --git a/examples/hydraMove.js b/examples/hydraMove.js index 675a885b6d..853c18ebce 100644 --- a/examples/hydraMove.js +++ b/examples/hydraMove.js @@ -21,10 +21,10 @@ var position = { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.pos var joysticksCaptured = false; var THRUST_CONTROLLER = 0; var VIEW_CONTROLLER = 1; -var INITIAL_THRUST_MULTPLIER = 1.0; +var INITIAL_THRUST_MULTIPLIER = 1.0; var THRUST_INCREASE_RATE = 1.05; var MAX_THRUST_MULTIPLIER = 75.0; -var thrustMultiplier = INITIAL_THRUST_MULTPLIER; +var thrustMultiplier = INITIAL_THRUST_MULTIPLIER; var grabDelta = { x: 0, y: 0, z: 0}; var grabStartPosition = { x: 0, y: 0, z: 0}; var grabDeltaVelocity = { x: 0, y: 0, z: 0}; @@ -34,6 +34,8 @@ var grabbingWithRightHand = false; var wasGrabbingWithRightHand = false; var grabbingWithLeftHand = false; var wasGrabbingWithLeftHand = false; +var movingWithHead = false; +var headStartPosition, headStartDeltaPitch, headStartFinalPitch, headStartRoll, headStartYaw; var EPSILON = 0.000001; var velocity = { x: 0, y: 0, z: 0}; var THRUST_MAG_UP = 100.0; @@ -241,6 +243,47 @@ function handleGrabBehavior(deltaTime) { wasGrabbingWithLeftHand = grabbingWithLeftHand; } +var HEAD_MOVE_DEAD_ZONE = 0.0; +var HEAD_STRAFE_DEAD_ZONE = 0.0; +var HEAD_ROTATE_DEAD_ZONE = 0.0; +var HEAD_THRUST_FWD_SCALE = 12000.0; +var HEAD_THRUST_STRAFE_SCALE = 1000.0; +var HEAD_YAW_RATE = 2.0; +var HEAD_PITCH_RATE = 1.0; +var HEAD_ROLL_THRUST_SCALE = 75.0; +var HEAD_PITCH_LIFT_THRUST = 3.0; + +function moveWithHead(deltaTime) { + if (movingWithHead) { + var deltaYaw = MyAvatar.getHeadFinalYaw() - headStartYaw; + var deltaPitch = MyAvatar.getHeadDeltaPitch() - headStartDeltaPitch; + + var bodyLocalCurrentHeadVector = Vec3.subtract(MyAvatar.getHeadPosition(), MyAvatar.position); + bodyLocalCurrentHeadVector = Vec3.multiplyQbyV(Quat.angleAxis(-deltaYaw, {x:0, y: 1, z:0}), bodyLocalCurrentHeadVector); + var headDelta = Vec3.subtract(bodyLocalCurrentHeadVector, headStartPosition); + headDelta = Vec3.multiplyQbyV(Quat.inverse(Camera.getOrientation()), headDelta); + headDelta.y = 0.0; // Don't respond to any of the vertical component of head motion + + // Thrust based on leaning forward and side-to-side + if (Math.abs(headDelta.z) > HEAD_MOVE_DEAD_ZONE) { + MyAvatar.addThrust(Vec3.multiply(Quat.getFront(Camera.getOrientation()), -headDelta.z * HEAD_THRUST_FWD_SCALE * deltaTime)); + } + if (Math.abs(headDelta.x) > HEAD_STRAFE_DEAD_ZONE) { + MyAvatar.addThrust(Vec3.multiply(Quat.getRight(Camera.getOrientation()), headDelta.x * HEAD_THRUST_STRAFE_SCALE * deltaTime)); + } + if (Math.abs(deltaYaw) > HEAD_ROTATE_DEAD_ZONE) { + var orientation = Quat.multiply(Quat.angleAxis(deltaYaw * HEAD_YAW_RATE * deltaTime, {x:0, y: 1, z:0}), MyAvatar.orientation); + MyAvatar.orientation = orientation; + } + // Thrust Up/Down based on head pitch + MyAvatar.addThrust(Vec3.multiply({ x:0, y:1, z:0 }, (MyAvatar.getHeadFinalPitch() - headStartFinalPitch) * HEAD_PITCH_LIFT_THRUST * deltaTime)); + // For head trackers, adjust pitch by head pitch + MyAvatar.headPitch += deltaPitch * HEAD_PITCH_RATE * deltaTime; + // Thrust strafe based on roll ange + MyAvatar.addThrust(Vec3.multiply(Quat.getRight(Camera.getOrientation()), -(MyAvatar.getHeadFinalRoll() - headStartRoll) * HEAD_ROLL_THRUST_SCALE * deltaTime)); + } +} + // Update for joysticks and move button function flyWithHydra(deltaTime) { var thrustJoystickPosition = Controller.getJoystickPosition(THRUST_CONTROLLER); @@ -262,7 +305,7 @@ function flyWithHydra(deltaTime) { thrustJoystickPosition.x * thrustMultiplier * deltaTime); MyAvatar.addThrust(thrustRight); } else { - thrustMultiplier = INITIAL_THRUST_MULTPLIER; + thrustMultiplier = INITIAL_THRUST_MULTIPLIER; } // View Controller @@ -280,6 +323,7 @@ function flyWithHydra(deltaTime) { MyAvatar.headPitch = newPitch; } handleGrabBehavior(deltaTime); + moveWithHead(deltaTime); displayDebug(); } @@ -296,3 +340,19 @@ function scriptEnding() { } Script.scriptEnding.connect(scriptEnding); +Controller.keyPressEvent.connect(function(event) { + if (event.text == "SPACE" && !movingWithHead) { + movingWithHead = true; + headStartPosition = Vec3.subtract(MyAvatar.getHeadPosition(), MyAvatar.position); + headStartDeltaPitch = MyAvatar.getHeadDeltaPitch(); + headStartFinalPitch = MyAvatar.getHeadFinalPitch(); + headStartRoll = MyAvatar.getHeadFinalRoll(); + headStartYaw = MyAvatar.getHeadFinalYaw(); + } +}); +Controller.keyReleaseEvent.connect(function(event) { + if (event.text == "SPACE") { + movingWithHead = false; + } +}); + diff --git a/examples/speechControl.js b/examples/speechControl.js new file mode 100644 index 0000000000..e2fb9699b7 --- /dev/null +++ b/examples/speechControl.js @@ -0,0 +1,201 @@ +// +// speechControl.js +// examples +// +// Created by Ryan Huffman on 07/31/14. +// Copyright 2014 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 +// + +var ACCELERATION = 80; +var STEP_DURATION = 1.0; // Duration of a step command in seconds +var TURN_DEGREES = 90; +var SLIGHT_TURN_DEGREES = 45; +var TURN_AROUND_DEGREES = 180; +var TURN_RATE = 90; // Turn rate in degrees per second + + +/*****************************************************************************/ +/** COMMANDS *****************************************************************/ +var CMD_MOVE_FORWARD = "Move forward"; +var CMD_MOVE_BACKWARD = "Move backward"; +var CMD_MOVE_UP = "Move up"; +var CMD_MOVE_DOWN = "Move down"; +var CMD_MOVE_LEFT = "Move left"; +var CMD_MOVE_RIGHT = "Move right"; + +var CMD_STEP_FORWARD = "Step forward"; +var CMD_STEP_BACKWARD = "Step backward"; +var CMD_STEP_LEFT = "Step left"; +var CMD_STEP_RIGHT = "Step right"; +var CMD_STEP_UP = "Step up"; +var CMD_STEP_DOWN = "Step down"; + +var CMD_TURN_LEFT = "Turn left"; +var CMD_TURN_SLIGHT_LEFT = "Turn slight left"; +var CMD_TURN_RIGHT = "Turn right"; +var CMD_TURN_SLIGHT_RIGHT = "Turn slight right"; +var CMD_TURN_AROUND = "Turn around"; + +var CMD_STOP = "Stop"; + +var CMD_SHOW_COMMANDS = "Show commands"; + +var MOVE_COMMANDS = [ + CMD_MOVE_FORWARD, + CMD_MOVE_BACKWARD, + CMD_MOVE_UP, + CMD_MOVE_DOWN, + CMD_MOVE_LEFT, + CMD_MOVE_RIGHT, +]; + +var STEP_COMMANDS = [ + CMD_STEP_FORWARD, + CMD_STEP_BACKWARD, + CMD_STEP_UP, + CMD_STEP_DOWN, + CMD_STEP_LEFT, + CMD_STEP_RIGHT, +]; + +var TURN_COMMANDS = [ + CMD_TURN_LEFT, + CMD_TURN_SLIGHT_LEFT, + CMD_TURN_RIGHT, + CMD_TURN_SLIGHT_RIGHT, + CMD_TURN_AROUND, +]; + +var OTHER_COMMANDS = [ + CMD_STOP, + CMD_SHOW_COMMANDS, +]; + +var ALL_COMMANDS = [] + .concat(MOVE_COMMANDS) + .concat(STEP_COMMANDS) + .concat(TURN_COMMANDS) + .concat(OTHER_COMMANDS); + +/** END OF COMMANDS **********************************************************/ +/*****************************************************************************/ + + +var currentCommandFunc = null; + +function handleCommandRecognized(command) { + if (MOVE_COMMANDS.indexOf(command) > -1 || STEP_COMMANDS.indexOf(command) > -1) { + // If this is a STEP_* command, we will want to countdown the duration + // of time to move. MOVE_* commands don't stop. + var timeRemaining = MOVE_COMMANDS.indexOf(command) > -1 ? 0 : STEP_DURATION; + var accel = { x: 0, y: 0, z: 0 }; + + if (command == CMD_MOVE_FORWARD || command == CMD_STEP_FORWARD) { + accel = { x: 0, y: 0, z: 1 }; + } else if (command == CMD_MOVE_BACKWARD || command == CMD_STEP_BACKWARD) { + accel = { x: 0, y: 0, z: -1 }; + } else if (command === CMD_MOVE_UP || command == CMD_STEP_UP) { + accel = { x: 0, y: 1, z: 0 }; + } else if (command == CMD_MOVE_DOWN || command == CMD_STEP_DOWN) { + accel = { x: 0, y: -1, z: 0 }; + } else if (command == CMD_MOVE_LEFT || command == CMD_STEP_LEFT) { + accel = { x: -1, y: 0, z: 0 }; + } else if (command == CMD_MOVE_RIGHT || command == CMD_STEP_RIGHT) { + accel = { x: 1, y: 0, z: 0 }; + } + + currentCommandFunc = function(dt) { + if (timeRemaining > 0 && dt >= timeRemaining) { + dt = timeRemaining; + } + + var headOrientation = MyAvatar.headOrientation; + var front = Quat.getFront(headOrientation); + var right = Quat.getRight(headOrientation); + var up = Quat.getUp(headOrientation); + + var thrust = Vec3.multiply(front, accel.z * ACCELERATION); + thrust = Vec3.sum(thrust, Vec3.multiply(right, accel.x * ACCELERATION)); + thrust = Vec3.sum(thrust, Vec3.multiply(up, accel.y * ACCELERATION)); + MyAvatar.addThrust(thrust); + + if (timeRemaining > 0) { + timeRemaining -= dt; + return timeRemaining > 0; + } + + return true; + }; + } else if (TURN_COMMANDS.indexOf(command) > -1) { + var degreesRemaining; + var sign; + if (command == CMD_TURN_LEFT) { + sign = 1; + degreesRemaining = TURN_DEGREES; + } else if (command == CMD_TURN_RIGHT) { + sign = -1; + degreesRemaining = TURN_DEGREES; + } else if (command == CMD_TURN_SLIGHT_LEFT) { + sign = 1; + degreesRemaining = SLIGHT_TURN_DEGREES; + } else if (command == CMD_TURN_SLIGHT_RIGHT) { + sign = -1; + degreesRemaining = SLIGHT_TURN_DEGREES; + } else if (command == CMD_TURN_AROUND) { + sign = 1; + degreesRemaining = TURN_AROUND_DEGREES; + } + currentCommandFunc = function(dt) { + // Determine how much to turn by + var turnAmount = TURN_RATE * dt; + if (turnAmount > degreesRemaining) { + turnAmount = degreesRemaining; + } + + // Apply turn + var orientation = MyAvatar.orientation; + var deltaOrientation = Quat.fromPitchYawRollDegrees(0, sign * turnAmount, 0); + MyAvatar.orientation = Quat.multiply(orientation, deltaOrientation); + + degreesRemaining -= turnAmount; + return turnAmount > 0; + } + } else if (command == CMD_STOP) { + currentCommandFunc = null; + } else if (command == CMD_SHOW_COMMANDS) { + var msg = ""; + for (var i = 0; i < ALL_COMMANDS.length; i++) { + msg += ALL_COMMANDS[i] + "\n"; + } + Window.alert(msg); + } +} + +function update(dt) { + if (currentCommandFunc) { + if (currentCommandFunc(dt) === false) { + currentCommandFunc = null; + } + } +} + +function setup() { + for (var i = 0; i < ALL_COMMANDS.length; i++) { + SpeechRecognizer.addCommand(ALL_COMMANDS[i]); + } +} + +function scriptEnding() { + for (var i = 0; i < ALL_COMMANDS.length; i++) { + SpeechRecognizer.removeCommand(ALL_COMMANDS[i]); + } +} + +Script.scriptEnding.connect(scriptEnding); +Script.update.connect(update); +SpeechRecognizer.commandRecognized.connect(handleCommandRecognized); + +setup(); diff --git a/examples/toolBars.js b/examples/toolBars.js index ede3b80062..064ae372fd 100644 --- a/examples/toolBars.js +++ b/examples/toolBars.js @@ -132,20 +132,34 @@ ToolBar = function(x, y, direction) { this.y = y; this.width = 0; this.height = 0; - + this.back = this.back = Overlays.addOverlay("text", { + backgroundColor: { red: 255, green: 255, blue: 255 }, + x: this.x, + y: this.y, + width: this.width, + height: this.height, + alpha: 1.0, + visible: false + }); this.addTool = function(properties, selectable, selected) { if (direction == ToolBar.HORIZONTAL) { properties.x = this.x + this.width; properties.y = this.y; this.width += properties.width + ToolBar.SPACING; - this.height += Math.max(properties.height, this.height); + this.height = Math.max(properties.height, this.height); } else { properties.x = this.x; properties.y = this.y + this.height; this.width = Math.max(properties.width, this.width); this.height += properties.height + ToolBar.SPACING; } + if (this.back != null) { + Overlays.editOverlay(this.back, { + width: this.width + 2 * ToolBar.SPACING, + height: this.height + 2 * ToolBar.SPACING + }); + } this.tools[this.tools.length] = new Tool(properties, selectable, selected); return ((this.tools.length) - 1); @@ -159,18 +173,48 @@ ToolBar = function(x, y, direction) { for(var tool in this.tools) { this.tools[tool].move(this.tools[tool].x() + dx, this.tools[tool].y() + dy); } + if (this.back != null) { + Overlays.editOverlay(this.back, { + x: x - ToolBar.SPACING, + y: y - ToolBar.SPACING + }); + } } - this.setAlpha = function(alpha) { - for(var tool in this.tools) { + this.setAlpha = function(alpha, tool) { + if(typeof(tool) === 'undefined') { + for(var tool in this.tools) { + this.tools[tool].setAlpha(alpha); + } + if (this.back != null) { + Overlays.editOverlay(this.back, { alpha: alpha}); + } + } else { this.tools[tool].setAlpha(alpha); } } + + this.setBack = function(color, alpha) { + if (color == null) { + Overlays.editOverlay(this.back, { + visible: false + }); + } else { + Overlays.editOverlay(this.back, { + visible: true, + backgroundColor: color, + alpha: alpha + }) + } + } this.show = function(doShow) { for(var tool in this.tools) { this.tools[tool].show(doShow); } + if (this.back != null) { + Overlays.editOverlay(this.back, { visible: doShow}); + } } this.clicked = function(clickedOverlay) { @@ -200,6 +244,11 @@ ToolBar = function(x, y, direction) { delete this.tools[tool]; } + if (this.back != null) { + Overlays.deleteOverlay(this.back); + this.back = null; + } + this.tools = []; this.x = x; this.y = y; diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 32070a4427..54edc97211 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -46,6 +46,15 @@ foreach(SUBDIR avatar devices renderer ui starfield location scripting voxels pa set(INTERFACE_SRCS ${INTERFACE_SRCS} "${SUBDIR_SRCS}") endforeach(SUBDIR) +# Add SpeechRecognizer if on OS X, otherwise remove +if (APPLE) + file(GLOB INTERFACE_OBJCPP_SRCS "src/SpeechRecognizer.mm") + set(INTERFACE_SRCS ${INTERFACE_SRCS} ${INTERFACE_OBJCPP_SRCS}) +else () + get_filename_component(SPEECHRECOGNIZER_H "src/SpeechRecognizer.h" ABSOLUTE) + list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_H}) +endif () + find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Script Svg WebKitWidgets) # grab the ui files in resources/ui @@ -165,8 +174,9 @@ if (APPLE) find_library(CoreFoundation CoreFoundation) find_library(GLUT GLUT) find_library(OpenGL OpenGL) + find_library(AppKit AppKit) - target_link_libraries(${TARGET_NAME} ${CoreAudio} ${CoreFoundation} ${GLUT} ${OpenGL}) + target_link_libraries(${TARGET_NAME} ${CoreAudio} ${CoreFoundation} ${GLUT} ${OpenGL} ${AppKit}) # install command for OS X bundle INSTALL(TARGETS ${TARGET_NAME} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 093f5e6610..8421ee05b1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -894,7 +894,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; - case Qt::Key_Space: + case Qt::Key_Apostrophe: resetSensors(); break; @@ -1051,20 +1051,6 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_R: if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::FrustumRenderMode); - } else if (isMeta) { - if (_myAvatar->isRecording()) { - _myAvatar->stopRecording(); - } else { - _myAvatar->startRecording(); - _audio.setRecorder(_myAvatar->getRecorder()); - } - } else { - if (_myAvatar->isPlaying()) { - _myAvatar->stopPlaying(); - } else { - _myAvatar->startPlaying(); - _audio.setPlayer(_myAvatar->getPlayer()); - } } break; case Qt::Key_Percent: @@ -3749,6 +3735,10 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript scriptEngine->registerGlobalObject("Camera", cameraScriptable); connect(scriptEngine, SIGNAL(finished(const QString&)), cameraScriptable, SLOT(deleteLater())); +#ifdef Q_OS_MAC + scriptEngine->registerGlobalObject("SpeechRecognizer", Menu::getInstance()->getSpeechRecognizer()); +#endif + ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); connect(scriptEngine, SIGNAL(finished(const QString&)), clipboardScriptable, SLOT(deleteLater())); @@ -3831,6 +3821,10 @@ void Application::stopAllScripts(bool restart) { it.value()->stop(); qDebug() << "stopping script..." << it.key(); } + // HACK: ATM scripts cannot set/get their animation priorities, so we clear priorities + // whenever a script stops in case it happened to have been setting joint rotations. + // TODO: expose animation priorities and provide a layered animation control system. + _myAvatar->clearJointAnimationPriorities(); } void Application::stopScript(const QString &scriptName) { @@ -3838,6 +3832,10 @@ void Application::stopScript(const QString &scriptName) { if (_scriptEnginesHash.contains(scriptURLString)) { _scriptEnginesHash.value(scriptURLString)->stop(); qDebug() << "stopping script..." << scriptName; + // HACK: ATM scripts cannot set/get their animation priorities, so we clear priorities + // whenever a script stops in case it happened to have been setting joint rotations. + // TODO: expose animation priorities and provide a layered animation control system. + _myAvatar->clearJointAnimationPriorities(); } } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index c21b533695..2e2c10458d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -94,6 +94,9 @@ Menu::Menu() : _octreeStatsDialog(NULL), _lodToolsDialog(NULL), _userLocationsDialog(NULL), +#ifdef Q_OS_MAC + _speechRecognizer(), +#endif _maxVoxels(DEFAULT_MAX_VOXELS_PER_SYSTEM), _voxelSizeScale(DEFAULT_OCTREE_SIZE_SCALE), _oculusUIAngularSize(DEFAULT_OCULUS_UI_ANGULAR_SIZE), @@ -221,19 +224,16 @@ Menu::Menu() : addActionToQMenuAndActionHash(editMenu, MenuOption::Attachments, 0, this, SLOT(editAttachments())); addActionToQMenuAndActionHash(editMenu, MenuOption::Animations, 0, this, SLOT(editAnimations())); - addDisabledActionAndSeparator(editMenu, "Physics"); - QObject* avatar = appInstance->getAvatar(); - addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ObeyEnvironmentalGravity, Qt::SHIFT | Qt::Key_G, false, - avatar, SLOT(updateMotionBehaviorsFromMenu())); - addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::StandOnNearbyFloors, 0, true, - avatar, SLOT(updateMotionBehaviorsFromMenu())); - - addAvatarCollisionSubMenu(editMenu); - QMenu* toolsMenu = addMenu("Tools"); addActionToQMenuAndActionHash(toolsMenu, MenuOption::MetavoxelEditor, 0, this, SLOT(showMetavoxelEditor())); addActionToQMenuAndActionHash(toolsMenu, MenuOption::ScriptEditor, Qt::ALT | Qt::Key_S, this, SLOT(showScriptEditor())); +#ifdef Q_OS_MAC + QAction* speechRecognizerAction = addCheckableActionToQMenuAndActionHash(toolsMenu, MenuOption::ControlWithSpeech, + Qt::CTRL | Qt::SHIFT | Qt::Key_C, _speechRecognizer.getEnabled(), &_speechRecognizer, SLOT(setEnabled(bool))); + connect(&_speechRecognizer, SIGNAL(enabledUpdated(bool)), speechRecognizerAction, SLOT(setChecked(bool))); +#endif + #ifdef HAVE_QXMPP _chatAction = addActionToQMenuAndActionHash(toolsMenu, MenuOption::Chat, @@ -257,6 +257,45 @@ Menu::Menu() : this, SLOT(toggleConsole())); + QMenu* avatarMenu = addMenu("Avatar"); + + QMenu* avatarSizeMenu = avatarMenu->addMenu("Size"); + addActionToQMenuAndActionHash(avatarSizeMenu, + MenuOption::IncreaseAvatarSize, + Qt::Key_Plus, + appInstance->getAvatar(), + SLOT(increaseSize())); + addActionToQMenuAndActionHash(avatarSizeMenu, + MenuOption::DecreaseAvatarSize, + Qt::Key_Minus, + appInstance->getAvatar(), + SLOT(decreaseSize())); + addActionToQMenuAndActionHash(avatarSizeMenu, + MenuOption::ResetAvatarSize, + Qt::Key_Equal, + appInstance->getAvatar(), + SLOT(resetSize())); + + QObject* avatar = appInstance->getAvatar(); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::ChatCircling, 0, false); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::GlowWhenSpeaking, 0, true); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::BlueSpeechSphere, 0, true); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::ObeyEnvironmentalGravity, Qt::SHIFT | Qt::Key_G, false, + avatar, SLOT(updateMotionBehaviorsFromMenu())); + addCheckableActionToQMenuAndActionHash(avatarMenu, MenuOption::StandOnNearbyFloors, 0, true, + avatar, SLOT(updateMotionBehaviorsFromMenu())); + + QMenu* collisionsMenu = avatarMenu->addMenu("Collide With..."); + addCheckableActionToQMenuAndActionHash(collisionsMenu, MenuOption::CollideAsRagdoll); + addCheckableActionToQMenuAndActionHash(collisionsMenu, MenuOption::CollideWithAvatars, + 0, true, avatar, SLOT(updateCollisionGroups())); + addCheckableActionToQMenuAndActionHash(collisionsMenu, MenuOption::CollideWithVoxels, + 0, false, avatar, SLOT(updateCollisionGroups())); + addCheckableActionToQMenuAndActionHash(collisionsMenu, MenuOption::CollideWithParticles, + 0, true, avatar, SLOT(updateCollisionGroups())); + addCheckableActionToQMenuAndActionHash(collisionsMenu, MenuOption::CollideWithEnvironment, + 0, false, avatar, SLOT(updateCollisionGroups())); + QMenu* viewMenu = addMenu("View"); #ifdef Q_OS_MAC @@ -304,25 +343,6 @@ Menu::Menu() : Qt::CTRL | Qt::SHIFT | Qt::Key_3, false, &nodeBounds, SLOT(setShowParticleNodes(bool))); - - QMenu* avatarSizeMenu = viewMenu->addMenu("Avatar Size"); - - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::IncreaseAvatarSize, - Qt::Key_Plus, - appInstance->getAvatar(), - SLOT(increaseSize())); - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::DecreaseAvatarSize, - Qt::Key_Minus, - appInstance->getAvatar(), - SLOT(decreaseSize())); - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::ResetAvatarSize, - Qt::Key_Equal, - appInstance->getAvatar(), - SLOT(resetSize())); - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::OffAxisProjection, 0, false); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::TurnWithHead, 0, false); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MoveWithLean, 0, false); @@ -338,111 +358,97 @@ Menu::Menu() : QMenu* developerMenu = addMenu("Developer"); - QMenu* renderOptionsMenu = developerMenu->addMenu("Rendering Options"); - - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Stars, Qt::Key_Asterisk, true); + QMenu* renderOptionsMenu = developerMenu->addMenu("Render"); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Atmosphere, Qt::SHIFT | Qt::Key_A, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Avatars, 0, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Models, 0, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); + + QMenu* shadowMenu = renderOptionsMenu->addMenu("Shadows"); + QActionGroup* shadowGroup = new QActionGroup(shadowMenu); + shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, "None", 0, true)); + shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::SimpleShadows, 0, false)); + shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::CascadedShadows, 0, false)); + shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::AvatarsReceiveShadows, 0, true)); + + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Stars, Qt::Key_Asterisk, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, + MenuOption::Voxels, + Qt::SHIFT | Qt::Key_V, + true, + appInstance, + SLOT(setRenderVoxels(bool))); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::EnableGlowEffect, 0, true); addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::GlowMode, 0, appInstance->getGlowEffect(), SLOT(cycleRenderMode())); - - QMenu* shadowMenu = renderOptionsMenu->addMenu("Shadows"); - QActionGroup* shadowGroup = new QActionGroup(shadowMenu); - shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, "None", 0, true)); - shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::SimpleShadows, 0, false)); - shadowGroup->addAction(addCheckableActionToQMenuAndActionHash(shadowMenu, MenuOption::CascadedShadows, 0, false)); - - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true); - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false); - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::StringHair, 0, false); - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true); addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, Qt::SHIFT | Qt::Key_L, this, SLOT(lodTools())); - QMenu* voxelOptionsMenu = developerMenu->addMenu("Voxel Options"); - - addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, - MenuOption::Voxels, - Qt::SHIFT | Qt::Key_V, - true, - appInstance, - SLOT(setRenderVoxels(bool))); - - addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::VoxelTextures); - addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::AmbientOcclusion); - addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::DontFadeOnVoxelServerChanges); - addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::DisableAutoAdjustLOD); - - QMenu* modelOptionsMenu = developerMenu->addMenu("Model Options"); - addCheckableActionToQMenuAndActionHash(modelOptionsMenu, MenuOption::Models, 0, true); - addCheckableActionToQMenuAndActionHash(modelOptionsMenu, MenuOption::DisplayModelBounds, 0, false); - addCheckableActionToQMenuAndActionHash(modelOptionsMenu, MenuOption::DisplayModelElementProxy, 0, false); - addCheckableActionToQMenuAndActionHash(modelOptionsMenu, MenuOption::DisplayModelElementChildProxies, 0, false); - - QMenu* avatarOptionsMenu = developerMenu->addMenu("Avatar Options"); - - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::Avatars, 0, true); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::AvatarsReceiveShadows, 0, true); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderSkeletonCollisionShapes); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderHeadCollisionShapes); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderBoundingCollisionShapes); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::CollideAsRagdoll); - - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::LookAtVectors, 0, false); + QMenu* avatarDebugMenu = developerMenu->addMenu("Avatar"); #ifdef HAVE_FACESHIFT - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::Faceshift, 0, true, appInstance->getFaceshift(), SLOT(setTCPEnabled(bool))); #endif - #ifdef HAVE_FACEPLUS - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::Faceplus, 0, true, - appInstance->getFaceplus(), SLOT(updateEnabled())); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::Faceplus, 0, true, + appInstance->getFaceplus(), SLOT(updateEnabled())); #endif - #ifdef HAVE_VISAGE - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::Visage, 0, false, - appInstance->getVisage(), SLOT(updateEnabled())); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::Visage, 0, false, + appInstance->getVisage(), SLOT(updateEnabled())); #endif - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::GlowWhenSpeaking, 0, true); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::ChatCircling, 0, false); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::FocusIndicators, 0, false); - - QMenu* sixenseOptionsMenu = developerMenu->addMenu("Sixense Options"); - addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseMouseInput, 0, true); - addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseLasers, 0, true); - - QMenu* handOptionsMenu = developerMenu->addMenu("Hand Options"); - - addCheckableActionToQMenuAndActionHash(handOptionsMenu, + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderSkeletonCollisionShapes); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderHeadCollisionShapes); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderBoundingCollisionShapes); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderLookAtVectors, 0, false); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderFocusIndicator, 0, false); + + QMenu* modelDebugMenu = developerMenu->addMenu("Models"); + addCheckableActionToQMenuAndActionHash(modelDebugMenu, MenuOption::DisplayModelBounds, 0, false); + addCheckableActionToQMenuAndActionHash(modelDebugMenu, MenuOption::DisplayModelElementProxy, 0, false); + addCheckableActionToQMenuAndActionHash(modelDebugMenu, MenuOption::DisplayModelElementChildProxies, 0, false); + + QMenu* voxelOptionsMenu = developerMenu->addMenu("Voxels"); + addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::VoxelTextures); + addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::AmbientOcclusion); + addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::DontFadeOnVoxelServerChanges); + addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::DisableAutoAdjustLOD); + + QMenu* handOptionsMenu = developerMenu->addMenu("Hands"); + addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::AlignForearmsWithWrists, 0, false); + addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::AlternateIK, 0, false); + addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHands, 0, true); + addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false); + addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::ShowIKConstraints, 0, false); + + QMenu* sixenseOptionsMenu = handOptionsMenu->addMenu("Sixense"); + addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::FilterSixense, 0, true, appInstance->getSixenseManager(), SLOT(setFilter(bool))); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, + addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::LowVelocityFilter, 0, true, appInstance, SLOT(setLowVelocityFilter(bool))); + addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseMouseInput, 0, true); + addCheckableActionToQMenuAndActionHash(sixenseOptionsMenu, MenuOption::SixenseLasers, 0, false); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHands, 0, true); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::HandsCollideWithSelf, 0, false); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::ShowIKConstraints, 0, false); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::AlignForearmsWithWrists, 0, true); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::AlternateIK, 0, false); - - addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::DisableNackPackets, 0, false); - addCheckableActionToQMenuAndActionHash(developerMenu, + QMenu* networkMenu = developerMenu->addMenu("Network"); + addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableNackPackets, 0, false); + addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::DisableActivityLogger, 0, false, @@ -451,9 +457,7 @@ Menu::Menu() : addActionToQMenuAndActionHash(developerMenu, MenuOption::WalletPrivateKey, 0, this, SLOT(changePrivateKey())); - addDisabledActionAndSeparator(developerMenu, "Testing"); - - QMenu* timingMenu = developerMenu->addMenu("Timing and Statistics Tools"); + QMenu* timingMenu = developerMenu->addMenu("Timing and Stats"); QMenu* perfTimerMenu = timingMenu->addMenu("Performance Timer"); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::DisplayTimingDetails, 0, true); addCheckableActionToQMenuAndActionHash(perfTimerMenu, MenuOption::ExpandUpdateTiming, 0, false); @@ -465,8 +469,10 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(timingMenu, MenuOption::TestPing, 0, true); addCheckableActionToQMenuAndActionHash(timingMenu, MenuOption::FrameTimer); addActionToQMenuAndActionHash(timingMenu, MenuOption::RunTimingTests, 0, this, SLOT(runTests())); + addCheckableActionToQMenuAndActionHash(timingMenu, MenuOption::PipelineWarnings); + addCheckableActionToQMenuAndActionHash(timingMenu, MenuOption::SuppressShortTimings); - QMenu* frustumMenu = developerMenu->addMenu("View Frustum Debugging Tools"); + QMenu* frustumMenu = developerMenu->addMenu("View Frustum"); addCheckableActionToQMenuAndActionHash(frustumMenu, MenuOption::DisplayFrustum, Qt::SHIFT | Qt::Key_F); addActionToQMenuAndActionHash(frustumMenu, MenuOption::FrustumRenderMode, @@ -476,11 +482,7 @@ Menu::Menu() : updateFrustumRenderModeAction(); - QMenu* renderDebugMenu = developerMenu->addMenu("Render Debugging Tools"); - addCheckableActionToQMenuAndActionHash(renderDebugMenu, MenuOption::PipelineWarnings); - addCheckableActionToQMenuAndActionHash(renderDebugMenu, MenuOption::SuppressShortTimings); - - QMenu* audioDebugMenu = developerMenu->addMenu("Audio Debugging Tools"); + QMenu* audioDebugMenu = developerMenu->addMenu("Audio"); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioNoiseReduction, 0, true, @@ -493,7 +495,7 @@ Menu::Menu() : appInstance->getAudio(), SLOT(toggleAudioFilter())); - QMenu* audioFilterMenu = audioDebugMenu->addMenu("Audio Filter Options"); + QMenu* audioFilterMenu = audioDebugMenu->addMenu("Audio Filter"); addDisabledActionAndSeparator(audioFilterMenu, "Filter Response"); { QAction *flat = addCheckableActionToQMenuAndActionHash(audioFilterMenu, MenuOption::AudioFilterFlat, @@ -557,7 +559,7 @@ Menu::Menu() : appInstance->getAudio(), SLOT(toggleScopePause())); - QMenu* audioScopeMenu = audioDebugMenu->addMenu("Audio Scope Options"); + QMenu* audioScopeMenu = audioDebugMenu->addMenu("Audio Scope"); addDisabledActionAndSeparator(audioScopeMenu, "Display Frames"); { QAction *fiveFrames = addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopeFiveFrames, @@ -648,13 +650,17 @@ Menu::Menu() : appInstance->getAudio(), SLOT(toggleStatsShowInjectedStreams())); + connect(appInstance->getAudio(), SIGNAL(muteToggled()), this, SLOT(audioMuteToggled())); + + QMenu* experimentalOptionsMenu = developerMenu->addMenu("Experimental"); + addCheckableActionToQMenuAndActionHash(experimentalOptionsMenu, MenuOption::BuckyBalls, 0, false); + addCheckableActionToQMenuAndActionHash(experimentalOptionsMenu, MenuOption::StringHair, 0, false); + addActionToQMenuAndActionHash(developerMenu, MenuOption::PasteToVoxel, Qt::CTRL | Qt::SHIFT | Qt::Key_V, this, SLOT(pasteToVoxel())); - connect(appInstance->getAudio(), SIGNAL(muteToggled()), this, SLOT(audioMuteToggled())); - #ifndef Q_OS_MAC QMenu* helpMenu = addMenu("Help"); QAction* helpAction = helpMenu->addAction(MenuOption::AboutApp); @@ -692,6 +698,10 @@ void Menu::loadSettings(QSettings* settings) { QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)).toString(); setScriptsLocation(settings->value("scriptsLocation", QString()).toString()); +#ifdef Q_OS_MAC + _speechRecognizer.setEnabled(settings->value("speechRecognitionEnabled", false).toBool()); +#endif + settings->beginGroup("View Frustum Offset Camera"); // in case settings is corrupt or missing loadSetting() will check for NaN _viewFrustumOffset.yaw = loadSetting(settings, "viewFrustumOffsetYaw", 0.0f); @@ -739,6 +749,9 @@ void Menu::saveSettings(QSettings* settings) { settings->setValue("boundaryLevelAdjust", _boundaryLevelAdjust); settings->setValue("snapshotsLocation", _snapshotsLocation); settings->setValue("scriptsLocation", _scriptsLocation); +#ifdef Q_OS_MAC + settings->setValue("speechRecognitionEnabled", _speechRecognizer.getEnabled()); +#endif settings->beginGroup("View Frustum Offset Camera"); settings->setValue("viewFrustumOffsetYaw", _viewFrustumOffset.yaw); settings->setValue("viewFrustumOffsetPitch", _viewFrustumOffset.pitch); @@ -1457,7 +1470,9 @@ void Menu::toggleConsole() { void Menu::audioMuteToggled() { QAction *muteAction = _actionHash.value(MenuOption::MuteAudio); - muteAction->setChecked(Application::getInstance()->getAudio()->getMuted()); + if (muteAction) { + muteAction->setChecked(Application::getInstance()->getAudio()->getMuted()); + } } void Menu::bandwidthDetailsClosed() { @@ -1629,45 +1644,31 @@ void Menu::runTests() { void Menu::updateFrustumRenderModeAction() { QAction* frustumRenderModeAction = _actionHash.value(MenuOption::FrustumRenderMode); - switch (_frustumDrawMode) { - default: - case FRUSTUM_DRAW_MODE_ALL: - frustumRenderModeAction->setText("Render Mode - All"); - break; - case FRUSTUM_DRAW_MODE_VECTORS: - frustumRenderModeAction->setText("Render Mode - Vectors"); - break; - case FRUSTUM_DRAW_MODE_PLANES: - frustumRenderModeAction->setText("Render Mode - Planes"); - break; - case FRUSTUM_DRAW_MODE_NEAR_PLANE: - frustumRenderModeAction->setText("Render Mode - Near"); - break; - case FRUSTUM_DRAW_MODE_FAR_PLANE: - frustumRenderModeAction->setText("Render Mode - Far"); - break; - case FRUSTUM_DRAW_MODE_KEYHOLE: - frustumRenderModeAction->setText("Render Mode - Keyhole"); - break; + if (frustumRenderModeAction) { + switch (_frustumDrawMode) { + default: + case FRUSTUM_DRAW_MODE_ALL: + frustumRenderModeAction->setText("Render Mode - All"); + break; + case FRUSTUM_DRAW_MODE_VECTORS: + frustumRenderModeAction->setText("Render Mode - Vectors"); + break; + case FRUSTUM_DRAW_MODE_PLANES: + frustumRenderModeAction->setText("Render Mode - Planes"); + break; + case FRUSTUM_DRAW_MODE_NEAR_PLANE: + frustumRenderModeAction->setText("Render Mode - Near"); + break; + case FRUSTUM_DRAW_MODE_FAR_PLANE: + frustumRenderModeAction->setText("Render Mode - Far"); + break; + case FRUSTUM_DRAW_MODE_KEYHOLE: + frustumRenderModeAction->setText("Render Mode - Keyhole"); + break; + } } } -void Menu::addAvatarCollisionSubMenu(QMenu* overMenu) { - // add avatar collisions subMenu to overMenu - QMenu* subMenu = overMenu->addMenu("Collision Options"); - - Application* appInstance = Application::getInstance(); - QObject* avatar = appInstance->getAvatar(); - addCheckableActionToQMenuAndActionHash(subMenu, MenuOption::CollideWithEnvironment, - 0, false, avatar, SLOT(updateCollisionGroups())); - addCheckableActionToQMenuAndActionHash(subMenu, MenuOption::CollideWithAvatars, - 0, true, avatar, SLOT(updateCollisionGroups())); - addCheckableActionToQMenuAndActionHash(subMenu, MenuOption::CollideWithVoxels, - 0, false, avatar, SLOT(updateCollisionGroups())); - addCheckableActionToQMenuAndActionHash(subMenu, MenuOption::CollideWithParticles, - 0, true, avatar, SLOT(updateCollisionGroups())); -} - QAction* Menu::getActionFromName(const QString& menuName, QMenu* menu) { QList menuActions; if (menu) { @@ -1887,10 +1888,9 @@ void Menu::removeMenuItem(const QString& menu, const QString& menuitem) { }; bool Menu::menuItemExists(const QString& menu, const QString& menuitem) { - QMenu* menuObj = getMenu(menu); QAction* menuItemAction = _actionHash.value(menuitem); - if (menuObj && menuItemAction) { - return true; + if (menuItemAction) { + return (getMenu(menu) != NULL); } return false; }; diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 7ef744e62e..efc812fb22 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -23,6 +23,10 @@ #include #include +#ifdef Q_OS_MAC +#include "SpeechRecognizer.h" +#endif + #include "location/LocationManager.h" #include "ui/PreferencesDialog.h" #include "ui/ChatWindow.h" @@ -137,6 +141,10 @@ public: void setBoundaryLevelAdjust(int boundaryLevelAdjust); int getBoundaryLevelAdjust() const { return _boundaryLevelAdjust; } +#ifdef Q_OS_MAC + SpeechRecognizer* getSpeechRecognizer() { return &_speechRecognizer; } +#endif + // User Tweakable PPS from Voxel Server int getMaxVoxelPacketsPerSecond() const { return _maxVoxelPacketsPerSecond; } void setMaxVoxelPacketsPerSecond(int maxVoxelPacketsPerSecond) { _maxVoxelPacketsPerSecond = maxVoxelPacketsPerSecond; } @@ -246,8 +254,6 @@ private: void updateFrustumRenderModeAction(); - void addAvatarCollisionSubMenu(QMenu* overMenu); - QAction* getActionFromName(const QString& menuName, QMenu* menu); QMenu* getSubMenuFromName(const QString& menuName, QMenu* menu); QMenu* getMenuParent(const QString& menuName, QString& finalMenuPart); @@ -274,6 +280,9 @@ private: OctreeStatsDialog* _octreeStatsDialog; LodToolsDialog* _lodToolsDialog; UserLocationsDialog* _userLocationsDialog; +#ifdef Q_OS_MAC + SpeechRecognizer _speechRecognizer; +#endif int _maxVoxels; float _voxelSizeScale; float _oculusUIAngularSize; @@ -342,25 +351,27 @@ namespace MenuOption { const QString AvatarsReceiveShadows = "Avatars Receive Shadows"; const QString Bandwidth = "Bandwidth Display"; const QString BandwidthDetails = "Bandwidth Details"; + const QString BlueSpeechSphere = "Blue Sphere While Speaking"; const QString BuckyBalls = "Bucky Balls"; const QString CascadedShadows = "Cascaded"; const QString Chat = "Chat..."; const QString ChatCircling = "Chat Circling"; - const QString CollideAsRagdoll = "Collide As Ragdoll"; - const QString CollideWithAvatars = "Collide With Avatars"; + const QString CollideAsRagdoll = "Collide With Self (Ragdoll)"; + const QString CollideWithAvatars = "Collide With Other Avatars"; const QString CollideWithEnvironment = "Collide With World Boundaries"; const QString CollideWithParticles = "Collide With Particles"; const QString CollideWithVoxels = "Collide With Voxels"; const QString Collisions = "Collisions"; const QString Console = "Console..."; + const QString ControlWithSpeech = "Control With Speech"; const QString DecreaseAvatarSize = "Decrease Avatar Size"; const QString DecreaseVoxelSize = "Decrease Voxel Size"; const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableAutoAdjustLOD = "Disable Automatically Adjusting LOD"; const QString DisableNackPackets = "Disable NACK Packets"; const QString DisplayFrustum = "Display Frustum"; - const QString DisplayHands = "Display Hands"; - const QString DisplayHandTargets = "Display Hand Targets"; + const QString DisplayHands = "Show Hand Info"; + const QString DisplayHandTargets = "Show Hand Targets"; const QString DisplayModelBounds = "Display Model Bounds"; const QString DisplayModelElementChildProxies = "Display Model Element Children"; const QString DisplayModelElementProxy = "Display Model Element Bounds"; @@ -380,7 +391,6 @@ namespace MenuOption { const QString Faceshift = "Faceshift"; const QString FilterSixense = "Smooth Sixense Movement"; const QString FirstPerson = "First Person"; - const QString FocusIndicators = "Focus Indicators"; const QString FrameTimer = "Show Timer"; const QString FrustumRenderMode = "Render Mode"; const QString Fullscreen = "Fullscreen"; @@ -391,7 +401,6 @@ namespace MenuOption { const QString GoToDomain = "Go To Domain..."; const QString GoTo = "Go To..."; const QString GoToLocation = "Go To Location..."; - const QString HandsCollideWithSelf = "Collide With Self"; const QString HeadMouse = "Head Mouse"; const QString IncreaseAvatarSize = "Increase Avatar Size"; const QString IncreaseVoxelSize = "Increase Voxel Size"; @@ -401,7 +410,6 @@ namespace MenuOption { const QString Login = "Login"; const QString Log = "Log"; const QString Logout = "Logout"; - const QString LookAtVectors = "Look-at Vectors"; const QString LowVelocityFilter = "Low Velocity Filter"; const QString MetavoxelEditor = "Metavoxel Editor..."; const QString Metavoxels = "Metavoxels"; @@ -421,13 +429,15 @@ namespace MenuOption { const QString Pair = "Pair"; const QString Particles = "Particles"; const QString PasteToVoxel = "Paste to Voxel..."; - const QString PipelineWarnings = "Show Render Pipeline Warnings"; + const QString PipelineWarnings = "Log Render Pipeline Warnings"; const QString Preferences = "Preferences..."; const QString Quit = "Quit"; const QString ReloadAllScripts = "Reload All Scripts"; - const QString RenderBoundingCollisionShapes = "Bounding Collision Shapes"; - const QString RenderHeadCollisionShapes = "Head Collision Shapes"; - const QString RenderSkeletonCollisionShapes = "Skeleton Collision Shapes"; + const QString RenderBoundingCollisionShapes = "Show Bounding Collision Shapes"; + const QString RenderFocusIndicator = "Show Eye Focus"; + const QString RenderHeadCollisionShapes = "Show Head Collision Shapes"; + const QString RenderLookAtVectors = "Show Look-at Vectors"; + const QString RenderSkeletonCollisionShapes = "Show Skeleton Collision Shapes"; const QString ResetAvatarSize = "Reset Avatar Size"; const QString RunningScripts = "Running Scripts"; const QString RunTimingTests = "Run Timing Tests"; @@ -459,7 +469,7 @@ namespace MenuOption { const QString VoxelMode = "Cycle Voxel Mode"; const QString Voxels = "Voxels"; const QString VoxelTextures = "Voxel Textures"; - const QString WalletPrivateKey = "Wallet Private Key"; + const QString WalletPrivateKey = "Wallet Private Key..."; } void sendFakeEnterEvent(); diff --git a/interface/src/Recorder.cpp b/interface/src/Recorder.cpp index 30b14ac589..fb5c92c849 100644 --- a/interface/src/Recorder.cpp +++ b/interface/src/Recorder.cpp @@ -11,7 +11,9 @@ #include +#include #include +#include #include "Recorder.h" @@ -241,9 +243,14 @@ void Player::stopPlaying() { // Cleanup audio thread _injector->stop(); + QObject::connect(_injector.data(), &AudioInjector::finished, + _injector.data(), &AudioInjector::deleteLater); + QObject::connect(_injector.data(), &AudioInjector::destroyed, + _audioThread, &QThread::quit); + QObject::connect(_audioThread, &QThread::finished, + _audioThread, &QThread::deleteLater); _injector.clear(); - _audioThread->exit(); - _audioThread->deleteLater(); + _audioThread = NULL; qDebug() << "Recorder::stopPlaying()"; } @@ -309,13 +316,255 @@ bool Player::computeCurrentFrame() { return true; } -void writeRecordingToFile(RecordingPointer recording, QString file) { - // TODO - qDebug() << "Writing recording to " << file; +void writeRecordingToFile(RecordingPointer recording, QString filename) { + if (!recording || recording->getFrameNumber() < 1) { + qDebug() << "Can't save empty recording"; + return; + } + + qDebug() << "Writing recording to " << filename << "."; + QElapsedTimer timer; + QFile file(filename); + if (!file.open(QIODevice::WriteOnly)){ + return; + } + timer.start(); + + + QDataStream fileStream(&file); + + fileStream << recording->_timestamps; + + RecordingFrame& baseFrame = recording->_frames[0]; + int totalLength = 0; + + // Blendshape coefficients + fileStream << baseFrame._blendshapeCoefficients; + totalLength += baseFrame._blendshapeCoefficients.size(); + + // Joint Rotations + int jointRotationSize = baseFrame._jointRotations.size(); + fileStream << jointRotationSize; + for (int i = 0; i < jointRotationSize; ++i) { + fileStream << baseFrame._jointRotations[i].x << baseFrame._jointRotations[i].y << baseFrame._jointRotations[i].z << baseFrame._jointRotations[i].w; + } + totalLength += jointRotationSize; + + // Translation + fileStream << baseFrame._translation.x << baseFrame._translation.y << baseFrame._translation.z; + totalLength += 1; + + // Rotation + fileStream << baseFrame._rotation.x << baseFrame._rotation.y << baseFrame._rotation.z << baseFrame._rotation.w; + totalLength += 1; + + // Scale + fileStream << baseFrame._scale; + totalLength += 1; + + // Head Rotation + fileStream << baseFrame._headRotation.x << baseFrame._headRotation.y << baseFrame._headRotation.z << baseFrame._headRotation.w; + totalLength += 1; + + // Lean Sideways + fileStream << baseFrame._leanSideways; + totalLength += 1; + + // Lean Forward + fileStream << baseFrame._leanForward; + totalLength += 1; + + for (int i = 1; i < recording->_timestamps.size(); ++i) { + QBitArray mask(totalLength); + int maskIndex = 0; + QByteArray buffer; + QDataStream stream(&buffer, QIODevice::WriteOnly); + RecordingFrame& previousFrame = recording->_frames[i - 1]; + RecordingFrame& frame = recording->_frames[i]; + + // Blendshape coefficients + for (int i = 0; i < frame._blendshapeCoefficients.size(); ++i) { + if (frame._blendshapeCoefficients[i] != previousFrame._blendshapeCoefficients[i]) { + stream << frame._blendshapeCoefficients[i]; + mask.setBit(maskIndex); + } + maskIndex++; + } + + // Joint Rotations + for (int i = 0; i < frame._jointRotations.size(); ++i) { + if (frame._jointRotations[i] != previousFrame._jointRotations[i]) { + stream << frame._jointRotations[i].x << frame._jointRotations[i].y << frame._jointRotations[i].z << frame._jointRotations[i].w; + mask.setBit(maskIndex); + } + maskIndex++; + } + + // Translation + if (frame._translation != previousFrame._translation) { + stream << frame._translation.x << frame._translation.y << frame._translation.z; + mask.setBit(maskIndex); + } + maskIndex++; + + // Rotation + if (frame._rotation != previousFrame._rotation) { + stream << frame._rotation.x << frame._rotation.y << frame._rotation.z << frame._rotation.w; + mask.setBit(maskIndex); + } + maskIndex++; + + // Scale + if (frame._scale != previousFrame._scale) { + stream << frame._scale; + mask.setBit(maskIndex); + } + maskIndex++; + + // Head Rotation + if (frame._headRotation != previousFrame._headRotation) { + stream << frame._headRotation.x << frame._headRotation.y << frame._headRotation.z << frame._headRotation.w; + mask.setBit(maskIndex); + } + maskIndex++; + + // Lean Sideways + if (frame._leanSideways != previousFrame._leanSideways) { + stream << frame._leanSideways; + mask.setBit(maskIndex); + } + maskIndex++; + + // Lean Forward + if (frame._leanForward != previousFrame._leanForward) { + stream << frame._leanForward; + mask.setBit(maskIndex); + } + maskIndex++; + + fileStream << mask; + fileStream << buffer; + } + + fileStream << recording->_audio->getByteArray(); + + qDebug() << "Wrote " << file.size() << " bytes in " << timer.elapsed() << " ms."; } -RecordingPointer readRecordingFromFile(RecordingPointer recording, QString file) { - // TODO - qDebug() << "Reading recording from " << file; +RecordingPointer readRecordingFromFile(RecordingPointer recording, QString filename) { + qDebug() << "Reading recording from " << filename << "."; + if (!recording) { + recording.reset(new Recording()); + } + + QElapsedTimer timer; + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)){ + return recording; + } + timer.start(); + QDataStream fileStream(&file); + + fileStream >> recording->_timestamps; + RecordingFrame baseFrame; + + // Blendshape coefficients + fileStream >> baseFrame._blendshapeCoefficients; + + // Joint Rotations + int jointRotationSize; + fileStream >> jointRotationSize; + baseFrame._jointRotations.resize(jointRotationSize); + for (int i = 0; i < jointRotationSize; ++i) { + fileStream >> baseFrame._jointRotations[i].x >> baseFrame._jointRotations[i].y >> baseFrame._jointRotations[i].z >> baseFrame._jointRotations[i].w; + } + + fileStream >> baseFrame._translation.x >> baseFrame._translation.y >> baseFrame._translation.z; + fileStream >> baseFrame._rotation.x >> baseFrame._rotation.y >> baseFrame._rotation.z >> baseFrame._rotation.w; + fileStream >> baseFrame._scale; + fileStream >> baseFrame._headRotation.x >> baseFrame._headRotation.y >> baseFrame._headRotation.z >> baseFrame._headRotation.w; + fileStream >> baseFrame._leanSideways; + fileStream >> baseFrame._leanForward; + + recording->_frames << baseFrame; + + for (int i = 1; i < recording->_timestamps.size(); ++i) { + QBitArray mask; + QByteArray buffer; + QDataStream stream(&buffer, QIODevice::ReadOnly); + RecordingFrame frame; + RecordingFrame& previousFrame = recording->_frames.last(); + + fileStream >> mask; + fileStream >> buffer; + int maskIndex = 0; + + // Blendshape Coefficients + frame._blendshapeCoefficients.resize(baseFrame._blendshapeCoefficients.size()); + for (int i = 0; i < baseFrame._blendshapeCoefficients.size(); ++i) { + if (mask[maskIndex++]) { + stream >> frame._blendshapeCoefficients[i]; + } else { + frame._blendshapeCoefficients[i] = previousFrame._blendshapeCoefficients[i]; + } + } + + // Joint Rotations + frame._jointRotations.resize(baseFrame._jointRotations.size()); + for (int i = 0; i < baseFrame._jointRotations.size(); ++i) { + if (mask[maskIndex++]) { + stream >> frame._jointRotations[i].x >> frame._jointRotations[i].y >> frame._jointRotations[i].z >> frame._jointRotations[i].w; + } else { + frame._jointRotations[i] = previousFrame._jointRotations[i]; + } + } + + if (mask[maskIndex++]) { + stream >> frame._translation.x >> frame._translation.y >> frame._translation.z; + } else { + frame._translation = previousFrame._translation; + } + + if (mask[maskIndex++]) { + stream >> frame._rotation.x >> frame._rotation.y >> frame._rotation.z >> frame._rotation.w; + } else { + frame._rotation = previousFrame._rotation; + } + + if (mask[maskIndex++]) { + stream >> frame._scale; + } else { + frame._scale = previousFrame._scale; + } + + if (mask[maskIndex++]) { + stream >> frame._headRotation.x >> frame._headRotation.y >> frame._headRotation.z >> frame._headRotation.w; + } else { + frame._headRotation = previousFrame._headRotation; + } + + if (mask[maskIndex++]) { + stream >> frame._leanSideways; + } else { + frame._leanSideways = previousFrame._leanSideways; + } + + if (mask[maskIndex++]) { + stream >> frame._leanForward; + } else { + frame._leanForward = previousFrame._leanForward; + } + + recording->_frames << frame; + } + + QByteArray audioArray; + fileStream >> audioArray; + recording->addAudioPacket(audioArray); + + + qDebug() << "Read " << file.size() << " bytes in " << timer.elapsed() << " ms."; return recording; -} \ No newline at end of file +} + + diff --git a/interface/src/Recorder.h b/interface/src/Recorder.h index 9f7eb66ec6..2670d78365 100644 --- a/interface/src/Recorder.h +++ b/interface/src/Recorder.h @@ -137,6 +137,8 @@ public: bool isPlaying() const; qint64 elapsed() const; + RecordingPointer getRecording() const { return _recording; } + // Those should only be called if isPlaying() returns true glm::quat getHeadRotation(); float getLeanSideways(); diff --git a/interface/src/SpeechRecognizer.h b/interface/src/SpeechRecognizer.h new file mode 100644 index 0000000000..edd4abe1d6 --- /dev/null +++ b/interface/src/SpeechRecognizer.h @@ -0,0 +1,47 @@ +// +// SpeechRecognizer.h +// interface/src +// +// Created by Ryan Huffman on 07/31/14. +// Copyright 2014 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 +// + +#ifndef hifi_SpeechRecognizer_h +#define hifi_SpeechRecognizer_h + +#include +#include +#include + +class SpeechRecognizer : public QObject { + Q_OBJECT +public: + SpeechRecognizer(); + ~SpeechRecognizer(); + + void handleCommandRecognized(const char* command); + bool getEnabled() const { return _enabled; } + +public slots: + void setEnabled(bool enabled); + void addCommand(const QString& command); + void removeCommand(const QString& command); + +signals: + void commandRecognized(const QString& command); + void enabledUpdated(bool enabled); + +protected: + void reloadCommands(); + +private: + bool _enabled; + QSet _commands; + void* _speechRecognizerDelegate; + void* _speechRecognizer; +}; + +#endif // hifi_SpeechRecognizer_h diff --git a/interface/src/SpeechRecognizer.mm b/interface/src/SpeechRecognizer.mm new file mode 100644 index 0000000000..038bcce3e4 --- /dev/null +++ b/interface/src/SpeechRecognizer.mm @@ -0,0 +1,109 @@ +// +// SpeechRecognizer.mm +// interface/src +// +// Created by Ryan Huffman on 07/31/14. +// Copyright 2014 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 +// + +#include +#ifdef Q_OS_MAC + +#import +#import +#import + +#include + +#include "SpeechRecognizer.h" + +@interface SpeechRecognizerDelegate : NSObject { + SpeechRecognizer* _listener; +} + +- (void)setListener:(SpeechRecognizer*)listener; +- (void)speechRecognizer:(NSSpeechRecognizer*)sender didRecognizeCommand:(id)command; + +@end + +@implementation SpeechRecognizerDelegate + +- (void)setListener:(SpeechRecognizer*)listener { + _listener = listener; +} + +- (void)speechRecognizer:(NSSpeechRecognizer*)sender didRecognizeCommand:(id)command { + _listener->handleCommandRecognized(((NSString*)command).UTF8String); +} + +@end + +SpeechRecognizer::SpeechRecognizer() : + QObject(), + _enabled(false), + _commands(), + _speechRecognizerDelegate([[SpeechRecognizerDelegate alloc] init]), + _speechRecognizer(NULL) { + + [(id)_speechRecognizerDelegate setListener:this]; +} + +SpeechRecognizer::~SpeechRecognizer() { + if (_speechRecognizer) { + [(id)_speechRecognizer dealloc]; + } + if (_speechRecognizerDelegate) { + [(id)_speechRecognizerDelegate dealloc]; + } +} + +void SpeechRecognizer::handleCommandRecognized(const char* command) { + emit commandRecognized(QString(command)); +} + +void SpeechRecognizer::setEnabled(bool enabled) { + if (enabled == _enabled) { + return; + } + + _enabled = enabled; + if (_enabled) { + _speechRecognizer = [[NSSpeechRecognizer alloc] init]; + + reloadCommands(); + + [(id)_speechRecognizer setDelegate:(id)_speechRecognizerDelegate]; + [(id)_speechRecognizer startListening]; + } else { + [(id)_speechRecognizer stopListening]; + [(id)_speechRecognizer dealloc]; + _speechRecognizer = NULL; + } + + emit enabledUpdated(_enabled); +} + +void SpeechRecognizer::reloadCommands() { + if (_speechRecognizer) { + NSMutableArray* cmds = [NSMutableArray array]; + for (QSet::const_iterator iter = _commands.constBegin(); iter != _commands.constEnd(); iter++) { + [cmds addObject:[NSString stringWithUTF8String:(*iter).toLocal8Bit().data()]]; + } + [(id)_speechRecognizer setCommands:cmds]; + } +} + +void SpeechRecognizer::addCommand(const QString& command) { + _commands.insert(command); + reloadCommands(); +} + +void SpeechRecognizer::removeCommand(const QString& command) { + _commands.remove(command); + reloadCommands(); +} + +#endif // Q_OS_MAC diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 41912afd09..6f2d5ecef6 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -302,7 +302,7 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { return; } - glm::vec3 toTarget = cameraPosition - Application::getInstance()->getAvatar()->getPosition(); + glm::vec3 toTarget = cameraPosition - getPosition(); float distanceToTarget = glm::length(toTarget); { @@ -349,7 +349,7 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { } // If this is the avatar being looked at, render a little ball above their head - if (_isLookAtTarget && Menu::getInstance()->isOptionChecked(MenuOption::FocusIndicators)) { + if (_isLookAtTarget && Menu::getInstance()->isOptionChecked(MenuOption::RenderFocusIndicator)) { const float LOOK_AT_INDICATOR_RADIUS = 0.03f; const float LOOK_AT_INDICATOR_OFFSET = 0.22f; const float LOOK_AT_INDICATOR_COLOR[] = { 0.8f, 0.0f, 0.0f, 0.75f }; @@ -368,10 +368,12 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { // quick check before falling into the code below: // (a 10 degree breadth of an almost 2 meter avatar kicks in at about 12m) const float MIN_VOICE_SPHERE_DISTANCE = 12.0f; - if (distanceToTarget > MIN_VOICE_SPHERE_DISTANCE) { + if (Menu::getInstance()->isOptionChecked(MenuOption::BlueSpeechSphere) + && distanceToTarget > MIN_VOICE_SPHERE_DISTANCE) { + // render voice intensity sphere for avatars that are farther away const float MAX_SPHERE_ANGLE = 10.0f * RADIANS_PER_DEGREE; - const float MIN_SPHERE_ANGLE = 1.0f * RADIANS_PER_DEGREE; + const float MIN_SPHERE_ANGLE = 0.5f * RADIANS_PER_DEGREE; const float MIN_SPHERE_SIZE = 0.01f; const float SPHERE_LOUDNESS_SCALING = 0.0005f; const float SPHERE_COLOR[] = { 0.5f, 0.8f, 0.8f }; @@ -392,7 +394,7 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) { } } - const float DISPLAYNAME_DISTANCE = 10.0f; + const float DISPLAYNAME_DISTANCE = 20.0f; setShowDisplayName(renderMode == NORMAL_RENDER_MODE && distanceToTarget < DISPLAYNAME_DISTANCE); if (renderMode != NORMAL_RENDER_MODE || (isMyAvatar() && Application::getInstance()->getCamera()->getMode() == CAMERA_MODE_FIRST_PERSON)) { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index c8ecb23913..46780e50ea 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -91,7 +91,7 @@ public: const QVector& getAttachmentModels() const { return _attachmentModels; } glm::vec3 getChestPosition() const; float getScale() const { return _scale; } - const glm::vec3& getVelocity() const { return _velocity; } + Q_INVOKABLE const glm::vec3& getVelocity() const { return _velocity; } const Head* getHead() const { return static_cast(_headData); } Head* getHead() { return static_cast(_headData); } Hand* getHand() { return static_cast(_handData); } @@ -152,9 +152,9 @@ public: Q_INVOKABLE glm::quat getJointCombinedRotation(int index) const; Q_INVOKABLE glm::quat getJointCombinedRotation(const QString& name) const; - glm::vec3 getAcceleration() const { return _acceleration; } - glm::vec3 getAngularVelocity() const { return _angularVelocity; } - glm::vec3 getAngularAcceleration() const { return _angularAcceleration; } + Q_INVOKABLE glm::vec3 getAcceleration() const { return _acceleration; } + Q_INVOKABLE glm::vec3 getAngularVelocity() const { return _angularVelocity; } + Q_INVOKABLE glm::vec3 getAngularAcceleration() const { return _angularAcceleration; } /// Scales a world space position vector relative to the avatar position and scale diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 5cc8812b40..0a9cbfe762 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -82,7 +82,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { void AvatarManager::renderAvatars(Avatar::RenderMode renderMode, bool selfAvatarOnly) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::renderAvatars()"); - bool renderLookAtVectors = Menu::getInstance()->isOptionChecked(MenuOption::LookAtVectors); + bool renderLookAtVectors = Menu::getInstance()->isOptionChecked(MenuOption::RenderLookAtVectors); glm::vec3 cameraPosition = Application::getInstance()->getCamera()->getPosition(); diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index 203dbf2283..521a4ddc57 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -54,7 +54,7 @@ void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBX state.setRotationInConstrainedFrame(glm::angleAxis(- RADIANS_PER_DEGREE * _owningHead->getFinalRoll(), glm::normalize(inverse * axes[2])) * glm::angleAxis(RADIANS_PER_DEGREE * _owningHead->getFinalYaw(), glm::normalize(inverse * axes[1])) * glm::angleAxis(- RADIANS_PER_DEGREE * _owningHead->getFinalPitch(), glm::normalize(inverse * axes[0])) - * joint.rotation); + * joint.rotation, DEFAULT_PRIORITY); } void FaceModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { @@ -69,7 +69,7 @@ void FaceModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJ glm::quat between = rotationBetween(front, lookAt); const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; state.setRotationInConstrainedFrame(glm::angleAxis(glm::clamp(glm::angle(between), -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) * - joint.rotation); + joint.rotation, DEFAULT_PRIORITY); } void FaceModel::updateJointState(int index) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e1d8274993..68c0275685 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -508,40 +508,162 @@ bool MyAvatar::setJointReferential(int id, int jointIndex) { } } -bool MyAvatar::isRecording() const { +bool MyAvatar::isRecording() { + if (!_recorder) { + return false; + } + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(this, "isRecording", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, result)); + return result; + } return _recorder && _recorder->isRecording(); } -RecorderPointer MyAvatar::startRecording() { +qint64 MyAvatar::recorderElapsed() { + if (!_recorder) { + return 0; + } + if (QThread::currentThread() != thread()) { + qint64 result; + QMetaObject::invokeMethod(this, "recorderElapsed", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(qint64, result)); + return result; + } + return _recorder->elapsed(); +} + +void MyAvatar::startRecording() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "startRecording", Qt::BlockingQueuedConnection); + return; + } if (!_recorder) { _recorder = RecorderPointer(new Recorder(this)); } + Application::getInstance()->getAudio()->setRecorder(_recorder); _recorder->startRecording(); - return _recorder; + } void MyAvatar::stopRecording() { + if (!_recorder) { + return; + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stopRecording", Qt::BlockingQueuedConnection); + return; + } if (_recorder) { _recorder->stopRecording(); } } -bool MyAvatar::isPlaying() const { +void MyAvatar::saveRecording(QString filename) { + if (!_recorder) { + qDebug() << "There is no recording to save"; + return; + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "saveRecording", Qt::BlockingQueuedConnection, + Q_ARG(QString, filename)); + return; + } + if (_recorder) { + _recorder->saveToFile(filename); + } +} + +bool MyAvatar::isPlaying() { + if (!_player) { + return false; + } + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(this, "isPlaying", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, result)); + return result; + } return _player && _player->isPlaying(); } -PlayerPointer MyAvatar::startPlaying() { +qint64 MyAvatar::playerElapsed() { + if (!_player) { + return 0; + } + if (QThread::currentThread() != thread()) { + qint64 result; + QMetaObject::invokeMethod(this, "playerElapsed", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(qint64, result)); + return result; + } + return _player->elapsed(); +} + +qint64 MyAvatar::playerLength() { + if (!_player) { + return 0; + } + if (QThread::currentThread() != thread()) { + qint64 result; + QMetaObject::invokeMethod(this, "playerLength", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(qint64, result)); + return result; + } + return _player->getRecording()->getLength(); +} + +void MyAvatar::loadRecording(QString filename) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadRecording", Qt::BlockingQueuedConnection, + Q_ARG(QString, filename)); + return; + } if (!_player) { _player = PlayerPointer(new Player(this)); } - if (_recorder) { - _player->loadRecording(_recorder->getRecording()); - _player->startPlaying(); + + _player->loadFromFile(filename); +} + +void MyAvatar::loadLastRecording() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadLastRecording", Qt::BlockingQueuedConnection); + return; } - return _player; + if (!_recorder) { + qDebug() << "There is no recording to load"; + return; + } + if (!_player) { + _player = PlayerPointer(new Player(this)); + } + + _player->loadRecording(_recorder->getRecording()); +} + +void MyAvatar::startPlaying() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "startPlaying", Qt::BlockingQueuedConnection); + return; + } + if (!_player) { + _player = PlayerPointer(new Player(this)); + } + + Application::getInstance()->getAudio()->setPlayer(_player); + _player->startPlaying(); } void MyAvatar::stopPlaying() { + if (!_player) { + return; + } + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stopPlaying", Qt::BlockingQueuedConnection); + return; + } if (_player) { _player->stopPlaying(); } @@ -927,36 +1049,39 @@ glm::vec3 MyAvatar::getUprightHeadPosition() const { return _position + getWorldAlignedOrientation() * glm::vec3(0.0f, getPelvisToHeadLength(), 0.0f); } -const float JOINT_PRIORITY = 2.0f; +const float SCRIPT_PRIORITY = DEFAULT_PRIORITY + 1.0f; +const float RECORDER_PRIORITY = SCRIPT_PRIORITY + 1.0f; void MyAvatar::setJointRotations(QVector jointRotations) { - for (int i = 0; i < jointRotations.size(); ++i) { - if (i < _jointData.size()) { - _skeletonModel.setJointState(i, true, jointRotations[i], JOINT_PRIORITY + 1.0f); - } + int numStates = glm::min(_skeletonModel.getJointStateCount(), jointRotations.size()); + for (int i = 0; i < numStates; ++i) { + // HACK: ATM only Recorder calls setJointRotations() so we hardcode its priority here + _skeletonModel.setJointState(i, true, jointRotations[i], RECORDER_PRIORITY); } } void MyAvatar::setJointData(int index, const glm::quat& rotation) { - Avatar::setJointData(index, rotation); if (QThread::currentThread() == thread()) { - _skeletonModel.setJointState(index, true, rotation, JOINT_PRIORITY); + // HACK: ATM only JS scripts call setJointData() on MyAvatar so we hardcode the priority + _skeletonModel.setJointState(index, true, rotation, SCRIPT_PRIORITY); } } void MyAvatar::clearJointData(int index) { - Avatar::clearJointData(index); if (QThread::currentThread() == thread()) { - _skeletonModel.setJointState(index, false, glm::quat(), JOINT_PRIORITY); + // HACK: ATM only JS scripts call clearJointData() on MyAvatar so we hardcode the priority + _skeletonModel.setJointState(index, false, glm::quat(), 0.0f); } } void MyAvatar::clearJointsData() { - for (int i = 0; i < _jointData.size(); ++i) { - Avatar::clearJointData(i); - if (QThread::currentThread() == thread()) { - _skeletonModel.clearJointState(i); - } + clearJointAnimationPriorities(); +} + +void MyAvatar::clearJointAnimationPriorities() { + int numStates = _skeletonModel.getJointStateCount(); + for (int i = 0; i < numStates; ++i) { + _skeletonModel.clearJointAnimationPriority(i); } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 2c1695a499..8c37fa8f26 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -106,7 +106,15 @@ public: virtual int parseDataAtOffset(const QByteArray& packet, int offset); static void sendKillAvatar(); - + + Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } + Q_INVOKABLE float getHeadFinalYaw() const { return getHead()->getFinalYaw(); } + Q_INVOKABLE float getHeadFinalRoll() const { return getHead()->getFinalRoll(); } + Q_INVOKABLE float getHeadFinalPitch() const { return getHead()->getFinalPitch(); } + Q_INVOKABLE float getHeadDeltaPitch() const { return getHead()->getDeltaPitch(); } + + Q_INVOKABLE glm::vec3 getEyePosition() const { return getHead()->getEyePosition(); } + Q_INVOKABLE glm::vec3 getTargetAvatarPosition() const { return _targetAvatarPosition; } AvatarData* getLookAtTargetAvatar() const { return _lookAtTargetAvatar.data(); } void updateLookAtTargetAvatar(); @@ -120,6 +128,8 @@ public: virtual void setSkeletonModelURL(const QUrl& skeletonModelURL); virtual void setAttachmentData(const QVector& attachmentData); + void clearJointAnimationPriorities(); + virtual void attach(const QString& modelURL, const QString& jointName = QString(), const glm::vec3& translation = glm::vec3(), const glm::quat& rotation = glm::quat(), float scale = 1.0f, bool allowDuplicates = false, bool useSaved = true); @@ -134,6 +144,10 @@ public: /// Renders a laser pointer for UI picking void renderLaserPointers(); glm::vec3 getLaserPointerTipPosition(const PalmData* palm); + + const RecorderPointer getRecorder() const { return _recorder; } + const PlayerPointer getPlayer() const { return _player; } + public slots: void goHome(); void increaseSize(); @@ -157,14 +171,18 @@ public slots: bool setModelReferential(int id); bool setJointReferential(int id, int jointIndex); - const RecorderPointer getRecorder() const { return _recorder; } - bool isRecording() const; - RecorderPointer startRecording(); + bool isRecording(); + qint64 recorderElapsed(); + void startRecording(); void stopRecording(); + void saveRecording(QString filename); - const PlayerPointer getPlayer() const { return _player; } - bool isPlaying() const; - PlayerPointer startPlaying(); + bool isPlaying(); + qint64 playerElapsed(); + qint64 playerLength(); + void loadRecording(QString filename); + void loadLastRecording(); + void startPlaying(); void stopPlaying(); diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index f9a73f2431..5a8eb5519d 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -51,7 +51,8 @@ void SkeletonModel::setJointStates(QVector states) { } } -const float PALM_PRIORITY = 3.0f; +const float PALM_PRIORITY = DEFAULT_PRIORITY; +const float LEAN_PRIORITY = DEFAULT_PRIORITY; void SkeletonModel::simulate(float deltaTime, bool fullUpdate) { setTranslation(_owningAvatar->getPosition()); @@ -230,7 +231,7 @@ void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { JointState& parentState = _jointStates[parentJointIndex]; parentState.setRotationInBindFrame(palmRotation, PALM_PRIORITY); // lock hand to forearm by slamming its rotation (in parent-frame) to identity - _jointStates[jointIndex].setRotationInConstrainedFrame(glm::quat()); + _jointStates[jointIndex].setRotationInConstrainedFrame(glm::quat(), PALM_PRIORITY); } else { inverseKinematics(jointIndex, palmPosition, palmRotation, PALM_PRIORITY); } @@ -243,7 +244,7 @@ void SkeletonModel::updateJointState(int index) { const JointState& parentState = _jointStates.at(joint.parentIndex); const FBXGeometry& geometry = _geometry->getFBXGeometry(); if (index == geometry.leanJointIndex) { - maybeUpdateLeanRotation(parentState, joint, state); + maybeUpdateLeanRotation(parentState, state); } else if (index == geometry.neckJointIndex) { maybeUpdateNeckRotation(parentState, joint, state); @@ -260,17 +261,18 @@ void SkeletonModel::updateJointState(int index) { } } -void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { +void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, JointState& state) { if (!_owningAvatar->isMyAvatar() || Application::getInstance()->getPrioVR()->isActive()) { return; } // get the rotation axes in joint space and use them to adjust the rotation - glm::mat3 axes = glm::mat3_cast(glm::quat()); - glm::mat3 inverse = glm::mat3(glm::inverse(parentState.getTransform() * glm::translate(state.getDefaultTranslationInConstrainedFrame()) * - joint.preTransform * glm::mat4_cast(joint.preRotation * joint.rotation))); - state.setRotationInConstrainedFrame(glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanSideways(), - glm::normalize(inverse * axes[2])) * glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanForward(), - glm::normalize(inverse * axes[0])) * joint.rotation); + glm::vec3 xAxis(1.0f, 0.0f, 0.0f); + glm::vec3 zAxis(0.0f, 0.0f, 1.0f); + glm::quat inverse = glm::inverse(parentState.getRotation() * state.getDefaultRotationInParentFrame()); + state.setRotationInConstrainedFrame( + glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanSideways(), inverse * zAxis) + * glm::angleAxis(- RADIANS_PER_DEGREE * _owningAvatar->getHead()->getFinalLeanForward(), inverse * xAxis) + * state.getFBXJoint().rotation, LEAN_PRIORITY); } void SkeletonModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) { @@ -576,6 +578,7 @@ SkeletonRagdoll* SkeletonModel::buildRagdoll() { if (!_ragdoll) { _ragdoll = new SkeletonRagdoll(this); if (_enableShapes) { + clearShapes(); buildShapes(); } } @@ -600,6 +603,7 @@ void SkeletonModel::buildShapes() { if (!_ragdoll) { _ragdoll = new SkeletonRagdoll(this); } + _ragdoll->setRootIndex(geometry.rootJointIndex); _ragdoll->initPoints(); QVector& points = _ragdoll->getPoints(); @@ -614,15 +618,14 @@ void SkeletonModel::buildShapes() { float radius = uniformScale * joint.boneRadius; float halfHeight = 0.5f * uniformScale * joint.distanceToParent; Shape::Type type = joint.shapeType; - if (i == 0 || (type == Shape::CAPSULE_SHAPE && halfHeight < EPSILON)) { + int parentIndex = joint.parentIndex; + if (parentIndex == -1 || radius < EPSILON) { + type = Shape::UNKNOWN_SHAPE; + } else if (type == Shape::CAPSULE_SHAPE && halfHeight < EPSILON) { // this shape is forced to be a sphere type = Shape::SPHERE_SHAPE; } - if (radius < EPSILON) { - type = Shape::UNKNOWN_SHAPE; - } Shape* shape = NULL; - int parentIndex = joint.parentIndex; if (type == Shape::SPHERE_SHAPE) { shape = new VerletSphereShape(radius, &(points[i])); shape->setEntity(this); @@ -637,18 +640,16 @@ void SkeletonModel::buildShapes() { points[i].setMass(mass); totalMass += mass; } - if (parentIndex != -1) { + if (shape && parentIndex != -1) { // always disable collisions between joint and its parent - if (shape) { - disableCollisions(i, parentIndex); - } + disableCollisions(i, parentIndex); } _shapes.push_back(shape); } // set the mass of the root if (numStates > 0) { - points[0].setMass(totalMass); + points[_ragdoll->getRootIndex()].setMass(totalMass); } // This method moves the shapes to their default positions in Model frame. diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index b0d6ed7325..9bd8df745a 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -127,7 +127,7 @@ protected: /// Updates the state of the joint at the specified index. virtual void updateJointState(int index); - void maybeUpdateLeanRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); + void maybeUpdateLeanRotation(const JointState& parentState, JointState& state); void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state); diff --git a/interface/src/avatar/SkeletonRagdoll.cpp b/interface/src/avatar/SkeletonRagdoll.cpp index 6318323990..7c0e056826 100644 --- a/interface/src/avatar/SkeletonRagdoll.cpp +++ b/interface/src/avatar/SkeletonRagdoll.cpp @@ -36,8 +36,9 @@ void SkeletonRagdoll::stepForward(float deltaTime) { void SkeletonRagdoll::slamPointPositions() { QVector& jointStates = _model->getJointStates(); - int numStates = jointStates.size(); - for (int i = 0; i < numStates; ++i) { + const int numPoints = _points.size(); + assert(numPoints == jointStates.size()); + for (int i = _rootIndex; i < numPoints; ++i) { _points[i].initPosition(jointStates.at(i).getPosition()); } } @@ -49,8 +50,7 @@ void SkeletonRagdoll::initPoints() { initTransform(); // one point for each joint - QVector& jointStates = _model->getJointStates(); - int numStates = jointStates.size(); + int numStates = _model->getJointStates().size(); _points.fill(VerletPoint(), numStates); slamPointPositions(); } @@ -67,7 +67,7 @@ void SkeletonRagdoll::buildConstraints() { float minBone = FLT_MAX; float maxBone = -FLT_MAX; QMultiMap families; - for (int i = 0; i < numPoints; ++i) { + for (int i = _rootIndex; i < numPoints; ++i) { const JointState& state = jointStates.at(i); int parentIndex = state.getParentIndex(); if (parentIndex != -1) { @@ -105,7 +105,7 @@ void SkeletonRagdoll::buildConstraints() { float MAX_STRENGTH = 0.6f; float MIN_STRENGTH = 0.05f; // each joint gets a MuscleConstraint to its parent - for (int i = 1; i < numPoints; ++i) { + for (int i = _rootIndex + 1; i < numPoints; ++i) { const JointState& state = jointStates.at(i); int p = state.getParentIndex(); if (p == -1) { diff --git a/interface/src/devices/CaraFaceTracker.cpp b/interface/src/devices/CaraFaceTracker.cpp index 27cf3b175b..9f056fab9b 100644 --- a/interface/src/devices/CaraFaceTracker.cpp +++ b/interface/src/devices/CaraFaceTracker.cpp @@ -389,7 +389,6 @@ void CaraFaceTracker::decodePacket(const QByteArray& buffer) { if (theta > EPSILON) { float rMag = glm::length(glm::vec3(r.x, r.y, r.z)); const float AVERAGE_CARA_FRAME_TIME = 0.04f; - const float ANGULAR_VELOCITY_MIN = 1.2f; const float YAW_STANDARD_DEV_DEG = 2.5f; _headAngularVelocity = theta / AVERAGE_CARA_FRAME_TIME * glm::vec3(r.x, r.y, r.z) / rMag; diff --git a/interface/src/devices/DdeFaceTracker.cpp b/interface/src/devices/DdeFaceTracker.cpp index ae5beb8c85..aab3e1deb4 100644 --- a/interface/src/devices/DdeFaceTracker.cpp +++ b/interface/src/devices/DdeFaceTracker.cpp @@ -194,12 +194,12 @@ float updateAndGetCoefficient(float * coefficient, float currentValue, bool scal coefficient[AVG] = LONG_TERM_AVERAGE * coefficient[AVG] + (1.f - LONG_TERM_AVERAGE) * currentValue; if (coefficient[MAX] > coefficient[MIN]) { if (scaleToRange) { - return glm::clamp((currentValue - coefficient[AVG]) / (coefficient[MAX] - coefficient[MIN]), 0.f, 1.f); + return glm::clamp((currentValue - coefficient[AVG]) / (coefficient[MAX] - coefficient[MIN]), 0.0f, 1.0f); } else { - return glm::clamp(currentValue - coefficient[AVG], 0.f, 1.f); + return glm::clamp(currentValue - coefficient[AVG], 0.0f, 1.0f); } } else { - return 0.f; + return 0.0f; } } @@ -242,13 +242,11 @@ void DdeFaceTracker::decodePacket(const QByteArray& buffer) { // Set blendshapes float EYE_MAGNIFIER = 4.0f; - - float rightEye = (updateAndGetCoefficient(_rightEye, packet.expressions[0])) * EYE_MAGNIFIER; + float rightEye = glm::clamp((updateAndGetCoefficient(_rightEye, packet.expressions[0])) * EYE_MAGNIFIER, 0.0f, 1.0f); _blendshapeCoefficients[_rightBlinkIndex] = rightEye; - float leftEye = (updateAndGetCoefficient(_leftEye, packet.expressions[1])) * EYE_MAGNIFIER; + float leftEye = glm::clamp((updateAndGetCoefficient(_leftEye, packet.expressions[1])) * EYE_MAGNIFIER, 0.0f, 1.0f); _blendshapeCoefficients[_leftBlinkIndex] = leftEye; - // Right eye = packet.expressions[0]; float leftBrow = 1.0f - rescaleCoef(packet.expressions[14]); if (leftBrow < 0.5f) { @@ -270,9 +268,9 @@ void DdeFaceTracker::decodePacket(const QByteArray& buffer) { float JAW_OPEN_MAGNIFIER = 1.4f; _blendshapeCoefficients[_jawOpenIndex] = rescaleCoef(packet.expressions[21]) * JAW_OPEN_MAGNIFIER; - - _blendshapeCoefficients[_mouthSmileLeftIndex] = rescaleCoef(packet.expressions[24]); - _blendshapeCoefficients[_mouthSmileRightIndex] = rescaleCoef(packet.expressions[23]); + float SMILE_MULTIPLIER = 2.0f; + _blendshapeCoefficients[_mouthSmileLeftIndex] = glm::clamp(packet.expressions[24] * SMILE_MULTIPLIER, 0.0f, 1.0f); + _blendshapeCoefficients[_mouthSmileRightIndex] = glm::clamp(packet.expressions[23] * SMILE_MULTIPLIER, 0.0f, 1.0f); } else { diff --git a/interface/src/devices/Faceshift.cpp b/interface/src/devices/Faceshift.cpp index b5cba8348c..345e635045 100644 --- a/interface/src/devices/Faceshift.cpp +++ b/interface/src/devices/Faceshift.cpp @@ -26,11 +26,17 @@ using namespace fs; using namespace std; const quint16 FACESHIFT_PORT = 33433; +float STARTING_FACESHIFT_FRAME_TIME = 0.033f; Faceshift::Faceshift() : _tcpEnabled(true), _tcpRetryCount(0), _lastTrackingStateReceived(0), + _averageFrameTime(STARTING_FACESHIFT_FRAME_TIME), + _headAngularVelocity(0), + _headLinearVelocity(0), + _lastHeadTranslation(0), + _filteredHeadTranslation(0), _eyeGazeLeftPitch(0.0f), _eyeGazeLeftYaw(0.0f), _eyeGazeRightPitch(0.0f), @@ -209,23 +215,41 @@ void Faceshift::receive(const QByteArray& buffer) { float theta = 2 * acos(r.w); if (theta > EPSILON) { float rMag = glm::length(glm::vec3(r.x, r.y, r.z)); - float AVERAGE_FACESHIFT_FRAME_TIME = 0.033f; - _headAngularVelocity = theta / AVERAGE_FACESHIFT_FRAME_TIME * glm::vec3(r.x, r.y, r.z) / rMag; + _headAngularVelocity = theta / _averageFrameTime * glm::vec3(r.x, r.y, r.z) / rMag; } else { _headAngularVelocity = glm::vec3(0,0,0); } - _headRotation = newRotation; + const float ANGULAR_VELOCITY_FILTER_STRENGTH = 0.3f; + _headRotation = safeMix(_headRotation, newRotation, glm::clamp(glm::length(_headAngularVelocity) * + ANGULAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f)); const float TRANSLATION_SCALE = 0.02f; - _headTranslation = glm::vec3(data.m_headTranslation.x, data.m_headTranslation.y, - -data.m_headTranslation.z) * TRANSLATION_SCALE; + glm::vec3 newHeadTranslation = glm::vec3(data.m_headTranslation.x, data.m_headTranslation.y, + -data.m_headTranslation.z) * TRANSLATION_SCALE; + + _headLinearVelocity = (newHeadTranslation - _lastHeadTranslation) / _averageFrameTime; + + const float LINEAR_VELOCITY_FILTER_STRENGTH = 0.3f; + float velocityFilter = glm::clamp(1.0f - glm::length(_headLinearVelocity) * + LINEAR_VELOCITY_FILTER_STRENGTH, 0.0f, 1.0f); + _filteredHeadTranslation = velocityFilter * _filteredHeadTranslation + (1.0f - velocityFilter) * newHeadTranslation; + + _lastHeadTranslation = newHeadTranslation; + _headTranslation = _filteredHeadTranslation; + _eyeGazeLeftPitch = -data.m_eyeGazeLeftPitch; _eyeGazeLeftYaw = data.m_eyeGazeLeftYaw; _eyeGazeRightPitch = -data.m_eyeGazeRightPitch; _eyeGazeRightYaw = data.m_eyeGazeRightYaw; _blendshapeCoefficients = QVector::fromStdVector(data.m_coeffs); - _lastTrackingStateReceived = usecTimestampNow(); + const float FRAME_AVERAGING_FACTOR = 0.99f; + quint64 usecsNow = usecTimestampNow(); + if (_lastTrackingStateReceived != 0) { + _averageFrameTime = FRAME_AVERAGING_FACTOR * _averageFrameTime + + (1.0f - FRAME_AVERAGING_FACTOR) * (float)(usecsNow - _lastTrackingStateReceived) / 1000000.0f; + } + _lastTrackingStateReceived = usecsNow; } break; } diff --git a/interface/src/devices/Faceshift.h b/interface/src/devices/Faceshift.h index 25abd8c0eb..e7d87827eb 100644 --- a/interface/src/devices/Faceshift.h +++ b/interface/src/devices/Faceshift.h @@ -98,8 +98,12 @@ private: int _tcpRetryCount; bool _tracking; quint64 _lastTrackingStateReceived; + float _averageFrameTime; glm::vec3 _headAngularVelocity; + glm::vec3 _headLinearVelocity; + glm::vec3 _lastHeadTranslation; + glm::vec3 _filteredHeadTranslation; // degrees float _eyeGazeLeftPitch; diff --git a/interface/src/renderer/JointState.cpp b/interface/src/renderer/JointState.cpp index 316dfeb9ca..0776891109 100644 --- a/interface/src/renderer/JointState.cpp +++ b/interface/src/renderer/JointState.cpp @@ -145,7 +145,7 @@ glm::quat JointState::getVisibleRotationInParentFrame() const { void JointState::restoreRotation(float fraction, float priority) { assert(_fbxJoint != NULL); if (priority == _animationPriority || _animationPriority == 0.0f) { - setRotationInConstrainedFrame(safeMix(_rotationInConstrainedFrame, _fbxJoint->rotation, fraction)); + setRotationInConstrainedFrameInternal(safeMix(_rotationInConstrainedFrame, _fbxJoint->rotation, fraction)); _animationPriority = 0.0f; } } @@ -158,7 +158,7 @@ void JointState::setRotationInBindFrame(const glm::quat& rotation, float priorit if (constrain && _constraint) { _constraint->softClamp(targetRotation, _rotationInConstrainedFrame, 0.5f); } - setRotationInConstrainedFrame(targetRotation); + setRotationInConstrainedFrameInternal(targetRotation); _animationPriority = priority; } } @@ -173,10 +173,6 @@ void JointState::clearTransformTranslation() { _visibleTransform[3][2] = 0.0f; } -void JointState::setRotation(const glm::quat& rotation, bool constrain, float priority) { - applyRotationDelta(rotation * glm::inverse(getRotation()), true, priority); -} - void JointState::applyRotationDelta(const glm::quat& delta, bool constrain, float priority) { // NOTE: delta is in model-frame assert(_fbxJoint != NULL); @@ -193,7 +189,7 @@ void JointState::applyRotationDelta(const glm::quat& delta, bool constrain, floa _rotation = delta * getRotation(); return; } - setRotationInConstrainedFrame(targetRotation); + setRotationInConstrainedFrameInternal(targetRotation); } /// Applies delta rotation to joint but mixes a little bit of the default pose as well. @@ -212,7 +208,7 @@ void JointState::mixRotationDelta(const glm::quat& delta, float mixFactor, float if (_constraint) { _constraint->softClamp(targetRotation, _rotationInConstrainedFrame, 0.5f); } - setRotationInConstrainedFrame(targetRotation); + setRotationInConstrainedFrameInternal(targetRotation); } void JointState::mixVisibleRotationDelta(const glm::quat& delta, float mixFactor) { @@ -236,7 +232,17 @@ glm::quat JointState::computeVisibleParentRotation() const { return _visibleRotation * glm::inverse(_fbxJoint->preRotation * _visibleRotationInConstrainedFrame * _fbxJoint->postRotation); } -void JointState::setRotationInConstrainedFrame(const glm::quat& targetRotation) { +void JointState::setRotationInConstrainedFrame(glm::quat targetRotation, float priority, bool constrain) { + if (priority >= _animationPriority || _animationPriority == 0.0f) { + if (constrain && _constraint) { + _constraint->softClamp(targetRotation, _rotationInConstrainedFrame, 0.5f); + } + setRotationInConstrainedFrameInternal(targetRotation); + _animationPriority = priority; + } +} + +void JointState::setRotationInConstrainedFrameInternal(const glm::quat& targetRotation) { glm::quat parentRotation = computeParentRotation(); _rotationInConstrainedFrame = targetRotation; _transformChanged = true; @@ -258,6 +264,11 @@ const bool JointState::rotationIsDefault(const glm::quat& rotation, float tolera glm::abs(rotation.w - defaultRotation.w) < tolerance; } +glm::quat JointState::getDefaultRotationInParentFrame() const { + // NOTE: the result is constant and could be cached in the FBXJoint + return _fbxJoint->preRotation * _fbxJoint->rotation * _fbxJoint->postRotation; +} + const glm::vec3& JointState::getDefaultTranslationInConstrainedFrame() const { assert(_fbxJoint != NULL); return _fbxJoint->translation; @@ -267,4 +278,4 @@ void JointState::slaveVisibleTransform() { _visibleTransform = _transform; _visibleRotation = getRotation(); _visibleRotationInConstrainedFrame = _rotationInConstrainedFrame; -} \ No newline at end of file +} diff --git a/interface/src/renderer/JointState.h b/interface/src/renderer/JointState.h index 21961ba48c..a2c5ee7801 100644 --- a/interface/src/renderer/JointState.h +++ b/interface/src/renderer/JointState.h @@ -19,6 +19,8 @@ #include #include +const float DEFAULT_PRIORITY = 3.0f; + class AngularConstraint; class JointState { @@ -60,9 +62,6 @@ public: int getParentIndex() const { return _fbxJoint->parentIndex; } - /// \param rotation rotation of joint in model-frame - void setRotation(const glm::quat& rotation, bool constrain, float priority); - /// \param delta is in the model-frame void applyRotationDelta(const glm::quat& delta, bool constrain = true, float priority = 1.0f); @@ -84,13 +83,14 @@ public: /// NOTE: the JointState's model-frame transform/rotation are NOT updated! void setRotationInBindFrame(const glm::quat& rotation, float priority, bool constrain = false); - void setRotationInConstrainedFrame(const glm::quat& targetRotation); + void setRotationInConstrainedFrame(glm::quat targetRotation, float priority, bool constrain = false); void setVisibleRotationInConstrainedFrame(const glm::quat& targetRotation); const glm::quat& getRotationInConstrainedFrame() const { return _rotationInConstrainedFrame; } const glm::quat& getVisibleRotationInConstrainedFrame() const { return _visibleRotationInConstrainedFrame; } const bool rotationIsDefault(const glm::quat& rotation, float tolerance = EPSILON) const; + glm::quat getDefaultRotationInParentFrame() const; const glm::vec3& getDefaultTranslationInConstrainedFrame() const; @@ -106,6 +106,7 @@ public: glm::quat computeVisibleParentRotation() const; private: + void setRotationInConstrainedFrameInternal(const glm::quat& targetRotation); /// debug helper function void loadBindRotation(); diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index 290f9b5c6f..92c19dd839 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -438,7 +438,7 @@ void Model::reset() { } const FBXGeometry& geometry = _geometry->getFBXGeometry(); for (int i = 0; i < _jointStates.size(); i++) { - _jointStates[i].setRotationInConstrainedFrame(geometry.joints.at(i).rotation); + _jointStates[i].setRotationInConstrainedFrame(geometry.joints.at(i).rotation, 0.0f); } } @@ -695,8 +695,13 @@ bool Model::getVisibleJointState(int index, glm::quat& rotation) const { void Model::clearJointState(int index) { if (index != -1 && index < _jointStates.size()) { JointState& state = _jointStates[index]; - state.setRotationInConstrainedFrame(glm::quat()); - state._animationPriority = 0.0f; + state.setRotationInConstrainedFrame(glm::quat(), 0.0f); + } +} + +void Model::clearJointAnimationPriority(int index) { + if (index != -1 && index < _jointStates.size()) { + _jointStates[index]._animationPriority = 0.0f; } } @@ -705,8 +710,7 @@ void Model::setJointState(int index, bool valid, const glm::quat& rotation, floa JointState& state = _jointStates[index]; if (priority >= state._animationPriority) { if (valid) { - state.setRotationInConstrainedFrame(rotation); - state._animationPriority = priority; + state.setRotationInConstrainedFrame(rotation, priority); } else { state.restoreRotation(1.0f, priority); } @@ -1739,10 +1743,7 @@ void AnimationHandle::applyFrame(float frameIndex) { int mapping = _jointMappings.at(i); if (mapping != -1) { JointState& state = _model->_jointStates[mapping]; - if (_priority >= state._animationPriority) { - state.setRotationInConstrainedFrame(safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction)); - state._animationPriority = _priority; - } + state.setRotationInConstrainedFrame(safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction), _priority); } } } diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 431d17bf92..66baaac90d 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -121,6 +121,9 @@ public: /// Clear the joint states void clearJointState(int index); + /// Clear the joint animation priority + void clearJointAnimationPriority(int index); + /// Sets the joint state at the specified index. void setJointState(int index, bool valid, const glm::quat& rotation = glm::quat(), float priority = 1.0f); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 7cf37b4424..7a85fc7117 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -295,7 +295,6 @@ QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QS // filename if the directory is valid. QString path = ""; QFileInfo fileInfo = QFileInfo(directory); - qDebug() << "File: " << directory << fileInfo.isFile(); if (fileInfo.isDir()) { fileInfo.setFile(directory, "__HIFI_INVALID_FILE__"); path = fileInfo.filePath(); @@ -303,7 +302,6 @@ QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QS QFileDialog fileDialog(Application::getInstance()->getWindow(), title, path, nameFilter); fileDialog.setAcceptMode(acceptMode); - qDebug() << "Opening!"; QUrl fileUrl(directory); if (acceptMode == QFileDialog::AcceptSave) { fileDialog.setFileMode(QFileDialog::Directory); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index c4e7d6ff30..58a93fa0ae 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -282,19 +282,10 @@ void Stats::display( pingVoxel = totalPingVoxel/voxelServerCount; } - - Audio* audio = Application::getInstance()->getAudio(); - lines = _expanded ? 4 : 3; drawBackground(backgroundColor, horizontalOffset, 0, _pingStatsWidth, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; - char audioJitter[30]; - sprintf(audioJitter, - "Buffer msecs %.1f", - audio->getDesiredJitterBufferFrames() * BUFFER_SEND_INTERVAL_USECS / (float)USECS_PER_MSEC); - drawText(30, glWidget->height() - 22, scale, rotation, font, audioJitter, color); - char audioPing[30]; sprintf(audioPing, "Audio ping: %d", pingAudio); @@ -698,27 +689,6 @@ void Stats::display( drawText(horizontalOffset, verticalOffset, 0.10f, 0.f, 2.f, reflectionsStatus, color); } - - // draw local light stats - QVector localLights = Application::getInstance()->getAvatarManager().getLocalLights(); - verticalOffset = 400; - horizontalOffset = 20; - - char buffer[128]; - for (int i = 0; i < localLights.size(); i++) { - glm::vec3 lightDirection = localLights.at(i).direction; - snprintf(buffer, sizeof(buffer), "Light %d direction (%.2f, %.2f, %.2f)", i, lightDirection.x, lightDirection.y, lightDirection.z); - drawText(horizontalOffset, verticalOffset, scale, rotation, font, buffer, color); - - verticalOffset += STATS_PELS_PER_LINE; - - glm::vec3 lightColor = localLights.at(i).color; - snprintf(buffer, sizeof(buffer), "Light %d color (%.2f, %.2f, %.2f)", i, lightColor.x, lightColor.y, lightColor.z); - drawText(horizontalOffset, verticalOffset, scale, rotation, font, buffer, color); - - verticalOffset += STATS_PELS_PER_LINE; - } - } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9653999555..0e7d3228f9 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -687,7 +687,7 @@ QVector AvatarData::getJointRotations() const { if (QThread::currentThread() != thread()) { QVector result; QMetaObject::invokeMethod(const_cast(this), - "getJointRotation", Qt::BlockingQueuedConnection, + "getJointRotations", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QVector, result)); return result; } @@ -702,7 +702,7 @@ void AvatarData::setJointRotations(QVector jointRotations) { if (QThread::currentThread() != thread()) { QVector result; QMetaObject::invokeMethod(const_cast(this), - "setJointRotation", Qt::BlockingQueuedConnection, + "setJointRotations", Qt::BlockingQueuedConnection, Q_ARG(QVector, jointRotations)); } for (int i = 0; i < jointRotations.size(); ++i) { diff --git a/libraries/script-engine/src/EventTypes.cpp b/libraries/script-engine/src/EventTypes.cpp index 9cf6c5b1a0..0e6a27bc42 100644 --- a/libraries/script-engine/src/EventTypes.cpp +++ b/libraries/script-engine/src/EventTypes.cpp @@ -78,6 +78,8 @@ KeyEvent::KeyEvent(const QKeyEvent& event) { text = "LEFT"; } else if (key == Qt::Key_Right) { text = "RIGHT"; + } else if (key == Qt::Key_Space) { + text = "SPACE"; } else if (key == Qt::Key_Escape) { text = "ESC"; } else if (key == Qt::Key_Tab) { @@ -220,6 +222,8 @@ void keyEventFromScriptValue(const QScriptValue& object, KeyEvent& event) { } else if (event.text.toUpper() == "RIGHT") { event.key = Qt::Key_Right; event.isKeypad = true; + } else if (event.text.toUpper() == "SPACE") { + event.key = Qt::Key_Space; } else if (event.text.toUpper() == "ESC") { event.key = Qt::Key_Escape; } else if (event.text.toUpper() == "TAB") { diff --git a/libraries/shared/src/Ragdoll.cpp b/libraries/shared/src/Ragdoll.cpp index 70ea63930b..c0f0eb4b27 100644 --- a/libraries/shared/src/Ragdoll.cpp +++ b/libraries/shared/src/Ragdoll.cpp @@ -20,7 +20,7 @@ #include "SharedUtil.h" // for EPSILON Ragdoll::Ragdoll() : _massScale(1.0f), _translation(0.0f), _translationInSimulationFrame(0.0f), - _accumulatedMovement(0.0f), _simulation(NULL) { + _rootIndex(0), _accumulatedMovement(0.0f), _simulation(NULL) { } Ragdoll::~Ragdoll() { @@ -35,7 +35,7 @@ void Ragdoll::stepForward(float deltaTime) { updateSimulationTransforms(_translation - _simulation->getTranslation(), _rotation); } int numPoints = _points.size(); - for (int i = 0; i < numPoints; ++i) { + for (int i = _rootIndex; i < numPoints; ++i) { _points[i].integrateForward(); } } @@ -77,7 +77,9 @@ void Ragdoll::initTransform() { } void Ragdoll::setTransform(const glm::vec3& translation, const glm::quat& rotation) { - _translation = translation; + if (translation != _translation) { + _translation = translation; + } _rotation = rotation; } @@ -95,7 +97,7 @@ void Ragdoll::updateSimulationTransforms(const glm::vec3& translation, const glm // apply the deltas to all ragdollPoints int numPoints = _points.size(); - for (int i = 0; i < numPoints; ++i) { + for (int i = _rootIndex; i < numPoints; ++i) { _points[i].move(deltaPosition, deltaRotation, _translationInSimulationFrame); } @@ -111,7 +113,7 @@ void Ragdoll::setMassScale(float scale) { if (scale != _massScale) { float rescale = scale / _massScale; int numPoints = _points.size(); - for (int i = 0; i < numPoints; ++i) { + for (int i = _rootIndex; i < numPoints; ++i) { _points[i].setMass(rescale * _points[i].getMass()); } _massScale = scale; @@ -122,10 +124,10 @@ void Ragdoll::removeRootOffset(bool accumulateMovement) { const int numPoints = _points.size(); if (numPoints > 0) { // shift all points so that the root aligns with the the ragdoll's position in the simulation - glm::vec3 offset = _translationInSimulationFrame - _points[0]._position; + glm::vec3 offset = _translationInSimulationFrame - _points[_rootIndex]._position; float offsetLength = glm::length(offset); if (offsetLength > EPSILON) { - for (int i = 0; i < numPoints; ++i) { + for (int i = _rootIndex; i < numPoints; ++i) { _points[i].shift(offset); } const float MIN_ROOT_OFFSET = 0.02f; diff --git a/libraries/shared/src/Ragdoll.h b/libraries/shared/src/Ragdoll.h index 1ffbdb29ab..5234397833 100644 --- a/libraries/shared/src/Ragdoll.h +++ b/libraries/shared/src/Ragdoll.h @@ -52,6 +52,10 @@ public: void setMassScale(float scale); float getMassScale() const { return _massScale; } + // the ragdoll's rootIndex (within a Model's joints) is not always zero so must be settable + void setRootIndex(int index) { _rootIndex = index; } + int getRootIndex() const { return _rootIndex; } + void clearConstraintsAndPoints(); virtual void initPoints() = 0; virtual void buildConstraints() = 0; @@ -66,6 +70,7 @@ protected: glm::quat _rotation; // world-frame glm::vec3 _translationInSimulationFrame; glm::quat _rotationInSimulationFrame; + int _rootIndex; QVector _points; QVector _boneConstraints;