diff --git a/cmake/modules/MacOSXBundleInfo.plist.in b/cmake/modules/MacOSXBundleInfo.plist.in new file mode 100644 index 0000000000..1682b6c022 --- /dev/null +++ b/cmake/modules/MacOSXBundleInfo.plist.in @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + CFBundleURLTypes + + + CFBundleURLName + ${MACOSX_BUNDLE_BUNDLE_NAME} URL + CFBundleURLSchemes + + hifi + + + + + diff --git a/examples/cameraExample.js b/examples/cameraExample.js index d42c3c1b0e..d55f376b76 100644 --- a/examples/cameraExample.js +++ b/examples/cameraExample.js @@ -69,6 +69,12 @@ function checkCamera() { var viewJoystickPosition = Controller.getJoystickPosition(VIEW_CONTROLLER); yaw -= viewJoystickPosition.x * JOYSTICK_YAW_MAG * deltaTime; pitch += viewJoystickPosition.y * JOYSTICK_PITCH_MAG * deltaTime; + if (yaw > 360) { + yaw -= 360; + } + if (yaw < -360) { + yaw += 360; + } var orientation = Quat.fromPitchYawRoll(pitch, yaw, roll); Camera.setOrientation(orientation); } diff --git a/examples/editVoxels.js b/examples/editVoxels.js index 96bb09e6d6..c1f0c8dc49 100644 --- a/examples/editVoxels.js +++ b/examples/editVoxels.js @@ -7,9 +7,11 @@ // // Captures mouse clicks and edits voxels accordingly. // -// click = create a new voxel on this face, same color as old -// Alt + click = delete this voxel +// click = create a new voxel on this face, same color as old (default color picker state) +// right click or control + click = delete this voxel // shift + click = recolor this voxel +// 1 - 8 = pick new color from palette +// 9 = create a new voxel in front of the camera // // Click and drag to create more new voxels in the same direction // @@ -18,15 +20,41 @@ function vLength(v) { return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } +function vMinus(a, b) { + var rval = { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }; + return rval; +} + +var NEW_VOXEL_SIZE = 1.0; +var NEW_VOXEL_DISTANCE_FROM_CAMERA = 3.0; +var ORBIT_RATE_ALTITUDE = 200.0; +var ORBIT_RATE_AZIMUTH = 90.0; +var PIXELS_PER_EXTRUDE_VOXEL = 16; + +var oldMode = Camera.getMode(); + var key_alt = false; var key_shift = false; var isAdding = false; - +var isExtruding = false; +var isOrbiting = false; +var orbitAzimuth = 0.0; +var orbitAltitude = 0.0; +var orbitCenter = { x: 0, y: 0, z: 0 }; +var orbitPosition = { x: 0, y: 0, z: 0 }; +var orbitRadius = 0.0; +var extrudeDirection = { x: 0, y: 0, z: 0 }; +var extrudeScale = 0.0; var lastVoxelPosition = { x: 0, y: 0, z: 0 }; var lastVoxelColor = { red: 0, green: 0, blue: 0 }; var lastVoxelScale = 0; var dragStart = { x: 0, y: 0 }; +var mouseX = 0; +var mouseY = 0; + + + // Create a table of the different colors you can choose var colors = new Array(); colors[0] = { red: 237, green: 175, blue: 0 }; @@ -37,31 +65,70 @@ colors[4] = { red: 193, green: 99, blue: 122 }; colors[5] = { red: 255, green: 54, blue: 69 }; colors[6] = { red: 124, green: 36, blue: 36 }; colors[7] = { red: 63, green: 35, blue: 19 }; -var numColors = 6; -var whichColor = 0; +var numColors = 8; +var whichColor = -1; // Starting color is 'Copy' mode // Create sounds for adding, deleting, recoloring voxels -var addSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Electronic/ElectronicBurst1.raw"); -var deleteSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Bubbles/bubbles1.raw"); -var changeColorSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Electronic/ElectronicBurst6.raw"); +var addSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+create.raw"); +var deleteSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+delete.raw"); +var changeColorSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Voxels/voxel+edit.raw"); +var clickSound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Switches+and+sliders/toggle+switch+-+medium.raw"); var audioOptions = new AudioInjectionOptions();
 +audioOptions.volume = 0.5; + +function setAudioPosition() { + var camera = Camera.getPosition(); + var forwardVector = Quat.getFront(MyAvatar.orientation); + audioOptions.position = Vec3.sum(camera, forwardVector); +} + +function getNewVoxelPosition() { + var camera = Camera.getPosition(); + var forwardVector = Quat.getFront(MyAvatar.orientation); + var newPosition = Vec3.sum(camera, Vec3.multiply(forwardVector, NEW_VOXEL_DISTANCE_FROM_CAMERA)); + return newPosition; +} + +function fixEulerAngles(eulers) { + var rVal = { x: 0, y: 0, z: eulers.z }; + if (eulers.x >= 90.0) { + rVal.x = 180.0 - eulers.x; + rVal.y = eulers.y - 180.0; + } else if (eulers.x <= -90.0) { + rVal.x = 180.0 - eulers.x; + rVal.y = eulers.y - 180.0; + } + return rVal; +} function mousePressEvent(event) { + mouseX = event.x; + mouseY = event.y; var pickRay = Camera.computePickRay(event.x, event.y); var intersection = Voxels.findRayIntersection(pickRay); - audioOptions.volume = 1.0; - audioOptions.position = { x: intersection.voxel.x, y: intersection.voxel.y, z: intersection.voxel.z }; - + audioOptions.position = Vec3.sum(pickRay.origin, pickRay.direction); if (intersection.intersects) { - if (key_alt) { + if (event.isAlt) { + // start orbit camera! + var cameraPosition = Camera.getPosition(); + oldMode = Camera.getMode(); + Camera.setMode("independent"); + isOrbiting = true; + Camera.keepLookingAt(intersection.intersection); + // get position for initial azimuth, elevation + orbitCenter = intersection.intersection; + var orbitVector = Vec3.subtract(cameraPosition, orbitCenter); + orbitRadius = vLength(orbitVector); + orbitAzimuth = Math.atan2(orbitVector.z, orbitVector.x); + orbitAltitude = Math.asin(orbitVector.y / Vec3.length(orbitVector)); + + } else if (event.isRightButton || event.isControl) { // Delete voxel Voxels.eraseVoxel(intersection.voxel.x, intersection.voxel.y, intersection.voxel.z, intersection.voxel.s); Audio.playSound(deleteSound, audioOptions); - } else if (key_shift) { + } else if (event.isShifted) { // Recolor Voxel - whichColor++; - if (whichColor == numColors) whichColor = 0; Voxels.setVoxel(intersection.voxel.x, intersection.voxel.y, intersection.voxel.z, @@ -70,7 +137,9 @@ function mousePressEvent(event) { Audio.playSound(changeColorSound, audioOptions); } else { // Add voxel on face - var newVoxel = { + if (whichColor == -1) { + // Copy mode - use clicked voxel color + var newVoxel = { x: intersection.voxel.x, y: intersection.voxel.y, z: intersection.voxel.z, @@ -78,6 +147,16 @@ function mousePressEvent(event) { red: intersection.voxel.red, green: intersection.voxel.green, blue: intersection.voxel.blue }; + } else { + var newVoxel = { + x: intersection.voxel.x, + y: intersection.voxel.y, + z: intersection.voxel.z, + s: intersection.voxel.s, + red: colors[whichColor].red, + green: colors[whichColor].green, + blue: colors[whichColor].blue }; + } if (intersection.face == "MIN_X_FACE") { newVoxel.x -= newVoxel.s; @@ -108,55 +187,113 @@ function mousePressEvent(event) { function keyPressEvent(event) { key_alt = event.isAlt; key_shift = event.isShifted; + var nVal = parseInt(event.text); + if (event.text == "0") { + print("Color = Copy"); + whichColor = -1; + Audio.playSound(clickSound, audioOptions); + } else if ((nVal > 0) && (nVal <= numColors)) { + whichColor = nVal - 1; + print("Color = " + (whichColor + 1)); + Audio.playSound(clickSound, audioOptions); + } else if (event.text == "9") { + // Create a brand new 1 meter voxel in front of your avatar + var color = whichColor; + if (color == -1) color = 0; + var newPosition = getNewVoxelPosition(); + var newVoxel = { + x: newPosition.x, + y: newPosition.y , + z: newPosition.z, + s: NEW_VOXEL_SIZE, + red: colors[color].red, + green: colors[color].green, + blue: colors[color].blue }; + Voxels.setVoxel(newVoxel.x, newVoxel.y, newVoxel.z, newVoxel.s, newVoxel.red, newVoxel.green, newVoxel.blue); + setAudioPosition(); + Audio.playSound(addSound, audioOptions); + } else if (event.text == " ") { + // Reset my orientation! + var orientation = { x:0, y:0, z:0, w:1 }; + Camera.setOrientation(orientation); + MyAvatar.orientation = orientation; + } } + function keyReleaseEvent(event) { key_alt = false; key_shift = false; } function mouseMoveEvent(event) { + if (isOrbiting) { + var cameraOrientation = Camera.getOrientation(); + var origEulers = Quat.safeEulerAngles(cameraOrientation); + var newEulers = fixEulerAngles(Quat.safeEulerAngles(cameraOrientation)); + var dx = event.x - mouseX; + var dy = event.y - mouseY; + orbitAzimuth += dx / ORBIT_RATE_AZIMUTH; + orbitAltitude += dy / ORBIT_RATE_ALTITUDE; + var orbitVector = { x:(Math.cos(orbitAltitude) * Math.cos(orbitAzimuth)) * orbitRadius, + y:Math.sin(orbitAltitude) * orbitRadius, + z:(Math.cos(orbitAltitude) * Math.sin(orbitAzimuth)) * orbitRadius }; + orbitPosition = Vec3.sum(orbitCenter, orbitVector); + Camera.setPosition(orbitPosition); + mouseX = event.x; + mouseY = event.y; + } if (isAdding) { - var pickRay = Camera.computePickRay(event.x, event.y); - var lastVoxelDistance = { x: pickRay.origin.x - lastVoxelPosition.x, + // Watch the drag direction to tell which way to 'extrude' this voxel + if (!isExtruding) { + var pickRay = Camera.computePickRay(event.x, event.y); + var lastVoxelDistance = { x: pickRay.origin.x - lastVoxelPosition.x, y: pickRay.origin.y - lastVoxelPosition.y, z: pickRay.origin.z - lastVoxelPosition.z }; - var distance = vLength(lastVoxelDistance); - var mouseSpot = { x: pickRay.direction.x * distance, y: pickRay.direction.y * distance, z: pickRay.direction.z * distance }; - mouseSpot.x += pickRay.origin.x; - mouseSpot.y += pickRay.origin.y; - mouseSpot.z += pickRay.origin.z; - var dx = mouseSpot.x - lastVoxelPosition.x; - var dy = mouseSpot.y - lastVoxelPosition.y; - var dz = mouseSpot.z - lastVoxelPosition.z; - if (dx > lastVoxelScale) { - lastVoxelPosition.x += lastVoxelScale; - Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, - lastVoxelScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); - } else if (dx < -lastVoxelScale) { - lastVoxelPosition.x -= lastVoxelScale; - Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, - lastVoxelScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); - } else if (dy > lastVoxelScale) { - lastVoxelPosition.y += lastVoxelScale; - Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, - lastVoxelScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); - } else if (dy < -lastVoxelScale) { - lastVoxelPosition.y -= lastVoxelScale; - Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, - lastVoxelScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); - } else if (dz > lastVoxelScale) { - lastVoxelPosition.z += lastVoxelScale; - Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, - lastVoxelScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); - } else if (dz < -lastVoxelScale) { - lastVoxelPosition.z -= lastVoxelScale; - Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, - lastVoxelScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); + var distance = vLength(lastVoxelDistance); + var mouseSpot = { x: pickRay.direction.x * distance, y: pickRay.direction.y * distance, z: pickRay.direction.z * distance }; + mouseSpot.x += pickRay.origin.x; + mouseSpot.y += pickRay.origin.y; + mouseSpot.z += pickRay.origin.z; + var dx = mouseSpot.x - lastVoxelPosition.x; + var dy = mouseSpot.y - lastVoxelPosition.y; + var dz = mouseSpot.z - lastVoxelPosition.z; + extrudeScale = lastVoxelScale; + extrudeDirection = { x: 0, y: 0, z: 0 }; + isExtruding = true; + if (dx > lastVoxelScale) extrudeDirection.x = extrudeScale; + else if (dx < -lastVoxelScale) extrudeDirection.x = -extrudeScale; + else if (dy > lastVoxelScale) extrudeDirection.y = extrudeScale; + else if (dy < -lastVoxelScale) extrudeDirection.y = -extrudeScale; + else if (dz > lastVoxelScale) extrudeDirection.z = extrudeScale; + else if (dz < -lastVoxelScale) extrudeDirection.z = -extrudeScale; + else isExtruding = false; + } else { + // We have got an extrusion direction, now look for mouse move beyond threshold to add new voxel + var dx = event.x - mouseX; + var dy = event.y - mouseY; + if (Math.sqrt(dx*dx + dy*dy) > PIXELS_PER_EXTRUDE_VOXEL) { + lastVoxelPosition = Vec3.sum(lastVoxelPosition, extrudeDirection); + Voxels.setVoxel(lastVoxelPosition.x, lastVoxelPosition.y, lastVoxelPosition.z, + extrudeScale, lastVoxelColor.red, lastVoxelColor.green, lastVoxelColor.blue); + mouseX = event.x; + mouseY = event.y; + } } } } function mouseReleaseEvent(event) { + if (isOrbiting) { + var cameraOrientation = Camera.getOrientation(); + var eulers = Quat.safeEulerAngles(cameraOrientation); + MyAvatar.position = Camera.getPosition(); + MyAvatar.orientation = cameraOrientation; + Camera.stopLooking(); + Camera.setMode(oldMode); + Camera.setOrientation(cameraOrientation); + } isAdding = false; + isOrbiting = false; + isExtruding = false; } Controller.mousePressEvent.connect(mousePressEvent); diff --git a/examples/hydraMove.js b/examples/hydraMove.js new file mode 100644 index 0000000000..6ce0ad884e --- /dev/null +++ b/examples/hydraMove.js @@ -0,0 +1,210 @@ +// +// hydraMove.js +// hifi +// +// Created by Brad Hefta-Gaub on 2/10/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// This is an example script that demonstrates use of the Controller and MyAvatar classes to implement +// avatar flying through the hydra/controller joysticks +// +// + +var damping = 0.9; +var position = { x: MyAvatar.position.x, y: MyAvatar.position.y, z: MyAvatar.position.z }; +var joysticksCaptured = false; +var THRUST_CONTROLLER = 0; +var VIEW_CONTROLLER = 1; +var INITIAL_THRUST_MULTPLIER = 1.0; +var THRUST_INCREASE_RATE = 1.05; +var MAX_THRUST_MULTIPLIER = 75.0; +var thrustMultiplier = INITIAL_THRUST_MULTPLIER; +var grabDelta = { x: 0, y: 0, z: 0}; +var grabDeltaVelocity = { x: 0, y: 0, z: 0}; +var grabStartRotation = { x: 0, y: 0, z: 0, w: 1}; +var grabCurrentRotation = { x: 0, y: 0, z: 0, w: 1}; +var grabbingWithRightHand = false; +var wasGrabbingWithRightHand = false; +var grabbingWithLeftHand = false; +var wasGrabbingWithLeftHand = false; +var EPSILON = 0.000001; +var velocity = { x: 0, y: 0, z: 0}; +var deltaTime = 1/60; // approximately our FPS - maybe better to be elapsed time since last call +var THRUST_MAG_UP = 800.0; +var THRUST_MAG_DOWN = 300.0; +var THRUST_MAG_FWD = 500.0; +var THRUST_MAG_BACK = 300.0; +var THRUST_MAG_LATERAL = 250.0; +var THRUST_JUMP = 120.0; + +var YAW_MAG = 500.0; +var PITCH_MAG = 100.0; +var THRUST_MAG_HAND_JETS = THRUST_MAG_FWD; +var JOYSTICK_YAW_MAG = YAW_MAG; +var JOYSTICK_PITCH_MAG = PITCH_MAG * 0.5; + + +var LEFT_PALM = 0; +var LEFT_BUTTON_4 = 4; +var RIGHT_PALM = 2; +var RIGHT_BUTTON_4 = 10; + +// Used by handleGrabBehavior() for managing the grab position changes +function getAndResetGrabDelta() { + var HAND_GRAB_SCALE_DISTANCE = 2.0; + var delta = Vec3.multiply(grabDelta, (MyAvatar.scale * HAND_GRAB_SCALE_DISTANCE)); + grabDelta = { x: 0, y: 0, z: 0}; + var avatarRotation = MyAvatar.orientation; + var result = Vec3.multiplyQbyV(avatarRotation, Vec3.multiply(delta, -1)); + return result; +} + +// Used by handleGrabBehavior() for managing the grab velocity feature +function getAndResetGrabDeltaVelocity() { + var HAND_GRAB_SCALE_VELOCITY = 50.0; + var delta = Vec3.multiply(grabDeltaVelocity, (MyAvatar.scale * HAND_GRAB_SCALE_VELOCITY)); + grabDeltaVelocity = { x: 0, y: 0, z: 0}; + var avatarRotation = MyAvatar.orientation; + var result = Quat.multiply(avatarRotation, Vec3.multiply(delta, -1)); + return result; +} + +// Used by handleGrabBehavior() for managing the grab rotation feature +function getAndResetGrabRotation() { + var quatDiff = Quat.multiply(grabCurrentRotation, Quat.inverse(grabStartRotation)); + grabStartRotation = grabCurrentRotation; + return quatDiff; +} + +// handles all the grab related behavior: position (crawl), velocity (flick), and rotate (twist) +function handleGrabBehavior() { + // check for and handle grab behaviors + grabbingWithRightHand = Controller.isButtonPressed(RIGHT_BUTTON_4); + grabbingWithLeftHand = Controller.isButtonPressed(LEFT_BUTTON_4); + stoppedGrabbingWithLeftHand = false; + stoppedGrabbingWithRightHand = false; + + if (grabbingWithRightHand && !wasGrabbingWithRightHand) { + // Just starting grab, capture starting rotation + grabStartRotation = Controller.getSpatialControlRawRotation(RIGHT_PALM); + } + if (grabbingWithRightHand) { + grabDelta = Vec3.sum(grabDelta, Vec3.multiply(Controller.getSpatialControlVelocity(RIGHT_PALM), deltaTime)); + grabCurrentRotation = Controller.getSpatialControlRawRotation(RIGHT_PALM); + } + if (!grabbingWithRightHand && wasGrabbingWithRightHand) { + // Just ending grab, capture velocity + grabDeltaVelocity = Controller.getSpatialControlVelocity(RIGHT_PALM); + stoppedGrabbingWithRightHand = true; + } + + if (grabbingWithLeftHand && !wasGrabbingWithLeftHand) { + // Just starting grab, capture starting rotation + grabStartRotation = Controller.getSpatialControlRawRotation(LEFT_PALM); + } + + if (grabbingWithLeftHand) { + grabDelta = Vec3.sum(grabDelta, Vec3.multiply(Controller.getSpatialControlVelocity(LEFT_PALM), deltaTime)); + grabCurrentRotation = Controller.getSpatialControlRawRotation(LEFT_PALM); + } + if (!grabbingWithLeftHand && wasGrabbingWithLeftHand) { + // Just ending grab, capture velocity + grabDeltaVelocity = Controller.getSpatialControlVelocity(LEFT_PALM); + stoppedGrabbingWithLeftHand = true; + } + + grabbing = grabbingWithRightHand || grabbingWithLeftHand; + stoppedGrabbing = stoppedGrabbingWithRightHand || stoppedGrabbingWithLeftHand; + + if (grabbing) { + + // move position + var moveFromGrab = getAndResetGrabDelta(); + if (Vec3.length(moveFromGrab) > EPSILON) { + MyAvatar.position = Vec3.sum(MyAvatar.position, moveFromGrab); + velocity = { x: 0, y: 0, z: 0}; + } + + // add some rotation... + var deltaRotation = getAndResetGrabRotation(); + var GRAB_CONTROLLER_TURN_SCALING = 0.5; + var euler = Vec3.multiply(Quat.safeEulerAngles(deltaRotation), GRAB_CONTROLLER_TURN_SCALING); + + // Adjust body yaw by yaw from controller + var orientation = Quat.multiply(Quat.angleAxis(-euler.y, {x:0, y: 1, z:0}), MyAvatar.orientation); + MyAvatar.orientation = orientation; + + // Adjust head pitch from controller + MyAvatar.headPitch = MyAvatar.headPitch - euler.x; + } + + // add some velocity... + if (stoppedGrabbing) { + velocity = Vec3.sum(velocity, getAndResetGrabDeltaVelocity()); + } + + // handle residual velocity + if(Vec3.length(velocity) > EPSILON) { + MyAvatar.position = Vec3.sum(MyAvatar.position, Vec3.multiply(velocity, deltaTime)); + // damp velocity + velocity = Vec3.multiply(velocity, damping); + } + + + wasGrabbingWithRightHand = grabbingWithRightHand; + wasGrabbingWithLeftHand = grabbingWithLeftHand; +} + +// Main update function that handles flying and grabbing behaviort +function flyWithHydra() { + var thrustJoystickPosition = Controller.getJoystickPosition(THRUST_CONTROLLER); + + if (thrustJoystickPosition.x != 0 || thrustJoystickPosition.y != 0) { + if (thrustMultiplier < MAX_THRUST_MULTIPLIER) { + thrustMultiplier *= 1 + (deltaTime * THRUST_INCREASE_RATE); + } + var currentOrientation = MyAvatar.orientation; + + var front = Quat.getFront(currentOrientation); + var right = Quat.getRight(currentOrientation); + var up = Quat.getUp(currentOrientation); + + var thrustFront = Vec3.multiply(front, MyAvatar.scale * THRUST_MAG_HAND_JETS * + thrustJoystickPosition.y * thrustMultiplier * deltaTime); + MyAvatar.addThrust(thrustFront); + var thrustRight = Vec3.multiply(right, MyAvatar.scale * THRUST_MAG_HAND_JETS * + thrustJoystickPosition.x * thrustMultiplier * deltaTime); + MyAvatar.addThrust(thrustRight); + } else { + thrustMultiplier = INITIAL_THRUST_MULTPLIER; + } + + // View Controller + var viewJoystickPosition = Controller.getJoystickPosition(VIEW_CONTROLLER); + if (viewJoystickPosition.x != 0 || viewJoystickPosition.y != 0) { + + // change the body yaw based on our x controller + var orientation = MyAvatar.orientation; + var deltaOrientation = Quat.fromPitchYawRoll(0, (-1 * viewJoystickPosition.x * JOYSTICK_YAW_MAG * deltaTime), 0); + MyAvatar.orientation = Quat.multiply(orientation, deltaOrientation); + + // change the headPitch based on our x controller + //pitch += viewJoystickPosition.y * JOYSTICK_PITCH_MAG * deltaTime; + var newPitch = MyAvatar.headPitch + (viewJoystickPosition.y * JOYSTICK_PITCH_MAG * deltaTime); + MyAvatar.headPitch = newPitch; + } + handleGrabBehavior(); +} + +Script.willSendVisualDataCallback.connect(flyWithHydra); +Controller.captureJoystick(THRUST_CONTROLLER); +Controller.captureJoystick(VIEW_CONTROLLER); + +// Map keyPress and mouse move events to our callbacks +function scriptEnding() { + // re-enabled the standard application for touch events + Controller.releaseJoystick(THRUST_CONTROLLER); + Controller.releaseJoystick(VIEW_CONTROLLER); +} +Script.scriptEnding.connect(scriptEnding); + diff --git a/examples/overlaysExample.js b/examples/overlaysExample.js new file mode 100644 index 0000000000..b57f82e7e9 --- /dev/null +++ b/examples/overlaysExample.js @@ -0,0 +1,279 @@ +// +// overlaysExample.js +// hifi +// +// Created by Brad Hefta-Gaub on 2/14/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// This is an example script that demonstrates use of the Overlays class +// +// + + +// The "Swatches" example of this script will create 9 different image overlays, that use the color feature to +// display different colors as color swatches. The overlays can be clicked on, to change the "selectedSwatch" variable +// and update the image used for the overlay so that it appears to have a selected indicator. +// These are our colors... +var swatchColors = new Array(); +swatchColors[0] = { red: 255, green: 0, blue: 0}; +swatchColors[1] = { red: 0, green: 255, blue: 0}; +swatchColors[2] = { red: 0, green: 0, blue: 255}; +swatchColors[3] = { red: 255, green: 255, blue: 0}; +swatchColors[4] = { red: 255, green: 0, blue: 255}; +swatchColors[5] = { red: 0, green: 255, blue: 255}; +swatchColors[6] = { red: 128, green: 128, blue: 128}; +swatchColors[7] = { red: 128, green: 0, blue: 0}; +swatchColors[8] = { red: 0, green: 240, blue: 240}; + +// The location of the placement of these overlays +var swatchesX = 100; +var swatchesY = 200; + +// These will be our "overlay IDs" +var swatches = new Array(); +var numberOfSwatches = 9; +var selectedSwatch = 0; + +// create the overlays, position them in a row, set their colors, and for the selected one, use a different source image +// location so that it displays the "selected" marker +for (s = 0; s < numberOfSwatches; s++) { + var imageFromX = 12 + (s * 27); + var imageFromY = 0; + if (s == selectedSwatch) { + imageFromY = 55; + } + + swatches[s] = Overlays.addOverlay("image", { + x: 100 + (30 * s), + y: 200, + width: 31, + height: 54, + subImage: { x: imageFromX, y: imageFromY, width: 30, height: 54 }, + imageURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/images/testing-swatches.svg", + color: swatchColors[s], + alpha: 1 + }); +} + +// This will create a text overlay that when you click on it, the text will change +var text = Overlays.addOverlay("text", { + x: 200, + y: 100, + width: 150, + height: 50, + color: { red: 0, green: 0, blue: 0}, + textColor: { red: 255, green: 0, blue: 0}, + topMargin: 4, + leftMargin: 4, + text: "Here is some text.\nAnd a second line." + }); + +// This will create an image overlay, which starts out as invisible +var toolA = Overlays.addOverlay("image", { + x: 100, + y: 100, + width: 62, + height: 40, + subImage: { x: 0, y: 0, width: 62, height: 40 }, + imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/hifi-interface-tools.svg", + color: { red: 255, green: 255, blue: 255}, + visible: false + }); + +// This will create a couple of image overlays that make a "slider", we will demonstrate how to trap mouse messages to +// move the slider +var slider = Overlays.addOverlay("image", { + // alternate form of expressing bounds + bounds: { x: 100, y: 300, width: 158, height: 35}, + imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/slider.png", + color: { red: 255, green: 255, blue: 255}, + alpha: 1 + }); + +// This is the thumb of our slider +var minThumbX = 130; +var maxThumbX = minThumbX + 65; +var thumbX = (minThumbX + maxThumbX) / 2; +var thumb = Overlays.addOverlay("image", { + x: thumbX, + y: 309, + width: 18, + height: 17, + imageURL: "https://s3-us-west-1.amazonaws.com/highfidelity-public/images/thumb.png", + color: { red: 255, green: 255, blue: 255}, + alpha: 1 + }); + + +// We will also demonstrate some 3D overlays. We will create a couple of cubes, spheres, and lines +// our 3D cube that moves around... +var cubePosition = { x: 2, y: 0, z: 2 }; +var cubeSize = 5; +var cubeMove = 0.1; +var minCubeX = 1; +var maxCubeX = 20; + +var cube = Overlays.addOverlay("cube", { + position: cubePosition, + size: cubeSize, + color: { red: 255, green: 0, blue: 0}, + alpha: 1, + solid: false + }); + +var solidCubePosition = { x: 0, y: 5, z: 0 }; +var solidCubeSize = 2; +var minSolidCubeX = 0; +var maxSolidCubeX = 10; +var solidCubeMove = 0.05; +var solidCube = Overlays.addOverlay("cube", { + position: solidCubePosition, + size: solidCubeSize, + color: { red: 0, green: 255, blue: 0}, + alpha: 1, + solid: true + }); + +var spherePosition = { x: 5, y: 5, z: 5 }; +var sphereSize = 1; +var minSphereSize = 0.5; +var maxSphereSize = 10; +var sphereSizeChange = 0.05; + +var sphere = Overlays.addOverlay("sphere", { + position: spherePosition, + size: sphereSize, + color: { red: 0, green: 0, blue: 255}, + alpha: 1, + solid: false + }); + +var line3d = Overlays.addOverlay("line3d", { + position: { x: 0, y: 0, z:0 }, + end: { x: 10, y: 10, z:10 }, + color: { red: 0, green: 255, blue: 255}, + alpha: 1, + lineWidth: 5 + }); + + +// When our script shuts down, we should clean up all of our overlays +function scriptEnding() { + Overlays.deleteOverlay(toolA); + for (s = 0; s < numberOfSwatches; s++) { + Overlays.deleteOverlay(swatches[s]); + } + Overlays.deleteOverlay(thumb); + Overlays.deleteOverlay(slider); + Overlays.deleteOverlay(text); + Overlays.deleteOverlay(cube); + Overlays.deleteOverlay(solidCube); + Overlays.deleteOverlay(sphere); + Overlays.deleteOverlay(line3d); +} +Script.scriptEnding.connect(scriptEnding); + + +var toolAVisible = false; +var count = 0; + +// Our update() function is called at approximately 60fps, and we will use it to animate our various overlays +function update() { + count++; + + // every second or so, toggle the visibility our our blinking tool + if (count % 60 == 0) { + if (toolAVisible) { + toolAVisible = false; + } else { + toolAVisible = true; + } + Overlays.editOverlay(toolA, { visible: toolAVisible } ); + } + + // move our 3D cube + cubePosition.x += cubeMove; + cubePosition.z += cubeMove; + if (cubePosition.x > maxCubeX || cubePosition.x < minCubeX) { + cubeMove = cubeMove * -1; + } + Overlays.editOverlay(cube, { position: cubePosition } ); + + // move our solid 3D cube + solidCubePosition.x += solidCubeMove; + solidCubePosition.z += solidCubeMove; + if (solidCubePosition.x > maxSolidCubeX || solidCubePosition.x < minSolidCubeX) { + solidCubeMove = solidCubeMove * -1; + } + Overlays.editOverlay(solidCube, { position: solidCubePosition } ); + + // adjust our 3D sphere + sphereSize += sphereSizeChange; + if (sphereSize > maxSphereSize || sphereSize < minSphereSize) { + sphereSizeChange = sphereSizeChange * -1; + } + Overlays.editOverlay(sphere, { size: sphereSize, solid: (sphereSizeChange < 0) } ); + + + // update our 3D line to go from origin to our avatar's position + Overlays.editOverlay(line3d, { end: MyAvatar.position } ); +} +Script.willSendVisualDataCallback.connect(update); + + +// The slider is handled in the mouse event callbacks. +var movingSlider = false; +var thumbClickOffsetX = 0; +function mouseMoveEvent(event) { + if (movingSlider) { + newThumbX = event.x - thumbClickOffsetX; + if (newThumbX < minThumbX) { + newThumbX = minThumbX; + } + if (newThumbX > maxThumbX) { + newThumbX = maxThumbX; + } + Overlays.editOverlay(thumb, { x: newThumbX } ); + } +} + +// we also handle click detection in our mousePressEvent() +function mousePressEvent(event) { + var clickedText = false; + var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y}); + + // If the user clicked on the thumb, handle the slider logic + if (clickedOverlay == thumb) { + movingSlider = true; + thumbClickOffsetX = event.x - thumbX; + + } else if (clickedOverlay == text) { // if the user clicked on the text, update text with where you clicked + + Overlays.editOverlay(text, { text: "you clicked here:\n " + event.x + "," + event.y } ); + clickedText = true; + + } else { // if the user clicked on one of the color swatches, update the selectedSwatch + + for (s = 0; s < numberOfSwatches; s++) { + if (clickedOverlay == swatches[s]) { + Overlays.editOverlay(swatches[selectedSwatch], { subImage: { y: 0 } } ); + Overlays.editOverlay(swatches[s], { subImage: { y: 55 } } ); + selectedSwatch = s; + } + } + } + if (!clickedText) { // if you didn't click on the text, then update the text accordningly + Overlays.editOverlay(text, { text: "you didn't click here" } ); + } +} + +function mouseReleaseEvent(event) { + if (movingSlider) { + movingSlider = false; + } +} + +Controller.mouseMoveEvent.connect(mouseMoveEvent); +Controller.mousePressEvent.connect(mousePressEvent); +Controller.mouseReleaseEvent.connect(mouseReleaseEvent); + diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 6af6ed478d..8e96006828 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -90,7 +90,13 @@ qt5_wrap_ui(QT_UI_HEADERS ${QT_UI_FILES}) set(INTERFACE_SRCS ${INTERFACE_SRCS} ${QT_UI_HEADERS}) if (APPLE) + + # configure CMake to use a custom Info.plist + SET_TARGET_PROPERTIES( ${this_target} PROPERTIES MACOSX_BUNDLE_INFO_PLIST MacOSXBundleInfo.plist.in ) + set(MACOSX_BUNDLE_BUNDLE_NAME Interface) + set(MACOSX_BUNDLE_GUI_IDENTIFIER io.highfidelity.Interface) + # set how the icon shows up in the Info.plist file SET(MACOSX_BUNDLE_ICON_FILE interface.icns) diff --git a/interface/resources/meshes/defaultAvatar_body.fbx b/interface/resources/meshes/defaultAvatar/body.fbx similarity index 100% rename from interface/resources/meshes/defaultAvatar_body.fbx rename to interface/resources/meshes/defaultAvatar/body.fbx diff --git a/interface/resources/meshes/body.jpg b/interface/resources/meshes/defaultAvatar/body.jpg similarity index 100% rename from interface/resources/meshes/body.jpg rename to interface/resources/meshes/defaultAvatar/body.jpg diff --git a/interface/resources/meshes/defaultAvatar_head.fbx b/interface/resources/meshes/defaultAvatar/head.fbx similarity index 100% rename from interface/resources/meshes/defaultAvatar_head.fbx rename to interface/resources/meshes/defaultAvatar/head.fbx diff --git a/interface/resources/meshes/tail.jpg b/interface/resources/meshes/defaultAvatar/tail.jpg similarity index 100% rename from interface/resources/meshes/tail.jpg rename to interface/resources/meshes/defaultAvatar/tail.jpg diff --git a/interface/resources/meshes/visor.png b/interface/resources/meshes/defaultAvatar/visor.png similarity index 100% rename from interface/resources/meshes/visor.png rename to interface/resources/meshes/defaultAvatar/visor.png diff --git a/interface/resources/meshes/defaultAvatar_body.fst b/interface/resources/meshes/defaultAvatar_body.fst index 3e8fa3ef45..5874c206db 100644 --- a/interface/resources/meshes/defaultAvatar_body.fst +++ b/interface/resources/meshes/defaultAvatar_body.fst @@ -1,3 +1,5 @@ +filename=defaultAvatar/body.fbx +texdir=defaultAvatar scale=130 joint = jointRoot = jointRoot joint = jointLean = jointSpine diff --git a/interface/resources/meshes/defaultAvatar_head.fst b/interface/resources/meshes/defaultAvatar_head.fst index 1352652efc..34cf44f0e4 100644 --- a/interface/resources/meshes/defaultAvatar_head.fst +++ b/interface/resources/meshes/defaultAvatar_head.fst @@ -1,7 +1,7 @@ # faceshift target mapping file name= defaultAvatar_head -filename=../../../Avatars/Jelly/jellyrob_blue.fbx -texdir=../../../Avatars/Jelly +filename=defaultAvatar/head.fbx +texdir=defaultAvatar scale=80 rx=0 ry=0 diff --git a/interface/resources/shaders/perlin_modulate.frag b/interface/resources/shaders/perlin_modulate.frag index eea0da3671..8ead57c238 100644 --- a/interface/resources/shaders/perlin_modulate.frag +++ b/interface/resources/shaders/perlin_modulate.frag @@ -12,7 +12,7 @@ uniform sampler2D permutationNormalTexture; // the noise frequency -const float frequency = 1024.0; +const float frequency = 65536.0; // looks better with current TREE_SCALE, was 1024 when TREE_SCALE was either 512 or 128 // the noise amplitude const float amplitude = 0.1; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c922120e48..6dd6d8b61f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,6 +101,8 @@ const QString SKIP_FILENAME = QStandardPaths::writableLocation(QStandardPaths::D const int STATS_PELS_PER_LINE = 20; +const QString CUSTOM_URL_SCHEME = "hifi:"; + void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { if (message.size() > 0) { QString messageWithNewLine = message + "\n"; @@ -289,6 +291,9 @@ Application::Application(int& argc, char** argv, timeval &startup_time) : _sixenseManager.setFilter(Menu::getInstance()->isOptionChecked(MenuOption::FilterSixense)); checkVersion(); + + _overlays.init(_glWidget); // do this before scripts load + // do this as late as possible so that all required subsystems are inialized loadScripts(); @@ -679,6 +684,38 @@ void Application::controlledBroadcastToNodes(const QByteArray& packet, const Nod } } +bool Application::event(QEvent* event) { + + // handle custom URL + if (event->type() == QEvent::FileOpen) { + QFileOpenEvent* fileEvent = static_cast(event); + if (!fileEvent->url().isEmpty() && fileEvent->url().toLocalFile().startsWith(CUSTOM_URL_SCHEME)) { + QString destination = fileEvent->url().toLocalFile().remove(CUSTOM_URL_SCHEME); + QStringList urlParts = destination.split('/', QString::SkipEmptyParts); + + if (urlParts.count() > 1) { + // if url has 2 or more parts, the first one is domain name + Menu::getInstance()->goToDomain(urlParts[0]); + + // location coordinates + Menu::getInstance()->goToDestination(urlParts[1]); + if (urlParts.count() > 2) { + + // location orientation + Menu::getInstance()->goToOrientation(urlParts[2]); + } + } else if (urlParts.count() == 1) { + + // location coordinates + Menu::getInstance()->goToDestination(urlParts[0]); + } + } + + return false; + } + return QApplication::event(event); +} + void Application::keyPressEvent(QKeyEvent* event) { _controllerScriptingInterface.emitKeyPressEvent(event); // send events to any registered scripts @@ -1875,6 +1912,8 @@ void Application::init() { connect(_rearMirrorTools, SIGNAL(restoreView()), SLOT(restoreMirrorView())); connect(_rearMirrorTools, SIGNAL(shrinkView()), SLOT(shrinkMirrorView())); connect(_rearMirrorTools, SIGNAL(resetView()), SLOT(resetSensors())); + + } void Application::closeMirrorView() { @@ -2146,15 +2185,6 @@ void Application::updateThreads(float deltaTime) { } } -void Application::updateParticles(float deltaTime) { - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateParticles()"); - - if (Menu::getInstance()->isOptionChecked(MenuOption::ParticleCloud)) { - _cloud.simulate(deltaTime); - } -} - void Application::updateMetavoxels(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateMetavoxels()"); @@ -2276,7 +2306,6 @@ void Application::update(float deltaTime) { updateMyAvatar(deltaTime); // Sample hardware, update view frustum if needed, and send avatar data to mixer/nodes updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... _avatarManager.updateOtherAvatars(deltaTime); //loop through all the other avatars and simulate them... - updateParticles(deltaTime); // Simulate particle cloud movements updateMetavoxels(deltaTime); // update metavoxels updateCamera(deltaTime); // handle various camera tweaks like off axis projection updateDialogs(deltaTime); // update various stats dialogs if present @@ -2711,16 +2740,18 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { // disable specular lighting for ground and voxels glMaterialfv(GL_FRONT, GL_SPECULAR, NO_SPECULAR_COLOR); - // Draw Cloud Particles - if (Menu::getInstance()->isOptionChecked(MenuOption::ParticleCloud)) { - _cloud.render(); - } // Draw voxels if (Menu::getInstance()->isOptionChecked(MenuOption::Voxels)) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "Application::displaySide() ... voxels..."); if (!Menu::getInstance()->isOptionChecked(MenuOption::DontRenderVoxels)) { - _voxels.render(Menu::getInstance()->isOptionChecked(MenuOption::VoxelTextures)); + _voxels.render(); + + // double check that our LOD doesn't need to be auto-adjusted + // only adjust if our option is set + if (Menu::getInstance()->isOptionChecked(MenuOption::AutoAdjustLOD)) { + Menu::getInstance()->autoAdjustLOD(_fps); + } } } @@ -2811,7 +2842,7 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { _mouseVoxel.s, _mouseVoxel.s); - _sharedVoxelSystem.render(true); + _sharedVoxelSystem.render(); glPopMatrix(); } } @@ -2851,6 +2882,9 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) { // give external parties a change to hook in emit renderingInWorldInterface(); + + // render JS/scriptable overlays + _overlays.render3D(); } } @@ -2997,6 +3031,8 @@ void Application::displayOverlay() { _pieMenu.render(); } + _overlays.render2D(); + glPopMatrix(); } @@ -3998,6 +4034,32 @@ void Application::saveScripts() { settings->endArray(); } +void Application::stopAllScripts() { + // stops all current running scripts + QList scriptActions = Menu::getInstance()->getActiveScriptsMenu()->actions(); + foreach (QAction* scriptAction, scriptActions) { + scriptAction->activate(QAction::Trigger); + qDebug() << "stopping script..." << scriptAction->text(); + } + _activeScripts.clear(); +} + +void Application::reloadAllScripts() { + // remember all the current scripts so we can reload them + QStringList reloadList = _activeScripts; + // reloads all current running scripts + QList scriptActions = Menu::getInstance()->getActiveScriptsMenu()->actions(); + foreach (QAction* scriptAction, scriptActions) { + scriptAction->activate(QAction::Trigger); + qDebug() << "stopping script..." << scriptAction->text(); + } + _activeScripts.clear(); + foreach (QString scriptName, reloadList){ + qDebug() << "reloading script..." << scriptName; + loadScript(scriptName); + } +} + void Application::removeScriptName(const QString& fileNameString) { _activeScripts.removeOne(fileNameString); } @@ -4042,12 +4104,14 @@ void Application::loadScript(const QString& fileNameString) { scriptEngine->getParticlesScriptingInterface()->setParticleTree(_particles.getTree()); // hook our avatar object into this script engine - scriptEngine->setAvatarData( static_cast(_myAvatar), "MyAvatar"); + scriptEngine->setAvatarData(_myAvatar, "MyAvatar"); // leave it as a MyAvatar class to expose thrust features CameraScriptableObject* cameraScriptable = new CameraScriptableObject(&_myCamera, &_viewFrustum); scriptEngine->registerGlobalObject("Camera", cameraScriptable); connect(scriptEngine, SIGNAL(finished(const QString&)), cameraScriptable, SLOT(deleteLater())); + scriptEngine->registerGlobalObject("Overlays", &_overlays); + QThread* workerThread = new QThread(this); // when the worker thread is started, call our engine's run.. diff --git a/interface/src/Application.h b/interface/src/Application.h index ff6e08758b..a9c20ac00d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -32,7 +32,6 @@ #include "BandwidthMeter.h" #include "Camera.h" -#include "Cloud.h" #include "DatagramProcessor.h" #include "Environment.h" #include "GLCanvas.h" @@ -71,6 +70,7 @@ #include "FileLogger.h" #include "ParticleTreeRenderer.h" #include "ControllerScriptingInterface.h" +#include "ui/Overlays.h" class QAction; @@ -127,7 +127,9 @@ public: void touchUpdateEvent(QTouchEvent* event); void wheelEvent(QWheelEvent* event); - + + bool event(QEvent* event); + void makeVoxel(glm::vec3 position, float scale, unsigned char red, @@ -232,6 +234,8 @@ public slots: void loadDialog(); void toggleLogDialog(); void initAvatarAndViewFrustum(); + void stopAllScripts(); + void reloadAllScripts(); private slots: void timer(); @@ -284,7 +288,6 @@ private: void updateSixense(float deltaTime); void updateSerialDevices(float deltaTime); void updateThreads(float deltaTime); - void updateParticles(float deltaTime); void updateMetavoxels(float deltaTime); void updateCamera(float deltaTime); void updateDialogs(float deltaTime); @@ -351,8 +354,6 @@ private: Stars _stars; - Cloud _cloud; - VoxelSystem _voxels; VoxelTree _clipboard; // if I copy/paste VoxelImporter* _voxelImporter; @@ -490,6 +491,8 @@ private: void takeSnapshot(); TouchEvent _lastTouchEvent; + + Overlays _overlays; }; #endif /* defined(__interface__Application__) */ diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 6eee4453d4..0cf67be2bf 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -37,6 +37,8 @@ static const short JITTER_BUFFER_SAMPLES = JITTER_BUFFER_LENGTH_MSECS * NUM_AUDI static const float AUDIO_CALLBACK_MSECS = (float) NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL / (float)SAMPLE_RATE * 1000.0; +static const int NUMBER_OF_NOISE_SAMPLE_FRAMES = 300; + // Mute icon configration static const int ICON_SIZE = 24; static const int ICON_LEFT = 0; @@ -65,7 +67,8 @@ Audio::Audio(Oscilloscope* scope, int16_t initialJitterBufferSamples, QObject* p _measuredJitter(0), _jitterBufferSamples(initialJitterBufferSamples), _lastInputLoudness(0), - _averageInputLoudness(0), + _noiseGateMeasuredFloor(0), + _noiseGateSampleCounter(0), _noiseGateOpen(false), _noiseGateEnabled(true), _noiseGateFramesToClose(0), @@ -82,6 +85,8 @@ Audio::Audio(Oscilloscope* scope, int16_t initialJitterBufferSamples, QObject* p { // clear the array of locally injected samples memset(_localProceduralSamples, 0, NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL); + // Create the noise sample array + _noiseSampleFrames = new float[NUMBER_OF_NOISE_SAMPLE_FRAMES]; } void Audio::init(QGLWidget *parent) { @@ -351,26 +356,66 @@ void Audio::handleAudioInput() { NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL, _inputFormat, _desiredInputFormat); + // + // Impose Noise Gate + // + // The Noise Gate is used to reject constant background noise by measuring the noise + // floor observed at the microphone and then opening the 'gate' to allow microphone + // signals to be transmitted when the microphone samples average level exceeds a multiple + // of the noise floor. + // + // NOISE_GATE_HEIGHT: How loud you have to speak relative to noise background to open the gate. + // Make this value lower for more sensitivity and less rejection of noise. + // NOISE_GATE_WIDTH: The number of samples in an audio frame for which the height must be exceeded + // to open the gate. + // NOISE_GATE_CLOSE_FRAME_DELAY: Once the noise is below the gate height for the frame, how many frames + // will we wait before closing the gate. + // NOISE_GATE_FRAMES_TO_AVERAGE: How many audio frames should we average together to compute noise floor. + // More means better rejection but also can reject continuous things like singing. + // NUMBER_OF_NOISE_SAMPLE_FRAMES: How often should we re-evaluate the noise floor? + + float loudness = 0; float thisSample = 0; int samplesOverNoiseGate = 0; - const float NOISE_GATE_HEIGHT = 3.f; + const float NOISE_GATE_HEIGHT = 7.f; const int NOISE_GATE_WIDTH = 5; - const int NOISE_GATE_CLOSE_FRAME_DELAY = 30; + const int NOISE_GATE_CLOSE_FRAME_DELAY = 5; + const int NOISE_GATE_FRAMES_TO_AVERAGE = 5; for (int i = 0; i < NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; i++) { thisSample = fabsf(monoAudioSamples[i]); loudness += thisSample; // Noise Reduction: Count peaks above the average loudness - if (thisSample > (_averageInputLoudness * NOISE_GATE_HEIGHT)) { + if (thisSample > (_noiseGateMeasuredFloor * NOISE_GATE_HEIGHT)) { samplesOverNoiseGate++; } } _lastInputLoudness = loudness / NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL; - const float LOUDNESS_AVERAGING_FRAMES = 1000.f; // This will be about 10 seconds - _averageInputLoudness = (1.f - 1.f / LOUDNESS_AVERAGING_FRAMES) * _averageInputLoudness + (1.f / LOUDNESS_AVERAGING_FRAMES) * _lastInputLoudness; + float averageOfAllSampleFrames = 0.f; + _noiseSampleFrames[_noiseGateSampleCounter++] = _lastInputLoudness; + if (_noiseGateSampleCounter == NUMBER_OF_NOISE_SAMPLE_FRAMES) { + float smallestSample = FLT_MAX; + for (int i = 0; i <= NUMBER_OF_NOISE_SAMPLE_FRAMES - NOISE_GATE_FRAMES_TO_AVERAGE; i+= NOISE_GATE_FRAMES_TO_AVERAGE) { + float thisAverage = 0.0f; + for (int j = i; j < i + NOISE_GATE_FRAMES_TO_AVERAGE; j++) { + thisAverage += _noiseSampleFrames[j]; + averageOfAllSampleFrames += _noiseSampleFrames[j]; + } + thisAverage /= NOISE_GATE_FRAMES_TO_AVERAGE; + + if (thisAverage < smallestSample) { + smallestSample = thisAverage; + } + } + averageOfAllSampleFrames /= NUMBER_OF_NOISE_SAMPLE_FRAMES; + _noiseGateMeasuredFloor = smallestSample; + _noiseGateSampleCounter = 0; + //qDebug("smallest sample = %.1f, avg of all = %.1f", _noiseGateMeasuredFloor, averageOfAllSampleFrames); + } + if (_noiseGateEnabled) { if (samplesOverNoiseGate > NOISE_GATE_WIDTH) { _noiseGateOpen = true; @@ -487,7 +532,7 @@ void Audio::addReceivedAudioToBuffer(const QByteArray& audioByteArray) { if (!_ringBuffer.isStarved() && _audioOutput->bytesFree() == _audioOutput->bufferSize()) { // we don't have any audio data left in the output buffer // we just starved - qDebug() << "Audio output just starved."; + //qDebug() << "Audio output just starved."; _ringBuffer.setIsStarved(true); _numFramesDisplayStarve = 10; } @@ -505,7 +550,7 @@ void Audio::addReceivedAudioToBuffer(const QByteArray& audioByteArray) { if (!_ringBuffer.isNotStarvedOrHasMinimumSamples(NETWORK_BUFFER_LENGTH_SAMPLES_STEREO + (_jitterBufferSamples * 2))) { // starved and we don't have enough to start, keep waiting - qDebug() << "Buffer is starved and doesn't have enough samples to start. Held back."; + //qDebug() << "Buffer is starved and doesn't have enough samples to start. Held back."; } else { // We are either already playing back, or we have enough audio to start playing back. _ringBuffer.setIsStarved(false); diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 88e2731006..34a3daad30 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -45,7 +45,7 @@ public: void render(int screenWidth, int screenHeight); - float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _averageInputLoudness, 0.f); } + float getLastInputLoudness() const { return glm::max(_lastInputLoudness - _noiseGateMeasuredFloor, 0.f); } void setNoiseGateEnabled(bool noiseGateEnabled) { _noiseGateEnabled = noiseGateEnabled; } @@ -109,7 +109,9 @@ private: float _measuredJitter; int16_t _jitterBufferSamples; float _lastInputLoudness; - float _averageInputLoudness; + float _noiseGateMeasuredFloor; + float* _noiseSampleFrames; + int _noiseGateSampleCounter; bool _noiseGateOpen; bool _noiseGateEnabled; int _noiseGateFramesToClose; diff --git a/interface/src/Cloud.cpp b/interface/src/Cloud.cpp deleted file mode 100644 index a89ee04810..0000000000 --- a/interface/src/Cloud.cpp +++ /dev/null @@ -1,85 +0,0 @@ -// -// Cloud.cpp -// interface -// -// Created by Philip Rosedale on 11/17/12. -// Copyright (c) 2012 High Fidelity, Inc. All rights reserved. -// - -#include -#include -#include "Cloud.h" -#include "Util.h" -#include "Field.h" - -const int NUM_PARTICLES = 100000; -const float FIELD_COUPLE = 0.001f; -const bool RENDER_FIELD = false; - -Cloud::Cloud() { - glm::vec3 box = glm::vec3(PARTICLE_WORLD_SIZE); - _bounds = box; - _count = NUM_PARTICLES; - _particles = new Particle[_count]; - _field = new Field(PARTICLE_WORLD_SIZE, FIELD_COUPLE); - - for (unsigned int i = 0; i < _count; i++) { - _particles[i].position = randVector() * box; - const float INIT_VEL_SCALE = 0.03f; - _particles[i].velocity = randVector() * ((float)PARTICLE_WORLD_SIZE * INIT_VEL_SCALE); - _particles[i].color = randVector(); - } -} - -void Cloud::render() { - if (RENDER_FIELD) { - _field->render(); - } - - glPointSize(3.0f); - glDisable(GL_TEXTURE_2D); - glEnable(GL_POINT_SMOOTH); - - glEnableClientState(GL_VERTEX_ARRAY); - glEnableClientState(GL_COLOR_ARRAY); - glVertexPointer(3, GL_FLOAT, 3 * sizeof(glm::vec3), &_particles[0].position); - glColorPointer(3, GL_FLOAT, 3 * sizeof(glm::vec3), &_particles[0].color); - glDrawArrays(GL_POINTS, 0, NUM_PARTICLES); - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - -} - -void Cloud::simulate (float deltaTime) { - unsigned int i; - _field->simulate(deltaTime); - for (i = 0; i < _count; ++i) { - - // Update position - _particles[i].position += _particles[i].velocity * deltaTime; - - // Decay Velocity (Drag) - const float CONSTANT_DAMPING = 0.15f; - _particles[i].velocity *= (1.f - CONSTANT_DAMPING * deltaTime); - - // Interact with Field - _field->interact(deltaTime, _particles[i].position, _particles[i].velocity); - - // Update color to velocity - _particles[i].color = (glm::normalize(_particles[i].velocity) * 0.5f) + 0.5f; - - // Bounce at bounds - if ((_particles[i].position.x > _bounds.x) || (_particles[i].position.x < 0.f)) { - _particles[i].position.x = glm::clamp(_particles[i].position.x, 0.f, _bounds.x); - _particles[i].velocity.x *= -1.f; - } - if ((_particles[i].position.y > _bounds.y) || (_particles[i].position.y < 0.f)) { - _particles[i].position.y = glm::clamp(_particles[i].position.y, 0.f, _bounds.y); - _particles[i].velocity.y *= -1.f; - } - if ((_particles[i].position.z > _bounds.z) || (_particles[i].position.z < 0.f)) { - _particles[i].position.z = glm::clamp(_particles[i].position.z, 0.f, _bounds.z); - _particles[i].velocity.z *= -1.f; - } - } - } diff --git a/interface/src/Cloud.h b/interface/src/Cloud.h deleted file mode 100644 index fcf414b62e..0000000000 --- a/interface/src/Cloud.h +++ /dev/null @@ -1,32 +0,0 @@ -// -// Cloud.h -// interface -// -// Created by Philip Rosedale on 11/17/12. -// Copyright (c) 2012 High Fidelity, Inc. All rights reserved. -// - -#ifndef __interface__Cloud__ -#define __interface__Cloud__ - -#include "Field.h" - -#define PARTICLE_WORLD_SIZE 256.0 - -class Cloud { -public: - Cloud(); - void simulate(float deltaTime); - void render(); - -private: - struct Particle { - glm::vec3 position, velocity, color; - }* _particles; - - unsigned int _count; - glm::vec3 _bounds; - Field* _field; -}; - -#endif diff --git a/interface/src/ControllerScriptingInterface.cpp b/interface/src/ControllerScriptingInterface.cpp index 8716116877..b3d6170bff 100644 --- a/interface/src/ControllerScriptingInterface.cpp +++ b/interface/src/ControllerScriptingInterface.cpp @@ -172,6 +172,21 @@ glm::vec3 ControllerScriptingInterface::getSpatialControlVelocity(int controlInd return glm::vec3(0); // bad index } +glm::quat ControllerScriptingInterface::getSpatialControlRawRotation(int controlIndex) const { + int palmIndex = controlIndex / NUMBER_OF_SPATIALCONTROLS_PER_PALM; + int controlOfPalm = controlIndex % NUMBER_OF_SPATIALCONTROLS_PER_PALM; + const PalmData* palmData = getActivePalm(palmIndex); + if (palmData) { + switch (controlOfPalm) { + case PALM_SPATIALCONTROL: + return palmData->getRawRotation(); + case TIP_SPATIALCONTROL: + return palmData->getRawRotation(); // currently the tip doesn't have a unique rotation, use the palm rotation + } + } + return glm::quat(); // bad index +} + glm::vec3 ControllerScriptingInterface::getSpatialControlNormal(int controlIndex) const { int palmIndex = controlIndex / NUMBER_OF_SPATIALCONTROLS_PER_PALM; int controlOfPalm = controlIndex % NUMBER_OF_SPATIALCONTROLS_PER_PALM; diff --git a/interface/src/ControllerScriptingInterface.h b/interface/src/ControllerScriptingInterface.h index e9fbd2d6d3..f0a50559f9 100644 --- a/interface/src/ControllerScriptingInterface.h +++ b/interface/src/ControllerScriptingInterface.h @@ -58,7 +58,7 @@ public slots: virtual glm::vec3 getSpatialControlPosition(int controlIndex) const; virtual glm::vec3 getSpatialControlVelocity(int controlIndex) const; virtual glm::vec3 getSpatialControlNormal(int controlIndex) const; - + virtual glm::quat getSpatialControlRawRotation(int controlIndex) const; virtual void captureKeyEvents(const KeyEvent& event); virtual void releaseKeyEvents(const KeyEvent& event); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 136f94166b..a8b9cd7bff 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -70,7 +70,8 @@ Menu::Menu() : _maxVoxels(DEFAULT_MAX_VOXELS_PER_SYSTEM), _voxelSizeScale(DEFAULT_OCTREE_SIZE_SCALE), _boundaryLevelAdjust(0), - _maxVoxelPacketsPerSecond(DEFAULT_MAX_VOXEL_PPS) + _maxVoxelPacketsPerSecond(DEFAULT_MAX_VOXEL_PPS), + _lastAdjust(usecTimestampNow()) { Application *appInstance = Application::getInstance(); @@ -93,6 +94,8 @@ Menu::Menu() : addDisabledActionAndSeparator(fileMenu, "Scripts"); addActionToQMenuAndActionHash(fileMenu, MenuOption::LoadScript, Qt::CTRL | Qt::Key_O, appInstance, SLOT(loadDialog())); + addActionToQMenuAndActionHash(fileMenu, MenuOption::StopAllScripts, 0, appInstance, SLOT(stopAllScripts())); + addActionToQMenuAndActionHash(fileMenu, MenuOption::ReloadAllScripts, 0, appInstance, SLOT(reloadAllScripts())); _activeScriptsMenu = fileMenu->addMenu("Running Scripts"); addDisabledActionAndSeparator(fileMenu, "Voxels"); @@ -161,7 +164,7 @@ Menu::Menu() : #endif addDisabledActionAndSeparator(editMenu, "Physics"); - addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::Gravity, Qt::SHIFT | Qt::Key_G, true); + addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::Gravity, Qt::SHIFT | Qt::Key_G, false); addCheckableActionToQMenuAndActionHash(editMenu, MenuOption::ClickToFly); @@ -238,9 +241,9 @@ Menu::Menu() : SLOT(setFullscreen(bool))); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FirstPerson, Qt::Key_P, true, appInstance,SLOT(cameraMenuChanged())); - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Mirror, Qt::SHIFT | Qt::Key_H); - addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FullscreenMirror, Qt::Key_H,false, - appInstance,SLOT(cameraMenuChanged())); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Mirror, Qt::SHIFT | Qt::Key_H, true); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::FullscreenMirror, Qt::Key_H, false, + appInstance, SLOT(cameraMenuChanged())); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Enable3DTVMode, 0, false, @@ -266,14 +269,8 @@ Menu::Menu() : appInstance->getAvatar(), SLOT(resetSize())); - addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::OffAxisProjection, - 0, - true); - addCheckableActionToQMenuAndActionHash(viewMenu, - MenuOption::TurnWithHead, - 0, - true); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::OffAxisProjection, 0, false); + addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::TurnWithHead, 0, false); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MoveWithLean, 0, false); addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::HeadMouse, 0, false); @@ -298,7 +295,6 @@ Menu::Menu() : appInstance->getGlowEffect(), SLOT(cycleRenderMode())); - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ParticleCloud, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Shadows, 0, false); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, false); @@ -323,6 +319,7 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::VoxelTextures); addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::AmbientOcclusion); addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::DontFadeOnVoxelServerChanges); + addCheckableActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::AutoAdjustLOD); addActionToQMenuAndActionHash(voxelOptionsMenu, MenuOption::LodTools, Qt::SHIFT | Qt::Key_L, this, SLOT(lodTools())); QMenu* voxelProtoOptionsMenu = voxelOptionsMenu->addMenu("Voxel Server Protocol Options"); @@ -340,14 +337,14 @@ Menu::Menu() : addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderSkeletonCollisionProxies); addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::RenderHeadCollisionProxies); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::LookAtVectors, 0, true); + addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::LookAtVectors, 0, false); addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::FaceshiftTCP, 0, false, appInstance->getFaceshift(), SLOT(setTCPEnabled(bool))); - addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::ChatCircling, 0, true); + addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::ChatCircling, 0, false); QMenu* handOptionsMenu = developerMenu->addMenu("Hand Options"); @@ -357,7 +354,7 @@ Menu::Menu() : true, appInstance->getSixenseManager(), SLOT(setFilter(bool))); - addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayLeapHands, 0, true); + addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHands, 0, true); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::VoxelDrumming, 0, false); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::PlaySlaps, 0, false); @@ -890,6 +887,17 @@ void Menu::editPreferences() { sendFakeEnterEvent(); } +void Menu::goToDomain(const QString newDomain) { + if (NodeList::getInstance()->getDomainHostname() != newDomain) { + + // send a node kill request, indicating to other clients that they should play the "disappeared" effect + Application::getInstance()->getAvatar()->sendKillAvatar(); + + // give our nodeList the new domain-server hostname + NodeList::getInstance()->setDomainHostname(newDomain); + } +} + void Menu::goToDomain() { QString currentDomainHostname = NodeList::getInstance()->getDomainHostname(); @@ -914,17 +922,77 @@ void Menu::goToDomain() { // the user input a new hostname, use that newHostname = domainDialog.textValue(); } - - // send a node kill request, indicating to other clients that they should play the "disappeared" effect - Application::getInstance()->getAvatar()->sendKillAvatar(); - - // give our nodeList the new domain-server hostname - NodeList::getInstance()->setDomainHostname(domainDialog.textValue()); + + goToDomain(newHostname); } sendFakeEnterEvent(); } +void Menu::goToOrientation(QString orientation) { + + if (orientation.isEmpty()) { + return; + } + + QStringList orientationItems = orientation.split(QRegExp("_|,"), QString::SkipEmptyParts); + + const int NUMBER_OF_ORIENTATION_ITEMS = 4; + const int W_ITEM = 0; + const int X_ITEM = 1; + const int Y_ITEM = 2; + const int Z_ITEM = 3; + + if (orientationItems.size() == NUMBER_OF_ORIENTATION_ITEMS) { + + double w = replaceLastOccurrence('-', '.', orientationItems[W_ITEM].trimmed()).toDouble(); + double x = replaceLastOccurrence('-', '.', orientationItems[X_ITEM].trimmed()).toDouble(); + double y = replaceLastOccurrence('-', '.', orientationItems[Y_ITEM].trimmed()).toDouble(); + double z = replaceLastOccurrence('-', '.', orientationItems[Z_ITEM].trimmed()).toDouble(); + + glm::quat newAvatarOrientation(w, x, y, z); + + MyAvatar* myAvatar = Application::getInstance()->getAvatar(); + glm::quat avatarOrientation = myAvatar->getOrientation(); + if (newAvatarOrientation != avatarOrientation) { + myAvatar->setOrientation(newAvatarOrientation); + } + } +} + +bool Menu::goToDestination(QString destination) { + + QStringList coordinateItems = destination.split(QRegExp("_|,"), QString::SkipEmptyParts); + + const int NUMBER_OF_COORDINATE_ITEMS = 3; + const int X_ITEM = 0; + const int Y_ITEM = 1; + const int Z_ITEM = 2; + if (coordinateItems.size() == NUMBER_OF_COORDINATE_ITEMS) { + + double x = replaceLastOccurrence('-', '.', coordinateItems[X_ITEM].trimmed()).toDouble(); + double y = replaceLastOccurrence('-', '.', coordinateItems[Y_ITEM].trimmed()).toDouble(); + double z = replaceLastOccurrence('-', '.', coordinateItems[Z_ITEM].trimmed()).toDouble(); + + glm::vec3 newAvatarPos(x, y, z); + + MyAvatar* myAvatar = Application::getInstance()->getAvatar(); + glm::vec3 avatarPos = myAvatar->getPosition(); + if (newAvatarPos != avatarPos) { + // send a node kill request, indicating to other clients that they should play the "disappeared" effect + MyAvatar::sendKillAvatar(); + + qDebug("Going To Location: %f, %f, %f...", x, y, z); + myAvatar->setPosition(newAvatarPos); + } + + return true; + } + + // no coordinates were parsed + return false; +} + void Menu::goTo() { QInputDialog gotoDialog(Application::getInstance()->getWindow()); @@ -940,31 +1008,8 @@ void Menu::goTo() { destination = gotoDialog.textValue(); - QStringList coordinateItems = destination.split(QRegExp("_|,"), QString::SkipEmptyParts); - - const int NUMBER_OF_COORDINATE_ITEMS = 3; - const int X_ITEM = 0; - const int Y_ITEM = 1; - const int Z_ITEM = 2; - if (coordinateItems.size() == NUMBER_OF_COORDINATE_ITEMS) { - - double x = replaceLastOccurrence('-', '.', coordinateItems[X_ITEM].trimmed()).toDouble(); - double y = replaceLastOccurrence('-', '.', coordinateItems[Y_ITEM].trimmed()).toDouble(); - double z = replaceLastOccurrence('-', '.', coordinateItems[Z_ITEM].trimmed()).toDouble(); - - glm::vec3 newAvatarPos(x, y, z); - - MyAvatar* myAvatar = Application::getInstance()->getAvatar(); - glm::vec3 avatarPos = myAvatar->getPosition(); - if (newAvatarPos != avatarPos) { - // send a node kill request, indicating to other clients that they should play the "disappeared" effect - MyAvatar::sendKillAvatar(); - - qDebug("Going To Location: %f, %f, %f...", x, y, z); - myAvatar->setPosition(newAvatarPos); - } - - } else { + // go to coordinate destination or to Username + if (!goToDestination(destination)) { // there's a username entered by the user, make a request to the data-server DataServerClient::getValuesForKeysAndUserString( QStringList() @@ -995,29 +1040,7 @@ void Menu::goToLocation() { int dialogReturn = coordinateDialog.exec(); if (dialogReturn == QDialog::Accepted && !coordinateDialog.textValue().isEmpty()) { - QByteArray newCoordinates; - - QString delimiterPattern(","); - QStringList coordinateItems = coordinateDialog.textValue().split(delimiterPattern); - - const int NUMBER_OF_COORDINATE_ITEMS = 3; - const int X_ITEM = 0; - const int Y_ITEM = 1; - const int Z_ITEM = 2; - if (coordinateItems.size() == NUMBER_OF_COORDINATE_ITEMS) { - double x = coordinateItems[X_ITEM].toDouble(); - double y = coordinateItems[Y_ITEM].toDouble(); - double z = coordinateItems[Z_ITEM].toDouble(); - glm::vec3 newAvatarPos(x, y, z); - - if (newAvatarPos != avatarPos) { - // send a node kill request, indicating to other clients that they should play the "disappeared" effect - MyAvatar::sendKillAvatar(); - - qDebug("Going To Location: %f, %f, %f...", x, y, z); - myAvatar->setPosition(newAvatarPos); - } - } + goToDestination(coordinateDialog.textValue()); } sendFakeEnterEvent(); @@ -1097,14 +1120,38 @@ void Menu::voxelStatsDetailsClosed() { } } +void Menu::autoAdjustLOD(float currentFPS) { + bool changed = false; + quint64 now = usecTimestampNow(); + quint64 elapsed = now - _lastAdjust; + + if (elapsed > ADJUST_LOD_DOWN_DELAY && currentFPS < ADJUST_LOD_DOWN_FPS && _voxelSizeScale > ADJUST_LOD_MIN_SIZE_SCALE) { + _voxelSizeScale *= ADJUST_LOD_DOWN_BY; + changed = true; + _lastAdjust = now; + qDebug() << "adjusting LOD down... currentFPS=" << currentFPS << "_voxelSizeScale=" << _voxelSizeScale; + } + + if (elapsed > ADJUST_LOD_UP_DELAY && currentFPS > ADJUST_LOD_UP_FPS && _voxelSizeScale < ADJUST_LOD_MAX_SIZE_SCALE) { + _voxelSizeScale *= ADJUST_LOD_UP_BY; + changed = true; + _lastAdjust = now; + qDebug() << "adjusting LOD up... currentFPS=" << currentFPS << "_voxelSizeScale=" << _voxelSizeScale; + } + + if (changed) { + if (_lodToolsDialog) { + _lodToolsDialog->reloadSliders(); + } + } +} + void Menu::setVoxelSizeScale(float sizeScale) { _voxelSizeScale = sizeScale; - Application::getInstance()->getVoxels()->redrawInViewVoxels(); } void Menu::setBoundaryLevelAdjust(int boundaryLevelAdjust) { _boundaryLevelAdjust = boundaryLevelAdjust; - Application::getInstance()->getVoxels()->redrawInViewVoxels(); } void Menu::lodTools() { diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 3df986d29a..c0db7105e4 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -16,6 +16,18 @@ #include +const float ADJUST_LOD_DOWN_FPS = 40.0; +const float ADJUST_LOD_UP_FPS = 55.0; + +const quint64 ADJUST_LOD_DOWN_DELAY = 1000 * 1000 * 5; +const quint64 ADJUST_LOD_UP_DELAY = ADJUST_LOD_DOWN_DELAY * 2; + +const float ADJUST_LOD_DOWN_BY = 0.9f; +const float ADJUST_LOD_UP_BY = 1.1f; + +const float ADJUST_LOD_MIN_SIZE_SCALE = TREE_SCALE * 1.0f; +const float ADJUST_LOD_MAX_SIZE_SCALE = DEFAULT_OCTREE_SIZE_SCALE; + enum FrustumDrawMode { FRUSTUM_DRAW_MODE_ALL, FRUSTUM_DRAW_MODE_VECTORS, @@ -68,6 +80,7 @@ public: void handleViewFrustumOffsetKeyModifier(int key); // User Tweakable LOD Items + void autoAdjustLOD(float currentFPS); void setVoxelSizeScale(float sizeScale); float getVoxelSizeScale() const { return _voxelSizeScale; } void setBoundaryLevelAdjust(int boundaryLevelAdjust); @@ -84,6 +97,9 @@ public: const char* member = NULL, QAction::MenuRole role = QAction::NoRole); virtual void removeAction(QMenu* menu, const QString& actionName); + bool goToDestination(QString destination); + void goToOrientation(QString orientation); + void goToDomain(const QString newDomain); public slots: void bandwidthDetails(); @@ -154,6 +170,7 @@ private: int _maxVoxelPacketsPerSecond; QMenu* _activeScriptsMenu; QString replaceLastOccurrence(QChar search, QChar replace, QString string); + quint64 _lastAdjust; }; namespace MenuOption { @@ -161,6 +178,7 @@ namespace MenuOption { const QString AmbientOcclusion = "Ambient Occlusion"; const QString Avatars = "Avatars"; const QString Atmosphere = "Atmosphere"; + const QString AutoAdjustLOD = "Automatically Adjust LOD"; const QString AutomaticallyAuditTree = "Automatically Audit Tree Stats"; const QString Bandwidth = "Bandwidth Display"; const QString BandwidthDetails = "Bandwidth Details"; @@ -182,7 +200,7 @@ namespace MenuOption { const QString DisableDeltaSending = "Disable Delta Sending"; const QString DisableLowRes = "Disable Lower Resolution While Moving"; const QString DisplayFrustum = "Display Frustum"; - const QString DisplayLeapHands = "Display Leap Hands"; + const QString DisplayHands = "Display Hands"; const QString DisplayHandTargets = "Display Hand Targets"; const QString FilterSixense = "Smooth Sixense Movement"; const QString DontRenderVoxels = "Don't call _voxels.render()"; @@ -222,7 +240,6 @@ namespace MenuOption { const QString KillLocalVoxels = "Kill Local Voxels"; const QString GoHome = "Go Home"; const QString Gravity = "Use Gravity"; - const QString ParticleCloud = "Particle Cloud"; const QString LodTools = "LOD Tools"; const QString Log = "Log"; const QString Login = "Login"; @@ -243,8 +260,10 @@ namespace MenuOption { const QString PasteVoxels = "Paste"; const QString PasteToVoxel = "Paste to Voxel..."; const QString PipelineWarnings = "Show Render Pipeline Warnings"; + const QString PlaySlaps = "Play Slaps"; const QString Preferences = "Preferences..."; const QString RandomizeVoxelColors = "Randomize Voxel TRUE Colors"; + const QString ReloadAllScripts = "Reload All Scripts"; const QString RenderSkeletonCollisionProxies = "Skeleton Collision Proxies"; const QString RenderHeadCollisionProxies = "Head Collision Proxies"; const QString ResetAvatarSize = "Reset Avatar Size"; @@ -255,11 +274,10 @@ namespace MenuOption { const QString SettingsExport = "Export Settings"; const QString ShowAllLocalVoxels = "Show All Local Voxels"; const QString ShowTrueColors = "Show TRUE Colors"; - const QString VoxelDrumming = "Voxel Drumming"; - const QString PlaySlaps = "Play Slaps"; const QString SuppressShortTimings = "Suppress Timings Less than 10ms"; const QString Stars = "Stars"; const QString Stats = "Stats"; + const QString StopAllScripts = "Stop All Scripts"; const QString TestPing = "Test Ping"; const QString TreeStats = "Calculate Tree Stats"; const QString TransmitterDrive = "Transmitter Drive"; @@ -270,6 +288,7 @@ namespace MenuOption { const QString VoxelAddMode = "Add Voxel Mode"; const QString VoxelColorMode = "Color Voxel Mode"; const QString VoxelDeleteMode = "Delete Voxel Mode"; + const QString VoxelDrumming = "Voxel Drumming"; const QString VoxelGetColorMode = "Get Color Mode"; const QString VoxelMode = "Cycle Voxel Mode"; const QString VoxelPaintColor = "Voxel Paint Color"; diff --git a/interface/src/Util.h b/interface/src/Util.h index 09d1fa0484..0c762ccd79 100644 --- a/interface/src/Util.h +++ b/interface/src/Util.h @@ -19,15 +19,6 @@ #include #include -// the standard sans serif font family -#define SANS_FONT_FAMILY "Helvetica" - -// the standard mono font family -#define MONO_FONT_FAMILY "Courier" - -// the Inconsolata font family -#define INCONSOLATA_FONT_FAMILY "Inconsolata" - void eulerToOrthonormals(glm::vec3 * angles, glm::vec3 * fwd, glm::vec3 * left, glm::vec3 * up); float azimuth_to(glm::vec3 head_pos, glm::vec3 source_pos); diff --git a/interface/src/VoxelSystem.cpp b/interface/src/VoxelSystem.cpp index 1ffc6c3e5e..a3352f36e7 100644 --- a/interface/src/VoxelSystem.cpp +++ b/interface/src/VoxelSystem.cpp @@ -57,9 +57,12 @@ GLubyte identityIndicesBack[] = { 4, 5, 6, 4, 6, 7 }; VoxelSystem::VoxelSystem(float treeScale, int maxVoxels) : NodeData(), - _treeScale(treeScale), - _maxVoxels(maxVoxels), - _initialized(false) { + _treeScale(treeScale), + _maxVoxels(maxVoxels), + _initialized(false), + _writeArraysLock(QReadWriteLock::Recursive), + _readArraysLock(QReadWriteLock::Recursive) + { _voxelsInReadArrays = _voxelsInWriteArrays = _voxelsUpdated = 0; _writeRenderFullVBO = true; @@ -99,6 +102,9 @@ VoxelSystem::VoxelSystem(float treeScale, int maxVoxels) _culledOnce = false; _inhideOutOfView = false; + + _lastKnownVoxelSizeScale = DEFAULT_OCTREE_SIZE_SCALE; + _lastKnownBoundaryLevelAdjust = 0; } void VoxelSystem::elementDeleted(OctreeElement* element) { @@ -121,6 +127,7 @@ void VoxelSystem::setDisableFastVoxelPipeline(bool disableFastVoxelPipeline) { void VoxelSystem::elementUpdated(OctreeElement* element) { VoxelTreeElement* voxel = (VoxelTreeElement*)element; + // If we're in SetupNewVoxelsForDrawing() or _writeRenderFullVBO then bail.. if (!_useFastVoxelPipeline || _inSetupNewVoxelsForDrawing || _writeRenderFullVBO) { return; @@ -249,6 +256,9 @@ VoxelSystem::~VoxelSystem() { delete _tree; } + +// This is called by the main application thread on both the initialization of the application and when +// the preferences dialog box is called/saved void VoxelSystem::setMaxVoxels(int maxVoxels) { if (maxVoxels == _maxVoxels) { return; @@ -267,6 +277,8 @@ void VoxelSystem::setMaxVoxels(int maxVoxels) { } } +// This is called by the main application thread on both the initialization of the application and when +// the use voxel shader menu item is chosen void VoxelSystem::setUseVoxelShader(bool useVoxelShader) { if (_useVoxelShader == useVoxelShader) { return; @@ -330,7 +342,7 @@ void VoxelSystem::setVoxelsAsPoints(bool voxelsAsPoints) { void VoxelSystem::cleanupVoxelMemory() { if (_initialized) { - _bufferWriteLock.lock(); + _readArraysLock.lockForWrite(); _initialized = false; // no longer initialized if (_useVoxelShader) { // these are used when in VoxelShader mode. @@ -368,7 +380,7 @@ void VoxelSystem::cleanupVoxelMemory() { delete[] _writeVoxelDirtyArray; delete[] _readVoxelDirtyArray; _writeVoxelDirtyArray = _readVoxelDirtyArray = NULL; - _bufferWriteLock.unlock(); + _readArraysLock.unlock(); } } @@ -401,7 +413,8 @@ void VoxelSystem::setupFaceIndices(GLuint& faceVBOID, GLubyte faceIdentityIndice } void VoxelSystem::initVoxelMemory() { - _bufferWriteLock.lock(); + _readArraysLock.lockForWrite(); + _writeArraysLock.lockForWrite(); _memoryUsageRAM = 0; _memoryUsageVBO = 0; // our VBO allocations as we know them @@ -516,7 +529,8 @@ void VoxelSystem::initVoxelMemory() { _initialized = true; - _bufferWriteLock.unlock(); + _writeArraysLock.unlock(); + _readArraysLock.unlock(); } void VoxelSystem::writeToSVOFile(const char* filename, VoxelTreeElement* element) const { @@ -646,7 +660,7 @@ void VoxelSystem::setupNewVoxelsForDrawing() { } _inSetupNewVoxelsForDrawing = true; - + bool didWriteFullVBO = _writeRenderFullVBO; if (_tree->isDirty()) { static char buffer[64] = { 0 }; @@ -673,7 +687,7 @@ void VoxelSystem::setupNewVoxelsForDrawing() { } // lock on the buffer write lock so we can't modify the data when the GPU is reading it - _bufferWriteLock.lock(); + _readArraysLock.lockForWrite(); if (_voxelsUpdated) { _voxelsDirty=true; @@ -682,7 +696,7 @@ void VoxelSystem::setupNewVoxelsForDrawing() { // copy the newly written data to the arrays designated for reading, only does something if _voxelsDirty && _voxelsUpdated copyWrittenDataToReadArrays(didWriteFullVBO); - _bufferWriteLock.unlock(); + _readArraysLock.unlock(); quint64 end = usecTimestampNow(); int elapsedmsec = (end - start) / 1000; @@ -713,8 +727,8 @@ void VoxelSystem::setupNewVoxelsForDrawingSingleNode(bool allowBailEarly) { // lock on the buffer write lock so we can't modify the data when the GPU is reading it { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "setupNewVoxelsForDrawingSingleNode()... _bufferWriteLock.lock();" ); - _bufferWriteLock.lock(); + "setupNewVoxelsForDrawingSingleNode()... _readArraysLock.lockForWrite();" ); + _readArraysLock.lockForWrite(); } _voxelsDirty = true; // if we got this far, then we can assume some voxels are dirty @@ -725,7 +739,7 @@ void VoxelSystem::setupNewVoxelsForDrawingSingleNode(bool allowBailEarly) { // after... _voxelsUpdated = 0; - _bufferWriteLock.unlock(); + _readArraysLock.unlock(); quint64 end = usecTimestampNow(); int elapsedmsec = (end - start) / 1000; @@ -733,8 +747,73 @@ void VoxelSystem::setupNewVoxelsForDrawingSingleNode(bool allowBailEarly) { _setupNewVoxelsForDrawingLastElapsed = elapsedmsec; } -void VoxelSystem::checkForCulling() { + +class recreateVoxelGeometryInViewArgs { +public: + VoxelSystem* thisVoxelSystem; + ViewFrustum thisViewFrustum; + unsigned long nodesScanned; + float voxelSizeScale; + int boundaryLevelAdjust; + + recreateVoxelGeometryInViewArgs(VoxelSystem* voxelSystem) : + thisVoxelSystem(voxelSystem), + thisViewFrustum(*voxelSystem->getViewFrustum()), + nodesScanned(0), + voxelSizeScale(Menu::getInstance()->getVoxelSizeScale()), + boundaryLevelAdjust(Menu::getInstance()->getBoundaryLevelAdjust()) + { + } +}; + +// The goal of this operation is to remove any old references to old geometry, and if the voxel +// should be visible, create new geometry for it. +bool VoxelSystem::recreateVoxelGeometryInViewOperation(OctreeElement* element, void* extraData) { + VoxelTreeElement* voxel = (VoxelTreeElement*)element; + recreateVoxelGeometryInViewArgs* args = (recreateVoxelGeometryInViewArgs*)extraData; + + args->nodesScanned++; + + // reset the old geometry... + // note: this doesn't "mark the voxel as changed", so it only releases the old buffer index thereby forgetting the + // old geometry + voxel->setBufferIndex(GLBUFFER_INDEX_UNKNOWN); + + bool shouldRender = voxel->calculateShouldRender(&args->thisViewFrustum, args->voxelSizeScale, args->boundaryLevelAdjust); + bool inView = voxel->isInView(args->thisViewFrustum); + voxel->setShouldRender(inView && shouldRender); + if (shouldRender && inView) { + // recreate the geometry + args->thisVoxelSystem->updateNodeInArrays(voxel, false, true); // DONT_REUSE_INDEX, FORCE_REDRAW + } + + return true; // keep recursing! +} + + +// TODO: does cleanupRemovedVoxels() ever get called? +// TODO: other than cleanupRemovedVoxels() is there anyplace we attempt to detect too many abandoned slots??? +void VoxelSystem::recreateVoxelGeometryInView() { + + qDebug() << "recreateVoxelGeometryInView()..."; + + recreateVoxelGeometryInViewArgs args(this); + _writeArraysLock.lockForWrite(); // don't let anyone read or write our write arrays until we're done + _tree->lockForRead(); // don't let anyone change our tree structure until we're run + + // reset our write arrays bookkeeping to think we've got no voxels in it + clearFreeBufferIndexes(); + + // do we need to reset out _writeVoxelDirtyArray arrays?? + memset(_writeVoxelDirtyArray, false, _maxVoxels * sizeof(bool)); + + _tree->recurseTreeWithOperation(recreateVoxelGeometryInViewOperation,(void*)&args); + _tree->unlock(); + _writeArraysLock.unlock(); +} + +void VoxelSystem::checkForCulling() { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "checkForCulling()"); quint64 start = usecTimestampNow(); @@ -762,7 +841,20 @@ void VoxelSystem::checkForCulling() { _hasRecentlyChanged = false; } - hideOutOfView(forceFullFrustum); + // This would be a good place to do a special processing pass, for example, switching the LOD of the scene + bool fullRedraw = (_lastKnownVoxelSizeScale != Menu::getInstance()->getVoxelSizeScale() || + _lastKnownBoundaryLevelAdjust != Menu::getInstance()->getBoundaryLevelAdjust()); + + // track that these values + _lastKnownVoxelSizeScale = Menu::getInstance()->getVoxelSizeScale(); + _lastKnownBoundaryLevelAdjust = Menu::getInstance()->getBoundaryLevelAdjust(); + + if (fullRedraw) { + // this will remove all old geometry and recreate the correct geometry for all in view voxels + recreateVoxelGeometryInView(); + } else { + hideOutOfView(forceFullFrustum); + } if (forceFullFrustum) { quint64 endViewCulling = usecTimestampNow(); @@ -880,12 +972,26 @@ void VoxelSystem::copyWrittenDataToReadArrays(bool fullVBOs) { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "copyWrittenDataToReadArrays()"); - if (_voxelsDirty && _voxelsUpdated) { - if (fullVBOs) { - copyWrittenDataToReadArraysFullVBOs(); + // attempt to get the writeArraysLock for reading and the readArraysLock for writing + // so we can copy from the write to the read... if we fail, that's ok, we'll get it the next + // time around, the only side effect is the VBOs won't be updated this frame + const int WAIT_FOR_LOCK_IN_MS = 5; + if (_readArraysLock.tryLockForWrite(WAIT_FOR_LOCK_IN_MS)) { + if (_writeArraysLock.tryLockForRead(WAIT_FOR_LOCK_IN_MS)) { + if (_voxelsDirty && _voxelsUpdated) { + if (fullVBOs) { + copyWrittenDataToReadArraysFullVBOs(); + } else { + copyWrittenDataToReadArraysPartialVBOs(); + } + } + _writeArraysLock.unlock(); } else { - copyWrittenDataToReadArraysPartialVBOs(); + qDebug() << "couldn't get _writeArraysLock.LockForRead()..."; } + _readArraysLock.unlock(); + } else { + qDebug() << "couldn't get _readArraysLock.LockForWrite()..."; } } @@ -1141,17 +1247,27 @@ void VoxelSystem::updateVBOs() { // would like to include _callsToTreesToArrays PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), buffer); if (_voxelsDirty) { - if (_readRenderFullVBO) { - updateFullVBOs(); + + // attempt to lock the read arrays, to for copying from them to the actual GPU VBOs. + // if we fail to get the lock, that's ok, our VBOs will update on the next frame... + const int WAIT_FOR_LOCK_IN_MS = 5; + if (_readArraysLock.tryLockForRead(WAIT_FOR_LOCK_IN_MS)) { + if (_readRenderFullVBO) { + updateFullVBOs(); + } else { + updatePartialVBOs(); + } + _voxelsDirty = false; + _readRenderFullVBO = false; + _readArraysLock.unlock(); } else { - updatePartialVBOs(); + qDebug() << "updateVBOs().... couldn't get _readArraysLock.tryLockForRead()"; } - _voxelsDirty = false; - _readRenderFullVBO = false; } _callsToTreesToArrays = 0; // clear it } +// this should only be called on the main application thread during render void VoxelSystem::updateVBOSegment(glBufferIndex segmentStart, glBufferIndex segmentEnd) { bool showWarning = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarning, "updateVBOSegment()"); @@ -1197,7 +1313,8 @@ void VoxelSystem::updateVBOSegment(glBufferIndex segmentStart, glBufferIndex seg } } -void VoxelSystem::render(bool texture) { +void VoxelSystem::render() { + bool texture = Menu::getInstance()->isOptionChecked(MenuOption::VoxelTextures); bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "render()"); @@ -1404,11 +1521,7 @@ void VoxelSystem::killLocalVoxels() { setupNewVoxelsForDrawing(); } -void VoxelSystem::redrawInViewVoxels() { - hideOutOfView(true); -} - - +// only called on main thread bool VoxelSystem::clearAllNodesBufferIndexOperation(OctreeElement* element, void* extraData) { _nodeCount++; VoxelTreeElement* voxel = (VoxelTreeElement*)element; @@ -1416,12 +1529,15 @@ bool VoxelSystem::clearAllNodesBufferIndexOperation(OctreeElement* element, void return true; } +// only called on main thread, and also always followed by a call to cleanupVoxelMemory() +// you shouldn't be calling this on any other thread or without also cleaning up voxel memory void VoxelSystem::clearAllNodesBufferIndex() { PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), "VoxelSystem::clearAllNodesBufferIndex()"); _nodeCount = 0; _tree->lockForRead(); // we won't change the tree so it's ok to treat this as a read _tree->recurseTreeWithOperation(clearAllNodesBufferIndexOperation); + clearFreeBufferIndexes(); // this should be called too _tree->unlock(); if (Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings)) { qDebug("clearing buffer index of %d nodes", _nodeCount); diff --git a/interface/src/VoxelSystem.h b/interface/src/VoxelSystem.h index d1404668bf..121a7f86c4 100644 --- a/interface/src/VoxelSystem.h +++ b/interface/src/VoxelSystem.h @@ -53,7 +53,7 @@ public: virtual void init(); void simulate(float deltaTime) { } - void render(bool texture); + void render(); void changeTree(VoxelTree* newTree); VoxelTree* getTree() const { return _tree; } @@ -79,7 +79,6 @@ public: unsigned long getVoxelMemoryUsageGPU(); void killLocalVoxels(); - void redrawInViewVoxels(); virtual void removeOutOfView(); virtual void hideOutOfView(bool forceFullFrustum = false); @@ -151,6 +150,7 @@ protected: static const bool DONT_BAIL_EARLY; // by default we will bail early, if you want to force not bailing, then use this void setupNewVoxelsForDrawingSingleNode(bool allowBailEarly = true); void checkForCulling(); + void recreateVoxelGeometryInView(); glm::vec3 computeVoxelVertex(const glm::vec3& startVertex, float voxelScale, int index) const; @@ -194,6 +194,7 @@ private: static bool showAllSubTreeOperation(OctreeElement* element, void* extraData); static bool showAllLocalVoxelsOperation(OctreeElement* element, void* extraData); static bool getVoxelEnclosingOperation(OctreeElement* element, void* extraData); + static bool recreateVoxelGeometryInViewOperation(OctreeElement* element, void* extraData); int updateNodeInArrays(VoxelTreeElement* node, bool reuseIndex, bool forceDraw); int forceRemoveNodeFromArrays(VoxelTreeElement* node); @@ -211,6 +212,11 @@ private: GLfloat* _readVerticesArray; GLubyte* _readColorsArray; + + QReadWriteLock _writeArraysLock; + QReadWriteLock _readArraysLock; + + GLfloat* _writeVerticesArray; GLubyte* _writeColorsArray; bool* _writeVoxelDirtyArray; @@ -253,9 +259,6 @@ private: GLuint _vboIndicesFront; GLuint _vboIndicesBack; - QMutex _bufferWriteLock; - QMutex _treeLock; - ViewFrustum _lastKnownViewFrustum; ViewFrustum _lastStableViewFrustum; ViewFrustum* _viewFrustum; @@ -299,6 +302,9 @@ private: bool _useFastVoxelPipeline; bool _inhideOutOfView; + + float _lastKnownVoxelSizeScale; + int _lastKnownBoundaryLevelAdjust; }; #endif diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index bc7f56f534..353a8aea5f 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -278,11 +278,14 @@ bool Avatar::findSphereCollisions(const glm::vec3& penetratorCenter, float penet bool didPenetrate = false; glm::vec3 skeletonPenetration; ModelCollisionInfo collisionInfo; + /* Temporarily disabling collisions against the skeleton because the collision proxies up + * near the neck are bad and prevent the hand from hitting the face. if (_skeletonModel.findSphereCollision(penetratorCenter, penetratorRadius, collisionInfo, 1.0f, skeletonSkipIndex)) { collisionInfo._model = &_skeletonModel; collisions.push_back(collisionInfo); didPenetrate = true; } + */ if (_head.getFaceModel().findSphereCollision(penetratorCenter, penetratorRadius, collisionInfo)) { collisionInfo._model = &(_head.getFaceModel()); collisions.push_back(collisionInfo); @@ -347,13 +350,13 @@ bool Avatar::findSphereCollisionWithSkeleton(const glm::vec3& sphereCenter, floa void Avatar::setFaceModelURL(const QUrl &faceModelURL) { AvatarData::setFaceModelURL(faceModelURL); - const QUrl DEFAULT_FACE_MODEL_URL = QUrl::fromLocalFile("resources/meshes/defaultAvatar_head.fbx"); + const QUrl DEFAULT_FACE_MODEL_URL = QUrl::fromLocalFile("resources/meshes/defaultAvatar_head.fst"); _head.getFaceModel().setURL(_faceModelURL, DEFAULT_FACE_MODEL_URL); } void Avatar::setSkeletonModelURL(const QUrl &skeletonModelURL) { AvatarData::setSkeletonModelURL(skeletonModelURL); - const QUrl DEFAULT_SKELETON_MODEL_URL = QUrl::fromLocalFile("resources/meshes/defaultAvatar_body.fbx"); + const QUrl DEFAULT_SKELETON_MODEL_URL = QUrl::fromLocalFile("resources/meshes/defaultAvatar_body.fst"); _skeletonModel.setURL(_skeletonModelURL, DEFAULT_SKELETON_MODEL_URL); } @@ -447,15 +450,36 @@ float Avatar::getHeight() const { return extents.maximum.y - extents.minimum.y; } -bool Avatar::poke(ModelCollisionInfo& collision) { - // ATM poke() can only affect the Skeleton (not the head) +bool Avatar::collisionWouldMoveAvatar(ModelCollisionInfo& collision) const { + // ATM only the Skeleton is pokeable // TODO: make poke affect head + if (!collision._model) { + return false; + } if (collision._model == &_skeletonModel && collision._jointIndex != -1) { - return _skeletonModel.poke(collision); + // collision response of skeleton is temporarily disabled + return false; + //return _skeletonModel.collisionHitsMoveableJoint(collision); + } + if (collision._model == &(_head.getFaceModel())) { + return true; } return false; } +void Avatar::applyCollision(ModelCollisionInfo& collision) { + if (!collision._model) { + return; + } + if (collision._model == &(_head.getFaceModel())) { + _head.applyCollision(collision); + } + // TODO: make skeleton respond to collisions + //if (collision._model == &_skeletonModel && collision._jointIndex != -1) { + // _skeletonModel.applyCollision(collision); + //} +} + float Avatar::getPelvisFloatingHeight() const { return -_skeletonModel.getBindExtents().minimum.y; } diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 8290115240..2fc26a36b5 100755 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -128,9 +128,11 @@ public: float getHeight() const; + /// \return true if we expect the avatar would move as a result of the collision + bool collisionWouldMoveAvatar(ModelCollisionInfo& collision) const; + /// \param collision a data structure for storing info about collisions against Models - /// \return true if the collision affects the Avatar models - bool poke(ModelCollisionInfo& collision); + void applyCollision(ModelCollisionInfo& collision); public slots: void updateCollisionFlags(); diff --git a/interface/src/avatar/FaceModel.cpp b/interface/src/avatar/FaceModel.cpp index 076ad074f2..2a1593b776 100644 --- a/interface/src/avatar/FaceModel.cpp +++ b/interface/src/avatar/FaceModel.cpp @@ -20,6 +20,7 @@ FaceModel::FaceModel(Head* owningHead) : void FaceModel::simulate(float deltaTime) { if (!isActive()) { + Model::simulate(deltaTime); return; } Avatar* owningAvatar = static_cast(_owningHead->_owningAvatar); @@ -55,7 +56,7 @@ void FaceModel::maybeUpdateNeckRotation(const JointState& parentState, const FBX glm::mat3 axes = glm::mat3_cast(_rotation); glm::mat3 inverse = glm::mat3(glm::inverse(parentState.transform * glm::translate(state.translation) * joint.preTransform * glm::mat4_cast(joint.preRotation))); - state.rotation = glm::angleAxis(-_owningHead->getRoll(), glm::normalize(inverse * axes[2])) * + state.rotation = glm::angleAxis(-_owningHead->getTweakedRoll(), glm::normalize(inverse * axes[2])) * glm::angleAxis(_owningHead->getTweakedYaw(), glm::normalize(inverse * axes[1])) * glm::angleAxis(-_owningHead->getTweakedPitch(), glm::normalize(inverse * axes[0])) * joint.rotation; } diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp index 939ebaeae9..37d7713276 100644 --- a/interface/src/avatar/Hand.cpp +++ b/interface/src/avatar/Hand.cpp @@ -32,12 +32,7 @@ Hand::Hand(Avatar* owningAvatar) : _ballColor(0.0, 0.0, 0.4), _collisionCenter(0,0,0), _collisionAge(0), - _collisionDuration(0), - _pitchUpdate(0), - _grabDelta(0, 0, 0), - _grabDeltaVelocity(0, 0, 0), - _grabStartRotation(0, 0, 0, 1), - _grabCurrentRotation(0, 0, 0, 1) + _collisionDuration(0) { } @@ -54,28 +49,6 @@ void Hand::init() { void Hand::reset() { } -glm::vec3 Hand::getAndResetGrabDelta() { - const float HAND_GRAB_SCALE_DISTANCE = 2.f; - glm::vec3 delta = _grabDelta * _owningAvatar->getScale() * HAND_GRAB_SCALE_DISTANCE; - _grabDelta = glm::vec3(0,0,0); - glm::quat avatarRotation = _owningAvatar->getOrientation(); - return avatarRotation * -delta; -} - -glm::vec3 Hand::getAndResetGrabDeltaVelocity() { - const float HAND_GRAB_SCALE_VELOCITY = 5.f; - glm::vec3 delta = _grabDeltaVelocity * _owningAvatar->getScale() * HAND_GRAB_SCALE_VELOCITY; - _grabDeltaVelocity = glm::vec3(0,0,0); - glm::quat avatarRotation = _owningAvatar->getOrientation(); - return avatarRotation * -delta; - -} -glm::quat Hand::getAndResetGrabRotation() { - glm::quat diff = _grabCurrentRotation * glm::inverse(_grabStartRotation); - _grabStartRotation = _grabCurrentRotation; - return diff; -} - void Hand::simulate(float deltaTime, bool isMine) { if (_collisionAge > 0.f) { @@ -84,7 +57,6 @@ void Hand::simulate(float deltaTime, bool isMine) { if (isMine) { _buckyBalls.simulate(deltaTime); - updateCollisions(); } calculateGeometry(); @@ -99,19 +71,6 @@ void Hand::simulate(float deltaTime, bool isMine) { _buckyBalls.grab(palm, fingerTipPosition, _owningAvatar->getOrientation(), deltaTime); - if (palm.getControllerButtons() & BUTTON_4) { - _grabDelta += palm.getRawVelocity() * deltaTime; - _grabCurrentRotation = palm.getRawRotation(); - } - if ((palm.getLastControllerButtons() & BUTTON_4) && !(palm.getControllerButtons() & BUTTON_4)) { - // Just ending grab, capture velocity - _grabDeltaVelocity = palm.getRawVelocity(); - } - if (!(palm.getLastControllerButtons() & BUTTON_4) && (palm.getControllerButtons() & BUTTON_4)) { - // Just starting grab, capture starting rotation - _grabStartRotation = palm.getRawRotation(); - } - if (palm.getControllerButtons() & BUTTON_1) { if (glm::length(fingerTipPosition - _lastFingerAddVoxel) > (FINGERTIP_VOXEL_SIZE / 2.f)) { QColor paintColor = Menu::getInstance()->getActionForOption(MenuOption::VoxelPaintColor)->data().value(); @@ -166,91 +125,105 @@ void Hand::simulate(float deltaTime, bool isMine) { } } -void Hand::updateCollisions() { - // use position to obtain the left and right palm indices - int leftPalmIndex, rightPalmIndex; - getLeftRightPalmIndices(leftPalmIndex, rightPalmIndex); - - ModelCollisionList collisions; - // check for collisions +void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) { + if (!avatar || avatar == _owningAvatar) { + // don't collide with our own hands (that is done elsewhere) + return; + } + float scaledPalmRadius = PALM_COLLISION_RADIUS * _owningAvatar->getScale(); for (size_t i = 0; i < getNumPalms(); i++) { PalmData& palm = getPalms()[i]; if (!palm.isActive()) { continue; } - float scaledPalmRadius = PALM_COLLISION_RADIUS * _owningAvatar->getScale(); glm::vec3 totalPenetration; - - if (Menu::getInstance()->isOptionChecked(MenuOption::CollideWithAvatars)) { - // check other avatars - foreach (const AvatarSharedPointer& avatarPointer, Application::getInstance()->getAvatarManager().getAvatarHash()) { - Avatar* avatar = static_cast(avatarPointer.data()); - if (avatar == _owningAvatar) { - // don't collid with our own hands + ModelCollisionList collisions; + if (isMyHand && Menu::getInstance()->isOptionChecked(MenuOption::PlaySlaps)) { + // Check for palm collisions + glm::vec3 myPalmPosition = palm.getPosition(); + float palmCollisionDistance = 0.1f; + bool wasColliding = palm.getIsCollidingWithPalm(); + palm.setIsCollidingWithPalm(false); + // If 'Play Slaps' is enabled, look for palm-to-palm collisions and make sound + for (size_t j = 0; j < avatar->getHand().getNumPalms(); j++) { + PalmData& otherPalm = avatar->getHand().getPalms()[j]; + if (!otherPalm.isActive()) { continue; } - if (Menu::getInstance()->isOptionChecked(MenuOption::PlaySlaps)) { - // Check for palm collisions - glm::vec3 myPalmPosition = palm.getPosition(); - float palmCollisionDistance = 0.1f; - bool wasColliding = palm.getIsCollidingWithPalm(); - palm.setIsCollidingWithPalm(false); - // If 'Play Slaps' is enabled, look for palm-to-palm collisions and make sound - for (size_t j = 0; j < avatar->getHand().getNumPalms(); j++) { - PalmData& otherPalm = avatar->getHand().getPalms()[j]; - if (!otherPalm.isActive()) { - continue; - } - glm::vec3 otherPalmPosition = otherPalm.getPosition(); - if (glm::length(otherPalmPosition - myPalmPosition) < palmCollisionDistance) { - palm.setIsCollidingWithPalm(true); - if (!wasColliding) { - const float PALM_COLLIDE_VOLUME = 1.f; - const float PALM_COLLIDE_FREQUENCY = 1000.f; - const float PALM_COLLIDE_DURATION_MAX = 0.75f; - const float PALM_COLLIDE_DECAY_PER_SAMPLE = 0.01f; - Application::getInstance()->getAudio()->startDrumSound(PALM_COLLIDE_VOLUME, - PALM_COLLIDE_FREQUENCY, - PALM_COLLIDE_DURATION_MAX, - PALM_COLLIDE_DECAY_PER_SAMPLE); - // If the other person's palm is in motion, move mine downward to show I was hit - const float MIN_VELOCITY_FOR_SLAP = 0.05f; - if (glm::length(otherPalm.getVelocity()) > MIN_VELOCITY_FOR_SLAP) { - // add slapback here - } - } - } - } - } - if (avatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, collisions)) { - for (size_t j = 0; j < collisions.size(); ++j) { - if (!avatar->poke(collisions[j])) { - totalPenetration = addPenetrations(totalPenetration, collisions[j]._penetration); + glm::vec3 otherPalmPosition = otherPalm.getPosition(); + if (glm::length(otherPalmPosition - myPalmPosition) < palmCollisionDistance) { + palm.setIsCollidingWithPalm(true); + if (!wasColliding) { + const float PALM_COLLIDE_VOLUME = 1.f; + const float PALM_COLLIDE_FREQUENCY = 1000.f; + const float PALM_COLLIDE_DURATION_MAX = 0.75f; + const float PALM_COLLIDE_DECAY_PER_SAMPLE = 0.01f; + Application::getInstance()->getAudio()->startDrumSound(PALM_COLLIDE_VOLUME, + PALM_COLLIDE_FREQUENCY, + PALM_COLLIDE_DURATION_MAX, + PALM_COLLIDE_DECAY_PER_SAMPLE); + // If the other person's palm is in motion, move mine downward to show I was hit + const float MIN_VELOCITY_FOR_SLAP = 0.05f; + if (glm::length(otherPalm.getVelocity()) > MIN_VELOCITY_FOR_SLAP) { + // add slapback here } } } } } - - if (Menu::getInstance()->isOptionChecked(MenuOption::HandsCollideWithSelf)) { - // and the current avatar (ignoring everything below the parent of the parent of the last free joint) - collisions.clear(); - const Model& skeletonModel = _owningAvatar->getSkeletonModel(); - int skipIndex = skeletonModel.getParentJointIndex(skeletonModel.getParentJointIndex( - skeletonModel.getLastFreeJointIndex((i == leftPalmIndex) ? skeletonModel.getLeftHandJointIndex() : - (i == rightPalmIndex) ? skeletonModel.getRightHandJointIndex() : -1))); - if (_owningAvatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, collisions, skipIndex)) { - for (size_t j = 0; j < collisions.size(); ++j) { - totalPenetration = addPenetrations(totalPenetration, collisions[j]._penetration); + if (avatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, collisions)) { + for (int j = 0; j < collisions.size(); ++j) { + if (isMyHand) { + if (!avatar->collisionWouldMoveAvatar(collisions[j])) { + // we resolve the hand from collision when it belongs to MyAvatar AND the other Avatar is + // not expected to respond to the collision (hand hit unmovable part of their Avatar) + totalPenetration = addPenetrations(totalPenetration, collisions[j]._penetration); + } + } else { + // when !isMyHand then avatar is MyAvatar and we apply the collision + // which might not do anything (hand hit unmovable part of MyAvatar) however + // we don't resolve the hand's penetration because we expect the remote + // simulation to do the right thing. + avatar->applyCollision(collisions[j]); } } } - - // un-penetrate - palm.addToPosition(-totalPenetration); + if (isMyHand) { + // resolve penetration + palm.addToPosition(-totalPenetration); + } + } +} - // we recycle the collisions container, so we clear it for the next loop +void Hand::collideAgainstOurself() { + if (!Menu::getInstance()->isOptionChecked(MenuOption::HandsCollideWithSelf)) { + return; + } + + ModelCollisionList collisions; + int leftPalmIndex, rightPalmIndex; + getLeftRightPalmIndices(leftPalmIndex, rightPalmIndex); + float scaledPalmRadius = PALM_COLLISION_RADIUS * _owningAvatar->getScale(); + + for (size_t i = 0; i < getNumPalms(); i++) { + PalmData& palm = getPalms()[i]; + if (!palm.isActive()) { + continue; + } + glm::vec3 totalPenetration; + // and the current avatar (ignoring everything below the parent of the parent of the last free joint) collisions.clear(); + const Model& skeletonModel = _owningAvatar->getSkeletonModel(); + int skipIndex = skeletonModel.getParentJointIndex(skeletonModel.getParentJointIndex( + skeletonModel.getLastFreeJointIndex((i == leftPalmIndex) ? skeletonModel.getLeftHandJointIndex() : + (i == rightPalmIndex) ? skeletonModel.getRightHandJointIndex() : -1))); + if (_owningAvatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, collisions, skipIndex)) { + for (int j = 0; j < collisions.size(); ++j) { + totalPenetration = addPenetrations(totalPenetration, collisions[j]._penetration); + } + } + // resolve penetration + palm.addToPosition(-totalPenetration); } } @@ -344,7 +317,7 @@ void Hand::render(bool isMine) { } } - if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayLeapHands)) { + if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayHands)) { renderLeapHands(isMine); } diff --git a/interface/src/avatar/Hand.h b/interface/src/avatar/Hand.h index 3c8ec2d562..5a423630b4 100755 --- a/interface/src/avatar/Hand.h +++ b/interface/src/avatar/Hand.h @@ -58,14 +58,8 @@ public: const glm::vec3& getLeapFingerTipBallPosition (int ball) const { return _leapFingerTipBalls [ball].position;} const glm::vec3& getLeapFingerRootBallPosition(int ball) const { return _leapFingerRootBalls[ball].position;} - // Pitch from controller input to view - const float getPitchUpdate() const { return _pitchUpdate; } - void setPitchUpdate(float pitchUpdate) { _pitchUpdate = pitchUpdate; } - - // Get the drag distance to move - glm::vec3 getAndResetGrabDelta(); - glm::vec3 getAndResetGrabDeltaVelocity(); - glm::quat getAndResetGrabRotation(); + void collideAgainstAvatar(Avatar* avatar, bool isMyHand); + void collideAgainstOurself(); private: // disallow copies of the Hand, copy of owning Avatar is disallowed too @@ -96,17 +90,9 @@ private: void renderLeapHands(bool isMine); void renderLeapFingerTrails(); - void updateCollisions(); void calculateGeometry(); void handleVoxelCollision(PalmData* palm, const glm::vec3& fingerTipPosition, VoxelTreeElement* voxel, float deltaTime); - - float _pitchUpdate; - - glm::vec3 _grabDelta; - glm::vec3 _grabDeltaVelocity; - glm::quat _grabStartRotation; - glm::quat _grabCurrentRotation; }; #endif diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp index e5d4724bb5..bb88530aa7 100644 --- a/interface/src/avatar/Head.cpp +++ b/interface/src/avatar/Head.cpp @@ -219,6 +219,34 @@ float Head::getTweakedRoll() const { return glm::clamp(_roll + _tweakedRoll, MIN_HEAD_ROLL, MAX_HEAD_ROLL); } +void Head::applyCollision(ModelCollisionInfo& collisionInfo) { + // HACK: the collision proxies for the FaceModel are bad. As a temporary workaround + // we collide against a hard coded collision proxy. + // TODO: get a better collision proxy here. + const float HEAD_RADIUS = 0.15f; + const glm::vec3 HEAD_CENTER = _position; + + // collide the contactPoint against the collision proxy to obtain a new penetration + // NOTE: that penetration is in opposite direction (points the way out for the point, not the sphere) + glm::vec3 penetration; + if (findPointSpherePenetration(collisionInfo._contactPoint, HEAD_CENTER, HEAD_RADIUS, penetration)) { + // compute lean angles + Avatar* owningAvatar = static_cast(_owningAvatar); + glm::quat bodyRotation = owningAvatar->getOrientation(); + glm::vec3 neckPosition; + if (owningAvatar->getSkeletonModel().getNeckPosition(neckPosition)) { + glm::vec3 xAxis = bodyRotation * glm::vec3(1.f, 0.f, 0.f); + glm::vec3 zAxis = bodyRotation * glm::vec3(0.f, 0.f, 1.f); + float neckLength = glm::length(_position - neckPosition); + if (neckLength > 0.f) { + float forward = glm::dot(collisionInfo._penetration, zAxis) / neckLength; + float sideways = - glm::dot(collisionInfo._penetration, xAxis) / neckLength; + addLean(sideways, forward); + } + } + } +} + void Head::renderLookatVectors(glm::vec3 leftEyePosition, glm::vec3 rightEyePosition, glm::vec3 lookatPosition) { Application::getInstance()->getGlowEffect()->begin(); diff --git a/interface/src/avatar/Head.h b/interface/src/avatar/Head.h index 19f9efd8e6..eae8223903 100644 --- a/interface/src/avatar/Head.h +++ b/interface/src/avatar/Head.h @@ -79,6 +79,8 @@ public: float getTweakedPitch() const; float getTweakedYaw() const; float getTweakedRoll() const; + + void applyCollision(ModelCollisionInfo& collisionInfo); private: // disallow copies of the Head, copy of owning Avatar is disallowed too diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3b1fa90959..26b67761f5 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -111,23 +111,7 @@ void MyAvatar::updateTransmitter(float deltaTime) { void MyAvatar::update(float deltaTime) { updateTransmitter(deltaTime); - // TODO: resurrect touch interactions between avatars - //// rotate body yaw for yaw received from multitouch - //setOrientation(getOrientation() * glm::quat(glm::vec3(0, _yawFromTouch, 0))); - //_yawFromTouch = 0.f; - // - //// apply pitch from touch - //_head.setPitch(_head.getPitch() + _pitchFromTouch); - //_pitchFromTouch = 0.0f; - // - //float TOUCH_YAW_SCALE = -0.25f; - //float TOUCH_PITCH_SCALE = -12.5f; - //float FIXED_TOUCH_TIMESTEP = 0.016f; - //_yawFromTouch += ((_touchAvgX - _lastTouchAvgX) * TOUCH_YAW_SCALE * FIXED_TOUCH_TIMESTEP); - //_pitchFromTouch += ((_touchAvgY - _lastTouchAvgY) * TOUCH_PITCH_SCALE * FIXED_TOUCH_TIMESTEP); - - // Update my avatar's state from gyros - updateFromGyros(Menu::getInstance()->isOptionChecked(MenuOption::TurnWithHead)); + updateFromGyros(deltaTime); // Update head mouse from faceshift if active Faceshift* faceshift = Application::getInstance()->getFaceshift(); @@ -224,8 +208,6 @@ void MyAvatar::simulate(float deltaTime) { updateCollisionWithVoxels(deltaTime, radius); } if (_collisionFlags & COLLISION_GROUP_AVATARS) { - // Note, hand-vs-avatar collisions are done elsewhere - // This is where we avatar-vs-avatar bounding capsule updateCollisionWithAvatars(deltaTime); } } @@ -327,24 +309,10 @@ void MyAvatar::simulate(float deltaTime) { updateChatCircle(deltaTime); - // Get any position, velocity, or rotation update from Grab Drag controller - glm::vec3 moveFromGrab = _hand.getAndResetGrabDelta(); - if (glm::length(moveFromGrab) > EPSILON) { - _position += moveFromGrab; - _velocity = glm::vec3(0, 0, 0); - } - _velocity += _hand.getAndResetGrabDeltaVelocity(); - glm::quat deltaRotation = _hand.getAndResetGrabRotation(); - const float GRAB_CONTROLLER_TURN_SCALING = 0.5f; - glm::vec3 euler = safeEulerAngles(deltaRotation) * GRAB_CONTROLLER_TURN_SCALING; - // Adjust body yaw by yaw from controller - setOrientation(glm::angleAxis(-euler.y, glm::vec3(0, 1, 0)) * getOrientation()); - // Adjust head pitch from controller - getHead().setPitch(getHead().getPitch() - euler.x); - _position += _velocity * deltaTime; // update avatar skeleton and simulate hand and head + _hand.collideAgainstOurself(); _hand.simulate(deltaTime, true); _skeletonModel.simulate(deltaTime); _head.setBodyRotation(glm::vec3(_bodyPitch, _bodyYaw, _bodyRoll)); @@ -364,7 +332,7 @@ void MyAvatar::simulate(float deltaTime) { const float MAX_PITCH = 90.0f; // Update avatar head rotation with sensor data -void MyAvatar::updateFromGyros(bool turnWithHead) { +void MyAvatar::updateFromGyros(float deltaTime) { Faceshift* faceshift = Application::getInstance()->getFaceshift(); glm::vec3 estimatedPosition, estimatedRotation; @@ -372,7 +340,7 @@ void MyAvatar::updateFromGyros(bool turnWithHead) { estimatedPosition = faceshift->getHeadTranslation(); estimatedRotation = safeEulerAngles(faceshift->getHeadRotation()); // Rotate the body if the head is turned beyond the screen - if (turnWithHead) { + if (Menu::getInstance()->isOptionChecked(MenuOption::TurnWithHead)) { const float FACESHIFT_YAW_TURN_SENSITIVITY = 0.5f; const float FACESHIFT_MIN_YAW_TURN = 15.f; const float FACESHIFT_MAX_YAW_TURN = 50.f; @@ -387,11 +355,12 @@ void MyAvatar::updateFromGyros(bool turnWithHead) { } } else { // restore rotation, lean to neutral positions - const float RESTORE_RATE = 0.05f; - _head.setYaw(glm::mix(_head.getYaw(), 0.0f, RESTORE_RATE)); - _head.setRoll(glm::mix(_head.getRoll(), 0.0f, RESTORE_RATE)); - _head.setLeanSideways(glm::mix(_head.getLeanSideways(), 0.0f, RESTORE_RATE)); - _head.setLeanForward(glm::mix(_head.getLeanForward(), 0.0f, RESTORE_RATE)); + const float RESTORE_PERIOD = 1.f; // seconds + float restorePercentage = glm::clamp(deltaTime/RESTORE_PERIOD, 0.f, 1.f); + _head.setYaw(glm::mix(_head.getYaw(), 0.0f, restorePercentage)); + _head.setRoll(glm::mix(_head.getRoll(), 0.0f, restorePercentage)); + _head.setLeanSideways(glm::mix(_head.getLeanSideways(), 0.0f, restorePercentage)); + _head.setLeanForward(glm::mix(_head.getLeanForward(), 0.0f, restorePercentage)); return; } @@ -809,39 +778,6 @@ void MyAvatar::updateThrust(float deltaTime) { up; } } - // Add thrust and rotation from hand controllers - const float THRUST_MAG_HAND_JETS = THRUST_MAG_FWD; - const float JOYSTICK_YAW_MAG = YAW_MAG; - const float JOYSTICK_PITCH_MAG = PITCH_MAG * 0.5f; - const int THRUST_CONTROLLER = 0; - const int VIEW_CONTROLLER = 1; - for (size_t i = 0; i < getHand().getPalms().size(); ++i) { - PalmData& palm = getHand().getPalms()[i]; - - // If the script hasn't captured this joystick, then let the default behavior work - if (!Application::getInstance()->getControllerScriptingInterface()->isJoystickCaptured(palm.getSixenseID())) { - if (palm.isActive() && (palm.getSixenseID() == THRUST_CONTROLLER)) { - if (palm.getJoystickY() != 0.f) { - FingerData& finger = palm.getFingers()[0]; - if (finger.isActive()) { - } - _thrust += front * _scale * THRUST_MAG_HAND_JETS * palm.getJoystickY() * _thrustMultiplier * deltaTime; - } - if (palm.getJoystickX() != 0.f) { - _thrust += right * _scale * THRUST_MAG_HAND_JETS * palm.getJoystickX() * _thrustMultiplier * deltaTime; - } - } else if (palm.isActive() && (palm.getSixenseID() == VIEW_CONTROLLER)) { - if (palm.getJoystickX() != 0.f) { - _bodyYawDelta -= palm.getJoystickX() * JOYSTICK_YAW_MAG * deltaTime; - } - if (palm.getJoystickY() != 0.f) { - getHand().setPitchUpdate(getHand().getPitchUpdate() + - (palm.getJoystickY() * JOYSTICK_PITCH_MAG * deltaTime)); - } - } - } - - } // Update speed brake status const float MIN_SPEED_BRAKE_VELOCITY = _scale * 0.4f; @@ -906,7 +842,6 @@ void MyAvatar::updateCollisionWithEnvironment(float deltaTime, float radius) { } } - void MyAvatar::updateCollisionWithVoxels(float deltaTime, float radius) { const float VOXEL_ELASTICITY = 0.4f; const float VOXEL_DAMPING = 0.0f; @@ -976,7 +911,43 @@ void MyAvatar::updateCollisionSound(const glm::vec3 &penetration, float deltaTim } } -const float DEFAULT_HAND_RADIUS = 0.1f; +bool findAvatarAvatarPenetration(const glm::vec3 positionA, float radiusA, float heightA, + const glm::vec3 positionB, float radiusB, float heightB, glm::vec3& penetration) { + glm::vec3 positionBA = positionB - positionA; + float xzDistance = sqrt(positionBA.x * positionBA.x + positionBA.z * positionBA.z); + if (xzDistance < (radiusA + radiusB)) { + float yDistance = fabs(positionBA.y); + float halfHeights = 0.5 * (heightA + heightB); + if (yDistance < halfHeights) { + // cylinders collide + if (xzDistance > 0.f) { + positionBA.y = 0.f; + // note, penetration should point from A into B + penetration = positionBA * ((radiusA + radiusB - xzDistance) / xzDistance); + return true; + } else { + // exactly coaxial -- we'll return false for this case + return false; + } + } else if (yDistance < halfHeights + radiusA + radiusB) { + // caps collide + if (positionBA.y < 0.f) { + // A is above B + positionBA.y += halfHeights; + float BA = glm::length(positionBA); + penetration = positionBA * (radiusA + radiusB - BA) / BA; + return true; + } else { + // A is below B + positionBA.y -= halfHeights; + float BA = glm::length(positionBA); + penetration = positionBA * (radiusA + radiusB - BA) / BA; + return true; + } + } + } + return false; +} void MyAvatar::updateCollisionWithAvatars(float deltaTime) { // Reset detector for nearest avatar @@ -986,7 +957,14 @@ void MyAvatar::updateCollisionWithAvatars(float deltaTime) { // no need to compute a bunch of stuff if we have one or fewer avatars return; } - float myRadius = getHeight(); + float myBoundingRadius = 0.5f * getHeight(); + + // HACK: body-body collision uses two coaxial capsules with axes parallel to y-axis + // TODO: make the collision work without assuming avatar orientation + Extents myStaticExtents = _skeletonModel.getStaticExtents(); + glm::vec3 staticScale = myStaticExtents.maximum - myStaticExtents.minimum; + float myCapsuleRadius = 0.25f * (staticScale.x + staticScale.z); + float myCapsuleHeight = staticScale.y; CollisionInfo collisionInfo; foreach (const AvatarSharedPointer& avatarPointer, avatars) { @@ -999,9 +977,25 @@ void MyAvatar::updateCollisionWithAvatars(float deltaTime) { if (_distanceToNearestAvatar > distance) { _distanceToNearestAvatar = distance; } - float theirRadius = avatar->getHeight(); - if (distance < myRadius + theirRadius) { - // TODO: Andrew to make avatar-avatar capsule collisions work here + float theirBoundingRadius = 0.5f * avatar->getHeight(); + if (distance < myBoundingRadius + theirBoundingRadius) { + Extents theirStaticExtents = _skeletonModel.getStaticExtents(); + glm::vec3 staticScale = theirStaticExtents.maximum - theirStaticExtents.minimum; + float theirCapsuleRadius = 0.25f * (staticScale.x + staticScale.z); + float theirCapsuleHeight = staticScale.y; + + glm::vec3 penetration(0.f); + if (findAvatarAvatarPenetration(_position, myCapsuleRadius, myCapsuleHeight, + avatar->getPosition(), theirCapsuleRadius, theirCapsuleHeight, penetration)) { + // move the avatar out by half the penetration + setPosition(_position - 0.5f * penetration); + } + + // collide our hands against them + _hand.collideAgainstAvatar(avatar, true); + + // collide their hands against us + avatar->getHand().collideAgainstAvatar(this, false); } } } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 36448a5529..1bc5de204b 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -34,7 +34,7 @@ public: void reset(); void update(float deltaTime); void simulate(float deltaTime); - void updateFromGyros(bool turnWithHead); + void updateFromGyros(float deltaTime); void updateTransmitter(float deltaTime); void render(bool forceRenderHead); @@ -45,7 +45,6 @@ public: // setters void setMousePressed(bool mousePressed) { _mousePressed = mousePressed; } - void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } void setVelocity(const glm::vec3 velocity) { _velocity = velocity; } void setLeanScale(float scale) { _leanScale = scale; } void setGravity(glm::vec3 gravity); @@ -78,9 +77,6 @@ public: static void sendKillAvatar(); - // Set/Get update the thrust that will move the avatar around - void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; - glm::vec3 getThrust() { return _thrust; }; void orbit(const glm::vec3& position, int deltaX, int deltaY); @@ -93,9 +89,13 @@ public slots: void increaseSize(); void decreaseSize(); void resetSize(); - void sendIdentityPacket(); + // Set/Get update the thrust that will move the avatar around + void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; + glm::vec3 getThrust() { return _thrust; }; + void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } + private: bool _mousePressed; float _bodyPitchDelta; diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index ac08c52b49..9bf2e0f727 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -21,9 +21,9 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar) : void SkeletonModel::simulate(float deltaTime) { if (!isActive()) { + Model::simulate(deltaTime); return; } - setTranslation(_owningAvatar->getPosition()); setRotation(_owningAvatar->getOrientation() * glm::angleAxis(180.0f, 0.0f, 1.0f, 0.0f)); const float MODEL_SCALE = 0.0006f; @@ -36,23 +36,24 @@ void SkeletonModel::simulate(float deltaTime) { HandData& hand = _owningAvatar->getHand(); hand.getLeftRightPalmIndices(leftPalmIndex, rightPalmIndex); - const float HAND_RESTORATION_RATE = 0.25f; + const float HAND_RESTORATION_PERIOD = 1.f; // seconds + float handRestorePercent = glm::clamp(deltaTime / HAND_RESTORATION_PERIOD, 0.f, 1.f); const FBXGeometry& geometry = _geometry->getFBXGeometry(); if (leftPalmIndex == -1) { // no Leap data; set hands from mouse if (_owningAvatar->getHandState() == HAND_STATE_NULL) { - restoreRightHandPosition(HAND_RESTORATION_RATE); + restoreRightHandPosition(handRestorePercent); } else { applyHandPosition(geometry.rightHandJointIndex, _owningAvatar->getHandPosition()); } - restoreLeftHandPosition(HAND_RESTORATION_RATE); + restoreLeftHandPosition(handRestorePercent); } else if (leftPalmIndex == rightPalmIndex) { // right hand only applyPalmData(geometry.rightHandJointIndex, geometry.rightFingerJointIndices, geometry.rightFingertipJointIndices, hand.getPalms()[leftPalmIndex]); - restoreLeftHandPosition(HAND_RESTORATION_RATE); + restoreLeftHandPosition(handRestorePercent); } else { applyPalmData(geometry.leftHandJointIndex, geometry.leftFingerJointIndices, geometry.leftFingertipJointIndices, diff --git a/interface/src/renderer/FBXReader.cpp b/interface/src/renderer/FBXReader.cpp index b4e7c4abc5..35512d88da 100644 --- a/interface/src/renderer/FBXReader.cpp +++ b/interface/src/renderer/FBXReader.cpp @@ -1274,6 +1274,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) geometry.bindExtents.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX); geometry.bindExtents.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); + geometry.staticExtents.minimum = glm::vec3(FLT_MAX, FLT_MAX, FLT_MAX); + geometry.staticExtents.maximum = glm::vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX); QVariantHash springs = mapping.value("spring").toHash(); QVariant defaultSpring = springs.value("default"); @@ -1430,6 +1432,8 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) boneDirection /= boneLength; } } + bool jointIsStatic = joint.freeLineage.isEmpty(); + glm::vec3 jointTranslation = extractTranslation(geometry.offset * joint.bindTransform); float radiusScale = extractUniformScale(joint.transform * fbxCluster.inverseBindMatrix); float totalWeight = 0.0f; for (int j = 0; j < cluster.indices.size(); j++) { @@ -1447,6 +1451,11 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) joint.boneRadius = glm::max(joint.boneRadius, radiusScale * glm::distance( vertex, boneEnd + boneDirection * proj)); } + if (jointIsStatic) { + // expand the extents of static (nonmovable) joints + geometry.staticExtents.minimum = glm::min(geometry.staticExtents.minimum, vertex + jointTranslation); + geometry.staticExtents.maximum = glm::max(geometry.staticExtents.maximum, vertex + jointTranslation); + } } // look for an unused slot in the weights vector @@ -1568,14 +1577,16 @@ FBXGeometry extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping) return geometry; } -FBXGeometry readFBX(const QByteArray& model, const QByteArray& mapping) { - QBuffer modelBuffer(const_cast(&model)); - modelBuffer.open(QIODevice::ReadOnly); +QVariantHash readMapping(const QByteArray& data) { + QBuffer buffer(const_cast(&data)); + buffer.open(QIODevice::ReadOnly); + return parseMapping(&buffer); +} - QBuffer mappingBuffer(const_cast(&mapping)); - mappingBuffer.open(QIODevice::ReadOnly); - - return extractFBXGeometry(parseFBX(&modelBuffer), parseMapping(&mappingBuffer)); +FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping) { + QBuffer buffer(const_cast(&model)); + buffer.open(QIODevice::ReadOnly); + return extractFBXGeometry(parseFBX(&buffer), mapping); } bool addMeshVoxelsOperation(OctreeElement* element, void* extraData) { diff --git a/interface/src/renderer/FBXReader.h b/interface/src/renderer/FBXReader.h index d700439460..8efb23c98c 100644 --- a/interface/src/renderer/FBXReader.h +++ b/interface/src/renderer/FBXReader.h @@ -159,13 +159,17 @@ public: glm::vec3 neckPivot; Extents bindExtents; + Extents staticExtents; QVector attachments; }; +/// Reads an FST mapping from the supplied data. +QVariantHash readMapping(const QByteArray& data); + /// Reads FBX geometry from the supplied model and mapping data. /// \exception QString if an error occurs in parsing -FBXGeometry readFBX(const QByteArray& model, const QByteArray& mapping); +FBXGeometry readFBX(const QByteArray& model, const QVariantHash& mapping); /// Reads SVO geometry from the supplied model data. FBXGeometry readSVO(const QByteArray& model); diff --git a/interface/src/renderer/GeometryCache.cpp b/interface/src/renderer/GeometryCache.cpp index 3526fa5050..78cc657018 100644 --- a/interface/src/renderer/GeometryCache.cpp +++ b/interface/src/renderer/GeometryCache.cpp @@ -7,11 +7,7 @@ #include -// include this before QOpenGLBuffer, which includes an earlier version of OpenGL -#include "InterfaceConfig.h" - #include -#include #include #include "Application.h" @@ -298,46 +294,86 @@ QSharedPointer GeometryCache::getGeometry(const QUrl& url, cons if (geometry.isNull()) { geometry = QSharedPointer(new NetworkGeometry(url, fallback.isValid() ? getGeometry(fallback) : QSharedPointer())); + geometry->setLODParent(geometry); _networkGeometry.insert(url, geometry); } return geometry; } -NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer& fallback) : - _modelRequest(url), - _modelReply(NULL), - _mappingReply(NULL), +const float NetworkGeometry::NO_HYSTERESIS = -1.0f; + +NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer& fallback, + const QVariantHash& mapping, const QUrl& textureBase) : + _request(url), + _reply(NULL), + _mapping(mapping), + _textureBase(textureBase.isValid() ? textureBase : url), _fallback(fallback), - _attempts(0) -{ + _startedLoading(false), + _failedToLoad(false), + _attempts(0) { + if (!url.isValid()) { return; } - _modelRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); - makeModelRequest(); + _request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); - QUrl mappingURL = url; - QString path = url.path(); - mappingURL.setPath(path.left(path.lastIndexOf('.')) + ".fst"); - QNetworkRequest mappingRequest(mappingURL); - mappingRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); - _mappingReply = Application::getInstance()->getNetworkAccessManager()->get(mappingRequest); - - connect(_mappingReply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(maybeReadModelWithMapping())); - connect(_mappingReply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(handleMappingReplyError())); + // if we already have a mapping (because we're an LOD), hold off on loading until we're requested + if (mapping.isEmpty()) { + makeRequest(); + } } NetworkGeometry::~NetworkGeometry() { - if (_modelReply != NULL) { - delete _modelReply; + if (_reply != NULL) { + delete _reply; } - if (_mappingReply != NULL) { - delete _mappingReply; +} + +QSharedPointer NetworkGeometry::getLODOrFallback(float distance, float& hysteresis) const { + if (_lodParent.data() != this) { + return _lodParent.data()->getLODOrFallback(distance, hysteresis); } - foreach (const NetworkMesh& mesh, _meshes) { - glDeleteBuffers(1, &mesh.indexBufferID); - glDeleteBuffers(1, &mesh.vertexBufferID); - } + if (_failedToLoad && _fallback) { + return _fallback; + } + QSharedPointer lod = _lodParent; + float lodDistance = 0.0f; + QMap >::const_iterator it = _lods.upperBound(distance); + if (it != _lods.constBegin()) { + it = it - 1; + lod = it.value(); + lodDistance = it.key(); + } + if (hysteresis != NO_HYSTERESIS && hysteresis != lodDistance) { + // if we previously selected a different distance, make sure we've moved far enough to justify switching + const float HYSTERESIS_PROPORTION = 0.1f; + if (glm::abs(distance - qMax(hysteresis, lodDistance)) / fabsf(hysteresis - lodDistance) < HYSTERESIS_PROPORTION) { + return getLODOrFallback(hysteresis, hysteresis); + } + } + if (lod->isLoaded()) { + hysteresis = lodDistance; + return lod; + } + // if the ideal LOD isn't loaded, we need to make sure it's started to load, and possibly return the closest loaded one + if (!lod->_startedLoading) { + lod->makeRequest(); + } + float closestDistance = FLT_MAX; + if (isLoaded()) { + lod = _lodParent; + closestDistance = distance; + } + for (it = _lods.constBegin(); it != _lods.constEnd(); it++) { + float distanceToLOD = glm::abs(distance - it.key()); + if (it.value()->isLoaded() && distanceToLOD < closestDistance) { + lod = it.value(); + closestDistance = distanceToLOD; + } + } + hysteresis = NO_HYSTERESIS; + return lod; } glm::vec4 NetworkGeometry::computeAverageColor() const { @@ -364,20 +400,167 @@ glm::vec4 NetworkGeometry::computeAverageColor() const { return (totalTriangles == 0) ? glm::vec4(1.0f, 1.0f, 1.0f, 1.0f) : totalColor / totalTriangles; } -void NetworkGeometry::makeModelRequest() { - _modelReply = Application::getInstance()->getNetworkAccessManager()->get(_modelRequest); +void NetworkGeometry::makeRequest() { + _startedLoading = true; + _reply = Application::getInstance()->getNetworkAccessManager()->get(_request); - connect(_modelReply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(maybeReadModelWithMapping())); - connect(_modelReply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(handleModelReplyError())); + connect(_reply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(handleDownloadProgress(qint64,qint64))); + connect(_reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(handleReplyError())); } -void NetworkGeometry::handleModelReplyError() { - QDebug debug = qDebug() << _modelReply->errorString(); +void NetworkGeometry::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + if (!_reply->isFinished()) { + return; + } - QNetworkReply::NetworkError error = _modelReply->error(); - _modelReply->disconnect(this); - _modelReply->deleteLater(); - _modelReply = NULL; + QUrl url = _reply->url(); + QByteArray data = _reply->readAll(); + _reply->disconnect(this); + _reply->deleteLater(); + _reply = NULL; + + if (url.path().toLower().endsWith(".fst")) { + // it's a mapping file; parse it and get the mesh filename + _mapping = readMapping(data); + QString filename = _mapping.value("filename").toString(); + if (filename.isNull()) { + qDebug() << "Mapping file " << url << " has no filename."; + _failedToLoad = true; + + } else { + QString texdir = _mapping.value("texdir").toString(); + if (!texdir.isNull()) { + if (!texdir.endsWith('/')) { + texdir += '/'; + } + _textureBase = url.resolved(texdir); + } + QVariantHash lods = _mapping.value("lod").toHash(); + for (QVariantHash::const_iterator it = lods.begin(); it != lods.end(); it++) { + QSharedPointer geometry(new NetworkGeometry(url.resolved(it.key()), + QSharedPointer(), _mapping, _textureBase)); + geometry->setLODParent(_lodParent); + _lods.insert(it.value().toFloat(), geometry); + } + _request.setUrl(url.resolved(filename)); + + // make the request immediately only if we have no LODs to switch between + _startedLoading = false; + if (_lods.isEmpty()) { + makeRequest(); + } + } + return; + } + + try { + _geometry = url.path().toLower().endsWith(".svo") ? readSVO(data) : readFBX(data, _mapping); + + } catch (const QString& error) { + qDebug() << "Error reading " << url << ": " << error; + _failedToLoad = true; + return; + } + + foreach (const FBXMesh& mesh, _geometry.meshes) { + NetworkMesh networkMesh = { QOpenGLBuffer(QOpenGLBuffer::IndexBuffer), QOpenGLBuffer(QOpenGLBuffer::VertexBuffer) }; + + int totalIndices = 0; + foreach (const FBXMeshPart& part, mesh.parts) { + NetworkMeshPart networkPart; + if (!part.diffuseFilename.isEmpty()) { + networkPart.diffuseTexture = Application::getInstance()->getTextureCache()->getTexture( + _textureBase.resolved(QUrl(part.diffuseFilename)), false, mesh.isEye); + } + if (!part.normalFilename.isEmpty()) { + networkPart.normalTexture = Application::getInstance()->getTextureCache()->getTexture( + _textureBase.resolved(QUrl(part.normalFilename)), true); + } + networkMesh.parts.append(networkPart); + + totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); + } + + networkMesh.indexBuffer.create(); + networkMesh.indexBuffer.bind(); + networkMesh.indexBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw); + networkMesh.indexBuffer.allocate(totalIndices * sizeof(int)); + int offset = 0; + foreach (const FBXMeshPart& part, mesh.parts) { + glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, offset, part.quadIndices.size() * sizeof(int), + part.quadIndices.constData()); + offset += part.quadIndices.size() * sizeof(int); + glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, offset, part.triangleIndices.size() * sizeof(int), + part.triangleIndices.constData()); + offset += part.triangleIndices.size() * sizeof(int); + } + networkMesh.indexBuffer.release(); + + networkMesh.vertexBuffer.create(); + networkMesh.vertexBuffer.bind(); + networkMesh.vertexBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw); + + // if we don't need to do any blending or springing, then the positions/normals can be static + if (mesh.blendshapes.isEmpty() && mesh.springiness == 0.0f) { + int normalsOffset = mesh.vertices.size() * sizeof(glm::vec3); + int tangentsOffset = normalsOffset + mesh.normals.size() * sizeof(glm::vec3); + int colorsOffset = tangentsOffset + mesh.tangents.size() * sizeof(glm::vec3); + int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); + int clusterIndicesOffset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); + int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); + + networkMesh.vertexBuffer.allocate(clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4)); + networkMesh.vertexBuffer.write(0, mesh.vertices.constData(), mesh.vertices.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(normalsOffset, mesh.normals.constData(), mesh.normals.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(tangentsOffset, mesh.tangents.constData(), + mesh.tangents.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(colorsOffset, mesh.colors.constData(), mesh.colors.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(texCoordsOffset, mesh.texCoords.constData(), + mesh.texCoords.size() * sizeof(glm::vec2)); + networkMesh.vertexBuffer.write(clusterIndicesOffset, mesh.clusterIndices.constData(), + mesh.clusterIndices.size() * sizeof(glm::vec4)); + networkMesh.vertexBuffer.write(clusterWeightsOffset, mesh.clusterWeights.constData(), + mesh.clusterWeights.size() * sizeof(glm::vec4)); + + // if there's no springiness, then the cluster indices/weights can be static + } else if (mesh.springiness == 0.0f) { + int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); + int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); + int clusterIndicesOffset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); + int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); + networkMesh.vertexBuffer.allocate(clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4)); + networkMesh.vertexBuffer.write(0, mesh.tangents.constData(), mesh.tangents.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(colorsOffset, mesh.colors.constData(), mesh.colors.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(texCoordsOffset, mesh.texCoords.constData(), + mesh.texCoords.size() * sizeof(glm::vec2)); + networkMesh.vertexBuffer.write(clusterIndicesOffset, mesh.clusterIndices.constData(), + mesh.clusterIndices.size() * sizeof(glm::vec4)); + networkMesh.vertexBuffer.write(clusterWeightsOffset, mesh.clusterWeights.constData(), + mesh.clusterWeights.size() * sizeof(glm::vec4)); + + } else { + int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); + int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); + networkMesh.vertexBuffer.allocate(texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2)); + networkMesh.vertexBuffer.write(0, mesh.tangents.constData(), mesh.tangents.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(colorsOffset, mesh.colors.constData(), mesh.colors.size() * sizeof(glm::vec3)); + networkMesh.vertexBuffer.write(texCoordsOffset, mesh.texCoords.constData(), + mesh.texCoords.size() * sizeof(glm::vec2)); + } + + networkMesh.vertexBuffer.release(); + + _meshes.append(networkMesh); + } +} + +void NetworkGeometry::handleReplyError() { + QDebug debug = qDebug() << _reply->errorString(); + + QNetworkReply::NetworkError error = _reply->error(); + _reply->disconnect(this); + _reply->deleteLater(); + _reply = NULL; // retry for certain types of failures switch (error) { @@ -394,164 +577,19 @@ void NetworkGeometry::handleModelReplyError() { const int MAX_ATTEMPTS = 8; const int BASE_DELAY_MS = 1000; if (++_attempts < MAX_ATTEMPTS) { - QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeModelRequest())); + QTimer::singleShot(BASE_DELAY_MS * (int)pow(2.0, _attempts), this, SLOT(makeRequest())); debug << " -- retrying..."; return; } // fall through to final failure } default: - maybeLoadFallback(); + _failedToLoad = true; break; } } -void NetworkGeometry::handleMappingReplyError() { - _mappingReply->disconnect(this); - _mappingReply->deleteLater(); - _mappingReply = NULL; - - maybeReadModelWithMapping(); -} - -void NetworkGeometry::maybeReadModelWithMapping() { - if (_modelReply == NULL || !_modelReply->isFinished() || (_mappingReply != NULL && !_mappingReply->isFinished())) { - return; - } - - QUrl url = _modelReply->url(); - QByteArray model = _modelReply->readAll(); - _modelReply->disconnect(this); - _modelReply->deleteLater(); - _modelReply = NULL; - - QByteArray mapping; - if (_mappingReply != NULL) { - mapping = _mappingReply->readAll(); - _mappingReply->disconnect(this); - _mappingReply->deleteLater(); - _mappingReply = NULL; - } - - try { - _geometry = url.path().toLower().endsWith(".svo") ? readSVO(model) : readFBX(model, mapping); - - } catch (const QString& error) { - qDebug() << "Error reading " << url << ": " << error; - maybeLoadFallback(); - return; - } - - foreach (const FBXMesh& mesh, _geometry.meshes) { - NetworkMesh networkMesh; - - int totalIndices = 0; - foreach (const FBXMeshPart& part, mesh.parts) { - NetworkMeshPart networkPart; - QString basePath = url.path(); - basePath = basePath.left(basePath.lastIndexOf('/') + 1); - if (!part.diffuseFilename.isEmpty()) { - url.setPath(basePath + part.diffuseFilename); - networkPart.diffuseTexture = Application::getInstance()->getTextureCache()->getTexture(url, false, mesh.isEye); - } - if (!part.normalFilename.isEmpty()) { - url.setPath(basePath + part.normalFilename); - networkPart.normalTexture = Application::getInstance()->getTextureCache()->getTexture(url, true); - } - networkMesh.parts.append(networkPart); - - totalIndices += (part.quadIndices.size() + part.triangleIndices.size()); - } - - glGenBuffers(1, &networkMesh.indexBufferID); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, networkMesh.indexBufferID); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, totalIndices * sizeof(int), NULL, GL_STATIC_DRAW); - int offset = 0; - foreach (const FBXMeshPart& part, mesh.parts) { - glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, offset, part.quadIndices.size() * sizeof(int), - part.quadIndices.constData()); - offset += part.quadIndices.size() * sizeof(int); - glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, offset, part.triangleIndices.size() * sizeof(int), - part.triangleIndices.constData()); - offset += part.triangleIndices.size() * sizeof(int); - } - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - - glGenBuffers(1, &networkMesh.vertexBufferID); - glBindBuffer(GL_ARRAY_BUFFER, networkMesh.vertexBufferID); - - // if we don't need to do any blending or springing, then the positions/normals can be static - if (mesh.blendshapes.isEmpty() && mesh.springiness == 0.0f) { - int normalsOffset = mesh.vertices.size() * sizeof(glm::vec3); - int tangentsOffset = normalsOffset + mesh.normals.size() * sizeof(glm::vec3); - int colorsOffset = tangentsOffset + mesh.tangents.size() * sizeof(glm::vec3); - int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); - int clusterIndicesOffset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); - int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); - glBufferData(GL_ARRAY_BUFFER, clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4), - NULL, GL_STATIC_DRAW); - glBufferSubData(GL_ARRAY_BUFFER, 0, mesh.vertices.size() * sizeof(glm::vec3), mesh.vertices.constData()); - glBufferSubData(GL_ARRAY_BUFFER, normalsOffset, mesh.normals.size() * sizeof(glm::vec3), mesh.normals.constData()); - glBufferSubData(GL_ARRAY_BUFFER, tangentsOffset, mesh.tangents.size() * sizeof(glm::vec3), mesh.tangents.constData()); - glBufferSubData(GL_ARRAY_BUFFER, colorsOffset, mesh.colors.size() * sizeof(glm::vec3), mesh.colors.constData()); - glBufferSubData(GL_ARRAY_BUFFER, texCoordsOffset, mesh.texCoords.size() * sizeof(glm::vec2), - mesh.texCoords.constData()); - glBufferSubData(GL_ARRAY_BUFFER, clusterIndicesOffset, mesh.clusterIndices.size() * sizeof(glm::vec4), - mesh.clusterIndices.constData()); - glBufferSubData(GL_ARRAY_BUFFER, clusterWeightsOffset, mesh.clusterWeights.size() * sizeof(glm::vec4), - mesh.clusterWeights.constData()); - - // if there's no springiness, then the cluster indices/weights can be static - } else if (mesh.springiness == 0.0f) { - int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); - int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); - int clusterIndicesOffset = texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2); - int clusterWeightsOffset = clusterIndicesOffset + mesh.clusterIndices.size() * sizeof(glm::vec4); - glBufferData(GL_ARRAY_BUFFER, clusterWeightsOffset + mesh.clusterWeights.size() * sizeof(glm::vec4), - NULL, GL_STATIC_DRAW); - glBufferSubData(GL_ARRAY_BUFFER, 0, mesh.tangents.size() * sizeof(glm::vec3), mesh.tangents.constData()); - glBufferSubData(GL_ARRAY_BUFFER, colorsOffset, mesh.colors.size() * sizeof(glm::vec3), mesh.colors.constData()); - glBufferSubData(GL_ARRAY_BUFFER, texCoordsOffset, mesh.texCoords.size() * sizeof(glm::vec2), mesh.texCoords.constData()); - glBufferSubData(GL_ARRAY_BUFFER, clusterIndicesOffset, mesh.clusterIndices.size() * sizeof(glm::vec4), - mesh.clusterIndices.constData()); - glBufferSubData(GL_ARRAY_BUFFER, clusterWeightsOffset, mesh.clusterWeights.size() * sizeof(glm::vec4), - mesh.clusterWeights.constData()); - - } else { - int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3); - int texCoordsOffset = colorsOffset + mesh.colors.size() * sizeof(glm::vec3); - glBufferData(GL_ARRAY_BUFFER, texCoordsOffset + mesh.texCoords.size() * sizeof(glm::vec2), NULL, GL_STATIC_DRAW); - glBufferSubData(GL_ARRAY_BUFFER, 0, mesh.tangents.size() * sizeof(glm::vec3), mesh.tangents.constData()); - glBufferSubData(GL_ARRAY_BUFFER, colorsOffset, mesh.colors.size() * sizeof(glm::vec3), mesh.colors.constData()); - glBufferSubData(GL_ARRAY_BUFFER, texCoordsOffset, mesh.texCoords.size() * sizeof(glm::vec2), - mesh.texCoords.constData()); - } - - glBindBuffer(GL_ARRAY_BUFFER, 0); - - _meshes.append(networkMesh); - } - - emit loaded(); -} - -void NetworkGeometry::loadFallback() { - _geometry = _fallback->_geometry; - _meshes = _fallback->_meshes; - emit loaded(); -} - -void NetworkGeometry::maybeLoadFallback() { - if (_fallback) { - if (_fallback->isLoaded()) { - loadFallback(); - } else { - connect(_fallback.data(), SIGNAL(loaded()), SLOT(loadFallback())); - } - } -} - bool NetworkMeshPart::isTranslucent() const { return diffuseTexture && diffuseTexture->isTranslucent(); } diff --git a/interface/src/renderer/GeometryCache.h b/interface/src/renderer/GeometryCache.h index 618796e907..ef53fe9c3e 100644 --- a/interface/src/renderer/GeometryCache.h +++ b/interface/src/renderer/GeometryCache.h @@ -9,17 +9,20 @@ #ifndef __interface__GeometryCache__ #define __interface__GeometryCache__ +// include this before QOpenGLBuffer, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + #include +#include #include #include +#include #include #include #include "FBXReader.h" -#include "InterfaceConfig.h" class QNetworkReply; -class QOpenGLBuffer; class NetworkGeometry; class NetworkMesh; @@ -59,41 +62,52 @@ class NetworkGeometry : public QObject { public: - NetworkGeometry(const QUrl& url, const QSharedPointer& fallback); + /// A hysteresis value indicating that we have no state memory. + static const float NO_HYSTERESIS; + + NetworkGeometry(const QUrl& url, const QSharedPointer& fallback, + const QVariantHash& mapping = QVariantHash(), const QUrl& textureBase = QUrl()); ~NetworkGeometry(); + /// Checks whether the geometry is fulled loaded. bool isLoaded() const { return !_geometry.joints.isEmpty(); } + /// Returns a pointer to the geometry appropriate for the specified distance. + /// \param hysteresis a hysteresis parameter that prevents rapid model switching + QSharedPointer getLODOrFallback(float distance, float& hysteresis) const; + const FBXGeometry& getFBXGeometry() const { return _geometry; } const QVector& getMeshes() const { return _meshes; } /// Returns the average color of all meshes in the geometry. glm::vec4 computeAverageColor() const; -signals: - - void loaded(); - private slots: - void makeModelRequest(); - void handleModelReplyError(); - void handleMappingReplyError(); - void maybeReadModelWithMapping(); - void loadFallback(); + void makeRequest(); + void handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void handleReplyError(); private: - void maybeLoadFallback(); + friend class GeometryCache; - QNetworkRequest _modelRequest; - QNetworkReply* _modelReply; - QNetworkReply* _mappingReply; + void setLODParent(const QWeakPointer& lodParent) { _lodParent = lodParent; } + + QNetworkRequest _request; + QNetworkReply* _reply; + QVariantHash _mapping; + QUrl _textureBase; QSharedPointer _fallback; + bool _startedLoading; + bool _failedToLoad; int _attempts; + QMap > _lods; FBXGeometry _geometry; QVector _meshes; + + QWeakPointer _lodParent; }; /// The state associated with a single mesh part. @@ -110,8 +124,8 @@ public: class NetworkMesh { public: - GLuint indexBufferID; - GLuint vertexBufferID; + QOpenGLBuffer indexBuffer; + QOpenGLBuffer vertexBuffer; QVector parts; diff --git a/interface/src/renderer/Model.cpp b/interface/src/renderer/Model.cpp index b14ed1036d..8c00842ea2 100644 --- a/interface/src/renderer/Model.cpp +++ b/interface/src/renderer/Model.cpp @@ -90,6 +90,16 @@ void Model::reset() { } void Model::simulate(float deltaTime) { + // update our LOD + if (_geometry) { + QSharedPointer geometry = _geometry->getLODOrFallback(glm::distance(_translation, + Application::getInstance()->getCamera()->getPosition()), _lodHysteresis); + if (_geometry != geometry) { + deleteGeometry(); + _dilatedTextures.clear(); + _geometry = geometry; + } + } if (!isActive()) { return; } @@ -305,6 +315,15 @@ Extents Model::getBindExtents() const { return scaledExtents; } +Extents Model::getStaticExtents() const { + if (!isActive()) { + return Extents(); + } + const Extents& staticExtents = _geometry->getFBXGeometry().staticExtents; + Extents scaledExtents = { staticExtents.minimum * _scale, staticExtents.maximum * _scale }; + return scaledExtents; +} + int Model::getParentJointIndex(int jointIndex) const { return (isActive() && jointIndex != -1) ? _geometry->getFBXGeometry().joints.at(jointIndex).parentIndex : -1; } @@ -400,8 +419,9 @@ void Model::setURL(const QUrl& url, const QUrl& fallback) { // delete our local geometry and custom textures deleteGeometry(); _dilatedTextures.clear(); + _lodHysteresis = NetworkGeometry::NO_HYSTERESIS; - _geometry = Application::getInstance()->getGeometryCache()->getGeometry(url, fallback); + _baseGeometry = _geometry = Application::getInstance()->getGeometryCache()->getGeometry(url, fallback); } glm::vec4 Model::computeAverageColor() const { @@ -468,7 +488,10 @@ bool Model::findSphereCollision(const glm::vec3& penetratorCenter, float penetra if (findSphereCapsuleConePenetration(relativeCenter, penetratorRadius, start, end, startRadius, endRadius, bonePenetration)) { totalPenetration = addPenetrations(totalPenetration, bonePenetration); - // TODO: Andrew to try to keep the joint furthest toward the root + // BUG: we currently overwrite the jointIndex with the last one found + // which can cause incorrect collisions when colliding against more than + // one joint. + // TODO: fix this. jointIndex = i; } outerContinue: ; @@ -713,7 +736,19 @@ void Model::renderCollisionProxies(float alpha) { glPopMatrix(); } -bool Model::poke(ModelCollisionInfo& collision) { +bool Model::collisionHitsMoveableJoint(ModelCollisionInfo& collision) const { + // the joint is pokable by a collision if it exists and is free to move + const FBXJoint& joint = _geometry->getFBXGeometry().joints[collision._jointIndex]; + if (joint.parentIndex == -1 || _jointStates.isEmpty()) { + return false; + } + // an empty freeLineage means the joint can't move + const FBXGeometry& geometry = _geometry->getFBXGeometry(); + const QVector& freeLineage = geometry.joints.at(collision._jointIndex).freeLineage; + return !freeLineage.isEmpty(); +} + +void Model::applyCollision(ModelCollisionInfo& collision) { // This needs work. At the moment it can wiggle joints that are free to move (such as arms) // but unmovable joints (such as torso) cannot be influenced at all. glm::vec3 jointPosition(0.f); @@ -737,11 +772,10 @@ bool Model::poke(ModelCollisionInfo& collision) { getJointPosition(jointIndex, end); glm::vec3 newEnd = start + glm::angleAxis(glm::degrees(angle), axis) * (end - start); // try to move it - return setJointPosition(jointIndex, newEnd, -1, true); + setJointPosition(jointIndex, newEnd, -1, true); } } } - return false; } void Model::deleteGeometry() { @@ -768,13 +802,17 @@ void Model::renderMeshes(float alpha, bool translucent) { (networkMesh.getTranslucentPartCount() == networkMesh.parts.size())) { continue; } - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, networkMesh.indexBufferID); - + const_cast(networkMesh.indexBuffer).bind(); + const FBXMesh& mesh = geometry.meshes.at(i); int vertexCount = mesh.vertices.size(); + if (vertexCount == 0) { + // sanity check + continue; + } + + const_cast(networkMesh.vertexBuffer).bind(); - glBindBuffer(GL_ARRAY_BUFFER, networkMesh.vertexBufferID); - ProgramObject* program = &_program; ProgramObject* skinProgram = &_skinProgram; SkinLocations* skinLocations = &_skinLocations; @@ -881,11 +919,11 @@ void Model::renderMeshes(float alpha, bool translucent) { qint64 offset = 0; for (int j = 0; j < networkMesh.parts.size(); j++) { const NetworkMeshPart& networkPart = networkMesh.parts.at(j); + const FBXMeshPart& part = mesh.parts.at(j); if (networkPart.isTranslucent() != translucent) { + offset += (part.quadIndices.size() + part.triangleIndices.size()) * sizeof(int); continue; } - const FBXMeshPart& part = mesh.parts.at(j); - // apply material properties glm::vec4 diffuse = glm::vec4(part.diffuseColor, alpha); glm::vec4 specular = glm::vec4(part.specularColor, alpha); diff --git a/interface/src/renderer/Model.h b/interface/src/renderer/Model.h index 423b2a4c81..41435c00b7 100644 --- a/interface/src/renderer/Model.h +++ b/interface/src/renderer/Model.h @@ -66,6 +66,9 @@ public: /// Returns the extents of the model in its bind pose. Extents getBindExtents() const; + + /// Returns the extents of the unmovable joints of the model. + Extents getStaticExtents() const; /// Returns a reference to the shared geometry. const QSharedPointer& getGeometry() const { return _geometry; } @@ -164,9 +167,12 @@ public: void renderCollisionProxies(float alpha); + /// \return true if the collision is against a moveable joint + bool collisionHitsMoveableJoint(ModelCollisionInfo& collision) const; + /// \param collisionInfo info about the collision - /// \return true if collision affects the Model - bool poke(ModelCollisionInfo& collisionInfo); + /// Use the collisionInfo to affect the model + void applyCollision(ModelCollisionInfo& collisionInfo); protected: @@ -230,6 +236,9 @@ private: void deleteGeometry(); void renderMeshes(float alpha, bool translucent); + QSharedPointer _baseGeometry; + float _lodHysteresis; + float _pupilDilation; std::vector _blendshapeCoefficients; diff --git a/interface/src/ui/Base3DOverlay.cpp b/interface/src/ui/Base3DOverlay.cpp new file mode 100644 index 0000000000..67e7ea25f2 --- /dev/null +++ b/interface/src/ui/Base3DOverlay.cpp @@ -0,0 +1,61 @@ +// +// Base3DOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "Base3DOverlay.h" +#include "TextRenderer.h" + +const glm::vec3 DEFAULT_POSITION = glm::vec3(0.0f, 0.0f, 0.0f); +const float DEFAULT_LINE_WIDTH = 1.0f; + +Base3DOverlay::Base3DOverlay() : + _position(DEFAULT_POSITION), + _lineWidth(DEFAULT_LINE_WIDTH) +{ +} + +Base3DOverlay::~Base3DOverlay() { +} + +void Base3DOverlay::setProperties(const QScriptValue& properties) { + Overlay::setProperties(properties); + + QScriptValue position = properties.property("position"); + + // if "position" property was not there, check to see if they included aliases: start, point, p1 + if (!position.isValid()) { + position = properties.property("start"); + if (!position.isValid()) { + position = properties.property("p1"); + if (!position.isValid()) { + position = properties.property("point"); + } + } + } + + if (position.isValid()) { + QScriptValue x = position.property("x"); + QScriptValue y = position.property("y"); + QScriptValue z = position.property("z"); + if (x.isValid() && y.isValid() && z.isValid()) { + glm::vec3 newPosition; + newPosition.x = x.toVariant().toFloat(); + newPosition.y = y.toVariant().toFloat(); + newPosition.z = z.toVariant().toFloat(); + setPosition(newPosition); + } + } + + if (properties.property("lineWidth").isValid()) { + setLineWidth(properties.property("lineWidth").toVariant().toFloat()); + } +} diff --git a/interface/src/ui/Base3DOverlay.h b/interface/src/ui/Base3DOverlay.h new file mode 100644 index 0000000000..286193393c --- /dev/null +++ b/interface/src/ui/Base3DOverlay.h @@ -0,0 +1,36 @@ +// +// Base3DOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Base3DOverlay__ +#define __interface__Base3DOverlay__ + +#include "Overlay.h" + +class Base3DOverlay : public Overlay { + Q_OBJECT + +public: + Base3DOverlay(); + ~Base3DOverlay(); + + // getters + const glm::vec3& getPosition() const { return _position; } + float getLineWidth() const { return _lineWidth; } + + // setters + void setPosition(const glm::vec3& position) { _position = position; } + void setLineWidth(float lineWidth) { _lineWidth = lineWidth; } + + virtual void setProperties(const QScriptValue& properties); + +protected: + glm::vec3 _position; + float _lineWidth; +}; + + +#endif /* defined(__interface__Base3DOverlay__) */ diff --git a/interface/src/ui/Cube3DOverlay.cpp b/interface/src/ui/Cube3DOverlay.cpp new file mode 100644 index 0000000000..992a18e451 --- /dev/null +++ b/interface/src/ui/Cube3DOverlay.cpp @@ -0,0 +1,44 @@ +// +// Cube3DOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "Cube3DOverlay.h" + +Cube3DOverlay::Cube3DOverlay() { +} + +Cube3DOverlay::~Cube3DOverlay() { +} + +void Cube3DOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + + const float MAX_COLOR = 255; + glColor4f(_color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, _alpha); + + + glDisable(GL_LIGHTING); + glPushMatrix(); + glTranslatef(_position.x + _size * 0.5f, + _position.y + _size * 0.5f, + _position.z + _size * 0.5f); + glLineWidth(_lineWidth); + if (_isSolid) { + glutSolidCube(_size); + } else { + glutWireCube(_size); + } + glPopMatrix(); + +} diff --git a/interface/src/ui/Cube3DOverlay.h b/interface/src/ui/Cube3DOverlay.h new file mode 100644 index 0000000000..a1705d47d0 --- /dev/null +++ b/interface/src/ui/Cube3DOverlay.h @@ -0,0 +1,23 @@ +// +// Cube3DOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Cube3DOverlay__ +#define __interface__Cube3DOverlay__ + +#include "Volume3DOverlay.h" + +class Cube3DOverlay : public Volume3DOverlay { + Q_OBJECT + +public: + Cube3DOverlay(); + ~Cube3DOverlay(); + virtual void render(); +}; + + +#endif /* defined(__interface__Cube3DOverlay__) */ diff --git a/interface/src/ui/ImageOverlay.cpp b/interface/src/ui/ImageOverlay.cpp new file mode 100644 index 0000000000..178383749b --- /dev/null +++ b/interface/src/ui/ImageOverlay.cpp @@ -0,0 +1,145 @@ +// +// ImageOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include + +#include "ImageOverlay.h" + +ImageOverlay::ImageOverlay() : + _textureID(0), + _renderImage(false), + _textureBound(false), + _wantClipFromImage(false) +{ +} + +ImageOverlay::~ImageOverlay() { + if (_parent && _textureID) { + // do we need to call this? + //_parent->deleteTexture(_textureID); + } +} + +// TODO: handle setting image multiple times, how do we manage releasing the bound texture? +void ImageOverlay::setImageURL(const QUrl& url) { + // TODO: are we creating too many QNetworkAccessManager() when multiple calls to setImageURL are made? + QNetworkAccessManager* manager = new QNetworkAccessManager(this); + connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply*))); + manager->get(QNetworkRequest(url)); +} + +void ImageOverlay::replyFinished(QNetworkReply* reply) { + + // replace our byte array with the downloaded data + QByteArray rawData = reply->readAll(); + _textureImage.loadFromData(rawData); + _renderImage = true; + +} + +void ImageOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + if (_renderImage && !_textureBound) { + _textureID = _parent->bindTexture(_textureImage); + _textureBound = true; + } + + if (_renderImage) { + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, _textureID); + } + const float MAX_COLOR = 255; + glColor4f(_color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, _alpha); + + float imageWidth = _textureImage.width(); + float imageHeight = _textureImage.height(); + + QRect fromImage; + if (_wantClipFromImage) { + fromImage = _fromImage; + } else { + fromImage.setX(0); + fromImage.setY(0); + fromImage.setWidth(imageWidth); + fromImage.setHeight(imageHeight); + } + float x = fromImage.x() / imageWidth; + float y = fromImage.y() / imageHeight; + float w = fromImage.width() / imageWidth; // ?? is this what we want? not sure + float h = fromImage.height() / imageHeight; + + glBegin(GL_QUADS); + if (_renderImage) { + glTexCoord2f(x, 1.0f - y); + } + glVertex2f(_bounds.left(), _bounds.top()); + + if (_renderImage) { + glTexCoord2f(x + w, 1.0f - y); + } + glVertex2f(_bounds.right(), _bounds.top()); + + if (_renderImage) { + glTexCoord2f(x + w, 1.0f - (y + h)); + } + glVertex2f(_bounds.right(), _bounds.bottom()); + + if (_renderImage) { + glTexCoord2f(x, 1.0f - (y + h)); + } + glVertex2f(_bounds.left(), _bounds.bottom()); + glEnd(); + if (_renderImage) { + glDisable(GL_TEXTURE_2D); + } +} + +void ImageOverlay::setProperties(const QScriptValue& properties) { + Overlay2D::setProperties(properties); + + QScriptValue subImageBounds = properties.property("subImage"); + if (subImageBounds.isValid()) { + QRect oldSubImageRect = _fromImage; + QRect subImageRect = _fromImage; + if (subImageBounds.property("x").isValid()) { + subImageRect.setX(subImageBounds.property("x").toVariant().toInt()); + } else { + subImageRect.setX(oldSubImageRect.x()); + } + if (subImageBounds.property("y").isValid()) { + subImageRect.setY(subImageBounds.property("y").toVariant().toInt()); + } else { + subImageRect.setY(oldSubImageRect.y()); + } + if (subImageBounds.property("width").isValid()) { + subImageRect.setWidth(subImageBounds.property("width").toVariant().toInt()); + } else { + subImageRect.setWidth(oldSubImageRect.width()); + } + if (subImageBounds.property("height").isValid()) { + subImageRect.setHeight(subImageBounds.property("height").toVariant().toInt()); + } else { + subImageRect.setHeight(oldSubImageRect.height()); + } + setClipFromSource(subImageRect); + } + + QScriptValue imageURL = properties.property("imageURL"); + if (imageURL.isValid()) { + setImageURL(imageURL.toVariant().toString()); + } +} + + diff --git a/interface/src/ui/ImageOverlay.h b/interface/src/ui/ImageOverlay.h new file mode 100644 index 0000000000..77cac3b3c6 --- /dev/null +++ b/interface/src/ui/ImageOverlay.h @@ -0,0 +1,60 @@ +// +// ImageOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__ImageOverlay__ +#define __interface__ImageOverlay__ + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Overlay.h" +#include "Overlay2D.h" + +class ImageOverlay : public Overlay2D { + Q_OBJECT + +public: + ImageOverlay(); + ~ImageOverlay(); + virtual void render(); + + // getters + const QRect& getClipFromSource() const { return _fromImage; } + const QUrl& getImageURL() const { return _imageURL; } + + // setters + void setClipFromSource(const QRect& bounds) { _fromImage = bounds; _wantClipFromImage = true; } + void setImageURL(const QUrl& url); + virtual void setProperties(const QScriptValue& properties); + +private slots: + void replyFinished(QNetworkReply* reply); // we actually want to hide this... + +private: + + QUrl _imageURL; + QImage _textureImage; + GLuint _textureID; + QRect _fromImage; // where from in the image to sample + bool _renderImage; // is there an image associated with this overlay, or is it just a colored rectangle + bool _textureBound; // has the texture been bound + bool _wantClipFromImage; +}; + + +#endif /* defined(__interface__ImageOverlay__) */ diff --git a/interface/src/ui/Line3DOverlay.cpp b/interface/src/ui/Line3DOverlay.cpp new file mode 100644 index 0000000000..c357233329 --- /dev/null +++ b/interface/src/ui/Line3DOverlay.cpp @@ -0,0 +1,60 @@ +// +// Line3DOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include "Line3DOverlay.h" + + +Line3DOverlay::Line3DOverlay() { +} + +Line3DOverlay::~Line3DOverlay() { +} + +void Line3DOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + + const float MAX_COLOR = 255; + glDisable(GL_LIGHTING); + glLineWidth(_lineWidth); + glColor4f(_color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, _alpha); + + glBegin(GL_LINES); + glVertex3f(_position.x, _position.y, _position.z); + glVertex3f(_end.x, _end.y, _end.z); + glEnd(); + glEnable(GL_LIGHTING); +} + +void Line3DOverlay::setProperties(const QScriptValue& properties) { + Base3DOverlay::setProperties(properties); + + QScriptValue end = properties.property("end"); + // if "end" property was not there, check to see if they included aliases: endPoint, or p2 + if (!end.isValid()) { + end = properties.property("endPoint"); + if (!end.isValid()) { + end = properties.property("p2"); + } + } + if (end.isValid()) { + QScriptValue x = end.property("x"); + QScriptValue y = end.property("y"); + QScriptValue z = end.property("z"); + if (x.isValid() && y.isValid() && z.isValid()) { + glm::vec3 newEnd; + newEnd.x = x.toVariant().toFloat(); + newEnd.y = y.toVariant().toFloat(); + newEnd.z = z.toVariant().toFloat(); + setEnd(newEnd); + } + } +} diff --git a/interface/src/ui/Line3DOverlay.h b/interface/src/ui/Line3DOverlay.h new file mode 100644 index 0000000000..d52b639d59 --- /dev/null +++ b/interface/src/ui/Line3DOverlay.h @@ -0,0 +1,34 @@ +// +// Line3DOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Line3DOverlay__ +#define __interface__Line3DOverlay__ + +#include "Base3DOverlay.h" + +class Line3DOverlay : public Base3DOverlay { + Q_OBJECT + +public: + Line3DOverlay(); + ~Line3DOverlay(); + virtual void render(); + + // getters + const glm::vec3& getEnd() const { return _end; } + + // setters + void setEnd(const glm::vec3& end) { _end = end; } + + virtual void setProperties(const QScriptValue& properties); + +protected: + glm::vec3 _end; +}; + + +#endif /* defined(__interface__Line3DOverlay__) */ diff --git a/interface/src/ui/LodToolsDialog.cpp b/interface/src/ui/LodToolsDialog.cpp index 788f7e5561..4cf4a29bf1 100644 --- a/interface/src/ui/LodToolsDialog.cpp +++ b/interface/src/ui/LodToolsDialog.cpp @@ -121,6 +121,12 @@ LodToolsDialog::~LodToolsDialog() { delete _boundaryLevelAdjust; } +void LodToolsDialog::reloadSliders() { + _lodSize->setValue(Menu::getInstance()->getVoxelSizeScale() / TREE_SCALE); + _boundaryLevelAdjust->setValue(Menu::getInstance()->getBoundaryLevelAdjust()); + _feedback->setText(getFeedbackText()); +} + void LodToolsDialog::sizeScaleValueChanged(int value) { float realValue = value * TREE_SCALE; Menu::getInstance()->setVoxelSizeScale(realValue); diff --git a/interface/src/ui/LodToolsDialog.h b/interface/src/ui/LodToolsDialog.h index ee14196188..ee96cffd7e 100644 --- a/interface/src/ui/LodToolsDialog.h +++ b/interface/src/ui/LodToolsDialog.h @@ -28,6 +28,7 @@ public slots: void sizeScaleValueChanged(int value); void boundaryLevelValueChanged(int value); void resetClicked(bool checked); + void reloadSliders(); protected: diff --git a/interface/src/ui/Overlay.cpp b/interface/src/ui/Overlay.cpp new file mode 100644 index 0000000000..40da2253f4 --- /dev/null +++ b/interface/src/ui/Overlay.cpp @@ -0,0 +1,55 @@ +// +// Overlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include + +#include "Overlay.h" + + +Overlay::Overlay() : + _parent(NULL), + _alpha(DEFAULT_ALPHA), + _color(DEFAULT_BACKGROUND_COLOR), + _visible(true) +{ +} + +void Overlay::init(QGLWidget* parent) { + _parent = parent; +} + + +Overlay::~Overlay() { +} + +void Overlay::setProperties(const QScriptValue& properties) { + QScriptValue color = properties.property("color"); + if (color.isValid()) { + QScriptValue red = color.property("red"); + QScriptValue green = color.property("green"); + QScriptValue blue = color.property("blue"); + if (red.isValid() && green.isValid() && blue.isValid()) { + _color.red = red.toVariant().toInt(); + _color.green = green.toVariant().toInt(); + _color.blue = blue.toVariant().toInt(); + } + } + + if (properties.property("alpha").isValid()) { + setAlpha(properties.property("alpha").toVariant().toFloat()); + } + + if (properties.property("visible").isValid()) { + setVisible(properties.property("visible").toVariant().toBool()); + } +} diff --git a/interface/src/ui/Overlay.h b/interface/src/ui/Overlay.h new file mode 100644 index 0000000000..df898ec741 --- /dev/null +++ b/interface/src/ui/Overlay.h @@ -0,0 +1,53 @@ +// +// Overlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Overlay__ +#define __interface__Overlay__ + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include + +#include // for xColor + +const xColor DEFAULT_BACKGROUND_COLOR = { 255, 255, 255 }; +const float DEFAULT_ALPHA = 0.7f; + +class Overlay : public QObject { + Q_OBJECT + +public: + Overlay(); + ~Overlay(); + void init(QGLWidget* parent); + virtual void render() = 0; + + // getters + bool getVisible() const { return _visible; } + const xColor& getColor() const { return _color; } + float getAlpha() const { return _alpha; } + + // setters + void setVisible(bool visible) { _visible = visible; } + void setColor(const xColor& color) { _color = color; } + void setAlpha(float alpha) { _alpha = alpha; } + + virtual void setProperties(const QScriptValue& properties); + +protected: + QGLWidget* _parent; + float _alpha; + xColor _color; + bool _visible; // should the overlay be drawn at all +}; + + +#endif /* defined(__interface__Overlay__) */ diff --git a/interface/src/ui/Overlay2D.cpp b/interface/src/ui/Overlay2D.cpp new file mode 100644 index 0000000000..0c459811c4 --- /dev/null +++ b/interface/src/ui/Overlay2D.cpp @@ -0,0 +1,63 @@ +// +// Overlay2D.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include + +#include "Overlay2D.h" + + +Overlay2D::Overlay2D() { +} + +Overlay2D::~Overlay2D() { +} + +void Overlay2D::setProperties(const QScriptValue& properties) { + Overlay::setProperties(properties); + + QScriptValue bounds = properties.property("bounds"); + if (bounds.isValid()) { + QRect boundsRect; + boundsRect.setX(bounds.property("x").toVariant().toInt()); + boundsRect.setY(bounds.property("y").toVariant().toInt()); + boundsRect.setWidth(bounds.property("width").toVariant().toInt()); + boundsRect.setHeight(bounds.property("height").toVariant().toInt()); + setBounds(boundsRect); + } else { + QRect oldBounds = getBounds(); + QRect newBounds = oldBounds; + + if (properties.property("x").isValid()) { + newBounds.setX(properties.property("x").toVariant().toInt()); + } else { + newBounds.setX(oldBounds.x()); + } + if (properties.property("y").isValid()) { + newBounds.setY(properties.property("y").toVariant().toInt()); + } else { + newBounds.setY(oldBounds.y()); + } + if (properties.property("width").isValid()) { + newBounds.setWidth(properties.property("width").toVariant().toInt()); + } else { + newBounds.setWidth(oldBounds.width()); + } + if (properties.property("height").isValid()) { + newBounds.setHeight(properties.property("height").toVariant().toInt()); + } else { + newBounds.setHeight(oldBounds.height()); + } + setBounds(newBounds); + //qDebug() << "set bounds to " << getBounds(); + } +} diff --git a/interface/src/ui/Overlay2D.h b/interface/src/ui/Overlay2D.h new file mode 100644 index 0000000000..3da8f8bca4 --- /dev/null +++ b/interface/src/ui/Overlay2D.h @@ -0,0 +1,51 @@ +// +// Overlay2D.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Overlay2D__ +#define __interface__Overlay2D__ + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include + +#include // for xColor + +#include "Overlay.h" + +class Overlay2D : public Overlay { + Q_OBJECT + +public: + Overlay2D(); + ~Overlay2D(); + + // getters + int getX() const { return _bounds.x(); } + int getY() const { return _bounds.y(); } + int getWidth() const { return _bounds.width(); } + int getHeight() const { return _bounds.height(); } + const QRect& getBounds() const { return _bounds; } + + // setters + void setX(int x) { _bounds.setX(x); } + void setY(int y) { _bounds.setY(y); } + void setWidth(int width) { _bounds.setWidth(width); } + void setHeight(int height) { _bounds.setHeight(height); } + void setBounds(const QRect& bounds) { _bounds = bounds; } + + virtual void setProperties(const QScriptValue& properties); + +protected: + QRect _bounds; // where on the screen to draw +}; + + +#endif /* defined(__interface__Overlay2D__) */ diff --git a/interface/src/ui/Overlays.cpp b/interface/src/ui/Overlays.cpp new file mode 100644 index 0000000000..c35c4fc5ec --- /dev/null +++ b/interface/src/ui/Overlays.cpp @@ -0,0 +1,129 @@ +// +// Overlays.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + + +#include "Cube3DOverlay.h" +#include "ImageOverlay.h" +#include "Line3DOverlay.h" +#include "Overlays.h" +#include "Sphere3DOverlay.h" +#include "TextOverlay.h" + + +unsigned int Overlays::_nextOverlayID = 1; + +Overlays::Overlays() { +} + +Overlays::~Overlays() { +} + +void Overlays::init(QGLWidget* parent) { + _parent = parent; +} + +void Overlays::render2D() { + foreach(Overlay* thisOverlay, _overlays2D) { + thisOverlay->render(); + } +} + +void Overlays::render3D() { + foreach(Overlay* thisOverlay, _overlays3D) { + thisOverlay->render(); + } +} + +// TODO: make multi-threaded safe +unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& properties) { + unsigned int thisID = 0; + bool created = false; + bool is3D = false; + Overlay* thisOverlay = NULL; + + if (type == "image") { + thisOverlay = new ImageOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + } else if (type == "text") { + thisOverlay = new TextOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + } else if (type == "cube") { + thisOverlay = new Cube3DOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; + } else if (type == "sphere") { + thisOverlay = new Sphere3DOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; + } else if (type == "line3d") { + thisOverlay = new Line3DOverlay(); + thisOverlay->init(_parent); + thisOverlay->setProperties(properties); + created = true; + is3D = true; + } + + if (created) { + thisID = _nextOverlayID; + _nextOverlayID++; + if (is3D) { + _overlays3D[thisID] = thisOverlay; + } else { + _overlays2D[thisID] = thisOverlay; + } + } + + return thisID; +} + +// TODO: make multi-threaded safe +bool Overlays::editOverlay(unsigned int id, const QScriptValue& properties) { + Overlay* thisOverlay = NULL; + if (_overlays2D.contains(id)) { + thisOverlay = _overlays2D[id]; + } else if (_overlays3D.contains(id)) { + thisOverlay = _overlays3D[id]; + } + if (thisOverlay) { + thisOverlay->setProperties(properties); + return true; + } + return false; +} + +// TODO: make multi-threaded safe +void Overlays::deleteOverlay(unsigned int id) { + if (_overlays2D.contains(id)) { + _overlays2D.erase(_overlays2D.find(id)); + } else if (_overlays3D.contains(id)) { + _overlays3D.erase(_overlays3D.find(id)); + } +} + +unsigned int Overlays::getOverlayAtPoint(const glm::vec2& point) { + QMapIterator i(_overlays2D); + i.toBack(); + while (i.hasPrevious()) { + i.previous(); + unsigned int thisID = i.key(); + Overlay2D* thisOverlay = static_cast(i.value()); + if (thisOverlay->getVisible() && thisOverlay->getBounds().contains(point.x, point.y, false)) { + return thisID; + } + } + return 0; // not found +} + + diff --git a/interface/src/ui/Overlays.h b/interface/src/ui/Overlays.h new file mode 100644 index 0000000000..cfd84fd44b --- /dev/null +++ b/interface/src/ui/Overlays.h @@ -0,0 +1,46 @@ +// +// Overlays.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Overlays__ +#define __interface__Overlays__ + +#include + +#include "Overlay.h" + +class Overlays : public QObject { + Q_OBJECT +public: + Overlays(); + ~Overlays(); + void init(QGLWidget* parent); + void render3D(); + void render2D(); + +public slots: + /// adds an overlay with the specific properties + unsigned int addOverlay(const QString& type, const QScriptValue& properties); + + /// edits an overlay updating only the included properties, will return the identified OverlayID in case of + /// successful edit, if the input id is for an unknown overlay this function will have no effect + bool editOverlay(unsigned int id, const QScriptValue& properties); + + /// deletes a particle + void deleteOverlay(unsigned int id); + + /// returns the top most overlay at the screen point, or 0 if not overlay at that point + unsigned int getOverlayAtPoint(const glm::vec2& point); + +private: + QMap _overlays2D; + QMap _overlays3D; + static unsigned int _nextOverlayID; + QGLWidget* _parent; +}; + + +#endif /* defined(__interface__Overlays__) */ diff --git a/interface/src/ui/Sphere3DOverlay.cpp b/interface/src/ui/Sphere3DOverlay.cpp new file mode 100644 index 0000000000..7fded5bedb --- /dev/null +++ b/interface/src/ui/Sphere3DOverlay.cpp @@ -0,0 +1,45 @@ +// +// Sphere3DOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "Sphere3DOverlay.h" + +Sphere3DOverlay::Sphere3DOverlay() { +} + +Sphere3DOverlay::~Sphere3DOverlay() { +} + +void Sphere3DOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + + const float MAX_COLOR = 255; + glColor4f(_color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, _alpha); + + + glDisable(GL_LIGHTING); + glPushMatrix(); + glTranslatef(_position.x + _size * 0.5f, + _position.y + _size * 0.5f, + _position.z + _size * 0.5f); + glLineWidth(_lineWidth); + const int slices = 15; + if (_isSolid) { + glutSolidSphere(_size, slices, slices); + } else { + glutWireSphere(_size, slices, slices); + } + glPopMatrix(); + +} diff --git a/interface/src/ui/Sphere3DOverlay.h b/interface/src/ui/Sphere3DOverlay.h new file mode 100644 index 0000000000..58ed0d7776 --- /dev/null +++ b/interface/src/ui/Sphere3DOverlay.h @@ -0,0 +1,23 @@ +// +// Sphere3DOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Sphere3DOverlay__ +#define __interface__Sphere3DOverlay__ + +#include "Volume3DOverlay.h" + +class Sphere3DOverlay : public Volume3DOverlay { + Q_OBJECT + +public: + Sphere3DOverlay(); + ~Sphere3DOverlay(); + virtual void render(); +}; + + +#endif /* defined(__interface__Sphere3DOverlay__) */ diff --git a/interface/src/ui/TextOverlay.cpp b/interface/src/ui/TextOverlay.cpp new file mode 100644 index 0000000000..edaec6849a --- /dev/null +++ b/interface/src/ui/TextOverlay.cpp @@ -0,0 +1,82 @@ +// +// TextOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "TextOverlay.h" +#include "TextRenderer.h" + +TextOverlay::TextOverlay() : + _leftMargin(DEFAULT_MARGIN), + _topMargin(DEFAULT_MARGIN) +{ +} + +TextOverlay::~TextOverlay() { +} + +void TextOverlay::render() { + if (!_visible) { + return; // do nothing if we're not visible + } + + const float MAX_COLOR = 255; + glColor4f(_color.red / MAX_COLOR, _color.green / MAX_COLOR, _color.blue / MAX_COLOR, _alpha); + + glBegin(GL_QUADS); + glVertex2f(_bounds.left(), _bounds.top()); + glVertex2f(_bounds.right(), _bounds.top()); + glVertex2f(_bounds.right(), _bounds.bottom()); + glVertex2f(_bounds.left(), _bounds.bottom()); + glEnd(); + + //TextRenderer(const char* family, int pointSize = -1, int weight = -1, bool italic = false, + // EffectType effect = NO_EFFECT, int effectThickness = 1); + + TextRenderer textRenderer(SANS_FONT_FAMILY, 11, 50); + const int leftAdjust = -1; // required to make text render relative to left edge of bounds + const int topAdjust = -2; // required to make text render relative to top edge of bounds + int x = _bounds.left() + _leftMargin + leftAdjust; + int y = _bounds.top() + _topMargin + topAdjust; + + glColor3f(1.0f, 1.0f, 1.0f); + QStringList lines = _text.split("\n"); + int lineOffset = 0; + foreach(QString thisLine, lines) { + if (lineOffset == 0) { + lineOffset = textRenderer.calculateHeight(qPrintable(thisLine)); + } + lineOffset += textRenderer.draw(x, y + lineOffset, qPrintable(thisLine)); + + const int lineGap = 2; + lineOffset += lineGap; + } + +} + +void TextOverlay::setProperties(const QScriptValue& properties) { + Overlay2D::setProperties(properties); + + QScriptValue text = properties.property("text"); + if (text.isValid()) { + setText(text.toVariant().toString()); + } + + if (properties.property("leftMargin").isValid()) { + setLeftMargin(properties.property("leftMargin").toVariant().toInt()); + } + + if (properties.property("topMargin").isValid()) { + setTopMargin(properties.property("topMargin").toVariant().toInt()); + } +} + + diff --git a/interface/src/ui/TextOverlay.h b/interface/src/ui/TextOverlay.h new file mode 100644 index 0000000000..d565aeb70d --- /dev/null +++ b/interface/src/ui/TextOverlay.h @@ -0,0 +1,59 @@ +// +// TextOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__TextOverlay__ +#define __interface__TextOverlay__ + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "Overlay.h" +#include "Overlay2D.h" + +const int DEFAULT_MARGIN = 10; + +class TextOverlay : public Overlay2D { + Q_OBJECT + +public: + TextOverlay(); + ~TextOverlay(); + virtual void render(); + + // getters + const QString& getText() const { return _text; } + int getLeftMargin() const { return _leftMargin; } + int getTopMargin() const { return _topMargin; } + + // setters + void setText(const QString& text) { _text = text; } + void setLeftMargin(int margin) { _leftMargin = margin; } + void setTopMargin(int margin) { _topMargin = margin; } + + virtual void setProperties(const QScriptValue& properties); + +private: + + QString _text; + int _leftMargin; + int _topMargin; + +}; + + +#endif /* defined(__interface__TextOverlay__) */ diff --git a/interface/src/ui/TextRenderer.cpp b/interface/src/ui/TextRenderer.cpp index 65056799e2..cacd730fd6 100644 --- a/interface/src/ui/TextRenderer.cpp +++ b/interface/src/ui/TextRenderer.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "InterfaceConfig.h" #include "TextRenderer.h" @@ -30,10 +32,25 @@ TextRenderer::~TextRenderer() { glDeleteTextures(_allTextureIDs.size(), _allTextureIDs.constData()); } -void TextRenderer::draw(int x, int y, const char* str) { +int TextRenderer::calculateHeight(const char* str) { + int maxHeight = 0; + for (const char* ch = str; *ch != 0; ch++) { + const Glyph& glyph = getGlyph(*ch); + if (glyph.textureID() == 0) { + continue; + } + + if (glyph.bounds().height() > maxHeight) { + maxHeight = glyph.bounds().height(); + } + } + return maxHeight; +} +int TextRenderer::draw(int x, int y, const char* str) { glEnable(GL_TEXTURE_2D); + int maxHeight = 0; for (const char* ch = str; *ch != 0; ch++) { const Glyph& glyph = getGlyph(*ch); if (glyph.textureID() == 0) { @@ -41,19 +58,23 @@ void TextRenderer::draw(int x, int y, const char* str) { continue; } + if (glyph.bounds().height() > maxHeight) { + maxHeight = glyph.bounds().height(); + } + glBindTexture(GL_TEXTURE_2D, glyph.textureID()); - + int left = x + glyph.bounds().x(); int right = x + glyph.bounds().x() + glyph.bounds().width(); int bottom = y + glyph.bounds().y(); int top = y + glyph.bounds().y() + glyph.bounds().height(); - + float scale = 1.0 / IMAGE_SIZE; float ls = glyph.location().x() * scale; float rs = (glyph.location().x() + glyph.bounds().width()) * scale; float bt = glyph.location().y() * scale; float tt = (glyph.location().y() + glyph.bounds().height()) * scale; - + glBegin(GL_QUADS); glTexCoord2f(ls, bt); glVertex2f(left, bottom); @@ -64,12 +85,13 @@ void TextRenderer::draw(int x, int y, const char* str) { glTexCoord2f(ls, tt); glVertex2f(left, top); glEnd(); - + x += glyph.width(); } - glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); + + return maxHeight; } int TextRenderer::computeWidth(char ch) diff --git a/interface/src/ui/TextRenderer.h b/interface/src/ui/TextRenderer.h index ff484066d8..d6c24c1ce8 100644 --- a/interface/src/ui/TextRenderer.h +++ b/interface/src/ui/TextRenderer.h @@ -20,6 +20,16 @@ // a special "character" that renders as a solid block const char SOLID_BLOCK_CHAR = 127; +// the standard sans serif font family +#define SANS_FONT_FAMILY "Helvetica" + +// the standard mono font family +#define MONO_FONT_FAMILY "Courier" + +// the Inconsolata font family +#define INCONSOLATA_FONT_FAMILY "Inconsolata" + + class Glyph; class TextRenderer { @@ -33,7 +43,11 @@ public: const QFontMetrics& metrics() const { return _metrics; } - void draw(int x, int y, const char* str); + // returns the height of the tallest character + int calculateHeight(const char* str); + + // also returns the height of the tallest character + int draw(int x, int y, const char* str); int computeWidth(char ch); int computeWidth(const char* str); diff --git a/interface/src/ui/Volume3DOverlay.cpp b/interface/src/ui/Volume3DOverlay.cpp new file mode 100644 index 0000000000..dbc1582cc5 --- /dev/null +++ b/interface/src/ui/Volume3DOverlay.cpp @@ -0,0 +1,47 @@ +// +// Volume3DOverlay.cpp +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "Volume3DOverlay.h" + +const float DEFAULT_SIZE = 1.0f; +const bool DEFAULT_IS_SOLID = false; + +Volume3DOverlay::Volume3DOverlay() : + _size(DEFAULT_SIZE), + _isSolid(DEFAULT_IS_SOLID) +{ +} + +Volume3DOverlay::~Volume3DOverlay() { +} + +void Volume3DOverlay::setProperties(const QScriptValue& properties) { + Base3DOverlay::setProperties(properties); + + if (properties.property("size").isValid()) { + setSize(properties.property("size").toVariant().toFloat()); + } + + if (properties.property("isSolid").isValid()) { + setIsSolid(properties.property("isSolid").toVariant().toBool()); + } + if (properties.property("isWire").isValid()) { + setIsSolid(!properties.property("isWire").toVariant().toBool()); + } + if (properties.property("solid").isValid()) { + setIsSolid(properties.property("solid").toVariant().toBool()); + } + if (properties.property("wire").isValid()) { + setIsSolid(!properties.property("wire").toVariant().toBool()); + } +} diff --git a/interface/src/ui/Volume3DOverlay.h b/interface/src/ui/Volume3DOverlay.h new file mode 100644 index 0000000000..8badbf2c33 --- /dev/null +++ b/interface/src/ui/Volume3DOverlay.h @@ -0,0 +1,42 @@ +// +// Volume3DOverlay.h +// interface +// +// Copyright (c) 2014 High Fidelity, Inc. All rights reserved. +// + +#ifndef __interface__Volume3DOverlay__ +#define __interface__Volume3DOverlay__ + +// include this before QGLWidget, which includes an earlier version of OpenGL +#include "InterfaceConfig.h" + +#include +#include + +#include "Base3DOverlay.h" + +class Volume3DOverlay : public Base3DOverlay { + Q_OBJECT + +public: + Volume3DOverlay(); + ~Volume3DOverlay(); + + // getters + float getSize() const { return _size; } + bool getIsSolid() const { return _isSolid; } + + // setters + void setSize(float size) { _size = size; } + void setIsSolid(bool isSolid) { _isSolid = isSolid; } + + virtual void setProperties(const QScriptValue& properties); + +protected: + float _size; + bool _isSolid; +}; + + +#endif /* defined(__interface__Volume3DOverlay__) */ diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 46d92c0f2e..6492d9b7ad 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -52,8 +52,8 @@ static const float MIN_AVATAR_SCALE = .005f; const float MAX_AUDIO_LOUDNESS = 1000.0; // close enough for mouth animation -const QUrl DEFAULT_HEAD_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_head.fbx"); -const QUrl DEFAULT_BODY_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_body.fbx"); +const QUrl DEFAULT_HEAD_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_head.fst"); +const QUrl DEFAULT_BODY_MODEL_URL = QUrl("http://public.highfidelity.io/meshes/defaultAvatar_body.fst"); enum KeyState { NO_KEY_DOWN = 0, @@ -67,6 +67,7 @@ class AvatarData : public NodeData { Q_OBJECT Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) + Q_PROPERTY(float scale READ getTargetScale WRITE setTargetScale) Q_PROPERTY(glm::vec3 handPosition READ getHandPosition WRITE setHandPosition) Q_PROPERTY(float bodyYaw READ getBodyYaw WRITE setBodyYaw) Q_PROPERTY(float bodyPitch READ getBodyPitch WRITE setBodyPitch) diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 72ada7d421..82485691c5 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -2,7 +2,7 @@ // AvatarHashMap.cpp // hifi // -// Created by Stephen AndrewMeadows on 1/28/2014. +// Created by AndrewMeadows on 1/28/2014. // Copyright (c) 2014 HighFidelity, Inc. All rights reserved. // diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index f863d6b592..62e8276bd3 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -45,10 +45,3 @@ void HeadData::addLean(float sideways, float forwards) { _leanForward += forwards; } -bool HeadData::findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration) const { - // we would like to update this to determine collisions/penetrations with the Avatar's head sphere... - // but right now it does not appear as if the HeadData has a position and radius. - // this is a placeholder for now. - return false; -} - diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index fde684bbf1..0f096059c0 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -58,13 +58,6 @@ public: void setLookAtPosition(const glm::vec3& lookAtPosition) { _lookAtPosition = lookAtPosition; } friend class AvatarData; - - /// Checks for penetration between the described sphere and the hand. - /// \param penetratorCenter the center of the penetration test sphere - /// \param penetratorRadius the radius of the penetration test sphere - /// \param penetration[out] the vector in which to store the penetration - /// \return whether or not the sphere penetrated - bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration) const; protected: float _yaw; diff --git a/libraries/octree/src/OctreeScriptingInterface.cpp b/libraries/octree/src/OctreeScriptingInterface.cpp index 553ab961df..1ed82564b6 100644 --- a/libraries/octree/src/OctreeScriptingInterface.cpp +++ b/libraries/octree/src/OctreeScriptingInterface.cpp @@ -11,7 +11,12 @@ #include "OctreeScriptingInterface.h" OctreeScriptingInterface::OctreeScriptingInterface(OctreeEditPacketSender* packetSender, - JurisdictionListener* jurisdictionListener) + JurisdictionListener* jurisdictionListener) : + _packetSender(NULL), + _jurisdictionListener(NULL), + _managedPacketSender(false), + _managedJurisdictionListener(false), + _initialized(false) { setPacketSender(packetSender); setJurisdictionListener(jurisdictionListener); @@ -45,6 +50,9 @@ void OctreeScriptingInterface::setJurisdictionListener(JurisdictionListener* jur } void OctreeScriptingInterface::init() { + if (_initialized) { + return; + } if (_jurisdictionListener) { _managedJurisdictionListener = false; } else { @@ -64,5 +72,5 @@ void OctreeScriptingInterface::init() { if (QCoreApplication::instance()) { connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(cleanupManagedObjects())); } - + _initialized = true; } diff --git a/libraries/octree/src/OctreeScriptingInterface.h b/libraries/octree/src/OctreeScriptingInterface.h index 34eddd8bed..3c832cbae8 100644 --- a/libraries/octree/src/OctreeScriptingInterface.h +++ b/libraries/octree/src/OctreeScriptingInterface.h @@ -93,6 +93,7 @@ protected: JurisdictionListener* _jurisdictionListener; bool _managedPacketSender; bool _managedJurisdictionListener; + bool _initialized; }; #endif /* defined(__hifi__OctreeScriptingInterface__) */ diff --git a/libraries/script-engine/src/AbstractControllerScriptingInterface.h b/libraries/script-engine/src/AbstractControllerScriptingInterface.h index 4fad5f6edc..d9878d0b71 100644 --- a/libraries/script-engine/src/AbstractControllerScriptingInterface.h +++ b/libraries/script-engine/src/AbstractControllerScriptingInterface.h @@ -12,6 +12,7 @@ #include #include +#include #include "EventTypes.h" @@ -37,6 +38,7 @@ public slots: virtual glm::vec3 getSpatialControlPosition(int controlIndex) const = 0; virtual glm::vec3 getSpatialControlVelocity(int controlIndex) const = 0; virtual glm::vec3 getSpatialControlNormal(int controlIndex) const = 0; + virtual glm::quat getSpatialControlRawRotation(int controlIndex) const = 0; virtual void captureKeyEvents(const KeyEvent& event) = 0; virtual void releaseKeyEvents(const KeyEvent& event) = 0; diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index a197d59aeb..2f1c39f9e3 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -9,7 +9,10 @@ // // +#include + #include +#include #include "Quat.h" glm::quat Quat::multiply(const glm::quat& q1, const glm::quat& q2) { @@ -24,6 +27,11 @@ glm::quat Quat::fromPitchYawRoll(float pitch, float yaw, float roll) { return glm::quat(glm::radians(glm::vec3(pitch, yaw, roll))); } +glm::quat Quat::inverse(const glm::quat& q) { + return glm::inverse(q); +} + + glm::vec3 Quat::getFront(const glm::quat& orientation) { return orientation * IDENTITY_FRONT; } @@ -35,3 +43,12 @@ glm::vec3 Quat::getRight(const glm::quat& orientation) { glm::vec3 Quat::getUp(const glm::quat& orientation) { return orientation * IDENTITY_UP; } + +glm::vec3 Quat::safeEulerAngles(const glm::quat& orientation) { + return ::safeEulerAngles(orientation); +} + +glm::quat Quat::angleAxis(float angle, const glm::vec3& v) { + return glm::angleAxis(angle, v); +} + diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 72fec6d6dc..867069d6d6 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -23,11 +23,12 @@ public slots: glm::quat multiply(const glm::quat& q1, const glm::quat& q2); glm::quat fromVec3(const glm::vec3& vec3); glm::quat fromPitchYawRoll(float pitch, float yaw, float roll); + glm::quat inverse(const glm::quat& q); glm::vec3 getFront(const glm::quat& orientation); glm::vec3 getRight(const glm::quat& orientation); glm::vec3 getUp(const glm::quat& orientation); + glm::vec3 safeEulerAngles(const glm::quat& orientation); + glm::quat angleAxis(float angle, const glm::vec3& v); }; - - #endif /* defined(__hifi__Quat__) */ diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp index 87b1b510a4..1ed3ae6915 100644 --- a/libraries/script-engine/src/Vec3.cpp +++ b/libraries/script-engine/src/Vec3.cpp @@ -19,6 +19,16 @@ glm::vec3 Vec3::multiply(const glm::vec3& v1, float f) { return v1 * f; } +glm::vec3 Vec3::multiplyQbyV(const glm::quat& q, const glm::vec3& v) { + return q * v; +} + glm::vec3 Vec3::sum(const glm::vec3& v1, const glm::vec3& v2) { return v1 + v2; } +glm::vec3 Vec3::subtract(const glm::vec3& v1, const glm::vec3& v2) { + return v1 - v2; +} +float Vec3::length(const glm::vec3& v) { + return glm::length(v); +} diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index 1cc44f3061..20ad3f7eaa 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -23,7 +23,10 @@ class Vec3 : public QObject { public slots: glm::vec3 multiply(const glm::vec3& v1, const glm::vec3& v2); glm::vec3 multiply(const glm::vec3& v1, float f); + glm::vec3 multiplyQbyV(const glm::quat& q, const glm::vec3& v); glm::vec3 sum(const glm::vec3& v1, const glm::vec3& v2); + glm::vec3 subtract(const glm::vec3& v1, const glm::vec3& v2); + float length(const glm::vec3& v); }; diff --git a/libraries/shared/src/PerfStat.h b/libraries/shared/src/PerfStat.h index fffb095021..a7a10b97b8 100644 --- a/libraries/shared/src/PerfStat.h +++ b/libraries/shared/src/PerfStat.h @@ -45,6 +45,8 @@ public: _alwaysDisplay(alwaysDisplay), _runningTotal(runningTotal), _totalCalls(totalCalls) { } + + quint64 elapsed() const { return (usecTimestampNow() - _start); }; ~PerformanceWarning();