diff --git a/README.md b/README.md index a4d193324d..a2eb058ae6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -High Fidelity (hifi) is an early-stage technology -lab experimenting with Virtual Worlds and VR. +High Fidelity (hifi) is an early-stage technology lab experimenting with Virtual Worlds and VR. In this repository you'll find the source to many of the components in our alpha-stage virtual world. The project embraces distributed development diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index aaa4eb2149..6a0a955a6f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -295,7 +295,8 @@ void AvatarMixer::broadcastAvatarData() { avatarPacketList.startSegment(); numAvatarDataBytes += avatarPacketList.write(otherNode->getUUID().toRfc4122()); - numAvatarDataBytes += avatarPacketList.write(otherAvatar.toByteArray(false)); + numAvatarDataBytes += + avatarPacketList.write(otherAvatar.toByteArray(false, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO)); avatarPacketList.endSegment(); @@ -364,6 +365,31 @@ void AvatarMixer::broadcastAvatarData() { } ); + // We're done encoding this version of the otherAvatars. Update their "lastSent" joint-states so + // that we can notice differences, next time around. + nodeList->eachMatchingNode( + [&](const SharedNodePointer& otherNode)->bool { + if (!otherNode->getLinkedData()) { + return false; + } + if (otherNode->getType() != NodeType::Agent) { + return false; + } + if (!otherNode->getActiveSocket()) { + return false; + } + return true; + }, + [&](const SharedNodePointer& otherNode) { + AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); + MutexTryLocker lock(otherNodeData->getMutex()); + if (!lock.isLocked()) { + return; + } + AvatarData& otherAvatar = otherNodeData->getAvatar(); + otherAvatar.doneEncoding(false); + }); + _lastFrameTimestamp = QDateTime::currentMSecsSinceEpoch(); } diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 3c75e8faad..80cb5950be 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -11,6 +11,8 @@ #include "NodeConnectionData.h" +#include + NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, const HifiSockAddr& senderSockAddr, bool isConnectRequest) { NodeConnectionData newHeader; diff --git a/examples/controllers/hydra/hydraGrab.js b/examples/controllers/hydra/hydraGrab.js index 34484eb9e8..bd060ecc14 100644 --- a/examples/controllers/hydra/hydraGrab.js +++ b/examples/controllers/hydra/hydraGrab.js @@ -1,307 +1,346 @@ -// // hydraGrab.js // examples // -// Created by Clément Brisset on 4/24/14. -// Updated by Eric Levin on 5/14/15. -// Copyright 2014 High Fidelity, Inc. +// Created by Eric Levin on 9/2/15 +// Copyright 2015 High Fidelity, Inc. // -// This script allows you to grab and move/rotate physical objects with the hydra -// -// Using the hydras : -// grab physical entities with the right trigger +// Grab's physically moveable entities with the hydra- works for either near or far objects. User can also grab a far away object and drag it towards them by pressing the "4" button on either the left or ride controller. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var entityProps, currentPosition, currentVelocity, currentRotation, distanceToTarget, velocityTowardTarget, desiredVelocity; -var addedVelocity, newVelocity, angularVelocity, dT, cameraEntityDistance; -var LEFT = 0; -var RIGHT = 1; -var LASER_WIDTH = 3; -var LASER_COLOR = { - red: 50, - green: 150, - blue: 200 -}; -var LASER_HOVER_COLOR = { - red: 200, - green: 50, - blue: 50 -}; +Script.include("../../libraries/utils.js"); -var DROP_DISTANCE = 5.0; -var DROP_COLOR = { - red: 200, - green: 200, - blue: 200 -}; +var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK"); +var rightTriggerAction = RIGHT_HAND_CLICK; -var FULL_STRENGTH = 0.05; -var LASER_LENGTH_FACTOR = 500; -var CLOSE_ENOUGH = 0.001; -var SPRING_RATE = 1.5; -var DAMPING_RATE = 0.8; -var SCREEN_TO_METERS = 0.001; -var DISTANCE_SCALE_FACTOR = 1000 +var GRAB_USER_DATA_KEY = "grabKey"; -var grabSound = SoundCache.getSound("https://hifi-public.s3.amazonaws.com/eric/sounds/CloseClamp.wav"); -var releaseSound = SoundCache.getSound("https://hifi-public.s3.amazonaws.com/eric/sounds/ReleaseClamp.wav"); +var LEFT_HAND_CLICK = Controller.findAction("LEFT_HAND_CLICK"); +var leftTriggerAction = LEFT_HAND_CLICK; -function getRayIntersection(pickRay) { - var intersection = Entities.findRayIntersection(pickRay, true); - return intersection; -} - - -function controller(side) { - this.triggerHeld = false; - this.triggerThreshold = 0.9; - this.side = side; - this.palm = 2 * side; - this.tip = 2 * side + 1; - this.trigger = side; - this.originalGravity = { +var ZERO_VEC = { x: 0, y: 0, z: 0 - }; +} +var LINE_LENGTH = 500; +var THICK_LINE_WIDTH = 7; +var THIN_LINE_WIDTH = 2; - this.laser = Overlays.addOverlay("line3d", { - start: { - x: 0, - y: 0, - z: 0 - }, - end: { - x: 0, - y: 0, - z: 0 - }, - color: LASER_COLOR, - alpha: 1, - lineWidth: LASER_WIDTH, - anchor: "MyAvatar" - }); +var NO_INTERSECT_COLOR = { + red: 10, + green: 10, + blue: 255 +}; +var INTERSECT_COLOR = { + red: 250, + green: 10, + blue: 10 +}; - this.dropLine = Overlays.addOverlay("line3d", { - color: DROP_COLOR, - alpha: 1, - visible: false, - lineWidth: 2 - }); +var GRAB_RADIUS = 0.5; +var GRAB_COLOR = { + red: 250, + green: 10, + blue: 250 +}; +var SHOW_LINE_THRESHOLD = 0.2; +var DISTANCE_HOLD_THRESHOLD = 0.8; - this.update = function(deltaTime) { - this.updateControllerState(); - this.moveLaser(); - this.checkTrigger(); - this.checkEntityIntersection(); - if (this.grabbing) { - this.updateEntity(deltaTime); - } +var right4Action = 18; +var left4Action = 17; - this.oldPalmPosition = this.palmPosition; - this.oldTipPosition = this.tipPosition; - } +var TRACTOR_BEAM_VELOCITY_THRESHOLD = 0.5; - this.updateEntity = function(deltaTime) { - this.dControllerPosition = Vec3.subtract(this.palmPosition, this.oldPalmPosition); - this.cameraEntityDistance = Vec3.distance(Camera.getPosition(), this.currentPosition); - this.targetPosition = Vec3.sum(this.targetPosition, Vec3.multiply(this.dControllerPosition, this.cameraEntityDistance * SCREEN_TO_METERS * DISTANCE_SCALE_FACTOR)); +var RIGHT = 1; +var LEFT = 0; +var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right") +var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left") - this.entityProps = Entities.getEntityProperties(this.grabbedEntity); - this.currentPosition = this.entityProps.position; - this.currentVelocity = this.entityProps.velocity; - - var dPosition = Vec3.subtract(this.targetPosition, this.currentPosition); - this.distanceToTarget = Vec3.length(dPosition); - if (this.distanceToTarget > CLOSE_ENOUGH) { - // compute current velocity in the direction we want to move - this.velocityTowardTarget = Vec3.dot(this.currentVelocity, Vec3.normalize(dPosition)); - this.velocityTowardTarget = Vec3.multiply(Vec3.normalize(dPosition), this.velocityTowardTarget); - // compute the speed we would like to be going toward the target position - - this.desiredVelocity = Vec3.multiply(dPosition, (1.0 / deltaTime) * SPRING_RATE); - // compute how much we want to add to the existing velocity - this.addedVelocity = Vec3.subtract(this.desiredVelocity, this.velocityTowardTarget); - //If target is to far, roll off force as inverse square of distance - if(this.distanceToTarget/ this.cameraEntityDistance > FULL_STRENGTH) { - this.addedVelocity = Vec3.multiply(this.addedVelocity, Math.pow(FULL_STRENGTH/ this.distanceToTarget, 2.0)); - } - this.newVelocity = Vec3.sum(this.currentVelocity, this.addedVelocity); - this.newVelocity = Vec3.subtract(this.newVelocity, Vec3.multiply(this.newVelocity, DAMPING_RATE)); +function controller(side, triggerAction, pullAction, hand) { + this.hand = hand; + if (hand === "right") { + this.getHandPosition = MyAvatar.getRightPalmPosition; + this.getHandRotation = MyAvatar.getRightPalmRotation; } else { - this.newVelocity = { - x: 0, - y: 0, - z: 0 - }; - } - this.transformedAngularVelocity = Controller.getSpatialControlRawAngularVelocity(this.tip); - this.transformedAngularVelocity = Vec3.multiplyQbyV(Camera.getOrientation(), this.transformedAngularVelocity); - Entities.editEntity(this.grabbedEntity, { - velocity: this.newVelocity, - angularVelocity: this.transformedAngularVelocity + this.getHandPosition = MyAvatar.getLeftPalmPosition; + this.getHandRotation = MyAvatar.getLeftPalmRotation; + } + this.triggerAction = triggerAction; + this.pullAction = pullAction; + this.actionID = null; + this.tractorBeamActive = false; + this.distanceHolding = false; + this.closeGrabbing = false; + this.triggerValue = 0; + this.prevTriggerValue = 0; + this.palm = 2 * side; + this.tip = 2 * side + 1; + this.pointer = Entities.addEntity({ + type: "Line", + name: "pointer", + color: NO_INTERSECT_COLOR, + dimensions: { + x: 1000, + y: 1000, + z: 1000 + }, + visible: false, + }); +} + + +controller.prototype.updateLine = function() { + var handPosition = Controller.getSpatialControlPosition(this.palm); + var direction = Controller.getSpatialControlNormal(this.tip); + + Entities.editEntity(this.pointer, { + position: handPosition, + linePoints: [ + ZERO_VEC, + Vec3.multiply(direction, LINE_LENGTH) + ] }); - this.updateDropLine(this.targetPosition); - - } - - - this.updateControllerState = function() { - this.palmPosition = Controller.getSpatialControlPosition(this.palm); - this.tipPosition = Controller.getSpatialControlPosition(this.tip); - this.triggerValue = Controller.getTriggerValue(this.trigger); - } - - this.checkTrigger = function() { - if (this.triggerValue > this.triggerThreshold && !this.triggerHeld) { - this.triggerHeld = true; - } else if (this.triggerValue < this.triggerThreshold && this.triggerHeld) { - this.triggerHeld = false; - if (this.grabbing) { - this.release(); - } + //only check if we havent already grabbed an object + if (this.distanceHolding) { + return; } - } + + //move origin a bit away from hand so nothing gets in way + var origin = Vec3.sum(handPosition, direction); + if (this.checkForIntersections(origin, direction)) { + Entities.editEntity(this.pointer, { + color: INTERSECT_COLOR, + }); + } else { + Entities.editEntity(this.pointer, { + color: NO_INTERSECT_COLOR, + }); + } +} - this.updateDropLine = function(position) { - - Overlays.editOverlay(this.dropLine, { - visible: true, - start: { - x: position.x, - y: position.y + DROP_DISTANCE, - z: position.z - }, - end: { - x: position.x, - y: position.y - DROP_DISTANCE, - z: position.z - } - }); - - } - - this.checkEntityIntersection = function() { +controller.prototype.checkForIntersections = function(origin, direction) { var pickRay = { - origin: this.palmPosition, - direction: Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition)) + origin: origin, + direction: direction }; - var intersection = getRayIntersection(pickRay, true); - if (intersection.intersects && intersection.properties.collisionsWillMove) { - this.laserWasHovered = true; - if (this.triggerHeld && !this.grabbing) { - this.grab(intersection.entityID); - } - Overlays.editOverlay(this.laser, { - color: LASER_HOVER_COLOR - }); - } else if (this.laserWasHovered) { - this.laserWasHovered = false; - Overlays.editOverlay(this.laser, { - color: LASER_COLOR - }); + + var intersection = Entities.findRayIntersection(pickRay, true); + if (intersection.intersects && intersection.properties.collisionsWillMove === 1) { + this.distanceToEntity = Vec3.distance(origin, intersection.properties.position); + Entities.editEntity(this.pointer, { + linePoints: [ + ZERO_VEC, + Vec3.multiply(direction, this.distanceToEntity) + ] + }); + this.grabbedEntity = intersection.entityID; + return true; } - } + return false; +} - this.grab = function(entityId) { - this.grabbing = true; - this.grabbedEntity = entityId; - this.entityProps = Entities.getEntityProperties(this.grabbedEntity); - this.targetPosition = this.entityProps.position; - this.currentPosition = this.targetPosition; - this.oldPalmPosition = this.palmPosition; - this.originalGravity = this.entityProps.gravity; - Entities.editEntity(this.grabbedEntity, { - gravity: { - x: 0, - y: 0, - z: 0 - } - }); - Overlays.editOverlay(this.laser, { - visible: false - }); - Audio.playSound(grabSound, { - position: this.entityProps.position, - volume: 0.25 - }); - } +controller.prototype.attemptMove = function() { + if (this.tractorBeamActive) { + return; + } + if (this.grabbedEntity || this.distanceHolding) { + var handPosition = Controller.getSpatialControlPosition(this.palm); + var direction = Controller.getSpatialControlNormal(this.tip); - this.release = function() { - this.grabbing = false; + var newPosition = Vec3.sum(handPosition, Vec3.multiply(direction, this.distanceToEntity)) + this.distanceHolding = true; + if (this.actionID === null) { + this.actionID = Entities.addAction("spring", this.grabbedEntity, { + targetPosition: newPosition, + linearTimeScale: .1 + }); + } else { + Entities.updateAction(this.grabbedEntity, this.actionID, { + targetPosition: newPosition + }); + } + } + +} + +controller.prototype.showPointer = function() { + Entities.editEntity(this.pointer, { + visible: true + }); + +} + +controller.prototype.hidePointer = function() { + Entities.editEntity(this.pointer, { + visible: false + }); +} + + +controller.prototype.letGo = function() { + if (this.grabbedEntity && this.actionID) { + this.deactivateEntity(this.grabbedEntity); + Entities.deleteAction(this.grabbedEntity, this.actionID); + } this.grabbedEntity = null; - Overlays.editOverlay(this.laser, { - visible: true - }); - Overlays.editOverlay(this.dropLine, { - visible: false - }); + this.actionID = null; + this.distanceHolding = false; + this.tractorBeamActive = false; + this.checkForEntityArrival = false; + this.closeGrabbing = false; +} - Audio.playSound(releaseSound, { - position: this.entityProps.position, - volume: 0.25 - }); - - // only restore the original gravity if it's not zero. This is to avoid... - // 1. interface A grabs an entity and locally saves off its gravity - // 2. interface A sets the entity's gravity to zero - // 3. interface B grabs the entity and saves off its gravity (which is zero) - // 4. interface A releases the entity and puts the original gravity back - // 5. interface B releases the entity and puts the original gravity back (to zero) - if(vectorIsZero(this.originalGravity)) { - Entities.editEntity(this.grabbedEntity, { - gravity: this.originalGravity - }); +controller.prototype.update = function() { + if (this.tractorBeamActive && this.checkForEntityArrival) { + var entityVelocity = Entities.getEntityProperties(this.grabbedEntity).velocity + if (Vec3.length(entityVelocity) < TRACTOR_BEAM_VELOCITY_THRESHOLD) { + this.letGo(); + } + return; + } + this.triggerValue = Controller.getActionValue(this.triggerAction); + if (this.triggerValue > SHOW_LINE_THRESHOLD && this.prevTriggerValue < SHOW_LINE_THRESHOLD) { + //First check if an object is within close range and then run the close grabbing logic + if (this.checkForInRangeObject()) { + this.grabEntity(); + } else { + this.showPointer(); + this.shouldDisplayLine = true; + } + } else if (this.triggerValue < SHOW_LINE_THRESHOLD && this.prevTriggerValue > SHOW_LINE_THRESHOLD) { + this.hidePointer(); + this.letGo(); + this.shouldDisplayLine = false; } - } - this.moveLaser = function() { - var inverseRotation = Quat.inverse(MyAvatar.orientation); - var startPosition = Vec3.multiplyQbyV(inverseRotation, Vec3.subtract(this.palmPosition, MyAvatar.position)); - // startPosition = Vec3.multiply(startPosition, 1 / MyAvatar.scale); - var direction = Vec3.multiplyQbyV(inverseRotation, Vec3.subtract(this.tipPosition, this.palmPosition)); - direction = Vec3.multiply(direction, LASER_LENGTH_FACTOR / (Vec3.length(direction) * MyAvatar.scale)); - var endPosition = Vec3.sum(startPosition, direction); + if (this.shouldDisplayLine) { + this.updateLine(); + } + if (this.triggerValue > DISTANCE_HOLD_THRESHOLD && !this.closeGrabbing) { + this.attemptMove(); + } - Overlays.editOverlay(this.laser, { - start: startPosition, - end: endPosition + + this.prevTriggerValue = this.triggerValue; +} + +controller.prototype.grabEntity = function() { + var handRotation = this.getHandRotation(); + var handPosition = this.getHandPosition(); + + var objectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation; + var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); + + var objectPosition = Entities.getEntityProperties(this.grabbedEntity).position; + var offset = Vec3.subtract(objectPosition, handPosition); + var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); + this.closeGrabbing = true; + this.actionID = Entities.addAction("hold", this.grabbedEntity, { + relativePosition: offsetPosition, + relativeRotation: offsetRotation, + hand: this.hand, + timeScale: 0.05 }); - - } - - this.cleanup = function() { - Overlays.deleteOverlay(this.laser); - Overlays.deleteOverlay(this.dropLine); - } } -function update(deltaTime) { - rightController.update(deltaTime); - leftController.update(deltaTime); + +controller.prototype.checkForInRangeObject = function() { + var handPosition = Controller.getSpatialControlPosition(this.palm); + var entities = Entities.findEntities(handPosition, GRAB_RADIUS); + var minDistance = GRAB_RADIUS; + var grabbedEntity = null; + //Get nearby entities and assign nearest + for (var i = 0; i < entities.length; i++) { + var props = Entities.getEntityProperties(entities[i]); + var distance = Vec3.distance(props.position, handPosition); + if (distance < minDistance && props.name !== "pointer" && props.collisionsWillMove === 1) { + grabbedEntity = entities[i]; + minDistance = distance; + } + } + if (grabbedEntity === null) { + return false; + } else { + //We are grabbing an entity, so let it know we've grabbed it + this.grabbedEntity = grabbedEntity; + this.activateEntity(this.grabbedEntity); + + return true; + } } -function scriptEnding() { - rightController.cleanup(); - leftController.cleanup(); +controller.prototype.activateEntity = function(entity) { + var data = { + activated: true, + avatarId: MyAvatar.sessionUUID + }; + setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); } -function vectorIsZero(v) { - return v.x === 0 && v.y === 0 && v.z === 0; +controller.prototype.deactivateEntity = function(entity) { + var data = { + activated: false, + avatarId: null + }; + setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data); } -var rightController = new controller(RIGHT); -var leftController = new controller(LEFT); +controller.prototype.onActionEvent = function(action, state) { + if (this.pullAction === action && state === 1) { + if (this.actionID !== null) { + var self = this; + this.tractorBeamActive = true; + //We need to wait a bit before checking for entity arrival at target destination (meaning checking for velocity being close to some + //low threshold) because otherwise we'll think the entity has arrived before its even really gotten moving! + Script.setTimeout(function() { + self.checkForEntityArrival = true; + }, 500); + var handPosition = Controller.getSpatialControlPosition(this.palm); + var direction = Controller.getSpatialControlNormal(this.tip); + //move final destination along line a bit, so it doesnt hit avatar hand + Entities.updateAction(this.grabbedEntity, this.actionID, { + targetPosition: Vec3.sum(handPosition, Vec3.multiply(2, direction)) + }); + } + } + +} + +controller.prototype.cleanup = function() { + Entities.deleteEntity(this.pointer); + if (this.grabbedEntity) { + Entities.deleteAction(this.grabbedEntity, this.actionID); + } +} + +function update() { + rightController.update(); + leftController.update(); +} + +function onActionEvent(action, state) { + rightController.onActionEvent(action, state); + leftController.onActionEvent(action, state); + +} -Script.update.connect(update); -Script.scriptEnding.connect(scriptEnding); \ No newline at end of file +function cleanup() { + rightController.cleanup(); + leftController.cleanup(); +} + + + +Script.scriptEnding.connect(cleanup); +Script.update.connect(update) +Controller.actionEvent.connect(onActionEvent); \ No newline at end of file diff --git a/examples/controllers/toybox.js b/examples/controllers/toybox.js index 4c0925f3b6..bf03974fda 100644 --- a/examples/controllers/toybox.js +++ b/examples/controllers/toybox.js @@ -13,32 +13,7 @@ Script.include("http://s3.amazonaws.com/hifi-public/scripts/libraries/toolBars.j HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -var nullActionID = "00000000-0000-0000-0000-000000000000"; -var controllerID; -var controllerActive; -var leftHandObjectID = null; -var rightHandObjectID = null; -var leftHandActionID = nullActionID; -var rightHandActionID = nullActionID; -var TRIGGER_THRESHOLD = 0.2; -var GRAB_RADIUS = 0.15; - -var LEFT_HAND_CLICK = Controller.findAction("LEFT_HAND_CLICK"); -var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK"); -var ACTION1 = Controller.findAction("ACTION1"); -var ACTION2 = Controller.findAction("ACTION2"); - -var rightHandGrabAction = RIGHT_HAND_CLICK; -var leftHandGrabAction = LEFT_HAND_CLICK; - -var rightHandGrabValue = 0; -var leftHandGrabValue = 0; -var prevRightHandGrabValue = 0 -var prevLeftHandGrabValue = 0; - -var grabColor = { red: 0, green: 255, blue: 0}; -var releaseColor = { red: 0, green: 0, blue: 255}; var toolBar = new ToolBar(0, 0, ToolBar.vertical, "highfidelity.toybox.toolbar", function() { return { @@ -63,25 +38,6 @@ var cleanupButton = toolBar.addOverlay("image", { alpha: 1 }); -var overlays = false; -var leftHandOverlay; -var rightHandOverlay; -if (overlays) { - leftHandOverlay = Overlays.addOverlay("sphere", { - position: MyAvatar.getLeftPalmPosition(), - size: GRAB_RADIUS, - color: releaseColor, - alpha: 0.5, - solid: false - }); - rightHandOverlay = Overlays.addOverlay("sphere", { - position: MyAvatar.getRightPalmPosition(), - size: GRAB_RADIUS, - color: releaseColor, - alpha: 0.5, - solid: false - }); -} var OBJECT_HEIGHT_OFFSET = 0.5; var MIN_OBJECT_SIZE = 0.05; @@ -98,8 +54,6 @@ var GRAVITY = { z: 0.0 } -var LEFT = 0; -var RIGHT = 1; var tableCreated = false; @@ -108,7 +62,6 @@ var tableEntities = Array(NUM_OBJECTS + 1); // Also includes table var VELOCITY_MAG = 0.3; -var entitiesToResize = []; var MODELS = Array( { modelURL: "https://hifi-public.s3.amazonaws.com/ozan/props/sword/sword.fbx" }, @@ -136,196 +89,15 @@ var COLLISION_SOUNDS = Array( var RESIZE_TIMER = 0.0; var RESIZE_WAIT = 0.05; // 50 milliseconds -var leftFist = Entities.addEntity( { - type: "Sphere", - shapeType: 'sphere', - position: MyAvatar.getLeftPalmPosition(), - dimensions: { x: GRAB_RADIUS, y: GRAB_RADIUS, z: GRAB_RADIUS }, - rotation: MyAvatar.getLeftPalmRotation(), - visible: false, - collisionsWillMove: false, - ignoreForCollisions: true - }); -var rightFist = Entities.addEntity( { - type: "Sphere", - shapeType: 'sphere', - position: MyAvatar.getRightPalmPosition(), - dimensions: { x: GRAB_RADIUS, y: GRAB_RADIUS, z: GRAB_RADIUS }, - rotation: MyAvatar.getRightPalmRotation(), - visible: false, - collisionsWillMove: false, - ignoreForCollisions: true - }); -function letGo(hand) { - var actionIDToRemove = (hand == LEFT) ? leftHandActionID : rightHandActionID; - var entityIDToEdit = (hand == LEFT) ? leftHandObjectID : rightHandObjectID; - var handVelocity = (hand == LEFT) ? MyAvatar.getLeftPalmVelocity() : MyAvatar.getRightPalmVelocity(); - var handAngularVelocity = (hand == LEFT) ? MyAvatar.getLeftPalmAngularVelocity() : - MyAvatar.getRightPalmAngularVelocity(); - if (actionIDToRemove != nullActionID && entityIDToEdit != null) { - Entities.deleteAction(entityIDToEdit, actionIDToRemove); - // TODO: upon successful letGo, restore collision groups - if (hand == LEFT) { - leftHandObjectID = null; - leftHandActionID = nullActionID; - } else { - rightHandObjectID = null; - rightHandActionID = nullActionID; - } - } -} -function setGrabbedObject(hand) { - var handPosition = (hand == LEFT) ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); - var entities = Entities.findEntities(handPosition, GRAB_RADIUS); - var objectID = null; - var minDistance = GRAB_RADIUS; - for (var i = 0; i < entities.length; i++) { - // Don't grab the object in your other hands, your fists, or the table - if ((hand == LEFT && entities[i] == rightHandObjectID) || - (hand == RIGHT && entities[i] == leftHandObjectID) || - entities[i] == leftFist || entities[i] == rightFist || - (tableCreated && entities[i] == tableEntities[0])) { - continue; - } else { - var distance = Vec3.distance(Entities.getEntityProperties(entities[i]).position, handPosition); - if (distance <= minDistance) { - objectID = entities[i]; - minDistance = distance; - } - } - } - if (objectID == null) { - return false; - } - if (hand == LEFT) { - leftHandObjectID = objectID; - } else { - rightHandObjectID = objectID; - } - return true; -} - -function grab(hand) { - if (!setGrabbedObject(hand)) { - // If you don't grab an object, make a fist - Entities.editEntity((hand == LEFT) ? leftFist : rightFist, { ignoreForCollisions: false } ); - return; - } - var objectID = (hand == LEFT) ? leftHandObjectID : rightHandObjectID; - var handRotation = (hand == LEFT) ? MyAvatar.getLeftPalmRotation() : MyAvatar.getRightPalmRotation(); - var handPosition = (hand == LEFT) ? MyAvatar.getLeftPalmPosition() : MyAvatar.getRightPalmPosition(); - - var objectRotation = Entities.getEntityProperties(objectID).rotation; - var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation); - - var objectPosition = Entities.getEntityProperties(objectID).position; - var offset = Vec3.subtract(objectPosition, handPosition); - var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset); - // print(JSON.stringify(offsetPosition)); - var actionID = Entities.addAction("hold", objectID, { - relativePosition: { x: 0, y: 0, z: 0 }, - relativeRotation: offsetRotation, - hand: (hand == LEFT) ? "left" : "right", - timeScale: 0.05 - }); - if (actionID == nullActionID) { - if (hand == LEFT) { - leftHandObjectID = null; - } else { - rightHandObjectID = null; - } - } else { - // TODO: upon successful grab, add to collision group so object doesn't collide with immovable entities - if (hand == LEFT) { - leftHandActionID = actionID; - } else { - rightHandActionID = actionID; - } - } -} - -function resizeModels() { - var newEntitiesToResize = []; - for (var i = 0; i < entitiesToResize.length; i++) { - var naturalDimensions = Entities.getEntityProperties(entitiesToResize[i]).naturalDimensions; - if (naturalDimensions.x != 1.0 || naturalDimensions.y != 1.0 || naturalDimensions.z != 1.0) { - // bigger range of sizes for models - var dimensions = Vec3.multiply(randFloat(MIN_OBJECT_SIZE, 3.0*MAX_OBJECT_SIZE), Vec3.normalize(naturalDimensions)); - Entities.editEntity(entitiesToResize[i], { - dimensions: dimensions, - shapeType: "box" - }); - } else { - newEntitiesToResize.push(entitiesToResize[i]); - } - - } - entitiesToResize = newEntitiesToResize; -} - -function update(deltaTime) { - if (overlays) { - Overlays.editOverlay(leftHandOverlay, { position: MyAvatar.getLeftPalmPosition() }); - Overlays.editOverlay(rightHandOverlay, { position: MyAvatar.getRightPalmPosition() }); - } - - // if (tableCreated && RESIZE_TIMER < RESIZE_WAIT) { - // RESIZE_TIMER += deltaTime; - // } else if (tableCreated) { - // resizeModels(); - // } - - rightHandGrabValue = Controller.getActionValue(rightHandGrabAction); - leftHandGrabValue = Controller.getActionValue(leftHandGrabAction); - - Entities.editEntity(leftFist, { position: MyAvatar.getLeftPalmPosition() }); - Entities.editEntity(rightFist, { position: MyAvatar.getRightPalmPosition() }); - - if (rightHandGrabValue > TRIGGER_THRESHOLD && - prevRightHandGrabValue < TRIGGER_THRESHOLD) { - if (overlays) { - Overlays.editOverlay(rightHandOverlay, { color: grabColor }); - } - grab(RIGHT); - } else if (rightHandGrabValue < TRIGGER_THRESHOLD && - prevRightHandGrabValue > TRIGGER_THRESHOLD) { - Entities.editEntity(rightFist, { ignoreForCollisions: true } ); - if (overlays) { - Overlays.editOverlay(rightHandOverlay, { color: releaseColor }); - } - letGo(RIGHT); - } - - if (leftHandGrabValue > TRIGGER_THRESHOLD && - prevLeftHandGrabValue < TRIGGER_THRESHOLD) { - if (overlays) { - Overlays.editOverlay(leftHandOverlay, { color: grabColor }); - } - grab(LEFT); - } else if (leftHandGrabValue < TRIGGER_THRESHOLD && - prevLeftHandGrabValue > TRIGGER_THRESHOLD) { - Entities.editEntity(leftFist, { ignoreForCollisions: true } ); - if (overlays) { - Overlays.editOverlay(leftHandOverlay, { color: releaseColor }); - } - letGo(LEFT); - } - - prevRightHandGrabValue = rightHandGrabValue; - prevLeftHandGrabValue = leftHandGrabValue; -} function cleanUp() { - letGo(RIGHT); - letGo(LEFT); + print("CLEANUP!!!") if (overlays) { Overlays.deleteOverlay(leftHandOverlay); Overlays.deleteOverlay(rightHandOverlay); } - Entities.deleteEntity(leftFist); - Entities.deleteEntity(rightFist); removeTable(); toolBar.cleanup(); } @@ -405,7 +177,6 @@ function createTable() { density: 0.5, collisionsWillMove: true, color: { red: randInt(0, 255), green: randInt(0, 255), blue: randInt(0, 255) }, - // collisionSoundURL: COLLISION_SOUNDS[randInt(0, COLLISION_SOUNDS.length)] }); if (type == "Model") { var randModel = randInt(0, MODELS.length); @@ -413,7 +184,6 @@ function createTable() { shapeType: "box", modelURL: MODELS[randModel].modelURL }); - entitiesToResize.push(tableEntities[i]); } } } @@ -426,5 +196,4 @@ function removeTable() { } Script.scriptEnding.connect(cleanUp); -Script.update.connect(update); Controller.mousePressEvent.connect(onClick); \ No newline at end of file diff --git a/examples/defaultScripts.js b/examples/defaultScripts.js index c602c36382..44801ef737 100644 --- a/examples/defaultScripts.js +++ b/examples/defaultScripts.js @@ -17,3 +17,4 @@ Script.load("users.js"); Script.load("grab.js"); Script.load("directory.js"); Script.load("dialTone.js"); +Script.load("libraries/omniTool.js"); diff --git a/examples/entityScripts/breakdanceEntity.js b/examples/entityScripts/breakdanceEntity.js new file mode 100644 index 0000000000..77980c0d13 --- /dev/null +++ b/examples/entityScripts/breakdanceEntity.js @@ -0,0 +1,88 @@ +// +// breakdanceEntity.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 9/3/15. +// Copyright 2015 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to an entity, will start the breakdance game if you grab and hold the entity +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + Script.include("../toys/breakdanceCore.js"); + Script.include("../libraries/utils.js"); + + var _this; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + BreakdanceEntity = function() { + _this = this; + print("BreakdanceEntity constructor"); + }; + + BreakdanceEntity.prototype = { + + // update() will be called regulary, because we've hooked the update signal in our preload() function + // we will check out userData for the grabData. In the case of the hydraGrab script, it will tell us + // if we're currently being grabbed and if the person grabbing us is the current interfaces avatar. + // we will watch this for state changes and print out if we're being grabbed or released when it changes. + update: function() { + var GRAB_USER_DATA_KEY = "grabKey"; + + // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID + var entityID = _this.entityID; + + // we want to assume that if there is no grab data, then we are not being grabbed + var defaultGrabData = { activated: false, avatarId: null }; + + // this handy function getEntityCustomData() is available in utils.js and it will return just the specific section + // of user data we asked for. If it's not available it returns our default data. + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, defaultGrabData); + + // if the grabData says we're being grabbed, and the owner ID is our session, then we are being grabbed by this interface + if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { + + if (!_this.beingGrabbed) { + // remember we're being grabbed so we can detect being released + _this.beingGrabbed = true; + breakdanceStart(); + print("I'm was grabbed..."); + } else { + breakdanceUpdate(); + } + + } else if (_this.beingGrabbed) { + + // if we are not being grabbed, and we previously were, then we were just released, remember that + // and print out a message + _this.beingGrabbed = false; + print("I'm was released..."); + breakdanceEnd(); + } + }, + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + this.entityID = entityID; + Script.update.connect(this.update); + }, + + // unload() will be called when our entity is no longer available. It may be because we were deleted, + // or because we've left the domain or quit the application. In all cases we want to unhook our connection + // to the update signal + unload: function(entityID) { + Script.update.disconnect(this.update); + }, + + }; + + // entity scripts always need to return a newly constructed object of our type + return new BreakdanceEntity(); +}) diff --git a/examples/entityScripts/detectGrabExample.js b/examples/entityScripts/detectGrabExample.js new file mode 100644 index 0000000000..cdc79e119d --- /dev/null +++ b/examples/entityScripts/detectGrabExample.js @@ -0,0 +1,81 @@ +// +// detectGrabExample.js +// examples/entityScripts +// +// Created by Brad Hefta-Gaub on 9/3/15. +// Copyright 2015 High Fidelity, Inc. +// +// This is an example of an entity script which when assigned to an entity, will detect when the entity is being grabbed by the hydraGrab script +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + Script.include("../libraries/utils.js"); + + var _this; + + // this is the "constructor" for the entity as a JS object we don't do much here, but we do want to remember + // our this object, so we can access it in cases where we're called without a this (like in the case of various global signals) + DetectGrabbed = function() { + _this = this; + }; + + DetectGrabbed.prototype = { + + // update() will be called regulary, because we've hooked the update signal in our preload() function + // we will check out userData for the grabData. In the case of the hydraGrab script, it will tell us + // if we're currently being grabbed and if the person grabbing us is the current interfaces avatar. + // we will watch this for state changes and print out if we're being grabbed or released when it changes. + update: function() { + var GRAB_USER_DATA_KEY = "grabKey"; + + // because the update() signal doesn't have a valid this, we need to use our memorized _this to access our entityID + var entityID = _this.entityID; + + // we want to assume that if there is no grab data, then we are not being grabbed + var defaultGrabData = { activated: false, avatarId: null }; + + // this handy function getEntityCustomData() is available in utils.js and it will return just the specific section + // of user data we asked for. If it's not available it returns our default data. + var grabData = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, defaultGrabData); + + // if the grabData says we're being grabbed, and the owner ID is our session, then we are being grabbed by this interface + if (grabData.activated && grabData.avatarId == MyAvatar.sessionUUID) { + + // remember we're being grabbed so we can detect being released + _this.beingGrabbed = true; + + // print out that we're being grabbed + print("I'm being grabbed..."); + + } else if (_this.beingGrabbed) { + + // if we are not being grabbed, and we previously were, then we were just released, remember that + // and print out a message + _this.beingGrabbed = false; + print("I'm was released..."); + } + }, + + // preload() will be called when the entity has become visible (or known) to the interface + // it gives us a chance to set our local JavaScript object up. In this case it means: + // * remembering our entityID, so we can access it in cases where we're called without an entityID + // * connecting to the update signal so we can check our grabbed state + preload: function(entityID) { + this.entityID = entityID; + Script.update.connect(this.update); + }, + + // unload() will be called when our entity is no longer available. It may be because we were deleted, + // or because we've left the domain or quit the application. In all cases we want to unhook our connection + // to the update signal + unload: function(entityID) { + Script.update.disconnect(this.update); + }, + }; + + // entity scripts always need to return a newly constructed object of our type + return new DetectGrabbed(); +}) diff --git a/examples/html/magBalls/addMode.html b/examples/html/magBalls/addMode.html new file mode 100644 index 0000000000..f1db142daa --- /dev/null +++ b/examples/html/magBalls/addMode.html @@ -0,0 +1,25 @@ + + + + + + + +
Add
+ + \ No newline at end of file diff --git a/examples/html/magBalls/deleteMode.html b/examples/html/magBalls/deleteMode.html new file mode 100644 index 0000000000..94f23fdef8 --- /dev/null +++ b/examples/html/magBalls/deleteMode.html @@ -0,0 +1,25 @@ + + + + + + + +
Delete
+ + \ No newline at end of file diff --git a/examples/html/magBalls/magBalls.css b/examples/html/magBalls/magBalls.css new file mode 100644 index 0000000000..339f094ea7 --- /dev/null +++ b/examples/html/magBalls/magBalls.css @@ -0,0 +1,14 @@ +.container { + display:table; + position:absolute; + width: 100%; + height: 100%; +} + +span { + font-size: 25vw; + display: table-cell; + vertical-align: middle; + line-height: normal; + text-align: center; +} \ No newline at end of file diff --git a/examples/html/magBalls/moveMode.html b/examples/html/magBalls/moveMode.html new file mode 100644 index 0000000000..eae53be635 --- /dev/null +++ b/examples/html/magBalls/moveMode.html @@ -0,0 +1,25 @@ + + + + + + + +
Move
+ + \ No newline at end of file diff --git a/examples/libraries/avatarRelativeOverlays.js b/examples/libraries/avatarRelativeOverlays.js new file mode 100644 index 0000000000..b12971037c --- /dev/null +++ b/examples/libraries/avatarRelativeOverlays.js @@ -0,0 +1,55 @@ + +AvatarRelativeOverlays = function() { + // id -> position & rotation + this.overlays = {}; + this.lastAvatarTransform = { + position: ZERO_VECTOR, + rotation: IDENTITY_QUATERNION, + }; +} + +// FIXME judder in movement is annoying.... add an option to +// automatically hide all overlays when the position or orientation change and then +// restore the ones that were previously visible once the movement stops. +AvatarRelativeOverlays.prototype.onUpdate = function(deltaTime) { + // cache avatar position and orientation and only update on change + if (Vec3.equal(this.lastAvatarTransform.position, MyAvatar.position) && + Quat.equal(this.lastAvatarTransform.rotation, MyAvatar.orientation)) { + return; + } + + this.lastAvatarTransform.position = MyAvatar.position; + this.lastAvatarTransform.rotation = MyAvatar.orientation; + for (var overlayId in this.overlays) { + this.updateOverlayTransform(overlayId); + } +} + +AvatarRelativeOverlays.prototype.updateOverlayTransform = function(overlayId) { + Overlays.editOverlay(overlayId, { + position: getEyeRelativePosition(this.overlays[overlayId].position), + rotation: getAvatarRelativeRotation(this.overlays[overlayId].rotation), + }) +} + +AvatarRelativeOverlays.prototype.addOverlay = function(type, overlayDefinition) { + var overlayId = Overlays.addOverlay(type, overlayDefinition); + if (!overlayId) { + logDebug("Failed to create overlay of type " + type); + return; + } + this.overlays[overlayId] = { + position: overlayDefinition.position || ZERO_VECTOR, + rotation: overlayDefinition.rotation || IDENTITY_QUATERNION, + }; + this.updateOverlayTransform(overlayId); + return overlayId; +} + +AvatarRelativeOverlays.prototype.deleteAll = function() { + for (var overlayId in this.overlays) { + Overlays.deleteOverlay(overlayId); + } + this.overlays = {}; +} + diff --git a/examples/libraries/constants.js b/examples/libraries/constants.js new file mode 100644 index 0000000000..117e72267c --- /dev/null +++ b/examples/libraries/constants.js @@ -0,0 +1,65 @@ +// +// Created by Bradley Austin Davis on 2015/08/27 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; +STICK_URL = HIFI_PUBLIC_BUCKET + "models/props/geo_stick.fbx"; + +ZERO_VECTOR = { x: 0, y: 0, z: 0 }; + +IDENTITY_QUATERNION = { w: 1, x: 0, y: 0, z: 0 }; + +COLORS = { + WHITE: { + red: 255, + green: 255, + blue: 255, + }, + BLACK: { + red: 0, + green: 0, + blue: 0, + }, + GREY: { + red: 128, + green: 128, + blue: 128, + }, + RED: { + red: 255, + green: 0, + blue: 0 + }, + BLUE: { + red: 0, + green: 0, + blue: 255 + }, + GREEN: { + red: 0, + green: 255, + blue: 0 + }, + CYAN: { + red: 0, + green: 255, + blue: 255 + }, + YELLOW: { + red: 255, + green: 255, + blue: 0 + }, + MAGENTA: { + red: 255, + green: 0, + blue: 255 + } +} + + + diff --git a/examples/toys/magBalls/highlighter.js b/examples/libraries/highlighter.js similarity index 66% rename from examples/toys/magBalls/highlighter.js rename to examples/libraries/highlighter.js index 149d9ec5b7..33aa445b88 100644 --- a/examples/toys/magBalls/highlighter.js +++ b/examples/libraries/highlighter.js @@ -29,7 +29,7 @@ var SELECTION_OVERLAY = { Highlighter = function() { this.highlightCube = Overlays.addOverlay("cube", this.SELECTION_OVERLAY); - this.hightlighted = null; + this.highlighted = null; var _this = this; Script.scriptEnding.connect(function() { _this.onCleanup(); @@ -40,9 +40,9 @@ Highlighter.prototype.onCleanup = function() { Overlays.deleteOverlay(this.highlightCube); } -Highlighter.prototype.highlight = function(entityId) { - if (entityId != this.hightlighted) { - this.hightlighted = entityId; +Highlighter.prototype.highlight = function(entityIdOrPosition) { + if (entityIdOrPosition != this.highlighted) { + this.highlighted = entityIdOrPosition; this.updateHighlight(); } } @@ -53,12 +53,29 @@ Highlighter.prototype.setSize = function(newSize) { }); } +Highlighter.prototype.setColor = function(color) { + Overlays.editOverlay(this.highlightCube, { + color: color + }); +} + + +Highlighter.prototype.setRotation = function(newRotation) { + Overlays.editOverlay(this.highlightCube, { + rotation: newRotation + }); +} + Highlighter.prototype.updateHighlight = function() { - if (this.hightlighted) { - var properties = Entities.getEntityProperties(this.hightlighted); + if (this.highlighted) { + var position = this.highlighted; + if (typeof this.highlighted === "string") { + var properties = Entities.getEntityProperties(this.highlighted); + position = properties.position; + } // logDebug("Making highlight " + this.highlightCube + " visible @ " + vec3toStr(properties.position)); Overlays.editOverlay(this.highlightCube, { - position: properties.position, + position: position, visible: true }); } else { diff --git a/examples/libraries/htmlColors.js b/examples/libraries/htmlColors.js new file mode 100644 index 0000000000..e9ebbba841 --- /dev/null +++ b/examples/libraries/htmlColors.js @@ -0,0 +1,145 @@ + +HTML_COLORS = { + AliceBlue: "#F0F8FF", + AntiqueWhite: "#FAEBD7", + Aqua: "#00FFFF", + Aquamarine: "#7FFFD4", + Azure: "#F0FFFF", + Beige: "#F5F5DC", + Bisque: "#FFE4C4", + Black: "#000000", + BlanchedAlmond: "#FFEBCD", + Blue: "#0000FF", + BlueViolet: "#8A2BE2", + Brown: "#A52A2A", + BurlyWood: "#DEB887", + CadetBlue: "#5F9EA0", + Chartreuse: "#7FFF00", + Chocolate: "#D2691E", + Coral: "#FF7F50", + CornflowerBlue: "#6495ED", + Cornsilk: "#FFF8DC", + Crimson: "#DC143C", + Cyan: "#00FFFF", + DarkBlue: "#00008B", + DarkCyan: "#008B8B", + DarkGoldenRod: "#B8860B", + DarkGray: "#A9A9A9", + DarkGreen: "#006400", + DarkKhaki: "#BDB76B", + DarkMagenta: "#8B008B", + DarkOliveGreen: "#556B2F", + DarkOrange: "#FF8C00", + DarkOrchid: "#9932CC", + DarkRed: "#8B0000", + DarkSalmon: "#E9967A", + DarkSeaGreen: "#8FBC8F", + DarkSlateBlue: "#483D8B", + DarkSlateGray: "#2F4F4F", + DarkTurquoise: "#00CED1", + DarkViolet: "#9400D3", + DeepPink: "#FF1493", + DeepSkyBlue: "#00BFFF", + DimGray: "#696969", + DodgerBlue: "#1E90FF", + FireBrick: "#B22222", + FloralWhite: "#FFFAF0", + ForestGreen: "#228B22", + Fuchsia: "#FF00FF", + Gainsboro: "#DCDCDC", + GhostWhite: "#F8F8FF", + Gold: "#FFD700", + GoldenRod: "#DAA520", + Gray: "#808080", + Green: "#008000", + GreenYellow: "#ADFF2F", + HoneyDew: "#F0FFF0", + HotPink: "#FF69B4", + IndianRed: "#CD5C5C", + Indigo: "#4B0082", + Ivory: "#FFFFF0", + Khaki: "#F0E68C", + Lavender: "#E6E6FA", + LavenderBlush: "#FFF0F5", + LawnGreen: "#7CFC00", + LemonChiffon: "#FFFACD", + LightBlue: "#ADD8E6", + LightCoral: "#F08080", + LightCyan: "#E0FFFF", + LightGoldenRodYellow: "#FAFAD2", + LightGray: "#D3D3D3", + LightGreen: "#90EE90", + LightPink: "#FFB6C1", + LightSalmon: "#FFA07A", + LightSeaGreen: "#20B2AA", + LightSkyBlue: "#87CEFA", + LightSlateGray: "#778899", + LightSteelBlue: "#B0C4DE", + LightYellow: "#FFFFE0", + Lime: "#00FF00", + LimeGreen: "#32CD32", + Linen: "#FAF0E6", + Magenta: "#FF00FF", + Maroon: "#800000", + MediumAquaMarine: "#66CDAA", + MediumBlue: "#0000CD", + MediumOrchid: "#BA55D3", + MediumPurple: "#9370DB", + MediumSeaGreen: "#3CB371", + MediumSlateBlue: "#7B68EE", + MediumSpringGreen: "#00FA9A", + MediumTurquoise: "#48D1CC", + MediumVioletRed: "#C71585", + MidnightBlue: "#191970", + MintCream: "#F5FFFA", + MistyRose: "#FFE4E1", + Moccasin: "#FFE4B5", + NavajoWhite: "#FFDEAD", + Navy: "#000080", + OldLace: "#FDF5E6", + Olive: "#808000", + OliveDrab: "#6B8E23", + Orange: "#FFA500", + OrangeRed: "#FF4500", + Orchid: "#DA70D6", + PaleGoldenRod: "#EEE8AA", + PaleGreen: "#98FB98", + PaleTurquoise: "#AFEEEE", + PaleVioletRed: "#DB7093", + PapayaWhip: "#FFEFD5", + PeachPuff: "#FFDAB9", + Peru: "#CD853F", + Pink: "#FFC0CB", + Plum: "#DDA0DD", + PowderBlue: "#B0E0E6", + Purple: "#800080", + RebeccaPurple: "#663399", + Red: "#FF0000", + RosyBrown: "#BC8F8F", + RoyalBlue: "#4169E1", + SaddleBrown: "#8B4513", + Salmon: "#FA8072", + SandyBrown: "#F4A460", + SeaGreen: "#2E8B57", + SeaShell: "#FFF5EE", + Sienna: "#A0522D", + Silver: "#C0C0C0", + SkyBlue: "#87CEEB", + SlateBlue: "#6A5ACD", + SlateGray: "#708090", + Snow: "#FFFAFA", + SpringGreen: "#00FF7F", + SteelBlue: "#4682B4", + Tan: "#D2B48C", + Teal: "#008080", + Thistle: "#D8BFD8", + Tomato: "#FF6347", + Turquoise: "#40E0D0", + Violet: "#EE82EE", + Wheat: "#F5DEB3", + White: "#FFFFFF", + WhiteSmoke: "#F5F5F5", + Yellow: "#FFFF00", + YellowGreen: "#9ACD32", +} + diff --git a/examples/libraries/omniTool.js b/examples/libraries/omniTool.js new file mode 100644 index 0000000000..26c299cdfb --- /dev/null +++ b/examples/libraries/omniTool.js @@ -0,0 +1,351 @@ +// +// Created by Bradley Austin Davis on 2015/09/01 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("constants.js"); +Script.include("utils.js"); +Script.include("highlighter.js"); +Script.include("omniTool/models/modelBase.js"); +Script.include("omniTool/models/wand.js"); +Script.include("omniTool/models/invisibleWand.js"); + +OmniToolModules = {}; +OmniToolModuleType = null; + +OmniTool = function(side) { + this.OMNI_KEY = "OmniTool"; + this.MAX_FRAMERATE = 60; + this.UPDATE_INTERVAL = 1.0 / this.MAX_FRAMERATE + this.SIDE = side; + this.PALM = 2 * side; + this.ACTION = findAction(side ? "ACTION2" : "ACTION1"); + this.ALT_ACTION = findAction(side ? "ACTION1" : "ACTION2"); + + this.highlighter = new Highlighter(); + this.ignoreEntities = {}; + this.nearestOmniEntity = { + id: null, + + inside: false, + position: null, + distance: Infinity, + radius: 0, + omniProperties: {}, + boundingBox: null, + }; + + this.activeOmniEntityId = null; + this.lastUpdateInterval = 0; + this.active = false; + this.module = null; + this.moduleEntityId = null; + this.lastScanPosition = ZERO_VECTOR; + this.showWand(false); + + // Connect to desired events + var _this = this; + Controller.actionEvent.connect(function(action, state) { + _this.onActionEvent(action, state); + }); + + Script.update.connect(function(deltaTime) { + _this.lastUpdateInterval += deltaTime; + if (_this.lastUpdateInterval >= _this.UPDATE_INTERVAL) { + _this.onUpdate(_this.lastUpdateInterval); + _this.lastUpdateInterval = 0; + } + }); + + Script.scriptEnding.connect(function() { + _this.onCleanup(); + }); +} + +OmniTool.prototype.showWand = function(show) { + if (this.model && this.model.onCleanup) { + this.model.onCleanup(); + } + logDebug("Showing wand: " + show); + if (show) { + this.model = new Wand(); + this.model.setLength(0.4); + this.model.setVisible(true); + } else { + this.model = new InvisibleWand(); + this.model.setLength(0.1); + this.model.setVisible(true); + } +} + + +OmniTool.prototype.onCleanup = function(action) { + this.unloadModule(); +} + +OmniTool.prototype.onActionEvent = function(action, state) { + // FIXME figure out the issues when only one spatial controller is active + // logDebug("Action: " + action + " " + state); + + if (this.module && this.module.onActionEvent) { + this.module.onActionEvent(action, state); + } + + if (action == this.ACTION) { + if (state) { + this.onClick(); + } else { + this.onRelease(); + } + } + + // FIXME Does not work + //// with only one controller active (listed as 2 here because 'tip' + 'palm') + //// then treat the alt action button as the action button +} + +OmniTool.prototype.getOmniToolData = function(entityId) { + return getEntityCustomData(this.OMNI_KEY, entityId, null); +} + +OmniTool.prototype.setOmniToolData = function(entityId, data) { + setEntityCustomData(this.OMNI_KEY, entityId, data); +} + +OmniTool.prototype.updateOmniToolData = function(entityId, data) { + var currentData = this.getOmniToolData(entityId) || {}; + for (var key in data) { + currentData[key] = data[key]; + } + setEntityCustomData(this.OMNI_KEY, entityId, currentData); +} + +OmniTool.prototype.setActive = function(active) { + if (active === this.active) { + return; + } + logDebug("OmniTool changing active state: " + active); + this.active = active; + this.model.setVisible(this.active); + if (this.module && this.module.onActiveChanged) { + this.module.onActiveChanged(this.side); + } +} + + +OmniTool.prototype.onUpdate = function(deltaTime) { + // FIXME this returns data if either the left or right controller is not on the base + this.position = Controller.getSpatialControlPosition(this.PALM); + // When on the base, hydras report a position of 0 + this.setActive(Vec3.length(this.position) > 0.001); + if (!this.active) { + return; + } + + + if (this.model) { + // Update the wand + var rawRotation = Controller.getSpatialControlRawRotation(this.PALM); + this.rotation = Quat.multiply(MyAvatar.orientation, rawRotation); + this.model.setTransform({ + rotation: this.rotation, + position: this.position, + }); + + if (this.model.onUpdate) { + this.model.onUpdate(deltaTime); + } + } + + + this.scan(); + + if (this.module && this.module.onUpdate) { + this.module.onUpdate(deltaTime); + } +} + +OmniTool.prototype.onClick = function() { + // First check to see if the user is switching to a new omni module + if (this.nearestOmniEntity.inside && this.nearestOmniEntity.omniProperties.script) { + + // If this is already the active entity, turn it off + // FIXME add a flag to allow omni modules to cause this entity to be + // ignored in order to support items that will be picked up. + if (this.moduleEntityId && this.moduleEntityId == this.nearestOmniEntity.id) { + this.showWand(false); + this.unloadModule(); + this.highlighter.setColor("White"); + return; + } + + this.showWand(true); + this.highlighter.setColor("Red"); + this.activateNewOmniModule(); + return; + } + + // Next check if there is an active module and if so propagate the click + // FIXME how to I switch to a new module? + if (this.module && this.module.onClick) { + this.module.onClick(); + return; + } +} + +OmniTool.prototype.onRelease = function() { + if (this.module && this.module.onRelease) { + this.module.onRelease(); + return; + } +} + +// FIXME resturn a structure of all nearby entities to distances +OmniTool.prototype.findNearestOmniEntity = function(maxDistance, selector) { + if (!maxDistance) { + maxDistance = 2.0; + } + var resultDistance = Infinity; + var resultId = null; + var resultProperties = null; + var resultOmniData = null; + var ids = Entities.findEntities(this.model.tipPosition, maxDistance); + for (var i in ids) { + var entityId = ids[i]; + if (this.ignoreEntities[entityId]) { + continue; + } + var omniData = this.getOmniToolData(entityId); + if (!omniData) { + // FIXME find a place to flush this information + this.ignoreEntities[entityId] = true; + continue; + } + + // Let searchers query specifically + if (selector && !selector(entityId, omniData)) { + continue; + } + + var properties = Entities.getEntityProperties(entityId); + var distance = Vec3.distance(this.model.tipPosition, properties.position); + if (distance < resultDistance) { + resultDistance = distance; + resultId = entityId; + } + } + + return resultId; +} + +OmniTool.prototype.getPosition = function() { + return this.model.tipPosition; +} + +OmniTool.prototype.onEnterNearestOmniEntity = function() { + this.nearestOmniEntity.inside = true; + this.highlighter.highlight(this.nearestOmniEntity.id); + if (this.moduleEntityId && this.moduleEntityId == this.nearestOmniEntity.id) { + this.highlighter.setColor("Red"); + } else { + this.highlighter.setColor("White"); + } + logDebug("On enter omniEntity " + this.nearestOmniEntity.id); +} + +OmniTool.prototype.onLeaveNearestOmniEntity = function() { + this.nearestOmniEntity.inside = false; + this.highlighter.highlight(null); + logDebug("On leave omniEntity " + this.nearestOmniEntity.id); +} + +OmniTool.prototype.setNearestOmniEntity = function(entityId) { + if (entityId && entityId !== this.nearestOmniEntity.id) { + if (this.nearestOmniEntity.id && this.nearestOmniEntity.inside) { + this.onLeaveNearestOmniEntity(); + } + this.nearestOmniEntity.id = entityId; + this.nearestOmniEntity.omniProperties = this.getOmniToolData(entityId); + var properties = Entities.getEntityProperties(entityId); + this.nearestOmniEntity.position = properties.position; + // FIXME use a real bounding box, not a sphere + var bbox = properties.boundingBox; + this.nearestOmniEntity.radius = Vec3.length(Vec3.subtract(bbox.center, bbox.brn)); + this.highlighter.setRotation(properties.rotation); + this.highlighter.setSize(Vec3.multiply(1.05, bbox.dimensions)); + } +} + +OmniTool.prototype.scan = function() { + var scanDistance = Vec3.distance(this.model.tipPosition, this.lastScanPosition); + + if (scanDistance < 0.005) { + return; + } + + this.lastScanPosition = this.model.tipPosition; + + this.setNearestOmniEntity(this.findNearestOmniEntity()); + if (this.nearestOmniEntity.id) { + var distance = Vec3.distance(this.model.tipPosition, this.nearestOmniEntity.position); + // track distance on a half centimeter basis + if (Math.abs(this.nearestOmniEntity.distance - distance) > 0.005) { + this.nearestOmniEntity.distance = distance; + if (!this.nearestOmniEntity.inside && distance < this.nearestOmniEntity.radius) { + this.onEnterNearestOmniEntity(); + } + + if (this.nearestOmniEntity.inside && distance > this.nearestOmniEntity.radius + 0.01) { + this.onLeaveNearestOmniEntity(); + } + } + } +} + +OmniTool.prototype.unloadModule = function() { + if (this.module && this.module.onUnload) { + this.module.onUnload(); + } + this.module = null; + this.moduleEntityId = null; +} + +OmniTool.prototype.activateNewOmniModule = function() { + // Support the ability for scripts to just run without replacing the current module + var script = this.nearestOmniEntity.omniProperties.script; + if (script.indexOf("/") < 0) { + script = "omniTool/modules/" + script; + } + + // Reset the tool type + OmniToolModuleType = null; + logDebug("Including script path: " + script); + try { + Script.include(script); + } catch(err) { + logWarn("Failed to include script: " + script + "\n" + err); + return; + } + + // If we're building a new module, unload the old one + if (OmniToolModuleType) { + logDebug("New OmniToolModule: " + OmniToolModuleType); + this.unloadModule(); + + try { + this.module = new OmniToolModules[OmniToolModuleType](this, this.nearestOmniEntity.id); + this.moduleEntityId = this.nearestOmniEntity.id; + if (this.module.onLoad) { + this.module.onLoad(); + } + } catch(err) { + logWarn("Failed to instantiate new module: " + err); + } + } +} + +// FIXME find a good way to sync the two omni tools +OMNI_TOOLS = [ new OmniTool(0), new OmniTool(1) ]; diff --git a/examples/libraries/omniTool/models/invisibleWand.js b/examples/libraries/omniTool/models/invisibleWand.js new file mode 100644 index 0000000000..63e0945af3 --- /dev/null +++ b/examples/libraries/omniTool/models/invisibleWand.js @@ -0,0 +1,6 @@ + +InvisibleWand = function() { +} + +InvisibleWand.prototype = Object.create( ModelBase.prototype ); + diff --git a/examples/libraries/omniTool/models/modelBase.js b/examples/libraries/omniTool/models/modelBase.js new file mode 100644 index 0000000000..87f93b1204 --- /dev/null +++ b/examples/libraries/omniTool/models/modelBase.js @@ -0,0 +1,25 @@ + +ModelBase = function() { + this.length = 0.2; +} + +ModelBase.prototype.setVisible = function(visible) { + this.visible = visible; +} + +ModelBase.prototype.setLength = function(length) { + this.length = length; +} + +ModelBase.prototype.setTransform = function(transform) { + this.rotation = transform.rotation; + this.position = transform.position; + this.tipVector = Vec3.multiplyQbyV(this.rotation, { x: 0, y: this.length, z: 0 }); + this.tipPosition = Vec3.sum(this.position, this.tipVector); +} + +ModelBase.prototype.setTipColors = function(color1, color2) { +} + +ModelBase.prototype.onCleanup = function() { +} diff --git a/examples/libraries/omniTool/models/wand.js b/examples/libraries/omniTool/models/wand.js new file mode 100644 index 0000000000..e14c90b36a --- /dev/null +++ b/examples/libraries/omniTool/models/wand.js @@ -0,0 +1,111 @@ + +Wand = function() { + // Max updates fps + this.DEFAULT_TIP_COLORS = [ { + red: 128, + green: 128, + blue: 128, + }, { + red: 0, + green: 0, + blue: 0, + }]; + this.POINTER_ROTATION = Quat.fromPitchYawRollDegrees(45, 0, 45); + + + this.pointers = [ + Overlays.addOverlay("cube", { + position: ZERO_VECTOR, + color: this.DEFAULT_TIP_COLORS[0], + alpha: 1.0, + solid: true, + visible: false, + }), + Overlays.addOverlay("cube", { + position: ZERO_VECTOR, + color: this.DEFAULT_TIP_COLORS[1], + alpha: 1.0, + solid: true, + visible: false, + }) + ]; + + this.wand = Overlays.addOverlay("cube", { + position: ZERO_VECTOR, + color: COLORS.WHITE, + dimensions: { x: 0.01, y: 0.01, z: 0.2 }, + alpha: 1.0, + solid: true, + visible: false + }); + + var _this = this; + Script.scriptEnding.connect(function() { + _this.onCleanup(); + }); +} + +Wand.prototype = Object.create( ModelBase.prototype ); + +Wand.prototype.setVisible = function(visible) { + ModelBase.prototype.setVisible.call(this, visible); + Overlays.editOverlay(this.pointers[0], { + visible: this.visible + }); + Overlays.editOverlay(this.pointers[1], { + visible: this.visible + }); + Overlays.editOverlay(this.wand, { + visible: this.visible + }); +} + +Wand.prototype.setTransform = function(transform) { + ModelBase.prototype.setTransform.call(this, transform); + var wandPosition = Vec3.sum(this.position, Vec3.multiply(0.5, this.tipVector)); + Overlays.editOverlay(this.pointers[0], { + position: this.tipPosition, + rotation: this.rotation, + visible: true, + }); + Overlays.editOverlay(this.pointers[1], { + position: this.tipPosition, + rotation: Quat.multiply(this.POINTER_ROTATION, this.rotation), + visible: true, + }); + Overlays.editOverlay(this.wand, { + dimensions: { x: 0.01, y: this.length * 0.9, z: 0.01 }, + position: wandPosition, + rotation: this.rotation, + visible: true, + }); +} + +Wand.prototype.setTipColors = function(color1, color2) { + Overlays.editOverlay(this.pointers[0], { + color: color1 || this.DEFAULT_TIP_COLORS[0], + }); + Overlays.editOverlay(this.pointers[1], { + color: color2 || this.DEFAULT_TIP_COLORS[1], + }); +} + +Wand.prototype.onUpdate = function(deltaTime) { + if (this.visible) { + var time = new Date().getTime() / 250; + var scale1 = Math.abs(Math.sin(time)); + var scale2 = Math.abs(Math.cos(time)); + Overlays.editOverlay(this.pointers[0], { + scale: scale1 * 0.01, + }); + Overlays.editOverlay(this.pointers[1], { + scale: scale2 * 0.01, + }); + } +} + +Wand.prototype.onCleanup = function() { + Overlays.deleteOverlay(this.pointers[0]); + Overlays.deleteOverlay(this.pointers[1]); + Overlays.deleteOverlay(this.wand); +} \ No newline at end of file diff --git a/examples/libraries/omniTool/modules/breakdanceOmniToolModule.js b/examples/libraries/omniTool/modules/breakdanceOmniToolModule.js new file mode 100644 index 0000000000..36ee6b1fee --- /dev/null +++ b/examples/libraries/omniTool/modules/breakdanceOmniToolModule.js @@ -0,0 +1,35 @@ +// +// breakdanceOmniToolModule.js +// examples/libraries/omniTool/modules +// +// This is an omniTool module version of the breakdance game +// +// Created by Brad Hefta-Gaub on Sept 3, 2015 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("../toys/breakdanceCore.js"); + +OmniToolModules.Breakdance = function() { + print("OmniToolModules.Breakdance..."); +} + +OmniToolModules.Breakdance.prototype.onLoad = function(deltaTime) { + print("OmniToolModules.Breakdance.prototype.onLoad()..."); + breakdanceStart(); + } + +OmniToolModules.Breakdance.prototype.onUpdate = function(deltaTime) { + print("OmniToolModules.Breakdance.prototype.onUpdate()..."); + breakdanceUpdate(); +} + + OmniToolModules.Breakdance.prototype.onUnload = function() { + print("OmniToolModules.Breakdance.prototype.onUnload()..."); + breakdanceEnd(); +} + +OmniToolModuleType = "Breakdance"; \ No newline at end of file diff --git a/examples/libraries/omniTool/modules/test.js b/examples/libraries/omniTool/modules/test.js new file mode 100644 index 0000000000..9f7191b2d0 --- /dev/null +++ b/examples/libraries/omniTool/modules/test.js @@ -0,0 +1,36 @@ + +Script.include("avatarRelativeOverlays.js"); + +OmniToolModules.Test = function(omniTool, activeEntityId) { + this.omniTool = omniTool; + this.activeEntityId = activeEntityId; + this.avatarOverlays = new AvatarRelativeOverlays(); +} + +OmniToolModules.Test.prototype.onUnload = function() { + if (this.testOverlay) { + Overlays.deleteOverlay(this.testOverlay); + this.testOverlay = 0; + } +} + +var CUBE_POSITION = { + x: 0.1, + y: -0.1, + z: -0.4 +}; + +OmniToolModules.Test.prototype.onClick = function() { + if (this.testOverlay) { + Overlays.deleteOverlay(this.testOverlay); + this.testOverlay = 0; + } +} + + +OmniToolModules.Test.prototype.onUpdate = function(deltaTime) { + this.avatarOverlays.onUpdate(deltaTime); +} + + +OmniToolModuleType = "Test" \ No newline at end of file diff --git a/examples/toys/magBalls/utils.js b/examples/libraries/utils.js similarity index 58% rename from examples/toys/magBalls/utils.js rename to examples/libraries/utils.js index ea1446f858..c6143a51a8 100644 --- a/examples/toys/magBalls/utils.js +++ b/examples/libraries/utils.js @@ -11,6 +11,15 @@ vec3toStr = function (v, digits) { return "{ " + v.x.toFixed(digits) + ", " + v.y.toFixed(digits) + ", " + v.z.toFixed(digits)+ " }"; } + +colorMix = function(colorA, colorB, mix) { + var result = {}; + for (var key in colorA) { + result[key] = (colorA[key] * (1 - mix)) + (colorB[key] * mix); + } + return result; +} + scaleLine = function (start, end, scale) { var v = Vec3.subtract(end, start); var length = Vec3.length(v); @@ -53,11 +62,17 @@ getEntityUserData = function(id) { var results = null; var properties = Entities.getEntityProperties(id); if (properties.userData) { - results = JSON.parse(properties.userData); + try { + results = JSON.parse(properties.userData); + } catch(err) { + logDebug(err); + logDebug(properties.userData); + } } return results ? results : {}; } + // Non-destructively modify the user data of an entity. setEntityCustomData = function(customKey, id, data) { var userData = getEntityUserData(id); @@ -70,14 +85,6 @@ getEntityCustomData = function(customKey, id, defaultValue) { return userData[customKey] ? userData[customKey] : defaultValue; } -getMagBallsData = function(id) { - return getEntityCustomData(CUSTOM_DATA_NAME, id, {}); -} - -setMagBallsData = function(id, value) { - setEntityCustomData(CUSTOM_DATA_NAME, id, value); -} - mergeObjects = function(proto, custom) { var result = {}; for (var attrname in proto) { @@ -103,4 +110,39 @@ logInfo = function(str) { logDebug = function(str) { print(str); -} \ No newline at end of file +} + +// Computes the penetration between a point and a sphere (centered at the origin) +// if point is inside sphere: returns true and stores the result in 'penetration' +// (the vector that would move the point outside the sphere) +// otherwise returns false +findSphereHit = function(point, sphereRadius) { + var EPSILON = 0.000001; //smallish positive number - used as margin of error for some computations + var vectorLength = Vec3.length(point); + if (vectorLength < EPSILON) { + return true; + } + var distance = vectorLength - sphereRadius; + if (distance < 0.0) { + return true; + } + return false; +} + +findSpherePointHit = function(sphereCenter, sphereRadius, point) { + return findSphereHit(Vec3.subtract(point,sphereCenter), sphereRadius); +} + +findSphereSphereHit = function(firstCenter, firstRadius, secondCenter, secondRadius) { + return findSpherePointHit(firstCenter, firstRadius + secondRadius, secondCenter); +} + +// Given a vec3 v, return a vec3 that is the same vector relative to the avatars +// DEFAULT eye position, rotated into the avatars reference frame. +getEyeRelativePosition = function(v) { + return Vec3.sum(MyAvatar.getDefaultEyePosition(), Vec3.multiplyQbyV(MyAvatar.orientation, v)); +} + +getAvatarRelativeRotation = function(q) { + return Quat.multiply(MyAvatar.orientation, q); +} diff --git a/examples/controllers/breakdanceToy.js b/examples/toys/breakdanceCore.js similarity index 73% rename from examples/controllers/breakdanceToy.js rename to examples/toys/breakdanceCore.js index db3b418c01..9bf63850ab 100644 --- a/examples/controllers/breakdanceToy.js +++ b/examples/toys/breakdanceCore.js @@ -1,7 +1,8 @@ // -// breakdanceToy.js -// examples +// breakdanceCore.js +// examples/toys // +// This is the core breakdance game library, it can be used as part of an entity script, or an omniTool module, or bootstapped on it's own // Created by Brad Hefta-Gaub on August 24, 2015 // Copyright 2015 High Fidelity, Inc. // @@ -9,34 +10,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; - -// helpers -// Computes the penetration between a point and a sphere (centered at the origin) -// if point is inside sphere: returns true and stores the result in 'penetration' -// (the vector that would move the point outside the sphere) -// otherwise returns false -function findSphereHit(point, sphereRadius) { - var EPSILON = 0.000001; //smallish positive number - used as margin of error for some computations - var vectorLength = Vec3.length(point); - if (vectorLength < EPSILON) { - return true; - } - var distance = vectorLength - sphereRadius; - if (distance < 0.0) { - return true; - } - return false; -} - -function findSpherePointHit(sphereCenter, sphereRadius, point) { - return findSphereHit(Vec3.subtract(point,sphereCenter), sphereRadius); -} - -function findSphereSphereHit(firstCenter, firstRadius, secondCenter, secondRadius) { - return findSpherePointHit(firstCenter, firstRadius + secondRadius, secondCenter); -} - function getPositionPuppet() { var DISTANCE_IN_FRONT = 2; @@ -245,13 +218,18 @@ function getPositionRightOnBase() { } -// We will also demonstrate some 3D overlays. We will create a couple of cubes, spheres, and lines -// our 3D cube that moves around... -var handSize = 0.25; -var leftCubePosition = MyAvatar.getLeftPalmPosition(); -var rightCubePosition = MyAvatar.getRightPalmPosition(); +// some globals we will need access to +var HAND_SIZE = 0.25; +var TARGET_SIZE = 0.3; +var TARGET_COLOR = { red: 128, green: 128, blue: 128}; +var TARGET_COLOR_HIT = { red: 0, green: 255, blue: 0}; -var text = Overlays.addOverlay("text", { +var textOverlay, leftHandOverlay, rightHandOverlay, + leftOnBaseOverlay, leftLoweredOverlay, leftOverheadOverlay, leftSideOverlay, leftFrontOverlay, + rightOnBaseOverlay, rightLoweredOverlay, rightOverheadOverlay, rightSideOverlay, rightFrontOverlay; + +function createOverlays() { + textOverlay = Overlays.addOverlay("text", { x: 100, y: 300, width: 900, @@ -265,31 +243,110 @@ var text = Overlays.addOverlay("text", { backgroundAlpha: 0.5 }); -var leftHand= Overlays.addOverlay("cube", { - position: leftCubePosition, - size: handSize, + leftHandOverlay = Overlays.addOverlay("cube", { + position: MyAvatar.getLeftPalmPosition(), + size: HAND_SIZE, color: { red: 0, green: 0, blue: 255}, alpha: 1, solid: false }); -var rightHand= Overlays.addOverlay("cube", { - position: rightCubePosition, - size: handSize, + rightHandOverlay = Overlays.addOverlay("cube", { + position: MyAvatar.getRightPalmPosition(), + size: HAND_SIZE, color: { red: 255, green: 0, blue: 0}, alpha: 1, solid: false }); + leftOnBaseOverlay = Overlays.addOverlay("cube", { + position: getPositionLeftOnBase(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); -var targetSize = 0.3; -var targetColor = { red: 128, green: 128, blue: 128}; -var targetColorHit = { red: 0, green: 255, blue: 0}; -var moveCycleColor = { red: 255, green: 255, blue: 0}; + + leftLoweredOverlay = Overlays.addOverlay("cube", { + position: getPositionLeftLowered(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + + leftOverheadOverlay = Overlays.addOverlay("cube", { + position: getPositionLeftOverhead(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + leftSideOverlay = Overlays.addOverlay("cube", { + position: getPositionLeftSide(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + + leftFrontOverlay = Overlays.addOverlay("cube", { + position: getPositionLeftFront(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + rightOnBaseOverlay = Overlays.addOverlay("cube", { + position: getPositionRightOnBase(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + rightLoweredOverlay = Overlays.addOverlay("cube", { + position: getPositionRightLowered(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + + rightOverheadOverlay = Overlays.addOverlay("cube", { + position: getPositionRightOverhead(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + rightSideOverlay = Overlays.addOverlay("cube", { + position: getPositionRightSide(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); + + + rightFrontOverlay = Overlays.addOverlay("cube", { + position: getPositionRightFront(), + size: TARGET_SIZE, + color: TARGET_COLOR, + alpha: 1, + solid: false + }); +} var TEMPORARY_LIFETIME = 60; - -var animationSettings = JSON.stringify({ +var ANIMATION_SETTINGS = JSON.stringify({ fps: 30, running: true, loop: true, @@ -297,107 +354,22 @@ var animationSettings = JSON.stringify({ lastFrame: 10000 }); -var naturalDimensions = { x: 1.63, y: 1.67, z: 0.31 }; -var dimensions = Vec3.multiply(naturalDimensions, 0.3); +var NATURAL_DIMENSIONS = { x: 1.63, y: 1.67, z: 0.31 }; +var DIMENSIONS = Vec3.multiply(NATURAL_DIMENSIONS, 0.3); +var puppetEntityID; -var puppetEntityID = Entities.addEntity({ +function createPuppet() { + puppetEntityID = Entities.addEntity({ type: "Model", modelURL: "https://hifi-public.s3.amazonaws.com/models/Bboys/bboy1/bboy1.fbx", animationURL: "http://s3.amazonaws.com/hifi-public/animations/Breakdancing/breakdance_ready.fbx", - animationSettings: animationSettings, + animationSettings: ANIMATION_SETTINGS, position: getPositionPuppet(), ignoreForCollisions: true, - dimensions: dimensions, + dimensions: DIMENSIONS, lifetime: TEMPORARY_LIFETIME }); - -var leftOnBase = Overlays.addOverlay("cube", { - position: getPositionLeftOnBase(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - - -var leftLowered = Overlays.addOverlay("cube", { - position: getPositionLeftLowered(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - - -var leftOverhead = Overlays.addOverlay("cube", { - position: getPositionLeftOverhead(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - -var leftSide= Overlays.addOverlay("cube", { - position: getPositionLeftSide(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - - -var leftFront= Overlays.addOverlay("cube", { - position: getPositionLeftFront(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - -var rightOnBase = Overlays.addOverlay("cube", { - position: getPositionRightOnBase(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - -var rightLowered = Overlays.addOverlay("cube", { - position: getPositionRightLowered(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - - -var rightOverhead = Overlays.addOverlay("cube", { - position: getPositionRightOverhead(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - -var rightSide= Overlays.addOverlay("cube", { - position: getPositionRightSide(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - - -var rightFront= Overlays.addOverlay("cube", { - position: getPositionRightFront(), - size: targetSize, - color: targetColor, - alpha: 1, - solid: false - }); - -var startDate = new Date(); -var lastTime = startDate.getTime(); +} var NO_POSE = 0; var LEFT_ON_BASE = 1; @@ -411,8 +383,6 @@ var RIGHT_LOWERED = 128; var RIGHT_SIDE = 256; var RIGHT_FRONT = 512; -var lastPoseValue = NO_POSE; - //http://s3.amazonaws.com/hifi-public/animations/Breakdancing/breakdance_ready.fbx //http://s3.amazonaws.com/hifi-public/animations/Breakdancing/bboy_pose_to_idle.fbx //http://s3.amazonaws.com/hifi-public/animations/Breakdancing/bboy_uprock.fbx @@ -461,8 +431,6 @@ poses[LEFT_ON_BASE + RIGHT_LOWERED ] = { name: "Left On Base + Right Lowered" poses[LEFT_ON_BASE + RIGHT_SIDE ] = { name: "Left On Base + Right Side", animation: "http://s3.amazonaws.com/hifi-public/animations/Breakdancing/breakdance_ready.fbx" }; poses[LEFT_ON_BASE + RIGHT_FRONT ] = { name: "Left On Base + Right Front", animation: "http://s3.amazonaws.com/hifi-public/animations/Breakdancing/breakdance_ready.fbx" }; - - poses[LEFT_OVERHEAD + RIGHT_OVERHEAD ] = { name: "Left Overhead + Right Overhead", animation: "http://s3.amazonaws.com/hifi-public/animations/Breakdancing/bboy_uprock.fbx" }; poses[LEFT_LOWERED + RIGHT_OVERHEAD ] = { name: "Left Lowered + Right Overhead", animation: "http://s3.amazonaws.com/hifi-public/animations/Breakdancing/breakdance_footwork_1.fbx" }; poses[LEFT_SIDE + RIGHT_OVERHEAD ] = { name: "Left Side + Right Overhead", animation: "http://s3.amazonaws.com/hifi-public/animations/Breakdancing/breakdance_footwork_2.fbx" }; @@ -484,43 +452,46 @@ poses[LEFT_SIDE + RIGHT_FRONT ] = { name: "Left Side + Right Front", poses[LEFT_FRONT + RIGHT_FRONT ] = { name: "Left Front + Right Front", animation: "http://s3.amazonaws.com/hifi-public/animations/Breakdancing/breakdance_uprock_var_1_end.fbx" }; -Script.update.connect(function(deltaTime) { - var date= new Date(); - var now= date.getTime(); - var elapsed = now - lastTime; - var inMoveCycle = false; +breakdanceStart = function() { + print("breakdanceStart..."); + createOverlays(); + createPuppet(); +} + +breakdanceUpdate = function(deltaTime) { + //print("breakdanceUpdate..."); var leftHandPos = MyAvatar.getLeftPalmPosition(); var rightHandPos = MyAvatar.getRightPalmPosition(); - Overlays.editOverlay(leftHand, { position: leftHandPos } ); - Overlays.editOverlay(rightHand, { position: rightHandPos } ); + Overlays.editOverlay(leftHandOverlay, { position: leftHandPos } ); + Overlays.editOverlay(rightHandOverlay, { position: rightHandPos } ); - var hitTargetLeftOnBase = findSphereSphereHit(leftHandPos, handSize/2, getPositionLeftOnBase(), targetSize/2); - var hitTargetLeftOverhead = findSphereSphereHit(leftHandPos, handSize/2, getPositionLeftOverhead(), targetSize/2); - var hitTargetLeftLowered = findSphereSphereHit(leftHandPos, handSize/2, getPositionLeftLowered(), targetSize/2); - var hitTargetLeftSide = findSphereSphereHit(leftHandPos, handSize/2, getPositionLeftSide(), targetSize/2); - var hitTargetLeftFront = findSphereSphereHit(leftHandPos, handSize/2, getPositionLeftFront(), targetSize/2); + var hitTargetLeftOnBase = findSphereSphereHit(leftHandPos, HAND_SIZE/2, getPositionLeftOnBase(), TARGET_SIZE/2); + var hitTargetLeftOverhead = findSphereSphereHit(leftHandPos, HAND_SIZE/2, getPositionLeftOverhead(), TARGET_SIZE/2); + var hitTargetLeftLowered = findSphereSphereHit(leftHandPos, HAND_SIZE/2, getPositionLeftLowered(), TARGET_SIZE/2); + var hitTargetLeftSide = findSphereSphereHit(leftHandPos, HAND_SIZE/2, getPositionLeftSide(), TARGET_SIZE/2); + var hitTargetLeftFront = findSphereSphereHit(leftHandPos, HAND_SIZE/2, getPositionLeftFront(), TARGET_SIZE/2); - var hitTargetRightOnBase = findSphereSphereHit(rightHandPos, handSize/2, getPositionRightOnBase(), targetSize/2); - var hitTargetRightOverhead = findSphereSphereHit(rightHandPos, handSize/2, getPositionRightOverhead(), targetSize/2); - var hitTargetRightLowered = findSphereSphereHit(rightHandPos, handSize/2, getPositionRightLowered(), targetSize/2); - var hitTargetRightSide = findSphereSphereHit(rightHandPos, handSize/2, getPositionRightSide(), targetSize/2); - var hitTargetRightFront = findSphereSphereHit(rightHandPos, handSize/2, getPositionRightFront(), targetSize/2); + var hitTargetRightOnBase = findSphereSphereHit(rightHandPos, HAND_SIZE/2, getPositionRightOnBase(), TARGET_SIZE/2); + var hitTargetRightOverhead = findSphereSphereHit(rightHandPos, HAND_SIZE/2, getPositionRightOverhead(), TARGET_SIZE/2); + var hitTargetRightLowered = findSphereSphereHit(rightHandPos, HAND_SIZE/2, getPositionRightLowered(), TARGET_SIZE/2); + var hitTargetRightSide = findSphereSphereHit(rightHandPos, HAND_SIZE/2, getPositionRightSide(), TARGET_SIZE/2); + var hitTargetRightFront = findSphereSphereHit(rightHandPos, HAND_SIZE/2, getPositionRightFront(), TARGET_SIZE/2); // determine target colors - var targetColorLeftOnBase = hitTargetLeftOnBase ? targetColorHit : targetColor; - var targetColorLeftOverhead = hitTargetLeftOverhead ? targetColorHit : targetColor; - var targetColorLeftLowered = hitTargetLeftLowered ? targetColorHit : targetColor; - var targetColorLeftSide = hitTargetLeftSide ? targetColorHit : targetColor; - var targetColorLeftFront = hitTargetLeftFront ? targetColorHit : targetColor; + var targetColorLeftOnBase = hitTargetLeftOnBase ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorLeftOverhead = hitTargetLeftOverhead ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorLeftLowered = hitTargetLeftLowered ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorLeftSide = hitTargetLeftSide ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorLeftFront = hitTargetLeftFront ? TARGET_COLOR_HIT : TARGET_COLOR; - var targetColorRightOnBase = hitTargetRightOnBase ? targetColorHit : targetColor; - var targetColorRightOverhead = hitTargetRightOverhead ? targetColorHit : targetColor; - var targetColorRightLowered = hitTargetRightLowered ? targetColorHit : targetColor; - var targetColorRightSide = hitTargetRightSide ? targetColorHit : targetColor; - var targetColorRightFront = hitTargetRightFront ? targetColorHit : targetColor; + var targetColorRightOnBase = hitTargetRightOnBase ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorRightOverhead = hitTargetRightOverhead ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorRightLowered = hitTargetRightLowered ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorRightSide = hitTargetRightSide ? TARGET_COLOR_HIT : TARGET_COLOR; + var targetColorRightFront = hitTargetRightFront ? TARGET_COLOR_HIT : TARGET_COLOR; // calculate a combined arm pose based on left and right hits var poseValue = NO_POSE; @@ -536,47 +507,48 @@ Script.update.connect(function(deltaTime) { poseValue += hitTargetRightFront ? RIGHT_FRONT : 0; if (poses[poseValue] == undefined) { - Overlays.editOverlay(text, { text: "no pose -- value:" + poseValue }); + Overlays.editOverlay(textOverlay, { text: "no pose -- value:" + poseValue }); } else { - Overlays.editOverlay(text, { text: "pose:" + poses[poseValue].name + "\n" + "animation:" + poses[poseValue].animation }); + Overlays.editOverlay(textOverlay, { text: "pose:" + poses[poseValue].name + "\n" + "animation:" + poses[poseValue].animation }); var props = Entities.getEntityProperties(puppetEntityID); + //print("puppetEntityID:" + puppetEntityID + "age:"+props.age); Entities.editEntity(puppetEntityID, { animationURL: poses[poseValue].animation, lifetime: TEMPORARY_LIFETIME + props.age // renew lifetime }); } - lastPoseValue = poseValue; - - Overlays.editOverlay(leftOnBase, { position: getPositionLeftOnBase(), color: targetColorLeftOnBase } ); - Overlays.editOverlay(leftOverhead, { position: getPositionLeftOverhead(), color: targetColorLeftOverhead } ); - Overlays.editOverlay(leftLowered, { position: getPositionLeftLowered(), color: targetColorLeftLowered } ); - Overlays.editOverlay(leftSide, { position: getPositionLeftSide() , color: targetColorLeftSide } ); - Overlays.editOverlay(leftFront, { position: getPositionLeftFront() , color: targetColorLeftFront } ); + Overlays.editOverlay(leftOnBaseOverlay, { position: getPositionLeftOnBase(), color: targetColorLeftOnBase } ); + Overlays.editOverlay(leftOverheadOverlay, { position: getPositionLeftOverhead(), color: targetColorLeftOverhead } ); + Overlays.editOverlay(leftLoweredOverlay, { position: getPositionLeftLowered(), color: targetColorLeftLowered } ); + Overlays.editOverlay(leftSideOverlay, { position: getPositionLeftSide() , color: targetColorLeftSide } ); + Overlays.editOverlay(leftFrontOverlay, { position: getPositionLeftFront() , color: targetColorLeftFront } ); - Overlays.editOverlay(rightOnBase, { position: getPositionRightOnBase(), color: targetColorRightOnBase } ); - Overlays.editOverlay(rightOverhead, { position: getPositionRightOverhead(), color: targetColorRightOverhead } ); - Overlays.editOverlay(rightLowered, { position: getPositionRightLowered(), color: targetColorRightLowered } ); - Overlays.editOverlay(rightSide, { position: getPositionRightSide() , color: targetColorRightSide } ); - Overlays.editOverlay(rightFront, { position: getPositionRightFront() , color: targetColorRightFront } ); - }); + Overlays.editOverlay(rightOnBaseOverlay, { position: getPositionRightOnBase(), color: targetColorRightOnBase } ); + Overlays.editOverlay(rightOverheadOverlay, { position: getPositionRightOverhead(), color: targetColorRightOverhead } ); + Overlays.editOverlay(rightLoweredOverlay, { position: getPositionRightLowered(), color: targetColorRightLowered } ); + Overlays.editOverlay(rightSideOverlay, { position: getPositionRightSide() , color: targetColorRightSide } ); + Overlays.editOverlay(rightFrontOverlay, { position: getPositionRightFront() , color: targetColorRightFront } ); + } -Script.scriptEnding.connect(function() { - Overlays.deleteOverlay(leftHand); - Overlays.deleteOverlay(rightHand); - Overlays.deleteOverlay(text); - Overlays.deleteOverlay(leftOnBase); - Overlays.deleteOverlay(leftOverhead); - Overlays.deleteOverlay(leftLowered); - Overlays.deleteOverlay(leftSide); - Overlays.deleteOverlay(leftFront); - Overlays.deleteOverlay(rightOnBase); - Overlays.deleteOverlay(rightOverhead); - Overlays.deleteOverlay(rightLowered); - Overlays.deleteOverlay(rightSide); - Overlays.deleteOverlay(rightFront); +breakdanceEnd= function() { + print("breakdanceEnd..."); + + Overlays.deleteOverlay(leftHandOverlay); + Overlays.deleteOverlay(rightHandOverlay); + + Overlays.deleteOverlay(textOverlay); + Overlays.deleteOverlay(leftOnBaseOverlay); + Overlays.deleteOverlay(leftOverheadOverlay); + Overlays.deleteOverlay(leftLoweredOverlay); + Overlays.deleteOverlay(leftSideOverlay); + Overlays.deleteOverlay(leftFrontOverlay); + Overlays.deleteOverlay(rightOnBaseOverlay); + Overlays.deleteOverlay(rightOverheadOverlay); + Overlays.deleteOverlay(rightLoweredOverlay); + Overlays.deleteOverlay(rightSideOverlay); + Overlays.deleteOverlay(rightFrontOverlay); - print("puppetEntityID:"+puppetEntityID); Entities.deleteEntity(puppetEntityID); -}); \ No newline at end of file +} \ No newline at end of file diff --git a/examples/toys/breakdanceToy.js b/examples/toys/breakdanceToy.js new file mode 100644 index 0000000000..0190ab74e9 --- /dev/null +++ b/examples/toys/breakdanceToy.js @@ -0,0 +1,18 @@ +// +// breakdanceToy.js +// examples/toys +// +// This is an local script version of the breakdance game +// +// Created by Brad Hefta-Gaub on Sept 3, 2015 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +Script.include("../libraries/utils.js"); +Script.include("breakdanceCore.js"); +breakdanceStart(); +Script.update.connect(breakdanceUpdate); +Script.scriptEnding.connect(breakdanceEnd); diff --git a/examples/toys/magBalls.js b/examples/toys/magBalls.js new file mode 100644 index 0000000000..e163aa5ffd --- /dev/null +++ b/examples/toys/magBalls.js @@ -0,0 +1,311 @@ +// +// Created by Bradley Austin Davis on 2015/08/25 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// FIXME Script paths have to be relative to the caller, in this case libraries/OmniTool.js +Script.include("../toys/magBalls/constants.js"); +Script.include("../toys/magBalls/graph.js"); +Script.include("../toys/magBalls/edgeSpring.js"); +Script.include("../toys/magBalls/magBalls.js"); +Script.include("avatarRelativeOverlays.js"); + +OmniToolModuleType = "MagBallsController" + +getMagBallsData = function(id) { + return getEntityCustomData(MAG_BALLS_DATA_NAME, id, {}); +} + +setMagBallsData = function(id, value) { + setEntityCustomData(MAG_BALLS_DATA_NAME, id, value); +} + +var UI_BALL_RADIUS = 0.01; +var MODE_INFO = { }; + +MODE_INFO[BALL_EDIT_MODE_ADD] = { + uiPosition: { + x: 0.15, + y: -0.08, + z: -0.35, + }, + colors: [ COLORS.GREEN, COLORS.BLUE ], + // FIXME use an http path or find a way to get the relative path to the file + url: Script.resolvePath('../html/magBalls/addMode.html'), +}; + +MODE_INFO[BALL_EDIT_MODE_DELETE] = { + uiPosition: { + x: 0.20, + y: -0.08, + z: -0.32, + }, + colors: [ COLORS.RED, COLORS.BLUE ], + // FIXME use an http path or find a way to get the relative path to the file + url: Script.resolvePath('../html/magBalls/deleteMode.html'), +}; + +var UI_POSITION_MODE_LABEL = Vec3.multiply(0.5, + Vec3.sum(MODE_INFO[BALL_EDIT_MODE_ADD].uiPosition, + MODE_INFO[BALL_EDIT_MODE_DELETE].uiPosition)); + +UI_POSITION_MODE_LABEL.y = -0.02; + +var UI_BALL_PROTOTYPE = { + size: UI_BALL_RADIUS * 2.0, + alpha: 1.0, + solid: true, + visible: true, +} + +OmniToolModules.MagBallsController = function(omniTool, entityId) { + this.omniTool = omniTool; + this.entityId = entityId; + + // In hold mode, holding a ball requires that you keep the action + // button pressed, while if this is false, clicking on a ball selects + // it and clicking again will drop it. + this.holdMode = true; + + this.highlighter = new Highlighter(); + this.magBalls = new MagBalls(); + this.highlighter.setSize(BALL_SIZE); + this.ghostEdges = {}; + this.selectionRadiusMultipler = 1.5; + this.uiOverlays = new AvatarRelativeOverlays(); + + + // create the overlay relative to the avatar + this.uiOverlays.addOverlay("sphere", mergeObjects(UI_BALL_PROTOTYPE, { + color: MODE_INFO[BALL_EDIT_MODE_ADD].colors[0], + position: MODE_INFO[BALL_EDIT_MODE_ADD].uiPosition, + })); + this.uiOverlays.addOverlay("sphere", mergeObjects(UI_BALL_PROTOTYPE, { + color: MODE_INFO[BALL_EDIT_MODE_DELETE].colors[0], + position: MODE_INFO[BALL_EDIT_MODE_DELETE].uiPosition, + })); + + // FIXME find the proper URLs to use + this.modeLabel = this.uiOverlays.addOverlay("web3d", { + isFacingAvatar: true, + alpha: 1.0, + dimensions: { x: 0.16, y: 0.12, z: 0.001}, + color: "White", + position: UI_POSITION_MODE_LABEL, + }); + + this.setMode(BALL_EDIT_MODE_ADD); + + // DEBUGGING ONLY - Fix old, bad edge bounding boxes + //for (var edgeId in this.magBalls.edges) { + // Entities.editEntity(edgeId, { + // dimensions: LINE_DIMENSIONS, + // }); + //} + // DEBUGGING ONLY - Clear any previous balls + // this.magBalls.clear(); + // DEBUGGING ONLY - Attempt to fix connections between balls + // and delete bad connections. Warning... if you haven't looked around + // and caused the domain server to send you all the nearby balls as well as the connections, + // this can break your structures + // this.magBalls.repair(); +} + +OmniToolModules.MagBallsController.prototype.onUnload = function() { + this.clearGhostEdges(); + this.uiOverlays.deleteAll(); +} + + +OmniToolModules.MagBallsController.prototype.setMode = function(mode) { + if (mode === this.mode) { + return; + } + + logDebug("Changing mode to '" + mode + "'"); + Overlays.editOverlay(this.modeLabel, { + url: MODE_INFO[mode].url + }); + + this.mode = mode; + var color1; + var color2; + switch (this.mode) { + case BALL_EDIT_MODE_ADD: + color1 = COLORS.BLUE; + color2 = COLORS.GREEN; + break; + + case BALL_EDIT_MODE_MOVE: + color1 = COLORS.GREEN; + color2 = COLORS.LIGHT_GREEN; + break; + + case BALL_EDIT_MODE_DELETE: + color1 = COLORS.RED; + color2 = COLORS.BLUE; + break; + + case BALL_EDIT_MODE_DELETE_SHAPE: + color1 = COLORS.RED; + color2 = COLORS.YELLOW; + break; + } + this.omniTool.model.setTipColors(color1, color2); + +} + +OmniToolModules.MagBallsController.prototype.findUiBallHit = function() { + var result = null; + for (var mode in MODE_INFO) { + var modeInfo = MODE_INFO[mode]; + var spherePoint = getEyeRelativePosition(modeInfo.uiPosition); + if (findSpherePointHit(spherePoint, UI_BALL_RADIUS * 2, this.tipPosition)) { + this.highlighter.highlight(spherePoint); + this.highlighter.setColor("White"); + // FIXME why doesn't this work? + this.highlighter.setSize(UI_BALL_RADIUS * 4); + return mode; + } + } + return; +} + +OmniToolModules.MagBallsController.prototype.onUpdateSelected = function(deltaTime) { + if (!this.selected) { + return; + } + + Entities.editEntity(this.selected, { position: this.tipPosition }); + var targetBalls = this.magBalls.findPotentialEdges(this.selected); + for (var ballId in targetBalls) { + var targetPosition = this.magBalls.getNodePosition(ballId); + var distance = Vec3.distance(targetPosition, this.tipPosition); + var variance = this.magBalls.getVariance(distance); + var mix = Math.abs(variance) / this.magBalls.MAX_VARIANCE; + var color = colorMix(COLORS.YELLOW, COLORS.RED, mix); + if (!this.ghostEdges[ballId]) { + // create the ovleray + this.ghostEdges[ballId] = Overlays.addOverlay("line3d", { + start: this.magBalls.getNodePosition(ballId), + end: this.tipPosition, + color: color, + alpha: 1, + lineWidth: 5, + visible: true, + }); + } else { + Overlays.editOverlay(this.ghostEdges[ballId], { + end: this.tipPosition, + color: color, + }); + } + } + for (var ballId in this.ghostEdges) { + if (!targetBalls[ballId]) { + Overlays.deleteOverlay(this.ghostEdges[ballId]); + delete this.ghostEdges[ballId]; + } + } +} + +OmniToolModules.MagBallsController.prototype.onUpdate = function(deltaTime) { + this.tipPosition = this.omniTool.getPosition(); + this.uiOverlays.onUpdate(deltaTime); + + this.onUpdateSelected(); + + if (this.findUiBallHit()) { + return; + } + + if (!this.selected) { + // Find the highlight target and set it. + var target = this.magBalls.findNearestNode(this.tipPosition, BALL_RADIUS * this.selectionRadiusMultipler); + this.highlighter.highlight(target); + this.highlighter.setColor(MODE_INFO[this.mode].colors[0]); + if (!target) { + this.magBalls.onUpdate(deltaTime); + } + return; + } +} + +OmniToolModules.MagBallsController.prototype.deselect = function() { + if (!this.selected) { + return false + } + this.clearGhostEdges(); + this.magBalls.releaseBall(this.selected); + this.selected = null; + return true; +} + + +OmniToolModules.MagBallsController.prototype.onClick = function() { + var newMode = this.findUiBallHit(); + if (newMode) { + if (this.selected) { + this.magBalls.destroyNode(highlighted); + this.selected = null; + } + this.setMode(newMode); + return; + } + + if (this.deselect()) { + return; + } + + logDebug("MagBallsController onClick: " + vec3toStr(this.tipPosition)); + + // TODO add checking against UI shapes for adding or deleting balls. + var highlighted = this.highlighter.highlighted; + if (this.mode == BALL_EDIT_MODE_ADD && !highlighted) { + highlighted = this.magBalls.createBall(this.tipPosition); + } + + // Nothing to select or create means we're done here. + if (!highlighted) { + return; + } + + switch (this.mode) { + case BALL_EDIT_MODE_ADD: + case BALL_EDIT_MODE_MOVE: + this.magBalls.selectBall(highlighted); + this.selected = highlighted; + logDebug("Selected " + this.selected); + break; + + case BALL_EDIT_MODE_DELETE: + this.magBalls.destroyNode(highlighted); + break; + + case BALL_EDIT_MODE_DELETE_SHAPE: + logDebug("Not implemented yet"); + break; + } + + if (this.selected) { + this.highlighter.highlight(null); + } +} + +OmniToolModules.MagBallsController.prototype.onRelease = function() { + if (this.holdMode) { + this.deselect(); + } +} + +OmniToolModules.MagBallsController.prototype.clearGhostEdges = function() { + for(var ballId in this.ghostEdges) { + Overlays.deleteOverlay(this.ghostEdges[ballId]); + delete this.ghostEdges[ballId]; + } +} + + diff --git a/examples/toys/magBalls/ballController.js b/examples/toys/magBalls/ballController.js deleted file mode 100644 index 0f178b2804..0000000000 --- a/examples/toys/magBalls/ballController.js +++ /dev/null @@ -1,103 +0,0 @@ -Script.include("handController.js"); -Script.include("highlighter.js"); - -BallController = function(side, magBalls) { - HandController.call(this, side); - this.magBalls = magBalls; - this.highlighter = new Highlighter(); - this.highlighter.setSize(BALL_SIZE); - this.ghostEdges = {}; -} - -BallController.prototype = Object.create( HandController.prototype ); - -BallController.prototype.onUpdate = function(deltaTime) { - HandController.prototype.onUpdate.call(this, deltaTime); - - if (!this.selected) { - // Find the highlight target and set it. - var target = this.magBalls.findNearestNode(this.tipPosition, BALL_SELECTION_RADIUS); - this.highlighter.highlight(target); - return; - } - this.highlighter.highlight(null); - Entities.editEntity(this.selected, { position: this.tipPosition }); - var targetBalls = this.magBalls.findPotentialEdges(this.selected); - for (var ballId in targetBalls) { - if (!this.ghostEdges[ballId]) { - // create the ovleray - this.ghostEdges[ballId] = Overlays.addOverlay("line3d", { - start: this.magBalls.getNodePosition(ballId), - end: this.tipPosition, - color: COLORS.RED, - alpha: 1, - lineWidth: 5, - visible: true, - }); - } else { - Overlays.editOverlay(this.ghostEdges[ballId], { - end: this.tipPosition, - }); - } - } - for (var ballId in this.ghostEdges) { - if (!targetBalls[ballId]) { - Overlays.deleteOverlay(this.ghostEdges[ballId]); - delete this.ghostEdges[ballId]; - } - } -} - -BallController.prototype.onClick = function() { - this.selected = this.magBalls.grabBall(this.tipPosition, BALL_SELECTION_RADIUS); - this.highlighter.highlight(null); -} - -BallController.prototype.onRelease = function() { - this.clearGhostEdges(); - this.magBalls.releaseBall(this.selected); - this.selected = null; -} - -BallController.prototype.clearGhostEdges = function() { - for(var ballId in this.ghostEdges) { - Overlays.deleteOverlay(this.ghostEdges[ballId]); - delete this.ghostEdges[ballId]; - } -} - -BallController.prototype.onCleanup = function() { - HandController.prototype.onCleanup.call(this); - this.clearGhostEdges(); -} - -BallController.prototype.onAltClick = function() { - return; - var target = this.magBalls.findNearestNode(this.tipPosition, BALL_SELECTION_RADIUS); - if (!target) { - logDebug(target); - return; - } - - // FIXME move to delete shape - var toDelete = {}; - var deleteQueue = [ target ]; - while (deleteQueue.length) { - var curNode = deleteQueue.shift(); - if (toDelete[curNode]) { - continue; - } - toDelete[curNode] = true; - for (var nodeId in this.magBalls.getConnectedNodes(curNode)) { - deleteQueue.push(nodeId); - } - } - for (var nodeId in toDelete) { - this.magBalls.destroyNode(nodeId); - } -} - - - -BallController.prototype.onAltRelease = function() { -} diff --git a/examples/toys/magBalls/constants.js b/examples/toys/magBalls/constants.js index d154910f91..d9dee94329 100644 --- a/examples/toys/magBalls/constants.js +++ b/examples/toys/magBalls/constants.js @@ -1,79 +1,18 @@ -// -// Created by Bradley Austin Davis on 2015/08/27 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; -STICK_URL = HIFI_PUBLIC_BUCKET + "models/props/geo_stick.fbx"; +MAG_BALLS_DATA_NAME = "magBalls"; // FIXME make this editable through some script UI, so the user can customize the size of the structure built -SCALE = 0.5; -BALL_SIZE = 0.08 * SCALE; -STICK_LENGTH = 0.24 * SCALE; +MAG_BALLS_SCALE = 0.5; +BALL_SIZE = 0.08 * MAG_BALLS_SCALE; +STICK_LENGTH = 0.24 * MAG_BALLS_SCALE; DEBUG_MAGSTICKS = true; -CUSTOM_DATA_NAME = "magBalls"; BALL_NAME = "MagBall"; EDGE_NAME = "MagStick"; -ZERO_VECTOR = { x: 0, y: 0, z: 0 }; - -COLORS = { - WHITE: { - red: 255, - green: 255, - blue: 255, - }, - BLACK: { - red: 0, - green: 0, - blue: 0, - }, - GREY: { - red: 128, - green: 128, - blue: 128, - }, - RED: { - red: 255, - green: 0, - blue: 0 - }, - BLUE: { - red: 0, - green: 0, - blue: 255 - }, - GREEN: { - red: 0, - green: 255, - blue: 0 - }, - CYAN: { - red: 0, - green: 255, - blue: 255 - }, - YELLOW: { - red: 255, - green: 255, - blue: 0 - }, - MAGENTA: { - red: 255, - green: 0, - blue: 255 - } -} - BALL_RADIUS = BALL_SIZE / 2.0; -BALL_SELECTION_RADIUS = BALL_RADIUS * 1.5; - BALL_DIMENSIONS = { x: BALL_SIZE, y: BALL_SIZE, @@ -106,10 +45,13 @@ BALL_PROTOTYPE = { // 2 millimeters BALL_EPSILON = (.002) / BALL_DISTANCE; +// FIXME better handling of the line bounding box would require putting the +// origin in the middle of the line, not at one end +LINE_DIAGONAL = Math.sqrt((STICK_LENGTH * STICK_LENGTH * 2) * 3) * 1.1; LINE_DIMENSIONS = { - x: 5, - y: 5, - z: 5 + x: LINE_DIAGONAL, + y: LINE_DIAGONAL, + z: LINE_DIAGONAL } LINE_PROTOTYPE = { @@ -138,3 +80,7 @@ EDGE_PROTOTYPE = LINE_PROTOTYPE; // } +BALL_EDIT_MODE_ADD = "add"; +BALL_EDIT_MODE_MOVE = "move"; +BALL_EDIT_MODE_DELETE = "delete"; +BALL_EDIT_MODE_DELETE_SHAPE = "deleteShape"; diff --git a/examples/toys/magBalls/debugUtils.js b/examples/toys/magBalls/debugUtils.js index 8dadd34679..e69de29bb2 100644 --- a/examples/toys/magBalls/debugUtils.js +++ b/examples/toys/magBalls/debugUtils.js @@ -1,95 +0,0 @@ -findMatchingNode = function(position, nodePositions) { - for (var nodeId in nodePositions) { - var nodePos = nodePositions[nodeId]; - var distance = Vec3.distance(position, nodePos); - if (distance < 0.03) { - return nodeId; - } - } -} - -repairConnections = function() { - var ids = Entities.findEntities(MyAvatar.position, 50); - - // Find all the balls and record their positions - var nodePositions = {}; - for (var i in ids) { - var id = ids[i]; - var properties = Entities.getEntityProperties(id); - if (properties.name == BALL_NAME) { - nodePositions[id] = properties.position; - } - } - - // Now check all the edges to see if they're valid (point to balls) - // and ensure that the balls point back to them - var ballsToEdges = {}; - for (var i in ids) { - var id = ids[i]; - var properties = Entities.getEntityProperties(id); - if (properties.name == EDGE_NAME) { - var startPos = properties.position; - var endPos = Vec3.sum(startPos, properties.linePoints[1]); - var magBallData = getMagBallsData(id); - var update = false; - if (!magBallData.start) { - var startNode = findMatchingNode(startPos, nodePositions); - if (startNode) { - logDebug("Found start node " + startNode) - magBallData.start = startNode; - update = true; - } - } - if (!magBallData.end) { - var endNode = findMatchingNode(endPos, nodePositions); - if (endNode) { - logDebug("Found end node " + endNode) - magBallData.end = endNode; - update = true; - } - } - if (!magBallData.start || !magBallData.end) { - logDebug("Didn't find both ends"); - Entities.deleteEntity(id); - continue; - } - if (!ballsToEdges[magBallData.start]) { - ballsToEdges[magBallData.start] = [ id ]; - } else { - ballsToEdges[magBallData.start].push(id); - } - if (!ballsToEdges[magBallData.end]) { - ballsToEdges[magBallData.end] = [ id ]; - } else { - ballsToEdges[magBallData.end].push(id); - } - if (update) { - logDebug("Updating incomplete edge " + id); - magBallData.length = BALL_DISTANCE; - setMagBallsData(id, magBallData); - } - } - } - for (var nodeId in ballsToEdges) { - var magBallData = getMagBallsData(nodeId); - var edges = magBallData.edges || []; - var edgeHash = {}; - for (var i in edges) { - edgeHash[edges[i]] = true; - } - var update = false; - for (var i in ballsToEdges[nodeId]) { - var edgeId = ballsToEdges[nodeId][i]; - if (!edgeHash[edgeId]) { - update = true; - edgeHash[edgeId] = true; - edges.push(edgeId); - } - } - if (update) { - logDebug("Fixing node with missing edge data"); - magBallData.edges = edges; - setMagBallsData(nodeId, magBallData); - } - } -} diff --git a/examples/toys/magBalls/edgeSpring.js b/examples/toys/magBalls/edgeSpring.js index 852c9257c2..e1b717c39b 100644 --- a/examples/toys/magBalls/edgeSpring.js +++ b/examples/toys/magBalls/edgeSpring.js @@ -9,7 +9,10 @@ EdgeSpring = function(edgeId, graph) { this.desiredLength = magBallsData.length || BALL_DISTANCE; } -EdgeSpring.prototype.adjust = function(results) { +// FIXME as iterations increase, start introducing some randomness +// to the adjustment so that we avoid false equilibriums +// Alternatively, larger iterations could increase the acceptable variance +EdgeSpring.prototype.adjust = function(results, iterations) { var startPos = this.getAdjustedPosition(this.start, results); var endPos = this.getAdjustedPosition(this.end, results); var vector = Vec3.subtract(endPos, startPos); diff --git a/examples/toys/magBalls/graph.js b/examples/toys/magBalls/graph.js index df02ee3628..198c1b0c16 100644 --- a/examples/toys/magBalls/graph.js +++ b/examples/toys/magBalls/graph.js @@ -101,6 +101,10 @@ Graph.prototype.getConnectedNodes = function(nodeId) { return result; } +Graph.prototype.getNodesForEdge = function(edgeId) { + return Object.keys(this.edges[edgeId]); +} + Graph.prototype.getEdgeLength = function(edgeId) { var nodesInEdge = Object.keys(this.edges[edgeId]); return this.getNodeDistance(nodesInEdge[0], nodesInEdge[1]); diff --git a/examples/toys/magBalls/handController.js b/examples/toys/magBalls/handController.js deleted file mode 100644 index 998d22c6f8..0000000000 --- a/examples/toys/magBalls/handController.js +++ /dev/null @@ -1,127 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015/08/29 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -LEFT_CONTROLLER = 0; -RIGHT_CONTROLLER = 1; - -// FIXME add a customizable wand model and a mechanism to switch between wands -HandController = function(side) { - this.side = side; - this.palm = 2 * side; - this.tip = 2 * side + 1; - this.action = findAction(side ? "ACTION2" : "ACTION1"); - this.altAction = findAction(side ? "ACTION1" : "ACTION2"); - this.active = false; - this.tipScale = 1.4; - this.pointer = Overlays.addOverlay("sphere", { - position: ZERO_VECTOR, - size: 0.01, - color: COLORS.YELLOW, - alpha: 1.0, - solid: true, - visible: false, - }); - - // Connect to desired events - var _this = this; - Controller.actionEvent.connect(function(action, state) { - _this.onActionEvent(action, state); - }); - - Script.update.connect(function(deltaTime) { - _this.onUpdate(deltaTime); - }); - - Script.scriptEnding.connect(function() { - _this.onCleanup(); - }); -} - -HandController.prototype.onActionEvent = function(action, state) { - var spatialControlCount = Controller.getNumberOfSpatialControls(); - // If only 2 spacial controls, then we only have one controller active, so use either button - // otherwise, only use the specified action - - if (action == this.action) { - if (state) { - this.onClick(); - } else { - this.onRelease(); - } - } - - if (action == this.altAction) { - if (state) { - this.onAltClick(); - } else { - this.onAltRelease(); - } - } -} - -HandController.prototype.setActive = function(active) { - if (active == this.active) { - return; - } - logDebug("Hand controller changing active state: " + active); - this.active = active; - Overlays.editOverlay(this.pointer, { - visible: this.active - }); - Entities.editEntity(this.wand, { - visible: this.active - }); -} - -HandController.prototype.updateControllerState = function() { - // FIXME this returns data if either the left or right controller is not on the base - this.palmPos = Controller.getSpatialControlPosition(this.palm); - var tipPos = Controller.getSpatialControlPosition(this.tip); - this.tipPosition = scaleLine(this.palmPos, tipPos, this.tipScale); - // When on the base, hydras report a position of 0 - this.setActive(Vec3.length(this.palmPos) > 0.001); - - //logDebug(Controller.getTriggerValue(0) + " " + Controller.getTriggerValue(1)); - - //if (this.active) { - // logDebug("#ctrls " + Controller.getNumberOfSpatialControls() + " Side: " + this.side + " Palm: " + this.palm + " " + vec3toStr(this.palmPos)) - //} -} - -HandController.prototype.onCleanup = function() { - Overlays.deleteOverlay(this.pointer); -} - -HandController.prototype.onUpdate = function(deltaTime) { - this.updateControllerState(); - if (this.active) { - Overlays.editOverlay(this.pointer, { - position: this.tipPosition - }); - Entities.editEntity(this.wand, { - position: this.tipPosition - }); - } -} - -HandController.prototype.onClick = function() { - logDebug("Base hand controller does nothing on click"); -} - -HandController.prototype.onRelease = function() { - logDebug("Base hand controller does nothing on release"); -} - -HandController.prototype.onAltClick = function() { - logDebug("Base hand controller does nothing on alt click"); -} - -HandController.prototype.onAltRelease = function() { - logDebug("Base hand controller does nothing on alt click"); -} - - diff --git a/examples/toys/magBalls/magBalls.js b/examples/toys/magBalls/magBalls.js index 187c550073..b689fb1272 100644 --- a/examples/toys/magBalls/magBalls.js +++ b/examples/toys/magBalls/magBalls.js @@ -8,30 +8,30 @@ var UPDATE_INTERVAL = 0.1; -Script.include("graph.js"); -Script.include("edgeSpring.js"); // A collection of balls and edges connecting them. MagBalls = function() { Graph.call(this); - this.MAX_ADJUST_ITERATIONS = 100; + this.REFRESH_WAIT_TICKS = 10; + this.MAX_VARIANCE = 0.25; this.lastUpdateAge = 0; - this.stable = false; + this.stable = true; this.adjustIterations = 0; this.selectedNodes = {}; this.edgeObjects = {}; + this.unstableEdges = {}; this.refresh(); var _this = this; - Script.update.connect(function(deltaTime) { - _this.onUpdate(deltaTime); - }); - Script.scriptEnding.connect(function() { _this.onCleanup(); }); + + Entities.addingEntity.connect(function(entityId) { + _this.onEntityAdded(entityId); + }); } MagBalls.prototype = Object.create( Graph.prototype ); @@ -40,16 +40,30 @@ MagBalls.prototype.onUpdate = function(deltaTime) { this.lastUpdateAge += deltaTime; if (this.lastUpdateAge > UPDATE_INTERVAL) { this.lastUpdateAge = 0; - if (!this.stable) { + if (this.refreshNeeded) { + if (++this.refreshNeeded > this.REFRESH_WAIT_TICKS) { + logDebug("Refreshing"); + this.refresh(); + this.refreshNeeded = 0; + } + } + if (!this.stable && !Object.keys(this.selectedNodes).length) { this.adjustIterations += 1; - // logDebug("Update"); var adjusted = false; var nodeAdjustResults = {}; var fixupEdges = {}; for(var edgeId in this.edges) { - adjusted |= this.edgeObjects[edgeId].adjust(nodeAdjustResults); + if (!this.unstableEdges[edgeId]) { + continue; + } + // FIXME need to add some randomness to this so that objects don't hit a + // false equilibrium + // FIXME should this be done node-wise, to more easily account for the number of edge + // connections for a node? + adjusted |= this.edgeObjects[edgeId].adjust(nodeAdjustResults, this.adjustIterations); } + for (var nodeId in nodeAdjustResults) { var curPos = this.getNodePosition(nodeId); var newPos = nodeAdjustResults[nodeId]; @@ -58,22 +72,32 @@ MagBalls.prototype.onUpdate = function(deltaTime) { fixupEdges[edgeId] = true; } // logDebug("Moving node Id " + nodeId + " " + (distance * 1000).toFixed(3) + " mm"); - Entities.editEntity(nodeId, { position: newPos, color: COLORS.RED }); + Entities.editEntity(nodeId, { + position: newPos, + // DEBUGGING, flashes moved balls + // color: COLORS.RED + }); } + // DEBUGGING, flashes moved balls + //Script.setTimeout(function(){ + // for (var nodeId in nodeAdjustResults) { + // Entities.editEntity(nodeId, { color: BALL_COLOR }); + // } + //}, ((UPDATE_INTERVAL * 1000) / 2)); + for (var edgeId in fixupEdges) { this.fixupEdge(edgeId); } - - Script.setTimeout(function(){ - for (var nodeId in nodeAdjustResults) { - Entities.editEntity(nodeId, { color: BALL_COLOR }); - } - }, ((UPDATE_INTERVAL * 1000) / 2)); + if (!adjusted || this.adjustIterations > this.MAX_ADJUST_ITERATIONS) { + if (adjusted) { + logDebug("Could not stabilized after " + this.MAX_ADJUST_ITERATIONS + " abandoning"); + } this.adjustIterations = 0; this.stable = true; + this.unstableEdges = {}; } } } @@ -118,7 +142,7 @@ MagBalls.prototype.findPotentialEdges = function(nodeId) { // Check distance to attempt var distance = this.getNodeDistance(nodeId, otherNodeId); var variance = this.getVariance(distance); - if (Math.abs(variance) > 0.25) { + if (Math.abs(variance) > this.MAX_VARIANCE) { continue; } @@ -127,26 +151,38 @@ MagBalls.prototype.findPotentialEdges = function(nodeId) { return variances; } -MagBalls.prototype.grabBall = function(position, maxDist) { - var selected = this.findNearestNode(position, maxDist); - if (!selected) { - selected = this.createNode({ position: position }); - } - if (selected) { - this.stable = true; - this.breakEdges(selected); - this.selectedNodes[selected] = true; - } - return selected; +MagBalls.prototype.breakEdges = function(nodeId) { + //var unstableNodes = this.findShape(Object.keys.target); + //for (var node in unstableNodes) { + // this.unstableNodes[node] = true; + //} + Graph.prototype.breakEdges.call(this, nodeId); } +MagBalls.prototype.createBall = function(position) { + var created = this.createNode({ position: position }); + this.selectBall(created); + return created; +} + +MagBalls.prototype.selectBall = function(selected) { + if (!selected) { + return; + } + // stop updating shapes while manipulating + this.stable = true; + this.selectedNodes[selected] = true; + this.breakEdges(selected); +} + + MagBalls.prototype.releaseBall = function(releasedBall) { delete this.selectedNodes[releasedBall]; logDebug("Released ball: " + releasedBall); + var releasePosition = this.getNodePosition(releasedBall); this.stable = false; - var releasePosition = this.getNodePosition(releasedBall); // iterate through the other balls and ensure we don't intersect with // any of them. If we do, just delete this ball and return. @@ -169,10 +205,34 @@ MagBalls.prototype.releaseBall = function(releasedBall) { for (var otherBallId in targets) { this.createEdge(otherBallId, releasedBall); } + + var unstableNodes = this.findShape(releasedBall); + for (var nodeId in unstableNodes) { + for (var edgeId in this.nodes[nodeId]) { + this.unstableEdges[edgeId] = true; + } + } this.validate(); } +MagBalls.prototype.findShape = function(nodeId) { + var result = {}; + var queue = [ nodeId ]; + while (queue.length) { + var curNode = queue.shift(); + if (result[curNode]) { + continue; + } + result[curNode] = true; + for (var otherNodeId in this.getConnectedNodes(curNode)) { + queue.push(otherNodeId); + } + } + return result; +} + + MagBalls.prototype.getVariance = function(distance) { // FIXME different balls or edges might have different ideas of variance... // let something else handle this @@ -263,8 +323,11 @@ MagBalls.prototype.refresh = function() { Script.setTimeout(function() { for (var i in deleteEdges) { var edgeId = deleteEdges[i]; - logDebug("deleting invalid edge " + edgeId); - Entities.deleteEntity(edgeId); + //logDebug("deleting invalid edge " + edgeId); + //Entities.deleteEntity(edgeId); + Entities.editEntity(edgeId, { + color: COLORS.RED + }) } }, 1000); } @@ -291,3 +354,106 @@ MagBalls.prototype.fixupEdge = function(edgeId) { Entities.editEntity(edgeId, this.findEdgeParams(ballsInEdge[0], ballsInEdge[1])); } +MagBalls.prototype.onEntityAdded = function(entityId) { + // We already have it + if (this.nodes[entityId] || this.edges[entityId]) { + return; + } + + var properties = Entities.getEntityProperties(entityId); + if (properties.name == BALL_NAME || properties.name == EDGE_NAME) { + this.refreshNeeded = 1; + } +} + +findMatchingNode = function(position, nodePositions) { + for (var nodeId in nodePositions) { + var nodePos = nodePositions[nodeId]; + var distance = Vec3.distance(position, nodePos); + if (distance < 0.03) { + return nodeId; + } + } +} + + +MagBalls.prototype.repair = function() { + // Find all the balls and record their positions + var nodePositions = {}; + for (var nodeId in this.nodes) { + nodePositions[nodeId] = this.getNodePosition(nodeId); + } + + // Now check all the edges to see if they're valid (point to balls) + // and ensure that the balls point back to them + var ballsToEdges = {}; + + // WARNING O(n^2) algorithm, every edge that is broken does + // an O(N) search against the nodes + for (var edgeId in this.edges) { + var properties = Entities.getEntityProperties(edgeId); + var startPos = properties.position; + var endPos = Vec3.sum(startPos, properties.linePoints[1]); + var magBallData = getMagBallsData(edgeId); + var update = false; + if (!magBallData.start) { + var startNode = findMatchingNode(startPos, nodePositions); + if (startNode) { + logDebug("Found start node " + startNode) + magBallData.start = startNode; + update = true; + } + } + if (!magBallData.end) { + var endNode = findMatchingNode(endPos, nodePositions); + if (endNode) { + logDebug("Found end node " + endNode) + magBallData.end = endNode; + update = true; + } + } + if (!magBallData.start || !magBallData.end) { + logDebug("Didn't find both ends"); + this.destroyEdge(edgeId); + continue; + } + if (!ballsToEdges[magBallData.start]) { + ballsToEdges[magBallData.start] = [ edgeId ]; + } else { + ballsToEdges[magBallData.start].push(edgeId); + } + if (!ballsToEdges[magBallData.end]) { + ballsToEdges[magBallData.end] = [ edgeId ]; + } else { + ballsToEdges[magBallData.end].push(edgeId); + } + if (update) { + logDebug("Updating incomplete edge " + edgeId); + magBallData.length = BALL_DISTANCE; + setMagBallsData(edgeId, magBallData); + } + } + for (var nodeId in ballsToEdges) { + var magBallData = getMagBallsData(nodeId); + var edges = magBallData.edges || []; + var edgeHash = {}; + for (var i in edges) { + edgeHash[edges[i]] = true; + } + var update = false; + for (var i in ballsToEdges[nodeId]) { + var edgeId = ballsToEdges[nodeId][i]; + if (!edgeHash[edgeId]) { + update = true; + edgeHash[edgeId] = true; + edges.push(edgeId); + } + } + if (update) { + logDebug("Fixing node with missing edge data"); + magBallData.edges = edges; + setMagBallsData(nodeId, magBallData); + } + } +} + diff --git a/examples/toys/magBalls/magBallsMain.js b/examples/toys/magBalls/magBallsMain.js deleted file mode 100644 index e54b818e4a..0000000000 --- a/examples/toys/magBalls/magBallsMain.js +++ /dev/null @@ -1,25 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015/08/25 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -Script.include("constants.js"); -Script.include("utils.js"); -Script.include("magBalls.js"); - -Script.include("ballController.js"); - -var magBalls = new MagBalls(); - -// Clear any previous balls -// magBalls.clear(); - -MenuController = function(side) { - HandController.call(this, side); -} - -// FIXME resolve some of the issues with dual controllers before allowing both controllers active -var handControllers = [new BallController(LEFT_CONTROLLER, magBalls)]; //, new HandController(RIGHT) ]; diff --git a/examples/toys/magBalls/menuController.js b/examples/toys/magBalls/menuController.js deleted file mode 100644 index 0a076d1ff8..0000000000 --- a/examples/toys/magBalls/menuController.js +++ /dev/null @@ -1,66 +0,0 @@ -Script.include("handController.js"); - -MenuController = function(side, magBalls) { - HandController.call(this, side); -} - -MenuController.prototype = Object.create( HandController.prototype ); - -MenuController.prototype.onUpdate = function(deltaTime) { - HandController.prototype.onUpdate.call(this, deltaTime); - if (!this.selected) { - // Find the highlight target and set it. - var target = this.magBalls.findNearestNode(this.tipPosition, BALL_SELECTION_RADIUS); - this.highlighter.highlight(target); - return; - } - this.highlighter.highlight(null); - Entities.editEntity(this.selected, { position: this.tipPosition }); - var targetBalls = this.magBalls.findPotentialEdges(this.selected); - for (var ballId in targetBalls) { - if (!this.ghostEdges[ballId]) { - // create the ovleray - this.ghostEdges[ballId] = Overlays.addOverlay("line3d", { - start: this.magBalls.getNodePosition(ballId), - end: this.tipPosition, - color: COLORS.RED, - alpha: 1, - lineWidth: 5, - visible: true, - }); - } else { - Overlays.editOverlay(this.ghostEdges[ballId], { - end: this.tipPosition, - }); - } - } - for (var ballId in this.ghostEdges) { - if (!targetBalls[ballId]) { - Overlays.deleteOverlay(this.ghostEdges[ballId]); - delete this.ghostEdges[ballId]; - } - } -} - -MenuController.prototype.onClick = function() { - this.selected = this.magBalls.grabBall(this.tipPosition, BALL_SELECTION_RADIUS); - this.highlighter.highlight(null); -} - -MenuController.prototype.onRelease = function() { - this.clearGhostEdges(); - this.magBalls.releaseBall(this.selected); - this.selected = null; -} - -MenuController.prototype.clearGhostEdges = function() { - for(var ballId in this.ghostEdges) { - Overlays.deleteOverlay(this.ghostEdges[ballId]); - delete this.ghostEdges[ballId]; - } -} - -MenuController.prototype.onCleanup = function() { - HandController.prototype.onCleanup.call(this); - this.clearGhostEdges(); -} diff --git a/examples/voxels.js b/examples/voxels.js index d9049e2b0c..0776bce627 100644 --- a/examples/voxels.js +++ b/examples/voxels.js @@ -3,6 +3,7 @@ var shiftHeld = false; Script.include([ "libraries/toolBars.js", + "libraries/utils.js", ]); var isActive = false; @@ -12,24 +13,25 @@ var toolWidth = 50; var addingVoxels = false; var deletingVoxels = false; +var addingSpheres = false; +var deletingSpheres = false; -offAlpha = 0.5; -onAlpha = 0.9; +var offAlpha = 0.5; +var onAlpha = 0.9; +var editSphereRadius = 4; function floorVector(v) { return {x: Math.floor(v.x), y: Math.floor(v.y), z: Math.floor(v.z)}; } -function vectorToString(v){ - return "{" + v.x + ", " + v.x + ", " + v.x + "}"; -} - var toolBar = (function () { var that = {}, toolBar, activeButton, addVoxelButton, deleteVoxelButton, + addSphereButton, + deleteSphereButton, addTerrainButton; function initialize() { @@ -66,6 +68,24 @@ var toolBar = (function () { visible: false }); + addSphereButton = toolBar.addTool({ + imageURL: toolIconUrl + "sphere-add.svg", + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + alpha: offAlpha, + visible: false + }); + + deleteSphereButton = toolBar.addTool({ + imageURL: toolIconUrl + "sphere-delete.svg", + subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, + width: toolWidth, + height: toolHeight, + alpha: offAlpha, + visible: false + }); + addTerrainButton = toolBar.addTool({ imageURL: toolIconUrl + "voxel-terrain.svg", subImage: { x: 0, y: Tool.IMAGE_WIDTH, width: Tool.IMAGE_WIDTH, height: Tool.IMAGE_HEIGHT }, @@ -78,6 +98,22 @@ var toolBar = (function () { that.setActive(false); } + function disableAllButtons() { + addingVoxels = false; + deletingVoxels = false; + addingSpheres = false; + deletingSpheres = false; + + toolBar.setAlpha(offAlpha, addVoxelButton); + toolBar.setAlpha(offAlpha, deleteVoxelButton); + toolBar.setAlpha(offAlpha, addSphereButton); + toolBar.setAlpha(offAlpha, deleteSphereButton); + + toolBar.selectTool(addVoxelButton, false); + toolBar.selectTool(deleteVoxelButton, false); + toolBar.selectTool(addSphereButton, false); + toolBar.selectTool(deleteSphereButton, false); + } that.setActive = function(active) { if (active != isActive) { @@ -91,6 +127,8 @@ var toolBar = (function () { that.showTools = function(doShow) { toolBar.showTool(addVoxelButton, doShow); toolBar.showTool(deleteVoxelButton, doShow); + toolBar.showTool(addSphereButton, doShow); + toolBar.showTool(deleteSphereButton, doShow); toolBar.showTool(addTerrainButton, doShow); }; @@ -103,37 +141,46 @@ var toolBar = (function () { } if (addVoxelButton === toolBar.clicked(clickedOverlay)) { - if (addingVoxels) { - addingVoxels = false; - deletingVoxels = false; - toolBar.setAlpha(offAlpha, addVoxelButton); - toolBar.setAlpha(offAlpha, deleteVoxelButton); - toolBar.selectTool(addVoxelButton, false); - toolBar.selectTool(deleteVoxelButton, false); - } else { + var wasAddingVoxels = addingVoxels; + disableAllButtons() + if (!wasAddingVoxels) { addingVoxels = true; - deletingVoxels = false; toolBar.setAlpha(onAlpha, addVoxelButton); - toolBar.setAlpha(offAlpha, deleteVoxelButton); } return true; } if (deleteVoxelButton === toolBar.clicked(clickedOverlay)) { - if (deletingVoxels) { - deletingVoxels = false; - addingVoxels = false; - toolBar.setAlpha(offAlpha, addVoxelButton); - toolBar.setAlpha(offAlpha, deleteVoxelButton); - } else { + var wasDeletingVoxels = deletingVoxels; + disableAllButtons() + if (!wasDeletingVoxels) { deletingVoxels = true; - addingVoxels = false; - toolBar.setAlpha(offAlpha, addVoxelButton); toolBar.setAlpha(onAlpha, deleteVoxelButton); } return true; } + if (addSphereButton === toolBar.clicked(clickedOverlay)) { + var wasAddingSpheres = addingSpheres + disableAllButtons() + if (!wasAddingSpheres) { + addingSpheres = true; + toolBar.setAlpha(onAlpha, addSphereButton); + } + return true; + } + + if (deleteSphereButton === toolBar.clicked(clickedOverlay)) { + var wasDeletingSpheres = deletingSpheres; + disableAllButtons() + if (!wasDeletingSpheres) { + deletingSpheres = true; + toolBar.setAlpha(onAlpha, deleteSphereButton); + } + return true; + } + + if (addTerrainButton === toolBar.clicked(clickedOverlay)) { addTerrainBlock(); return true; @@ -155,85 +202,212 @@ var toolBar = (function () { }()); + +function getTerrainAlignedLocation(pos) { + var posDiv16 = Vec3.multiply(pos, 1.0 / 16.0); + var posDiv16Floored = floorVector(posDiv16); + return Vec3.multiply(posDiv16Floored, 16.0); +} + + +function lookupTerrainForLocation(pos) { + var baseLocation = getTerrainAlignedLocation(pos); + entitiesAtLoc = Entities.findEntities(baseLocation, 1.0); + for (var i = 0; i < entitiesAtLoc.length; i++) { + var id = entitiesAtLoc[i]; + var properties = Entities.getEntityProperties(id); + if (properties.name == "terrain") { + return id; + } + } + + return false; +} + + +function grabLowestJointY() { + var jointNames = MyAvatar.getJointNames(); + var floorY = MyAvatar.position.y; + for (var jointName in jointNames) { + if (MyAvatar.getJointPosition(jointNames[jointName]).y < floorY) { + floorY = MyAvatar.getJointPosition(jointNames[jointName]).y; + } + } + return floorY; +} + + + function addTerrainBlock() { - - var myPosDiv16 = Vec3.multiply(Vec3.sum(MyAvatar.position, {x:8, x:8, z:8}), 1.0 / 16.0); - var myPosDiv16Floored = floorVector(myPosDiv16); - var baseLocation = Vec3.multiply(myPosDiv16Floored, 16.0); - - if (baseLocation.y + 8 > MyAvatar.position.y) { + var baseLocation = getTerrainAlignedLocation(Vec3.sum(MyAvatar.position, {x:8, y:8, z:8})); + if (baseLocation.y > MyAvatar.position.y) { baseLocation.y -= 16; } - print("myPosDiv16 is " + vectorToString(myPosDiv16)); - print("MyPosDiv16Floored is " + vectorToString(myPosDiv16Floored)); - print("baseLocation is " + vectorToString(baseLocation)); - - alreadyThere = Entities.findEntities(baseLocation, 1.0); - for (var i = 0; i < alreadyThere.length; i++) { - var id = alreadyThere[i]; - var properties = Entities.getEntityProperties(id); - if (properties.name == "terrain") { - print("already terrain there"); + var alreadyThere = lookupTerrainForLocation(baseLocation); + if (alreadyThere) { + // there is already a terrain block under MyAvatar. + // try in front of the avatar. + facingPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(8.0, Quat.getFront(Camera.getOrientation()))); + facingPosition = Vec3.sum(facingPosition, {x:8, y:8, z:8}); + baseLocation = getTerrainAlignedLocation(facingPosition); + alreadyThere = lookupTerrainForLocation(baseLocation); + if (alreadyThere) { return; } } - var polyVoxId = Entities.addEntity({ + var polyVoxID = Entities.addEntity({ type: "PolyVox", name: "terrain", position: baseLocation, - dimensions: { x: 16, y: 16, z: 16 }, - voxelVolumeSize: {x:16, y:16, z:16}, - voxelSurfaceStyle: 2 + dimensions: { x:16, y:16, z:16 }, + voxelVolumeSize: {x:16, y:64, z:16}, + voxelSurfaceStyle: 0, + xTextureURL: "http://headache.hungry.com/~seth/hifi/dirt.jpeg", + yTextureURL: "http://headache.hungry.com/~seth/hifi/grass.png", + zTextureURL: "http://headache.hungry.com/~seth/hifi/dirt.jpeg" }); - Entities.setAllVoxels(polyVoxId, 255); - for (var y = 8; y < 16; y++) { - for (var x = 0; x < 16; x++) { - for (var z = 0; z < 16; z++) { - Entities.setVoxel(polyVoxId, {x: x, y: y, z: z}, 0); - } - } + var AvatarPositionInVoxelCoords = Entities.worldCoordsToVoxelCoords(polyVoxID, MyAvatar.position); + // TODO -- how to find the avatar's feet? + var topY = Math.round(AvatarPositionInVoxelCoords.y) - 4; + Entities.setVoxelsInCuboid(polyVoxID, {x:0, y:0, z:0}, {x:16, y:topY, z:16}, 255); + + + ////////// + // stitch together the terrain with x/y/z NeighorID properties + ////////// + + // link neighbors to this plot + imXNNeighborFor = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:16, y:0, z:0})); + imYNNeighborFor = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:16, z:0})); + imZNNeighborFor = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:0, z:16})); + imXPNeighborFor = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:-16, y:0, z:0})); + imYPNeighborFor = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:-16, z:0})); + imZPNeighborFor = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:0, z:-16})); + + if (imXNNeighborFor) { + var properties = Entities.getEntityProperties(imXNNeighborFor); + properties.xNNeighborID = polyVoxID; + Entities.editEntity(imXNNeighborFor, properties); } + if (imYNNeighborFor) { + var properties = Entities.getEntityProperties(imYNNeighborFor); + properties.yNNeighborID = polyVoxID; + Entities.editEntity(imYNNeighborFor, properties); + } + if (imZNNeighborFor) { + var properties = Entities.getEntityProperties(imZNNeighborFor); + properties.zNNeighborID = polyVoxID; + Entities.editEntity(imZNNeighborFor, properties); + } + + if (imXPNeighborFor) { + var properties = Entities.getEntityProperties(imXPNeighborFor); + properties.xPNeighborID = polyVoxID; + Entities.editEntity(imXPNeighborFor, properties); + } + if (imYPNeighborFor) { + var properties = Entities.getEntityProperties(imYPNeighborFor); + properties.yPNeighborID = polyVoxID; + Entities.editEntity(imYPNeighborFor, properties); + } + if (imZPNeighborFor) { + var properties = Entities.getEntityProperties(imZPNeighborFor); + properties.zPNeighborID = polyVoxID; + Entities.editEntity(imZPNeighborFor, properties); + } + + + // link this plot to its neighbors + var properties = Entities.getEntityProperties(polyVoxID); + properties.xNNeighborID = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:-16, y:0, z:0})); + properties.yNNeighborID = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:-16, z:0})); + properties.zNNeighborID = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:0, z:-16})); + properties.xPNeighborID = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:16, y:0, z:0})); + properties.yPNeighborID = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:16, z:0})); + properties.zPNeighborID = lookupTerrainForLocation(Vec3.sum(baseLocation, {x:0, y:0, z:16})); + Entities.editEntity(polyVoxID, properties); return true; } -function attemptVoxelChange(pickRayDir, intersection) { +function attemptVoxelChangeForEntity(entityID, pickRayDir, intersectionLocation) { - var properties = Entities.getEntityProperties(intersection.entityID); + var properties = Entities.getEntityProperties(entityID); if (properties.type != "PolyVox") { return false; } - if (addingVoxels == false && deletingVoxels == false) { + if (addingVoxels == false && deletingVoxels == false && addingSpheres == false && deletingSpheres == false) { return false; } - var voxelPosition = Entities.worldCoordsToVoxelCoords(intersection.entityID, intersection.intersection); - var pickRayDirInVoxelSpace = Entities.localCoordsToVoxelCoords(intersection.entityID, pickRayDir); + var voxelOrigin = Entities.worldCoordsToVoxelCoords(entityID, Vec3.subtract(intersectionLocation, pickRayDir)); + var voxelPosition = Entities.worldCoordsToVoxelCoords(entityID, intersectionLocation); + var pickRayDirInVoxelSpace = Vec3.subtract(voxelPosition, voxelOrigin); pickRayDirInVoxelSpace = Vec3.normalize(pickRayDirInVoxelSpace); var doAdd = addingVoxels; var doDelete = deletingVoxels; + var doAddSphere = addingSpheres; + var doDeleteSphere = deletingSpheres; if (controlHeld) { - doAdd = deletingVoxels; - doDelete = addingVoxels; + if (doAdd) { + doAdd = false; + doDelete = true; + } else if (doDelete) { + doDelete = false; + doAdd = true; + } else if (doAddSphere) { + doAddSphere = false; + doDeleteSphere = true; + } else if (doDeleteSphere) { + doDeleteSphere = false; + doAddSphere = true; + } } if (doDelete) { var toErasePosition = Vec3.sum(voxelPosition, Vec3.multiply(pickRayDirInVoxelSpace, 0.1)); - return Entities.setVoxel(intersection.entityID, floorVector(toErasePosition), 0); + return Entities.setVoxel(entityID, floorVector(toErasePosition), 0); } if (doAdd) { var toDrawPosition = Vec3.subtract(voxelPosition, Vec3.multiply(pickRayDirInVoxelSpace, 0.1)); - return Entities.setVoxel(intersection.entityID, floorVector(toDrawPosition), 255); + return Entities.setVoxel(entityID, floorVector(toDrawPosition), 255); + } + if (doDeleteSphere) { + var toErasePosition = intersectionLocation; + return Entities.setVoxelSphere(entityID, floorVector(toErasePosition), editSphereRadius, 0); + } + if (doAddSphere) { + var toDrawPosition = intersectionLocation; + return Entities.setVoxelSphere(entityID, floorVector(toDrawPosition), editSphereRadius, 255); } } + +function attemptVoxelChange(pickRayDir, intersection) { + + var ids; + + ids = Entities.findEntities(intersection.intersection, editSphereRadius + 1.0); + if (ids.indexOf(intersection.entityID) < 0) { + ids.push(intersection.entityID); + } + + var success = false; + for (var i = 0; i < ids.length; i++) { + var entityID = ids[i]; + success |= attemptVoxelChangeForEntity(entityID, pickRayDir, intersection.intersection) + } + return success; +} + + function mousePressEvent(event) { if (!event.isLeftButton) { return; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b308c79adf..978b19767c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -65,6 +65,7 @@ #include #include +#include #include #include #include @@ -150,10 +151,10 @@ #include "ui/Stats.h" #include "ui/AddressBarDialog.h" #include "ui/UpdateDialog.h" - #include "ui/overlays/Cube3DOverlay.h" #include "PluginContainerProxy.h" +#include "AnimDebugDraw.h" // ON WIndows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -1025,6 +1026,16 @@ void Application::paintGL() { if (nullptr == _displayPlugin) { return; } + + // Some plugins process message events, potentially leading to + // re-entering a paint event. don't allow further processing if this + // happens + if (_inPaint) { + return; + } + _inPaint = true; + Finally clearFlagLambda([this] { _inPaint = false; }); + auto displayPlugin = getActiveDisplayPlugin(); displayPlugin->preRender(); _offscreenContext->makeCurrent(); @@ -1219,7 +1230,7 @@ void Application::paintGL() { // Ensure all operations from the previous context are complete before we try to read the fbo glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); glDeleteSync(sync); - + { PROFILE_RANGE(__FUNCTION__ "/pluginDisplay"); displayPlugin->display(finalTexture, toGlm(size)); @@ -1235,7 +1246,6 @@ void Application::paintGL() { _frameCount++; Stats::getInstance()->setRenderDetails(renderArgs._details); - // Reset the gpu::Context Stages // Back to the default framebuffer; gpu::Batch batch; @@ -2896,6 +2906,11 @@ void Application::update(float deltaTime) { loadViewFrustum(_myCamera, _viewFrustum); } + // Update animation debug draw renderer + { + AnimDebugDraw::getInstance().update(); + } + quint64 now = usecTimestampNow(); // Update my voxel servers with my current voxel query... @@ -3560,6 +3575,7 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se renderContext._drawHitEffect = sceneInterface->doEngineDisplayHitEffect(); renderContext._occlusionStatus = Menu::getInstance()->isOptionChecked(MenuOption::DebugAmbientOcclusion); + renderContext._fxaaStatus = Menu::getInstance()->isOptionChecked(MenuOption::Antialiasing); renderArgs->_shouldRender = LODManager::shouldRender; @@ -4727,53 +4743,68 @@ void Application::updateDisplayMode() { auto offscreenUi = DependencyManager::get(); DisplayPluginPointer oldDisplayPlugin = _displayPlugin; - if (oldDisplayPlugin != newDisplayPlugin) { - if (!_currentDisplayPluginActions.isEmpty()) { - auto menu = Menu::getInstance(); - foreach(auto itemInfo, _currentDisplayPluginActions) { - menu->removeMenuItem(itemInfo.first, itemInfo.second); - } - _currentDisplayPluginActions.clear(); - } - - if (newDisplayPlugin) { - _offscreenContext->makeCurrent(); - _activatingDisplayPlugin = true; - newDisplayPlugin->activate(); - _activatingDisplayPlugin = false; - _offscreenContext->makeCurrent(); - offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); - _offscreenContext->makeCurrent(); - } - - oldDisplayPlugin = _displayPlugin; - _displayPlugin = newDisplayPlugin; - - // If the displayPlugin is a screen based HMD, then it will want the HMDTools displayed - // Direct Mode HMDs (like windows Oculus) will be isHmd() but will have a screen of -1 - bool newPluginWantsHMDTools = newDisplayPlugin ? - (newDisplayPlugin->isHmd() && (newDisplayPlugin->getHmdScreen() >= 0)) : false; - bool oldPluginWantedHMDTools = oldDisplayPlugin ? - (oldDisplayPlugin->isHmd() && (oldDisplayPlugin->getHmdScreen() >= 0)) : false; - - // Only show the hmd tools after the correct plugin has - // been activated so that it's UI is setup correctly - if (newPluginWantsHMDTools) { - _pluginContainer->showDisplayPluginsTools(); - } - - if (oldDisplayPlugin) { - oldDisplayPlugin->deactivate(); - _offscreenContext->makeCurrent(); - - // if the old plugin was HMD and the new plugin is not HMD, then hide our hmdtools - if (oldPluginWantedHMDTools && !newPluginWantsHMDTools) { - DependencyManager::get()->hmdTools(false); - } - } - emit activeDisplayPluginChanged(); - resetSensors(); + if (newDisplayPlugin == oldDisplayPlugin) { + return; } + + // Some plugins *cough* Oculus *cough* process message events from inside their + // display function, and we don't want to change the display plugin underneath + // the paintGL call, so we need to guard against that + if (_inPaint) { + qDebug() << "Deferring plugin switch until out of painting"; + // Have the old plugin stop requesting renders + oldDisplayPlugin->stop(); + QCoreApplication::postEvent(this, new LambdaEvent([this] { + updateDisplayMode(); + })); + return; + } + + if (!_currentDisplayPluginActions.isEmpty()) { + auto menu = Menu::getInstance(); + foreach(auto itemInfo, _currentDisplayPluginActions) { + menu->removeMenuItem(itemInfo.first, itemInfo.second); + } + _currentDisplayPluginActions.clear(); + } + + if (newDisplayPlugin) { + _offscreenContext->makeCurrent(); + _activatingDisplayPlugin = true; + newDisplayPlugin->activate(); + _activatingDisplayPlugin = false; + _offscreenContext->makeCurrent(); + offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); + _offscreenContext->makeCurrent(); + } + + oldDisplayPlugin = _displayPlugin; + _displayPlugin = newDisplayPlugin; + + // If the displayPlugin is a screen based HMD, then it will want the HMDTools displayed + // Direct Mode HMDs (like windows Oculus) will be isHmd() but will have a screen of -1 + bool newPluginWantsHMDTools = newDisplayPlugin ? + (newDisplayPlugin->isHmd() && (newDisplayPlugin->getHmdScreen() >= 0)) : false; + bool oldPluginWantedHMDTools = oldDisplayPlugin ? + (oldDisplayPlugin->isHmd() && (oldDisplayPlugin->getHmdScreen() >= 0)) : false; + + // Only show the hmd tools after the correct plugin has + // been activated so that it's UI is setup correctly + if (newPluginWantsHMDTools) { + _pluginContainer->showDisplayPluginsTools(); + } + + if (oldDisplayPlugin) { + oldDisplayPlugin->deactivate(); + _offscreenContext->makeCurrent(); + + // if the old plugin was HMD and the new plugin is not HMD, then hide our hmdtools + if (oldPluginWantedHMDTools && !newPluginWantsHMDTools) { + DependencyManager::get()->hmdTools(false); + } + } + emit activeDisplayPluginChanged(); + resetSensors(); Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); } @@ -4943,7 +4974,7 @@ void Application::setPalmData(Hand* hand, UserInputMapper::PoseValue pose, float // Store the one fingertip in the palm structure so we can track velocity const float FINGER_LENGTH = 0.3f; // meters - const glm::vec3 FINGER_VECTOR(0.0f, 0.0f, FINGER_LENGTH); + const glm::vec3 FINGER_VECTOR(0.0f, FINGER_LENGTH, 0.0f); const glm::vec3 newTipPosition = position + rotation * FINGER_VECTOR; glm::vec3 oldTipPosition = palm->getTipRawPosition(); if (deltaTime > 0.0f) { @@ -5076,3 +5107,15 @@ void Application::crashApplication() { qCDebug(interfaceapp) << "Intentionally crashed Interface"; } + +void Application::setActiveDisplayPlugin(const QString& pluginName) { + auto menu = Menu::getInstance(); + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + QString name = displayPlugin->getName(); + QAction* action = menu->getActionForOption(name); + if (pluginName == name) { + action->setChecked(true); + } + } + updateDisplayMode(); +} diff --git a/interface/src/Application.h b/interface/src/Application.h index a362f40ace..81fd8415ed 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -673,6 +673,7 @@ private: int _simsPerSecondReport = 0; quint64 _lastSimsPerSecondUpdate = 0; bool _isForeground = true; // starts out assumed to be in foreground + bool _inPaint = false; friend class PluginContainerProxy; }; diff --git a/interface/src/FileLogger.cpp b/interface/src/FileLogger.cpp index 00da80814b..0b2128cf17 100644 --- a/interface/src/FileLogger.cpp +++ b/interface/src/FileLogger.cpp @@ -10,16 +10,39 @@ // #include "FileLogger.h" -#include "HifiSockAddr.h" -#include -#include -#include -#include -#include -const QString FILENAME_FORMAT = "hifi-log_%1_%2.txt"; -const QString DATETIME_FORMAT = "yyyy-MM-dd_hh.mm.ss"; -const QString LOGS_DIRECTORY = "Logs"; +#include +#include +#include +#include + +#include +#include +#include + +#include "HifiSockAddr.h" + +static const QString FILENAME_FORMAT = "hifi-log_%1_%2.txt"; +static const QString DATETIME_FORMAT = "yyyy-MM-dd_hh.mm.ss"; +static const QString LOGS_DIRECTORY = "Logs"; +// Max log size is 1 MB +static const uint64_t MAX_LOG_SIZE = 1024 * 1024; +// Max log age is 1 hour +static const uint64_t MAX_LOG_AGE_USECS = USECS_PER_SECOND * 3600; + +QString getLogRollerFilename() { + QString result = FileUtils::standardPath(LOGS_DIRECTORY); + QHostAddress clientAddress = getLocalAddress(); + QDateTime now = QDateTime::currentDateTime(); + result.append(QString(FILENAME_FORMAT).arg(clientAddress.toString(), now.toString(DATETIME_FORMAT))); + return result; +} + +const QString& getLogFilename() { + static QString fileName = FileUtils::standardPath(LOGS_DIRECTORY) + "hifi-log.txt"; + return fileName; +} + class FilePersistThread : public GenericQueueThread < QString > { public: @@ -28,8 +51,22 @@ public: } protected: + void rollFileIfNecessary(QFile& file) { + uint64_t now = usecTimestampNow(); + if ((file.size() > MAX_LOG_SIZE) || (now - _lastRollTime) > MAX_LOG_AGE_USECS) { + QString newFileName = getLogRollerFilename(); + if (file.copy(newFileName)) { + _lastRollTime = now; + file.open(QIODevice::WriteOnly | QIODevice::Truncate); + file.close(); + qDebug() << "Rolled log file: " << newFileName; + } + } + } + virtual bool processQueueItems(const Queue& messages) { QFile file(_logger._fileName); + rollFileIfNecessary(file); if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { QTextStream out(&file); foreach(const QString& message, messages) { @@ -40,20 +77,17 @@ protected: } private: const FileLogger& _logger; + uint64_t _lastRollTime = 0; }; static FilePersistThread* _persistThreadInstance; FileLogger::FileLogger(QObject* parent) : - AbstractLoggerInterface(parent) + AbstractLoggerInterface(parent), _fileName(getLogFilename()) { _persistThreadInstance = new FilePersistThread(*this); _persistThreadInstance->initialize(true, QThread::LowestPriority); - _fileName = FileUtils::standardPath(LOGS_DIRECTORY); - QHostAddress clientAddress = getLocalAddress(); - QDateTime now = QDateTime::currentDateTime(); - _fileName.append(QString(FILENAME_FORMAT).arg(clientAddress.toString(), now.toString(DATETIME_FORMAT))); } FileLogger::~FileLogger() { diff --git a/interface/src/FileLogger.h b/interface/src/FileLogger.h index 549654ca5c..122da20ae7 100644 --- a/interface/src/FileLogger.h +++ b/interface/src/FileLogger.h @@ -27,7 +27,7 @@ public: virtual void locateLog() override; private: - QString _fileName; + const QString _fileName; friend class FilePersistThread; }; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 3d68fa233a..26b026acb3 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -318,6 +318,7 @@ Menu::Menu() { 0, // QML Qt::SHIFT | Qt::Key_A, true); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::DebugAmbientOcclusion); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Antialiasing); MenuWrapper* ambientLightMenu = renderOptionsMenu->addMenu(MenuOption::RenderAmbientLight); QActionGroup* ambientLightGroup = new QActionGroup(ambientLightMenu); @@ -448,6 +449,14 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableRigAnimations, 0, false, avatar, SLOT(setEnableRigAnimations(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableAnimGraph, 0, false, + avatar, SLOT(setEnableAnimGraph(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawBindPose, 0, false, + avatar, SLOT(setEnableDebugDrawBindPose(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawAnimPose, 0, false, + avatar, SLOT(setEnableDebugDrawAnimPose(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::MeshVisible, 0, true, + avatar, SLOT(setEnableMeshVisible(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::DisableEyelidAdjustment, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::Connexion, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 4cf7e87c48..0c416f4cc2 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -132,6 +132,9 @@ namespace MenuOption { const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; + const QString AnimDebugDrawAnimPose = "Debug Draw Animation"; + const QString AnimDebugDrawBindPose = "Debug Draw Bind Pose"; + const QString Antialiasing = "Antialiasing"; const QString Atmosphere = "Atmosphere"; const QString Attachments = "Attachments..."; const QString AudioNoiseReduction = "Audio Noise Reduction"; @@ -185,6 +188,7 @@ namespace MenuOption { const QString EchoServerAudio = "Echo Server Audio"; const QString EditEntitiesHelp = "Edit Entities Help..."; const QString Enable3DTVMode = "Enable 3DTV Mode"; + const QString EnableAnimGraph = "Enable Anim Graph"; const QString EnableCharacterController = "Enable avatar collisions"; const QString EnableRigAnimations = "Enable Rig Animations"; const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation"; @@ -214,6 +218,7 @@ namespace MenuOption { const QString Log = "Log"; const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; + const QString MeshVisible = "Draw Mesh"; const QString Mirror = "Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; diff --git a/interface/src/PluginContainerProxy.cpp b/interface/src/PluginContainerProxy.cpp index e172dbbd9e..4ef755bd1b 100644 --- a/interface/src/PluginContainerProxy.cpp +++ b/interface/src/PluginContainerProxy.cpp @@ -147,15 +147,3 @@ void PluginContainerProxy::showDisplayPluginsTools() { QGLWidget* PluginContainerProxy::getPrimarySurface() { return qApp->_glWidget; } - -void Application::setActiveDisplayPlugin(const QString& pluginName) { - auto menu = Menu::getInstance(); - foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { - QString name = displayPlugin->getName(); - QAction* action = menu->getActionForOption(name); - if (pluginName == name) { - action->setChecked(true); - } - } - updateDisplayMode(); -} diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 6d1505ced9..55699bccbe 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -782,8 +782,10 @@ Transform Avatar::calculateDisplayNameTransform(const ViewFrustum& frustum, floa void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& frustum, const glm::ivec4& viewport) const { bool shouldShowReceiveStats = DependencyManager::get()->shouldShowReceiveStats() && !isMyAvatar(); - // If we have nothing to draw, or it's tottaly transparent, return - if ((_displayName.isEmpty() && !shouldShowReceiveStats) || _displayNameAlpha == 0.0f) { + // If we have nothing to draw, or it's totally transparent, or it's too close or behind the camera, return + const float CLIP_DISTANCE = 0.2f; + if ((_displayName.isEmpty() && !shouldShowReceiveStats) || _displayNameAlpha == 0.0f + || (glm::dot(frustum.getDirection(), getDisplayNamePosition() - frustum.getPosition()) <= CLIP_DISTANCE)) { return; } auto renderer = textRenderer(DISPLAYNAME); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6939720f32..de7e2ba8fb 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include "devices/Faceshift.h" @@ -68,7 +69,7 @@ float DEFAULT_SCRIPTED_MOTOR_TIMESCALE = 1.0e6f; const int SCRIPTED_MOTOR_CAMERA_FRAME = 0; const int SCRIPTED_MOTOR_AVATAR_FRAME = 1; const int SCRIPTED_MOTOR_WORLD_FRAME = 2; -const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = "https://s3.amazonaws.com/hifi-public/sounds/Collisions-hitsandslaps/airhockey_hit1.wav"; +const QString& DEFAULT_AVATAR_COLLISION_SOUND_URL = "https://hifi-public.s3.amazonaws.com/sounds/Collisions-otherorganic/Body_Hits_Impact.wav"; const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; @@ -123,18 +124,18 @@ MyAvatar::~MyAvatar() { _lookAtTargetAvatar.reset(); } -QByteArray MyAvatar::toByteArray(bool cullSmallChanges) { +QByteArray MyAvatar::toByteArray(bool cullSmallChanges, bool sendAll) { CameraMode mode = Application::getInstance()->getCamera()->getMode(); if (mode == CAMERA_MODE_THIRD_PERSON || mode == CAMERA_MODE_INDEPENDENT) { // fake the avatar position that is sent up to the AvatarMixer glm::vec3 oldPosition = _position; _position = getSkeletonPosition(); - QByteArray array = AvatarData::toByteArray(cullSmallChanges); + QByteArray array = AvatarData::toByteArray(cullSmallChanges, sendAll); // copy the correct position back _position = oldPosition; return array; } - return AvatarData::toByteArray(cullSmallChanges); + return AvatarData::toByteArray(cullSmallChanges, sendAll); } void MyAvatar::reset() { @@ -711,6 +712,38 @@ void MyAvatar::setEnableRigAnimations(bool isEnabled) { } } +void MyAvatar::setEnableAnimGraph(bool isEnabled) { + _rig->setEnableAnimGraph(isEnabled); + if (isEnabled) { + if (_skeletonModel.readyToAddToScene()) { + initAnimGraph(); + } + } else { + destroyAnimGraph(); + } +} + +void MyAvatar::setEnableDebugDrawBindPose(bool isEnabled) { + _enableDebugDrawBindPose = isEnabled; + + if (!isEnabled) { + AnimDebugDraw::getInstance().removeSkeleton("myAvatar"); + } +} + +void MyAvatar::setEnableDebugDrawAnimPose(bool isEnabled) { + _enableDebugDrawAnimPose = isEnabled; + + if (!isEnabled) { + AnimDebugDraw::getInstance().removeAnimNode("myAvatar"); + } +} + +void MyAvatar::setEnableMeshVisible(bool isEnabled) { + render::ScenePointer scene = Application::getInstance()->getMain3DScene(); + _skeletonModel.setVisibleInScene(isEnabled, scene); +} + void MyAvatar::loadData() { Settings settings; settings.beginGroup("Avatar"); @@ -1205,6 +1238,20 @@ void MyAvatar::initHeadBones() { } } +void MyAvatar::initAnimGraph() { + // https://gist.github.com/hyperlogic/7d6a0892a7319c69e2b9 + // python2 -m SimpleHTTPServer& + //auto graphUrl = QUrl("http://localhost:8000/avatar.json"); + auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/7d6a0892a7319c69e2b9/raw/e2cb37aee601b6fba31d60eac3f6ae3ef72d4a66/avatar.json"); + _skeletonModel.initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); +} + +void MyAvatar::destroyAnimGraph() { + _rig->destroyAnimGraph(); + AnimDebugDraw::getInstance().removeSkeleton("myAvatar"); + AnimDebugDraw::getInstance().removeAnimNode("myAvatar"); +} + void MyAvatar::preRender(RenderArgs* renderArgs) { render::ScenePointer scene = Application::getInstance()->getMain3DScene(); @@ -1213,6 +1260,28 @@ void MyAvatar::preRender(RenderArgs* renderArgs) { if (_skeletonModel.initWhenReady(scene)) { initHeadBones(); _skeletonModel.setCauterizeBoneSet(_headBoneSet); + initAnimGraph(); + } + + if (_enableDebugDrawBindPose || _enableDebugDrawAnimPose) { + + AnimSkeleton::ConstPointer animSkeleton = _rig->getAnimSkeleton(); + AnimNode::ConstPointer animNode = _rig->getAnimNode(); + + // bones space is rotated + glm::quat rotY180 = glm::angleAxis((float)M_PI, glm::vec3(0.0f, 1.0f, 0.0f)); + AnimPose xform(glm::vec3(1), rotY180 * getOrientation(), getPosition()); + + if (animSkeleton && _enableDebugDrawBindPose) { + glm::vec4 gray(0.2f, 0.2f, 0.2f, 0.2f); + AnimDebugDraw::getInstance().addSkeleton("myAvatar", animSkeleton, xform, gray); + } + + // This only works for when new anim system is enabled, at the moment. + if (animNode && animSkeleton && _enableDebugDrawAnimPose && _rig->getEnableAnimGraph()) { + glm::vec4 cyan(0.1f, 0.6f, 0.6f, 1.0f); + AnimDebugDraw::getInstance().addAnimNode("myAvatar", animNode, xform, cyan); + } } if (shouldDrawHead != _prevShouldDrawHead) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 89f3b236f4..fadb5d669c 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -19,6 +19,7 @@ #include "Avatar.h" class ModelItemID; +class AnimNode; enum eyeContactTarget { LEFT_EYE, @@ -189,7 +190,12 @@ public slots: void loadLastRecording(); virtual void rebuildSkeletonBody(); + void setEnableRigAnimations(bool isEnabled); + void setEnableAnimGraph(bool isEnabled); + void setEnableDebugDrawBindPose(bool isEnabled); + void setEnableDebugDrawAnimPose(bool isEnabled); + void setEnableMeshVisible(bool isEnabled); signals: void transformChanged(); @@ -200,7 +206,7 @@ private: glm::vec3 getWorldBodyPosition() const; glm::quat getWorldBodyOrientation() const; - QByteArray toByteArray(bool cullSmallChanges); + QByteArray toByteArray(bool cullSmallChanges, bool sendAll); void simulate(float deltaTime); void updateFromTrackers(float deltaTime); virtual void render(RenderArgs* renderArgs, const glm::vec3& cameraPositio) override; @@ -283,6 +289,8 @@ private: void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void maybeUpdateBillboard(); void initHeadBones(); + void initAnimGraph(); + void destroyAnimGraph(); // Avatar Preferences QUrl _fullAvatarURLFromPreferences; @@ -310,6 +318,9 @@ private: std::unordered_set _headBoneSet; RigPointer _rig; bool _prevShouldDrawHead; + + bool _enableDebugDrawBindPose = false; + bool _enableDebugDrawAnimPose = false; }; #endif // hifi_MyAvatar_h diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index a2e5908477..5ffd0f8dec 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -86,7 +86,7 @@ void SkeletonModel::initJointStates(QVector states) { _rig->updateJointState(i, rootTransform); } - buildShapes(); + computeBoundingShape(); Extents meshExtents = getMeshExtents(); _headClipDistance = -(meshExtents.minimum.z / _scale.z - _defaultEyeModelPosition.z); @@ -248,6 +248,7 @@ void SkeletonModel::applyHandPosition(int jointIndex, const glm::vec3& position) rotationBetween(handRotation * glm::vec3(-sign, 0.0f, 0.0f), forearmVector), true, PALM_PRIORITY); } + void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { if (jointIndex == -1 || jointIndex >= _rig->getJointStateCount()) { return; @@ -261,9 +262,7 @@ void SkeletonModel::applyPalmData(int jointIndex, PalmData& palm) { // the palm's position must be transformed into the model-frame glm::quat inverseRotation = glm::inverse(_rotation); glm::vec3 palmPosition = inverseRotation * (palm.getPosition() - _translation); - - // the palm's "raw" rotation is already in the model-frame - glm::quat palmRotation = palm.getRawRotation(); + glm::quat palmRotation = inverseRotation * palm.getRotation(); inverseKinematics(jointIndex, palmPosition, palmRotation, PALM_PRIORITY); } @@ -346,9 +345,9 @@ void SkeletonModel::renderOrientationDirections(gpu::Batch& batch, int jointInde } OrientationLineIDs& jointLineIDs = _jointOrientationLines[jointIndex]; - glm::vec3 pRight = position + orientation * IDENTITY_RIGHT * size; - glm::vec3 pUp = position + orientation * IDENTITY_UP * size; - glm::vec3 pFront = position + orientation * IDENTITY_FRONT * size; + glm::vec3 pRight = position + orientation * IDENTITY_RIGHT * size; + glm::vec3 pUp = position + orientation * IDENTITY_UP * size; + glm::vec3 pFront = position + orientation * IDENTITY_FRONT * size; glm::vec3 red(1.0f, 0.0f, 0.0f); geometryCache->renderLine(batch, position, pRight, red, jointLineIDs._right); @@ -466,7 +465,7 @@ float MIN_JOINT_MASS = 1.0f; float VERY_BIG_MASS = 1.0e6f; // virtual -void SkeletonModel::buildShapes() { +void SkeletonModel::computeBoundingShape() { if (_geometry == NULL || _rig->jointStatesEmpty()) { return; } @@ -476,34 +475,81 @@ void SkeletonModel::buildShapes() { // rootJointIndex == -1 if the avatar model has no skeleton return; } - computeBoundingShape(geometry); -} -void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { - // compute default joint transforms - int numStates = _rig->getJointStateCount(); - QVector transforms; - transforms.fill(glm::mat4(), numStates); + // BOUNDING SHAPE HACK: before we measure the bounds of the joints we use IK to put the + // hands and feet into positions that are more correct than the default pose. + + // Measure limb lengths so we can specify IK targets that will pull hands and feet tight to body + QVector endEffectors; + endEffectors.push_back("RightHand"); + endEffectors.push_back("LeftHand"); + endEffectors.push_back("RightFoot"); + endEffectors.push_back("LeftFoot"); + + QVector baseJoints; + baseJoints.push_back("RightArm"); + baseJoints.push_back("LeftArm"); + baseJoints.push_back("RightUpLeg"); + baseJoints.push_back("LeftUpLeg"); + + for (int i = 0; i < endEffectors.size(); ++i) { + QString tipName = endEffectors[i]; + QString baseName = baseJoints[i]; + float limbLength = 0.0f; + int tipIndex = _rig->indexOfJoint(tipName); + if (tipIndex == -1) { + continue; + } + // save tip's relative rotation for later + glm::quat tipRotation = _rig->getJointState(tipIndex).getRotationInConstrainedFrame(); + + // IK on each endpoint + int jointIndex = tipIndex; + QVector freeLineage; + float priority = 1.0f; + while (jointIndex > -1) { + JointState limbJoint = _rig->getJointState(jointIndex); + freeLineage.push_back(jointIndex); + if (limbJoint.getName() == baseName) { + glm::vec3 targetPosition = limbJoint.getPosition() - glm::vec3(0.0f, 1.5f * limbLength, 0.0f); + // do IK a few times to make sure the endpoint gets close to its target + for (int j = 0; j < 5; ++j) { + _rig->inverseKinematics(tipIndex, + targetPosition, + glm::quat(), + priority, + freeLineage, + glm::mat4()); + } + break; + } + limbLength += limbJoint.getDistanceToParent(); + jointIndex = limbJoint.getParentIndex(); + } + + // since this IK target is totally bogus we restore the tip's relative rotation + _rig->setJointRotationInConstrainedFrame(tipIndex, tipRotation, priority); + } + + // recompute all joint model-frame transforms + glm::mat4 rootTransform = glm::scale(_scale) * glm::translate(_offset) * geometry.offset; + for (int i = 0; i < _rig->getJointStateCount(); i++) { + _rig->updateJointState(i, rootTransform); + } + // END BOUNDING SHAPE HACK // compute bounding box that encloses all shapes Extents totalExtents; totalExtents.reset(); totalExtents.addPoint(glm::vec3(0.0f)); + int numStates = _rig->getJointStateCount(); for (int i = 0; i < numStates; i++) { // compute the default transform of this joint const JointState& state = _rig->getJointState(i); - int parentIndex = state.getParentIndex(); - if (parentIndex == -1) { - transforms[i] = _rig->getJointTransform(i); - } else { - glm::quat modifiedRotation = state.getPreRotation() * state.getDefaultRotation() * state.getPostRotation(); - transforms[i] = transforms[parentIndex] * glm::translate(state.getTranslation()) - * state.getPreTransform() * glm::mat4_cast(modifiedRotation) * state.getPostTransform(); - } // Each joint contributes a sphere at its position glm::vec3 axis(state.getBoneRadius()); - glm::vec3 jointPosition = extractTranslation(transforms[i]); + glm::vec3 jointPosition = state.getPosition(); totalExtents.addPoint(jointPosition + axis); totalExtents.addPoint(jointPosition - axis); } @@ -517,6 +563,11 @@ void SkeletonModel::computeBoundingShape(const FBXGeometry& geometry) { glm::vec3 rootPosition = _rig->getJointState(geometry.rootJointIndex).getPosition(); _boundingCapsuleLocalOffset = 0.5f * (totalExtents.maximum + totalExtents.minimum) - rootPosition; + + // RECOVER FROM BOUNINDG SHAPE HACK: now that we're all done, restore the default pose + for (int i = 0; i < numStates; i++) { + _rig->restoreJointRotation(i, 1.0f, 1.0f); + } } void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha) { @@ -535,7 +586,7 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha glm::vec4(0.6f, 0.6f, 0.8f, alpha)); // draw a yellow sphere at the capsule bottom point - glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, -_boundingCapsuleHeight, 0.0f); + glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, _boundingCapsuleHeight, 0.0f); glm::vec3 axis = topPoint - bottomPoint; transform.setTranslation(bottomPoint); batch.setModelTransform(transform); @@ -555,3 +606,7 @@ bool SkeletonModel::hasSkeleton() { void SkeletonModel::onInvalidate() { } + +void SkeletonModel::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) { + _rig->initAnimGraph(url, fbxGeometry); +} diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 4ae615eadd..389b1e7d36 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -94,7 +94,6 @@ public: /// \return whether or not the head was found. glm::vec3 getDefaultEyeModelPosition() const; - void computeBoundingShape(const FBXGeometry& geometry); void renderBoundingCollisionShapes(gpu::Batch& batch, float alpha); float getBoundingCapsuleRadius() const { return _boundingCapsuleRadius; } float getBoundingCapsuleHeight() const { return _boundingCapsuleHeight; } @@ -106,13 +105,15 @@ public: virtual void onInvalidate() override; + void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry); + signals: void skeletonLoaded(); protected: - void buildShapes(); + void computeBoundingShape(); /// \param jointIndex index of joint in model /// \param position position of joint in model-frame diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 7824c0c498..0c909a1bfb 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -66,16 +66,7 @@ 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(); - } - } + xColorFromScriptValue(properties.property("color"), _color); if (properties.property("alpha").isValid()) { setAlpha(properties.property("alpha").toVariant().toFloat()); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 39b8892f13..563d90f6b9 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -30,6 +30,7 @@ #include "Grid3DOverlay.h" #include "TextOverlay.h" #include "Text3DOverlay.h" +#include "Web3DOverlay.h" Overlays::Overlays() : _nextOverlayID(1) { @@ -170,6 +171,8 @@ unsigned int Overlays::addOverlay(const QString& type, const QScriptValue& prope thisOverlay = std::make_shared(Application::getInstance()->getEntityClipboardRenderer()); } else if (type == ModelOverlay::TYPE) { thisOverlay = std::make_shared(); + } else if (type == Web3DOverlay::TYPE) { + thisOverlay = std::make_shared(); } if (thisOverlay) { diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp new file mode 100644 index 0000000000..f14fcf4dc0 --- /dev/null +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -0,0 +1,168 @@ +// +// Web3DOverlay.cpp +// +// +// Created by Clement on 7/1/14. +// Modified and renamed by Zander Otavka on 8/4/15 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Web3DOverlay.h" + +#include +#include +#include + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// #include "Application.h" +// #include "GeometryUtil.h" + +static const float DPI = 30.47f; +static const float METERS_TO_INCHES = 39.3701f; +static const float INCHES_TO_METERS = 1.0f / 39.3701f; + +QString const Web3DOverlay::TYPE = "web3d"; + +Web3DOverlay::Web3DOverlay() : _dpi(DPI) { } + +Web3DOverlay::Web3DOverlay(const Web3DOverlay* Web3DOverlay) : + Billboard3DOverlay(Web3DOverlay), + _url(Web3DOverlay->_url), + _dpi(Web3DOverlay->_dpi), + _resolution(Web3DOverlay->_resolution) +{ +} + +Web3DOverlay::~Web3DOverlay() { + if (_webSurface) { + _webSurface->pause(); + _webSurface->disconnect(_connection); + // The lifetime of the QML surface MUST be managed by the main thread + // Additionally, we MUST use local variables copied by value, rather than + // member variables, since they would implicitly refer to a this that + // is no longer valid + auto webSurface = _webSurface; + AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] { + webSurface->deleteLater(); + }); + } +} + +void Web3DOverlay::update(float deltatime) { + applyTransformTo(_transform); +} + +void Web3DOverlay::render(RenderArgs* args) { + if (!_visible || !getParentVisible()) { + return; + } + + QOpenGLContext * currentContext = QOpenGLContext::currentContext(); + QSurface * currentSurface = currentContext->surface(); + if (!_webSurface) { + _webSurface = new OffscreenQmlSurface(); + _webSurface->create(currentContext); + _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); + _webSurface->load("WebEntity.qml"); + _webSurface->resume(); + _webSurface->getRootItem()->setProperty("url", _url); + _webSurface->resize(QSize(_resolution.x, _resolution.y)); + _connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) { + _texture = textureId; + }); + currentContext->makeCurrent(currentSurface); + } + + vec2 size = _resolution / _dpi * INCHES_TO_METERS; + vec2 halfSize = size / 2.0f; + vec4 color(toGlm(getColor()), getAlpha()); + + applyTransformTo(_transform, true); + Transform transform = _transform; + if (glm::length2(getDimensions()) != 1.0f) { + transform.postScale(vec3(getDimensions(), 1.0f)); + } + + Q_ASSERT(args->_batch); + gpu::Batch& batch = *args->_batch; + if (_texture) { + batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture); + } else { + batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); + } + + batch.setModelTransform(transform); + DependencyManager::get()->bindSimpleProgram(batch, true, false, false, true); + DependencyManager::get()->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color); + batch.setResourceTexture(0, args->_whiteTexture); // restore default white color after me +} + +void Web3DOverlay::setProperties(const QScriptValue &properties) { + Billboard3DOverlay::setProperties(properties); + + QScriptValue urlValue = properties.property("url"); + if (urlValue.isValid()) { + QString newURL = urlValue.toVariant().toString(); + if (newURL != _url) { + setURL(newURL); + } + } + + QScriptValue resolution = properties.property("resolution"); + if (resolution.isValid()) { + vec2FromScriptValue(resolution, _resolution); + } + + + QScriptValue dpi = properties.property("dpi"); + if (dpi.isValid()) { + _dpi = dpi.toVariant().toFloat(); + } +} + +QScriptValue Web3DOverlay::getProperty(const QString& property) { + if (property == "url") { + return _url; + } + if (property == "dpi") { + return _dpi; + } + return Billboard3DOverlay::getProperty(property); +} + +void Web3DOverlay::setURL(const QString& url) { + _url = url; + if (_webSurface) { + AbstractViewStateInterface::instance()->postLambdaEvent([this, url] { + _webSurface->getRootItem()->setProperty("url", url); + }); + } + +} + +bool Web3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face) { + //// Make sure position and rotation is updated. + applyTransformTo(_transform, true); + vec2 size = _resolution / _dpi * INCHES_TO_METERS * vec2(getDimensions()); + // Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale. + return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), size, distance); +} + +Web3DOverlay* Web3DOverlay::createClone() const { + return new Web3DOverlay(this); +} diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h new file mode 100644 index 0000000000..59c8fae0fe --- /dev/null +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -0,0 +1,50 @@ +// +// Created by Bradley Austin Davis on 2015/08/31 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Web3DOverlay_h +#define hifi_Web3DOverlay_h + +#include "Billboard3DOverlay.h" + +class OffscreenQmlSurface; + +class Web3DOverlay : public Billboard3DOverlay { + Q_OBJECT + +public: + static QString const TYPE; + virtual QString getType() const { return TYPE; } + + Web3DOverlay(); + Web3DOverlay(const Web3DOverlay* Web3DOverlay); + virtual ~Web3DOverlay(); + + virtual void render(RenderArgs* args); + + virtual void update(float deltatime); + + // setters + void setURL(const QString& url); + + virtual void setProperties(const QScriptValue& properties); + virtual QScriptValue getProperty(const QString& property); + + virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face); + + virtual Web3DOverlay* createClone() const; + +private: + OffscreenQmlSurface* _webSurface{ nullptr }; + QMetaObject::Connection _connection; + uint32_t _texture{ 0 }; + QString _url; + float _dpi; + vec2 _resolution{ 640, 480 }; +}; + +#endif // hifi_Web3DOverlay_h diff --git a/libraries/animation/src/AnimBlendLinear.cpp b/libraries/animation/src/AnimBlendLinear.cpp new file mode 100644 index 0000000000..63c66a2b9d --- /dev/null +++ b/libraries/animation/src/AnimBlendLinear.cpp @@ -0,0 +1,62 @@ +// +// AnimBlendLinear.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimBlendLinear.h" +#include "GLMHelpers.h" +#include "AnimationLogging.h" +#include "AnimUtil.h" + +AnimBlendLinear::AnimBlendLinear(const std::string& id, float alpha) : + AnimNode(AnimNode::Type::BlendLinear, id), + _alpha(alpha) { + +} + +AnimBlendLinear::~AnimBlendLinear() { + +} + +const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + _alpha = animVars.lookup(_alphaVar, _alpha); + + if (_children.size() == 0) { + for (auto&& pose : _poses) { + pose = AnimPose::identity; + } + } else if (_children.size() == 1) { + _poses = _children[0]->evaluate(animVars, dt, triggersOut); + } else { + float clampedAlpha = glm::clamp(_alpha, 0.0f, (float)(_children.size() - 1)); + size_t prevPoseIndex = glm::floor(clampedAlpha); + size_t nextPoseIndex = glm::ceil(clampedAlpha); + float alpha = glm::fract(clampedAlpha); + if (prevPoseIndex == nextPoseIndex) { + // this can happen if alpha is on an integer boundary + _poses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); + } else { + // need to eval and blend between two children. + auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); + auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, dt, triggersOut); + + if (prevPoses.size() > 0 && prevPoses.size() == nextPoses.size()) { + _poses.resize(prevPoses.size()); + + ::blend(_poses.size(), &prevPoses[0], &nextPoses[0], alpha, &_poses[0]); + } + } + } + return _poses; +} + +// for AnimDebugDraw rendering +const AnimPoseVec& AnimBlendLinear::getPosesInternal() const { + return _poses; +} diff --git a/libraries/animation/src/AnimBlendLinear.h b/libraries/animation/src/AnimBlendLinear.h new file mode 100644 index 0000000000..3a09245575 --- /dev/null +++ b/libraries/animation/src/AnimBlendLinear.h @@ -0,0 +1,52 @@ +// +// AnimBlendLinear.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimBlendLinear_h +#define hifi_AnimBlendLinear_h + +#include "AnimNode.h" + +// Linear blend between two AnimNodes. +// the amount of blending is determined by the alpha parameter. +// If the number of children is 2, then the alpha parameters should be between +// 0 and 1. The first animation will have a (1 - alpha) factor, and the second +// will have factor of alpha. +// This node supports more then 2 children. In this case the alpha should be +// between 0 and n - 1. This alpha can be used to linearly interpolate between +// the closest two children poses. This can be used to sweep through a series +// of animation poses. + +class AnimBlendLinear : public AnimNode { +public: + friend class AnimTests; + + AnimBlendLinear(const std::string& id, float alpha); + virtual ~AnimBlendLinear() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + +protected: + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimPoseVec _poses; + + float _alpha; + + std::string _alphaVar; + + // no copies + AnimBlendLinear(const AnimBlendLinear&) = delete; + AnimBlendLinear& operator=(const AnimBlendLinear&) = delete; +}; + +#endif // hifi_AnimBlendLinear_h diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp new file mode 100644 index 0000000000..fdc5fc504a --- /dev/null +++ b/libraries/animation/src/AnimClip.cpp @@ -0,0 +1,161 @@ +// +// AnimClip.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLMHelpers.h" +#include "AnimClip.h" +#include "AnimationLogging.h" +#include "AnimUtil.h" + +AnimClip::AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag) : + AnimNode(AnimNode::Type::Clip, id), + _startFrame(startFrame), + _endFrame(endFrame), + _timeScale(timeScale), + _loopFlag(loopFlag), + _frame(startFrame) +{ + loadURL(url); +} + +AnimClip::~AnimClip() { + +} + +const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + // lookup parameters from animVars, using current instance variables as defaults. + _startFrame = animVars.lookup(_startFrameVar, _startFrame); + _endFrame = animVars.lookup(_endFrameVar, _endFrame); + _timeScale = animVars.lookup(_timeScaleVar, _timeScale); + _loopFlag = animVars.lookup(_loopFlagVar, _loopFlag); + _frame = accumulateTime(animVars.lookup(_frameVar, _frame), dt, triggersOut); + + // poll network anim to see if it's finished loading yet. + if (_networkAnim && _networkAnim->isLoaded() && _skeleton) { + // loading is complete, copy animation frames from network animation, then throw it away. + copyFromNetworkAnim(); + _networkAnim.reset(); + } + + if (_anim.size()) { + int frameCount = _anim.size(); + + int prevIndex = (int)glm::floor(_frame); + int nextIndex = (int)glm::ceil(_frame); + if (_loopFlag && nextIndex >= frameCount) { + nextIndex = 0; + } + + // It can be quite possible for the user to set _startFrame and _endFrame to + // values before or past valid ranges. We clamp the frames here. + prevIndex = std::min(std::max(0, prevIndex), frameCount - 1); + nextIndex = std::min(std::max(0, nextIndex), frameCount - 1); + + const AnimPoseVec& prevFrame = _anim[prevIndex]; + const AnimPoseVec& nextFrame = _anim[nextIndex]; + float alpha = glm::fract(_frame); + + ::blend(_poses.size(), &prevFrame[0], &nextFrame[0], alpha, &_poses[0]); + } + + return _poses; +} + +void AnimClip::loadURL(const std::string& url) { + auto animCache = DependencyManager::get(); + _networkAnim = animCache->getAnimation(QString::fromStdString(url)); + _url = url; +} + +void AnimClip::setCurrentFrameInternal(float frame) { + // because dt is 0, we should not encounter any triggers + const float dt = 0.0f; + Triggers triggers; + _frame = accumulateTime(frame * _timeScale, dt, triggers); +} + +float AnimClip::accumulateTime(float frame, float dt, Triggers& triggersOut) const { + const float startFrame = std::min(_startFrame, _endFrame); + if (startFrame == _endFrame) { + // when startFrame >= endFrame + frame = _endFrame; + } else if (_timeScale > 0.0f) { + // accumulate time, keeping track of loops and end of animation events. + const float FRAMES_PER_SECOND = 30.0f; + float framesRemaining = (dt * _timeScale) * FRAMES_PER_SECOND; + while (framesRemaining > 0.0f) { + float framesTillEnd = _endFrame - _frame; + if (framesRemaining >= framesTillEnd) { + if (_loopFlag) { + // anim loop + triggersOut.push_back(_id + "OnLoop"); + framesRemaining -= framesTillEnd; + frame = startFrame; + } else { + // anim end + triggersOut.push_back(_id + "OnDone"); + frame = _endFrame; + framesRemaining = 0.0f; + } + } else { + frame += framesRemaining; + framesRemaining = 0.0f; + } + } + } + return frame; +} + +void AnimClip::copyFromNetworkAnim() { + assert(_networkAnim && _networkAnim->isLoaded() && _skeleton); + _anim.clear(); + + // build a mapping from animation joint indices to skeleton joint indices. + // by matching joints with the same name. + const FBXGeometry& geom = _networkAnim->getGeometry(); + const QVector& animJoints = geom.joints; + std::vector jointMap; + const int animJointCount = animJoints.count(); + jointMap.reserve(animJointCount); + for (int i = 0; i < animJointCount; i++) { + int skeletonJoint = _skeleton->nameToJointIndex(animJoints.at(i).name); + if (skeletonJoint == -1) { + qCWarning(animation) << "animation contains joint =" << animJoints.at(i).name << " which is not in the skeleton, url =" << _url.c_str(); + } + jointMap.push_back(skeletonJoint); + } + + const int frameCount = geom.animationFrames.size(); + const int skeletonJointCount = _skeleton->getNumJoints(); + _anim.resize(frameCount); + for (int i = 0; i < frameCount; i++) { + + // init all joints in animation to bind pose + _anim[i].reserve(skeletonJointCount); + for (int j = 0; j < skeletonJointCount; j++) { + _anim[i].push_back(_skeleton->getRelativeBindPose(j)); + } + + // init over all joint animations + for (int j = 0; j < animJointCount; j++) { + int k = jointMap[j]; + if (k >= 0 && k < skeletonJointCount) { + // currently FBX animations only have rotation. + _anim[i][k].rot = _skeleton->getRelativeBindPose(k).rot * geom.animationFrames[i].rotations[j]; + } + } + } + _poses.resize(skeletonJointCount); +} + + +const AnimPoseVec& AnimClip::getPosesInternal() const { + return _poses; +} diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h new file mode 100644 index 0000000000..1b9649cc3e --- /dev/null +++ b/libraries/animation/src/AnimClip.h @@ -0,0 +1,74 @@ +// +// AnimClip.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimClip_h +#define hifi_AnimClip_h + +#include +#include "AnimationCache.h" +#include "AnimNode.h" + +// Playback a single animation timeline. +// url determines the location of the fbx file to use within this clip. +// startFrame and endFrame are in frames 1/30th of a second. +// timescale can be used to speed-up or slow-down the animation. +// loop flag, when true, will loop the animation as it reaches the end frame. + +class AnimClip : public AnimNode { +public: + friend class AnimTests; + + AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag); + virtual ~AnimClip() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setStartFrameVar(const std::string& startFrameVar) { _startFrameVar = startFrameVar; } + void setEndFrameVar(const std::string& endFrameVar) { _endFrameVar = endFrameVar; } + void setTimeScaleVar(const std::string& timeScaleVar) { _timeScaleVar = timeScaleVar; } + void setLoopFlagVar(const std::string& loopFlagVar) { _loopFlagVar = loopFlagVar; } + void setFrameVar(const std::string& frameVar) { _frameVar = frameVar; } + +protected: + void loadURL(const std::string& url); + + virtual void setCurrentFrameInternal(float frame) override; + + float accumulateTime(float frame, float dt, Triggers& triggersOut) const; + void copyFromNetworkAnim(); + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimationPointer _networkAnim; + AnimPoseVec _poses; + + // _anim[frame][joint] + std::vector _anim; + + std::string _url; + float _startFrame; + float _endFrame; + float _timeScale; + bool _loopFlag; + float _frame; + + std::string _startFrameVar; + std::string _endFrameVar; + std::string _timeScaleVar; + std::string _loopFlagVar; + std::string _frameVar; + + // no copies + AnimClip(const AnimClip&) = delete; + AnimClip& operator=(const AnimClip&) = delete; +}; + +#endif // hifi_AnimClip_h diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h new file mode 100644 index 0000000000..4675ae358f --- /dev/null +++ b/libraries/animation/src/AnimNode.h @@ -0,0 +1,115 @@ +// +// AnimNode.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimNode_h +#define hifi_AnimNode_h + +#include +#include +#include +#include +#include +#include + +#include "AnimSkeleton.h" +#include "AnimVariant.h" + +class QJsonObject; + +// Base class for all elements in the animation blend tree. +// It provides the following categories of functions: +// +// * id getter, id is used to identify a node, useful for debugging and node searching. +// * type getter, helpful for determining the derived type of this node. +// * hierarchy accessors, for adding, removing and iterating over child AnimNodes +// * skeleton accessors, the skeleton is from the model whose bones we are going to manipulate +// * evaluate method, perform actual joint manipulations here and return result by reference. +// Also, append any triggers that are detected during evaluation. + +class AnimNode { +public: + enum class Type { + Clip = 0, + BlendLinear, + Overlay, + StateMachine, + NumTypes + }; + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + using Triggers = std::vector; + + friend class AnimDebugDraw; + friend void buildChildMap(std::map& map, Pointer node); + friend class AnimStateMachine; + + AnimNode(Type type, const std::string& id) : _type(type), _id(id) {} + virtual ~AnimNode() {} + + const std::string& getID() const { return _id; } + Type getType() const { return _type; } + + // hierarchy accessors + void addChild(Pointer child) { _children.push_back(child); } + void removeChild(Pointer child) { + auto iter = std::find(_children.begin(), _children.end(), child); + if (iter != _children.end()) { + _children.erase(iter); + } + } + Pointer getChild(int i) const { + assert(i >= 0 && i < (int)_children.size()); + return _children[i]; + } + int getChildCount() const { return (int)_children.size(); } + + // pair this AnimNode graph with a skeleton. + void setSkeleton(const AnimSkeleton::Pointer skeleton) { + setSkeletonInternal(skeleton); + for (auto&& child : _children) { + child->setSkeleton(skeleton); + } + } + + AnimSkeleton::ConstPointer getSkeleton() const { return _skeleton; } + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) = 0; + virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + return evaluate(animVars, dt, triggersOut); + } + +protected: + + void setCurrentFrame(float frame) { + setCurrentFrameInternal(frame); + for (auto&& child : _children) { + child->setCurrentFrameInternal(frame); + } + } + + virtual void setCurrentFrameInternal(float frame) {} + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { + _skeleton = skeleton; + } + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const = 0; + + Type _type; + std::string _id; + std::vector _children; + AnimSkeleton::ConstPointer _skeleton; + + // no copies + AnimNode(const AnimNode&) = delete; + AnimNode& operator=(const AnimNode&) = delete; +}; + +#endif // hifi_AnimNode_h diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp new file mode 100644 index 0000000000..5f0260db6d --- /dev/null +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -0,0 +1,434 @@ +// +// AnimNodeLoader.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include "AnimNode.h" +#include "AnimClip.h" +#include "AnimBlendLinear.h" +#include "AnimationLogging.h" +#include "AnimOverlay.h" +#include "AnimNodeLoader.h" +#include "AnimStateMachine.h" + +using NodeLoaderFunc = AnimNode::Pointer (*)(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +using NodeProcessFunc = bool (*)(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); + +// factory functions +static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); + +// called after children have been loaded +static bool processClipNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } +static bool processBlendLinearNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } +static bool processOverlayNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } +bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); + +static const char* animNodeTypeToString(AnimNode::Type type) { + switch (type) { + case AnimNode::Type::Clip: return "clip"; + case AnimNode::Type::BlendLinear: return "blendLinear"; + case AnimNode::Type::Overlay: return "overlay"; + case AnimNode::Type::StateMachine: return "stateMachine"; + case AnimNode::Type::NumTypes: return nullptr; + }; + return nullptr; +} + +static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) { + switch (type) { + case AnimNode::Type::Clip: return loadClipNode; + case AnimNode::Type::BlendLinear: return loadBlendLinearNode; + case AnimNode::Type::Overlay: return loadOverlayNode; + case AnimNode::Type::StateMachine: return loadStateMachineNode; + case AnimNode::Type::NumTypes: return nullptr; + }; + return nullptr; +} + +static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) { + switch (type) { + case AnimNode::Type::Clip: return processClipNode; + case AnimNode::Type::BlendLinear: return processBlendLinearNode; + case AnimNode::Type::Overlay: return processOverlayNode; + case AnimNode::Type::StateMachine: return processStateMachineNode; + case AnimNode::Type::NumTypes: return nullptr; + }; + return nullptr; +} + +#define READ_STRING(NAME, JSON_OBJ, ID, URL) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + if (!NAME##_VAL.isString()) { \ + qCCritical(animation) << "AnimNodeLoader, error reading string" \ + << #NAME << ", id =" << ID \ + << ", url =" << URL.toDisplayString(); \ + return nullptr; \ + } \ + QString NAME = NAME##_VAL.toString() + +#define READ_OPTIONAL_STRING(NAME, JSON_OBJ) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + QString NAME; \ + if (NAME##_VAL.isString()) { \ + NAME = NAME##_VAL.toString(); \ + } + +#define READ_BOOL(NAME, JSON_OBJ, ID, URL) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + if (!NAME##_VAL.isBool()) { \ + qCCritical(animation) << "AnimNodeLoader, error reading bool" \ + << #NAME << ", id =" << ID \ + << ", url =" << URL.toDisplayString(); \ + return nullptr; \ + } \ + bool NAME = NAME##_VAL.toBool() + +#define READ_FLOAT(NAME, JSON_OBJ, ID, URL) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + if (!NAME##_VAL.isDouble()) { \ + qCCritical(animation) << "AnimNodeLoader, error reading double" \ + << #NAME << "id =" << ID \ + << ", url =" << URL.toDisplayString(); \ + return nullptr; \ + } \ + float NAME = (float)NAME##_VAL.toDouble() + +static AnimNode::Type stringToEnum(const QString& str) { + // O(n), move to map when number of types becomes large. + const int NUM_TYPES = static_cast(AnimNode::Type::NumTypes); + for (int i = 0; i < NUM_TYPES; i++ ) { + AnimNode::Type type = static_cast(i); + if (str == animNodeTypeToString(type)) { + return type; + } + } + return AnimNode::Type::NumTypes; +} + +static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUrl) { + auto idVal = jsonObj.value("id"); + if (!idVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"id\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QString id = idVal.toString(); + + auto typeVal = jsonObj.value("type"); + if (!typeVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad object \"type\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QString typeStr = typeVal.toString(); + AnimNode::Type type = stringToEnum(typeStr); + if (type == AnimNode::Type::NumTypes) { + qCCritical(animation) << "AnimNodeLoader, unknown node type" << typeStr << ", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto dataValue = jsonObj.value("data"); + if (!dataValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"data\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto dataObj = dataValue.toObject(); + + assert((int)type >= 0 && type < AnimNode::Type::NumTypes); + auto node = (animNodeTypeToLoaderFunc(type))(dataObj, id, jsonUrl); + + auto childrenValue = jsonObj.value("children"); + if (!childrenValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto childrenArray = childrenValue.toArray(); + for (const auto& childValue : childrenArray) { + if (!childValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + node->addChild(loadNode(childValue.toObject(), jsonUrl)); + } + + if ((animNodeTypeToProcessFunc(type))(node, dataObj, id, jsonUrl)) { + return node; + } else { + return nullptr; + } +} + +static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + + READ_STRING(url, jsonObj, id, jsonUrl); + READ_FLOAT(startFrame, jsonObj, id, jsonUrl); + READ_FLOAT(endFrame, jsonObj, id, jsonUrl); + READ_FLOAT(timeScale, jsonObj, id, jsonUrl); + READ_BOOL(loopFlag, jsonObj, id, jsonUrl); + + READ_OPTIONAL_STRING(startFrameVar, jsonObj); + READ_OPTIONAL_STRING(endFrameVar, jsonObj); + READ_OPTIONAL_STRING(timeScaleVar, jsonObj); + READ_OPTIONAL_STRING(loopFlagVar, jsonObj); + + auto node = std::make_shared(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag); + + if (!startFrameVar.isEmpty()) { + node->setStartFrameVar(startFrameVar.toStdString()); + } + if (!endFrameVar.isEmpty()) { + node->setEndFrameVar(endFrameVar.toStdString()); + } + if (!timeScaleVar.isEmpty()) { + node->setTimeScaleVar(timeScaleVar.toStdString()); + } + if (!loopFlagVar.isEmpty()) { + node->setLoopFlagVar(loopFlagVar.toStdString()); + } + + return node; +} + +static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + + READ_FLOAT(alpha, jsonObj, id, jsonUrl); + + READ_OPTIONAL_STRING(alphaVar, jsonObj); + + auto node = std::make_shared(id.toStdString(), alpha); + + if (!alphaVar.isEmpty()) { + node->setAlphaVar(alphaVar.toStdString()); + } + + return node; +} + +static const char* boneSetStrings[AnimOverlay::NumBoneSets] = { + "fullBody", + "upperBody", + "lowerBody", + "rightArm", + "leftArm", + "aboveTheHead", + "belowTheHead", + "headOnly", + "spineOnly", + "empty" +}; + +static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { + for (int i = 0; i < (int)AnimOverlay::NumBoneSets; i++) { + if (str == boneSetStrings[i]) { + return (AnimOverlay::BoneSet)i; + } + } + return AnimOverlay::NumBoneSets; +} + +static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + + READ_STRING(boneSet, jsonObj, id, jsonUrl); + READ_FLOAT(alpha, jsonObj, id, jsonUrl); + + auto boneSetEnum = stringToBoneSetEnum(boneSet); + if (boneSetEnum == AnimOverlay::NumBoneSets) { + qCCritical(animation) << "AnimNodeLoader, unknown bone set =" << boneSet << ", defaulting to \"fullBody\", url =" << jsonUrl.toDisplayString(); + boneSetEnum = AnimOverlay::FullBodyBoneSet; + } + + READ_OPTIONAL_STRING(boneSetVar, jsonObj); + READ_OPTIONAL_STRING(alphaVar, jsonObj); + + auto node = std::make_shared(id.toStdString(), boneSetEnum, alpha); + + if (!boneSetVar.isEmpty()) { + node->setBoneSetVar(boneSetVar.toStdString()); + } + if (!alphaVar.isEmpty()) { + node->setAlphaVar(alphaVar.toStdString()); + } + + return node; +} + +static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + auto node = std::make_shared(id.toStdString()); + return node; +} + +void buildChildMap(std::map& map, AnimNode::Pointer node) { + for ( auto child : node->_children ) { + map.insert(std::pair(child->_id, child)); + } +} + +bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl) { + auto smNode = std::static_pointer_cast(node); + assert(smNode); + + READ_STRING(currentState, jsonObj, nodeId, jsonUrl); + + auto statesValue = jsonObj.value("states"); + if (!statesValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"states\" in stateMachine node, id =" << nodeId << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + // build a map for all children by name. + std::map childMap; + buildChildMap(childMap, node); + + // first pass parse all the states and build up the state and transition map. + using StringPair = std::pair; + using TransitionMap = std::multimap; + TransitionMap transitionMap; + + using StateMap = std::map; + StateMap stateMap; + + auto statesArray = statesValue.toArray(); + for (const auto& stateValue : statesArray) { + if (!stateValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad state object in \"states\", id =" << nodeId << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto stateObj = stateValue.toObject(); + + READ_STRING(id, stateObj, nodeId, jsonUrl); + READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl); + READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl); + + READ_OPTIONAL_STRING(interpTargetVar, stateObj); + READ_OPTIONAL_STRING(interpDurationVar, stateObj); + + auto stdId = id.toStdString(); + + auto iter = childMap.find(stdId); + if (iter == childMap.end()) { + qCCritical(animation) << "AnimNodeLoader, could not find stateMachine child (state) with nodeId =" << nodeId << "stateId =" << id << "url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto statePtr = std::make_shared(stdId, iter->second, interpTarget, interpDuration); + assert(statePtr); + + if (!interpTargetVar.isEmpty()) { + statePtr->setInterpTargetVar(interpTargetVar.toStdString()); + } + if (!interpDurationVar.isEmpty()) { + statePtr->setInterpDurationVar(interpDurationVar.toStdString()); + } + + smNode->addState(statePtr); + stateMap.insert(StateMap::value_type(statePtr->getID(), statePtr)); + + auto transitionsValue = stateObj.value("transitions"); + if (!transitionsValue.isArray()) { + qCritical(animation) << "AnimNodeLoader, bad array \"transitions\" in stateMachine node, stateId =" << id << "nodeId =" << nodeId << "url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto transitionsArray = transitionsValue.toArray(); + for (const auto& transitionValue : transitionsArray) { + if (!transitionValue.isObject()) { + qCritical(animation) << "AnimNodeLoader, bad transition object in \"transtions\", stateId =" << id << "nodeId =" << nodeId << "url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto transitionObj = transitionValue.toObject(); + + READ_STRING(var, transitionObj, nodeId, jsonUrl); + READ_STRING(state, transitionObj, nodeId, jsonUrl); + + transitionMap.insert(TransitionMap::value_type(statePtr, StringPair(var.toStdString(), state.toStdString()))); + } + } + + // second pass: now iterate thru all transitions and add them to the appropriate states. + for (auto& transition : transitionMap) { + AnimStateMachine::State::Pointer srcState = transition.first; + auto iter = stateMap.find(transition.second.second); + if (iter != stateMap.end()) { + srcState->addTransition(AnimStateMachine::State::Transition(transition.second.first, iter->second)); + } else { + qCCritical(animation) << "AnimNodeLoader, bad state machine transtion from srcState =" << srcState->_id.c_str() << "dstState =" << transition.second.second.c_str() << "nodeId =" << nodeId << "url = " << jsonUrl.toDisplayString(); + return nullptr; + } + } + + auto iter = stateMap.find(currentState.toStdString()); + if (iter == stateMap.end()) { + qCCritical(animation) << "AnimNodeLoader, bad currentState =" << currentState << "could not find child node" << "id =" << nodeId << "url = " << jsonUrl.toDisplayString(); + } + smNode->setCurrentState(iter->second); + + return true; +} + +AnimNodeLoader::AnimNodeLoader(const QUrl& url) : + _url(url), + _resource(nullptr) { + + _resource = new Resource(url); + connect(_resource, SIGNAL(loaded(QNetworkReply&)), SLOT(onRequestDone(QNetworkReply&))); + connect(_resource, SIGNAL(failed(QNetworkReply::NetworkError)), SLOT(onRequestError(QNetworkReply::NetworkError))); +} + +AnimNode::Pointer AnimNodeLoader::load(const QByteArray& contents, const QUrl& jsonUrl) { + + // convert string into a json doc + QJsonParseError error; + auto doc = QJsonDocument::fromJson(contents, &error); + if (error.error != QJsonParseError::NoError) { + qCCritical(animation) << "AnimNodeLoader, failed to parse json, error =" << error.errorString() << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QJsonObject obj = doc.object(); + + // version + QJsonValue versionVal = obj.value("version"); + if (!versionVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"version\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QString version = versionVal.toString(); + + // check version + if (version != "1.0") { + qCCritical(animation) << "AnimNodeLoader, bad version number" << version << "expected \"1.0\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + // root + QJsonValue rootVal = obj.value("root"); + if (!rootVal.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad object \"root\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + return loadNode(rootVal.toObject(), jsonUrl); +} + +void AnimNodeLoader::onRequestDone(QNetworkReply& request) { + auto node = load(request.readAll(), _url); + if (node) { + emit success(node); + } else { + emit error(0, "json parse error"); + } +} + +void AnimNodeLoader::onRequestError(QNetworkReply::NetworkError netError) { + emit error((int)netError, "Resource download error"); +} diff --git a/libraries/animation/src/AnimNodeLoader.h b/libraries/animation/src/AnimNodeLoader.h new file mode 100644 index 0000000000..71b5552879 --- /dev/null +++ b/libraries/animation/src/AnimNodeLoader.h @@ -0,0 +1,52 @@ +// +// AnimNodeLoader.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimNodeLoader_h +#define hifi_AnimNodeLoader_h + +#include + +#include +#include +#include + +#include "AnimNode.h" + +class Resource; + +class AnimNodeLoader : public QObject { + Q_OBJECT + +public: + AnimNodeLoader(const QUrl& url); + +signals: + void success(AnimNode::Pointer node); + void error(int error, QString str); + +protected: + // synchronous + static AnimNode::Pointer load(const QByteArray& contents, const QUrl& jsonUrl); + +protected slots: + void onRequestDone(QNetworkReply& request); + void onRequestError(QNetworkReply::NetworkError error); + +protected: + QUrl _url; + Resource* _resource; +private: + + // no copies + AnimNodeLoader(const AnimNodeLoader&) = delete; + AnimNodeLoader& operator=(const AnimNodeLoader&) = delete; +}; + +#endif // hifi_AnimNodeLoader diff --git a/libraries/animation/src/AnimOverlay.cpp b/libraries/animation/src/AnimOverlay.cpp new file mode 100644 index 0000000000..52026f7711 --- /dev/null +++ b/libraries/animation/src/AnimOverlay.cpp @@ -0,0 +1,181 @@ +// +// AnimOverlay.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimOverlay.h" +#include "AnimUtil.h" +#include + +AnimOverlay::AnimOverlay(const std::string& id, BoneSet boneSet, float alpha) : + AnimNode(AnimNode::Type::Overlay, id), _boneSet(boneSet), _alpha(alpha) { +} + +AnimOverlay::~AnimOverlay() { + +} + +void AnimOverlay::buildBoneSet(BoneSet boneSet) { + assert(_skeleton); + switch (boneSet) { + case FullBodyBoneSet: buildFullBodyBoneSet(); break; + case UpperBodyBoneSet: buildUpperBodyBoneSet(); break; + case LowerBodyBoneSet: buildLowerBodyBoneSet(); break; + case RightArmBoneSet: buildRightArmBoneSet(); break; + case LeftArmBoneSet: buildLeftArmBoneSet(); break; + case AboveTheHeadBoneSet: buildAboveTheHeadBoneSet(); break; + case BelowTheHeadBoneSet: buildBelowTheHeadBoneSet(); break; + case HeadOnlyBoneSet: buildHeadOnlyBoneSet(); break; + case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break; + default: + case EmptyBoneSet: buildEmptyBoneSet(); break; + } +} + +const AnimPoseVec& AnimOverlay::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + // lookup parameters from animVars, using current instance variables as defaults. + // NOTE: switching bonesets can be an expensive operation, let's try to avoid it. + auto prevBoneSet = _boneSet; + _boneSet = (BoneSet)animVars.lookup(_boneSetVar, (int)_boneSet); + if (_boneSet != prevBoneSet && _skeleton) { + buildBoneSet(_boneSet); + } + _alpha = animVars.lookup(_alphaVar, _alpha); + + if (_children.size() >= 2) { + auto underPoses = _children[1]->evaluate(animVars, dt, triggersOut); + auto overPoses = _children[0]->overlay(animVars, dt, triggersOut, underPoses); + + if (underPoses.size() > 0 && underPoses.size() == overPoses.size()) { + _poses.resize(underPoses.size()); + assert(_boneSetVec.size() == _poses.size()); + + for (size_t i = 0; i < _poses.size(); i++) { + float alpha = _boneSetVec[i] * _alpha; + ::blend(1, &underPoses[i], &overPoses[i], alpha, &_poses[i]); + } + } + } + return _poses; +} + +template +void for_each_child_joint(AnimSkeleton::ConstPointer skeleton, int startJoint, Func f) { + std::queue q; + q.push(startJoint); + while(q.size() > 0) { + int jointIndex = q.front(); + for (int i = 0; i < skeleton->getNumJoints(); i++) { + if (jointIndex == skeleton->getParentIndex(i)) { + f(i); + q.push(i); + } + } + q.pop(); + } +} + +void AnimOverlay::buildFullBodyBoneSet() { + assert(_skeleton); + _boneSetVec.resize(_skeleton->getNumJoints()); + for (int i = 0; i < _skeleton->getNumJoints(); i++) { + _boneSetVec[i] = 1.0f; + } +} + +void AnimOverlay::buildUpperBodyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int spineJoint = _skeleton->nameToJointIndex("Spine"); + for_each_child_joint(_skeleton, spineJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildLowerBodyBoneSet() { + assert(_skeleton); + buildFullBodyBoneSet(); + int hipsJoint = _skeleton->nameToJointIndex("Hips"); + int spineJoint = _skeleton->nameToJointIndex("Spine"); + _boneSetVec.resize(_skeleton->getNumJoints()); + for_each_child_joint(_skeleton, spineJoint, [&](int i) { + _boneSetVec[i] = 0.0f; + }); + _boneSetVec[hipsJoint] = 0.0f; +} + +void AnimOverlay::buildRightArmBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int rightShoulderJoint = _skeleton->nameToJointIndex("RightShoulder"); + for_each_child_joint(_skeleton, rightShoulderJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildLeftArmBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int leftShoulderJoint = _skeleton->nameToJointIndex("LeftShoulder"); + for_each_child_joint(_skeleton, leftShoulderJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildAboveTheHeadBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("Head"); + for_each_child_joint(_skeleton, headJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildBelowTheHeadBoneSet() { + assert(_skeleton); + buildFullBodyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("Head"); + for_each_child_joint(_skeleton, headJoint, [&](int i) { + _boneSetVec[i] = 0.0f; + }); +} + +void AnimOverlay::buildHeadOnlyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("Head"); + _boneSetVec[headJoint] = 1.0f; +} + +void AnimOverlay::buildSpineOnlyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int spineJoint = _skeleton->nameToJointIndex("Spine"); + _boneSetVec[spineJoint] = 1.0f; +} + +void AnimOverlay::buildEmptyBoneSet() { + assert(_skeleton); + _boneSetVec.resize(_skeleton->getNumJoints()); + for (int i = 0; i < _skeleton->getNumJoints(); i++) { + _boneSetVec[i] = 0.0f; + } +} + +// for AnimDebugDraw rendering +const AnimPoseVec& AnimOverlay::getPosesInternal() const { + return _poses; +} + +void AnimOverlay::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { + _skeleton = skeleton; + + // we have to re-build the bone set when the skeleton changes. + buildBoneSet(_boneSet); +} diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h new file mode 100644 index 0000000000..de563cc403 --- /dev/null +++ b/libraries/animation/src/AnimOverlay.h @@ -0,0 +1,80 @@ +// +// AnimOverlay.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimOverlay_h +#define hifi_AnimOverlay_h + +#include "AnimNode.h" + +// Overlay the AnimPoses from one AnimNode on top of another AnimNode. +// child[0] is overlayed on top of child[1]. The boneset is used +// to control blending on a per-bone bases. +// alpha gives the ability to fade in and fade out overlays. +// alpha of 0, will have no overlay, final pose will be 100% from child[1]. +// alpha of 1, will be a full overlay. + +class AnimOverlay : public AnimNode { +public: + friend class AnimTests; + + enum BoneSet { + FullBodyBoneSet = 0, + UpperBodyBoneSet, + LowerBodyBoneSet, + RightArmBoneSet, + LeftArmBoneSet, + AboveTheHeadBoneSet, + BelowTheHeadBoneSet, + HeadOnlyBoneSet, + SpineOnlyBoneSet, + EmptyBoneSet, + NumBoneSets, + }; + + AnimOverlay(const std::string& id, BoneSet boneSet, float alpha); + virtual ~AnimOverlay() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setBoneSetVar(const std::string& boneSetVar) { _boneSetVar = boneSetVar; } + void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + + protected: + void buildBoneSet(BoneSet boneSet); + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; + + AnimPoseVec _poses; + BoneSet _boneSet; + float _alpha; + std::vector _boneSetVec; + + std::string _boneSetVar; + std::string _alphaVar; + + void buildFullBodyBoneSet(); + void buildUpperBodyBoneSet(); + void buildLowerBodyBoneSet(); + void buildRightArmBoneSet(); + void buildLeftArmBoneSet(); + void buildAboveTheHeadBoneSet(); + void buildBelowTheHeadBoneSet(); + void buildHeadOnlyBoneSet(); + void buildSpineOnlyBoneSet(); + void buildEmptyBoneSet(); + + // no copies + AnimOverlay(const AnimOverlay&) = delete; + AnimOverlay& operator=(const AnimOverlay&) = delete; +}; + +#endif // hifi_AnimOverlay_h diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp new file mode 100644 index 0000000000..3f11607f26 --- /dev/null +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -0,0 +1,176 @@ +// +// AnimSkeleton.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimSkeleton.h" +#include "AnimationLogging.h" +#include "GLMHelpers.h" +#include +#include + +const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), + glm::quat(), + glm::vec3(0.0f)); + +AnimPose::AnimPose(const glm::mat4& mat) { + scale = extractScale(mat); + rot = extractRotation(mat); + trans = extractTranslation(mat); +} + +glm::vec3 AnimPose::operator*(const glm::vec3& rhs) const { + return trans + (rot * (scale * rhs)); +} + +glm::vec3 AnimPose::xformPoint(const glm::vec3& rhs) const { + return *this * rhs; +} + +// really slow +glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { + glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f); + glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f); + glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z); + glm::mat3 mat(xAxis, yAxis, zAxis); + glm::mat3 transInvMat = glm::inverse(glm::transpose(mat)); + return transInvMat * rhs; +} + +AnimPose AnimPose::operator*(const AnimPose& rhs) const { + return AnimPose(static_cast(*this) * static_cast(rhs)); +} + +AnimPose AnimPose::inverse() const { + return AnimPose(glm::inverse(static_cast(*this))); +} + +AnimPose::operator glm::mat4() const { + glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f); + glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f); + glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z); + return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), + glm::vec4(zAxis, 0.0f), glm::vec4(trans, 1.0f)); +} + +AnimSkeleton::AnimSkeleton(const std::vector& joints, const AnimPose& geometryOffset) { + _joints = joints; + + // build a cache of bind poses + _absoluteBindPoses.reserve(joints.size()); + _relativeBindPoses.reserve(joints.size()); + + // iterate over FBXJoints and extract the bind pose information. + for (size_t i = 0; i < joints.size(); i++) { + if (_joints[i].bindTransformFoundInCluster) { + // Use the FBXJoint::bindTransform, which is absolute model coordinates + // i.e. not relative to it's parent. + AnimPose absoluteBindPose(_joints[i].bindTransform); + _absoluteBindPoses.push_back(absoluteBindPose); + int parentIndex = getParentIndex(i); + if (parentIndex >= 0) { + AnimPose inverseParentAbsoluteBindPose = _absoluteBindPoses[parentIndex].inverse(); + _relativeBindPoses.push_back(inverseParentAbsoluteBindPose * absoluteBindPose); + } else { + _relativeBindPoses.push_back(absoluteBindPose); + } + } else { + // use FBXJoint's local transform, instead + glm::mat4 rotTransform = glm::mat4_cast(_joints[i].preRotation * _joints[i].rotation * _joints[i].postRotation); + glm::mat4 relBindMat = glm::translate(_joints[i].translation) * _joints[i].preTransform * rotTransform * _joints[i].postTransform; + AnimPose relBindPose(relBindMat); + _relativeBindPoses.push_back(relBindPose); + + int parentIndex = getParentIndex(i); + if (parentIndex >= 0) { + _absoluteBindPoses.push_back(_absoluteBindPoses[parentIndex] * relBindPose); + } else { + _absoluteBindPoses.push_back(relBindPose); + } + } + } + + // now we want to normalize scale from geometryOffset to all poses. + // This will ensure our bone translations will be in meters, even if the model was authored with some other unit of mesure. + for (auto& absPose : _absoluteBindPoses) { + absPose.trans = (geometryOffset * absPose).trans; + absPose.scale = vec3(1, 1, 1); + } + + // re-compute relative poses based on the modified absolute poses. + for (size_t i = 0; i < _relativeBindPoses.size(); i++) { + int parentIndex = getParentIndex(i); + if (parentIndex >= 0) { + _relativeBindPoses[i] = _absoluteBindPoses[parentIndex].inverse() * _absoluteBindPoses[i]; + } else { + _relativeBindPoses[i] = _absoluteBindPoses[i]; + } + } +} + +int AnimSkeleton::nameToJointIndex(const QString& jointName) const { + for (size_t i = 0; i < _joints.size(); i++) { + if (_joints[i].name == jointName) { + return i; + } + } + return -1; +} + +int AnimSkeleton::getNumJoints() const { + return _joints.size(); +} + +AnimPose AnimSkeleton::getAbsoluteBindPose(int jointIndex) const { + return _absoluteBindPoses[jointIndex]; +} + +AnimPose AnimSkeleton::getRelativeBindPose(int jointIndex) const { + return _relativeBindPoses[jointIndex]; +} + +int AnimSkeleton::getParentIndex(int jointIndex) const { + return _joints[jointIndex].parentIndex; +} + +const QString& AnimSkeleton::getJointName(int jointIndex) const { + return _joints[jointIndex].name; +} + +#ifndef NDEBUG +void AnimSkeleton::dump() const { + qCDebug(animation) << "["; + for (int i = 0; i < getNumJoints(); i++) { + qCDebug(animation) << " {"; + qCDebug(animation) << " name =" << getJointName(i); + qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i); + qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i); + if (getParentIndex(i) >= 0) { + qCDebug(animation) << " parent =" << getJointName(getParentIndex(i)); + } + qCDebug(animation) << " },"; + } + qCDebug(animation) << "]"; +} + +void AnimSkeleton::dump(const AnimPoseVec& poses) const { + qCDebug(animation) << "["; + for (int i = 0; i < getNumJoints(); i++) { + qCDebug(animation) << " {"; + qCDebug(animation) << " name =" << getJointName(i); + qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i); + qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i); + qCDebug(animation) << " pose =" << poses[i]; + if (getParentIndex(i) >= 0) { + qCDebug(animation) << " parent =" << getJointName(getParentIndex(i)); + } + qCDebug(animation) << " },"; + } + qCDebug(animation) << "]"; +} +#endif diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h new file mode 100644 index 0000000000..c0c5036cc7 --- /dev/null +++ b/libraries/animation/src/AnimSkeleton.h @@ -0,0 +1,78 @@ +// +// AnimSkeleton.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimSkeleton +#define hifi_AnimSkeleton + +#include + +#include "FBXReader.h" + +struct AnimPose { + AnimPose() {} + explicit AnimPose(const glm::mat4& mat); + AnimPose(const glm::vec3& scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : scale(scaleIn), rot(rotIn), trans(transIn) {} + static const AnimPose identity; + + glm::vec3 xformPoint(const glm::vec3& rhs) const; + glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow + + glm::vec3 operator*(const glm::vec3& rhs) const; // same as xformPoint + AnimPose operator*(const AnimPose& rhs) const; + + AnimPose inverse() const; + operator glm::mat4() const; + + glm::vec3 scale; + glm::quat rot; + glm::vec3 trans; +}; + +inline QDebug operator<<(QDebug debug, const AnimPose& pose) { + debug << "AnimPose, trans = (" << pose.trans.x << pose.trans.y << pose.trans.z << "), rot = (" << pose.rot.x << pose.rot.y << pose.rot.z << pose.rot.w << "), scale = (" << pose.scale.x << pose.scale.y << pose.scale.z << ")"; + return debug; +} + +using AnimPoseVec = std::vector; + +class AnimSkeleton { +public: + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + + AnimSkeleton(const std::vector& joints, const AnimPose& geometryOffset); + int nameToJointIndex(const QString& jointName) const; + const QString& getJointName(int jointIndex) const; + int getNumJoints() const; + + // absolute pose, not relative to parent + AnimPose getAbsoluteBindPose(int jointIndex) const; + + // relative to parent pose + AnimPose getRelativeBindPose(int jointIndex) const; + + int getParentIndex(int jointIndex) const; + +#ifndef NDEBUG + void dump() const; + void dump(const AnimPoseVec& poses) const; +#endif + +protected: + std::vector _joints; + AnimPoseVec _absoluteBindPoses; + AnimPoseVec _relativeBindPoses; + + // no copies + AnimSkeleton(const AnimSkeleton&) = delete; + AnimSkeleton& operator=(const AnimSkeleton&) = delete; +}; + +#endif diff --git a/libraries/animation/src/AnimStateMachine.cpp b/libraries/animation/src/AnimStateMachine.cpp new file mode 100644 index 0000000000..5de379dd33 --- /dev/null +++ b/libraries/animation/src/AnimStateMachine.cpp @@ -0,0 +1,114 @@ +// +// AnimStateMachine.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimStateMachine.h" +#include "AnimUtil.h" +#include "AnimationLogging.h" + +AnimStateMachine::AnimStateMachine(const std::string& id) : + AnimNode(AnimNode::Type::StateMachine, id) { + +} + +AnimStateMachine::~AnimStateMachine() { + +} + +const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + std::string desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID()); + if (_currentState->getID() != desiredStateID) { + // switch states + bool foundState = false; + for (auto& state : _states) { + if (state->getID() == desiredStateID) { + switchState(animVars, state); + foundState = true; + break; + } + } + if (!foundState) { + qCCritical(animation) << "AnimStateMachine could not find state =" << desiredStateID.c_str() << ", referenced by _currentStateVar =" << _currentStateVar.c_str(); + } + } + + // evaluate currentState transitions + auto desiredState = evaluateTransitions(animVars); + if (desiredState != _currentState) { + switchState(animVars, desiredState); + } + + assert(_currentState); + auto currentStateNode = _currentState->getNode(); + assert(currentStateNode); + + if (_duringInterp) { + _alpha += _alphaVel * dt; + if (_alpha < 1.0f) { + if (_poses.size() > 0) { + ::blend(_poses.size(), &_prevPoses[0], &_nextPoses[0], _alpha, &_poses[0]); + } + } else { + _duringInterp = false; + _prevPoses.clear(); + _nextPoses.clear(); + } + } + if (!_duringInterp) { + _poses = currentStateNode->evaluate(animVars, dt, triggersOut); + } + return _poses; +} + +void AnimStateMachine::setCurrentState(State::Pointer state) { + _currentState = state; +} + +void AnimStateMachine::addState(State::Pointer state) { + _states.push_back(state); +} + +void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointer desiredState) { + + qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID().c_str() << "->" << desiredState->getID().c_str(); + + const float FRAMES_PER_SECOND = 30.0f; + + auto prevStateNode = _currentState->getNode(); + auto nextStateNode = desiredState->getNode(); + + _duringInterp = true; + _alpha = 0.0f; + float duration = std::max(0.001f, animVars.lookup(desiredState->_interpDurationVar, desiredState->_interpDuration)); + _alphaVel = FRAMES_PER_SECOND / duration; + _prevPoses = _poses; + nextStateNode->setCurrentFrame(desiredState->_interpTarget); + + // because dt is 0, we should not encounter any triggers + const float dt = 0.0f; + Triggers triggers; + _nextPoses = nextStateNode->evaluate(animVars, dt, triggers); + + _currentState = desiredState; +} + +AnimStateMachine::State::Pointer AnimStateMachine::evaluateTransitions(const AnimVariantMap& animVars) const { + assert(_currentState); + for (auto& transition : _currentState->_transitions) { + if (animVars.lookup(transition._var, false)) { + return transition._state; + } + } + return _currentState; +} + +const AnimPoseVec& AnimStateMachine::getPosesInternal() const { + return _poses; +} diff --git a/libraries/animation/src/AnimStateMachine.h b/libraries/animation/src/AnimStateMachine.h new file mode 100644 index 0000000000..f2d941a568 --- /dev/null +++ b/libraries/animation/src/AnimStateMachine.h @@ -0,0 +1,134 @@ +// +// AnimStateMachine.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimStateMachine_h +#define hifi_AnimStateMachine_h + +#include +#include +#include "AnimNode.h" + +// State Machine for transitioning between children AnimNodes +// +// This is mechinisim for playing animations and smoothly interpolating/fading +// between them. A StateMachine has a set of States, which typically reference +// child AnimNodes. Each State has a list of Transitions, which are evaluated +// to determine when we should switch to a new State. Parameters for the smooth +// interpolation/fading are read from the State that you are transitioning to. +// +// The currentState can be set directly via the setCurrentStateVar() and will override +// any State transitions. +// +// Each State has two parameters that can be changed via AnimVars, +// * interpTarget - (frames) The destination frame of the interpolation. i.e. the first frame of the animation that will +// visible after interpolation is complete. +// * interpDuration - (frames) The total length of time it will take to interp between the current pose and the +// interpTarget frame. + +class AnimStateMachine : public AnimNode { +public: + friend class AnimNodeLoader; + friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl); + +protected: + class State { + public: + friend AnimStateMachine; + friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl); + + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + + class Transition { + public: + friend AnimStateMachine; + Transition(const std::string& var, State::Pointer state) : _var(var), _state(state) {} + protected: + std::string _var; + State::Pointer _state; + }; + + State(const std::string& id, AnimNode::Pointer node, float interpTarget, float interpDuration) : + _id(id), + _node(node), + _interpTarget(interpTarget), + _interpDuration(interpDuration) {} + + void setInterpTargetVar(const std::string& interpTargetVar) { _interpTargetVar = interpTargetVar; } + void setInterpDurationVar(const std::string& interpDurationVar) { _interpDurationVar = interpDurationVar; } + + AnimNode::Pointer getNode() const { return _node; } + const std::string& getID() const { return _id; } + + protected: + + void setInterpTarget(float interpTarget) { _interpTarget = interpTarget; } + void setInterpDuration(float interpDuration) { _interpDuration = interpDuration; } + + void addTransition(const Transition& transition) { _transitions.push_back(transition); } + + std::string _id; + AnimNode::Pointer _node; + float _interpTarget; // frames + float _interpDuration; // frames + + std::string _interpTargetVar; + std::string _interpDurationVar; + + std::vector _transitions; + + private: + // no copies + State(const State&) = delete; + State& operator=(const State&) = delete; + }; + +public: + + AnimStateMachine(const std::string& id); + virtual ~AnimStateMachine() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setCurrentStateVar(std::string& currentStateVar) { _currentStateVar = currentStateVar; } + +protected: + + void setCurrentState(State::Pointer state); + + void addState(State::Pointer state); + + void switchState(const AnimVariantMap& animVars, State::Pointer desiredState); + State::Pointer evaluateTransitions(const AnimVariantMap& animVars) const; + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimPoseVec _poses; + + // interpolation state + bool _duringInterp = false; + float _alphaVel = 0.0f; + float _alpha = 0.0f; + AnimPoseVec _prevPoses; + AnimPoseVec _nextPoses; + + State::Pointer _currentState; + std::vector _states; + + std::string _currentStateVar; + +private: + // no copies + AnimStateMachine(const AnimStateMachine&) = delete; + AnimStateMachine& operator=(const AnimStateMachine&) = delete; +}; + +#endif // hifi_AnimStateMachine_h diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp new file mode 100644 index 0000000000..81b294e66c --- /dev/null +++ b/libraries/animation/src/AnimUtil.cpp @@ -0,0 +1,22 @@ +// +// AnimUtil.cpp +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimUtil.h" +#include "GLMHelpers.h" + +void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result) { + for (size_t i = 0; i < numPoses; i++) { + const AnimPose& aPose = a[i]; + const AnimPose& bPose = b[i]; + result[i].scale = lerp(aPose.scale, bPose.scale, alpha); + result[i].rot = glm::normalize(glm::lerp(aPose.rot, bPose.rot, alpha)); + result[i].trans = lerp(aPose.trans, bPose.trans, alpha); + } +} diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h new file mode 100644 index 0000000000..23c02b6183 --- /dev/null +++ b/libraries/animation/src/AnimUtil.h @@ -0,0 +1,24 @@ +// +// AnimUtil.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimUtil_h +#define hifi_AnimUtil_h + +#include "AnimNode.h" + +// TODO: use restrict keyword +// TODO: excellent candidate for simd vectorization. + +// this is where the magic happens +void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result); + +#endif + + diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h new file mode 100644 index 0000000000..1d720ba565 --- /dev/null +++ b/libraries/animation/src/AnimVariant.h @@ -0,0 +1,161 @@ +// +// AnimVariant.h +// +// Created by Anthony J. Thibault on 9/2/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimVariant_h +#define hifi_AnimVariant_h + +#include +#include +#include +#include +#include + +class AnimVariant { +public: + enum class Type { + Bool = 0, + Int, + Float, + Vec3, + Quat, + Mat4, + String, + NumTypes + }; + + AnimVariant() : _type(Type::Bool) { memset(&_val, 0, sizeof(_val)); } + AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; } + AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; } + AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; } + AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast(&_val) = value; } + AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast(&_val) = value; } + AnimVariant(const glm::mat4& value) : _type(Type::Mat4) { *reinterpret_cast(&_val) = value; } + AnimVariant(const std::string& value) : _type(Type::String) { _stringVal = value; } + + bool isBool() const { return _type == Type::Bool; } + bool isInt() const { return _type == Type::Int; } + bool isFloat() const { return _type == Type::Float; } + bool isVec3() const { return _type == Type::Vec3; } + bool isQuat() const { return _type == Type::Quat; } + bool isMat4() const { return _type == Type::Mat4; } + bool isString() const { return _type == Type::String; } + + void setBool(bool value) { assert(_type == Type::Bool); _val.boolVal = value; } + void setInt(int value) { assert(_type == Type::Int); _val.intVal = value; } + void setFloat(float value) { assert(_type == Type::Float); _val.floats[0] = value; } + void setVec3(const glm::vec3& value) { assert(_type == Type::Vec3); *reinterpret_cast(&_val) = value; } + void setQuat(const glm::quat& value) { assert(_type == Type::Quat); *reinterpret_cast(&_val) = value; } + void setMat4(const glm::mat4& value) { assert(_type == Type::Mat4); *reinterpret_cast(&_val) = value; } + void setString(const std::string& value) { assert(_type == Type::String); _stringVal = value; } + + bool getBool() const { assert(_type == Type::Bool); return _val.boolVal; } + int getInt() const { assert(_type == Type::Int); return _val.intVal; } + float getFloat() const { assert(_type == Type::Float); return _val.floats[0]; } + const glm::vec3& getVec3() const { assert(_type == Type::Vec3); return *reinterpret_cast(&_val); } + const glm::quat& getQuat() const { assert(_type == Type::Quat); return *reinterpret_cast(&_val); } + const glm::mat4& getMat4() const { assert(_type == Type::Mat4); return *reinterpret_cast(&_val); } + const std::string& getString() const { assert(_type == Type::String); return _stringVal; } + +protected: + Type _type; + std::string _stringVal; + union { + bool boolVal; + int intVal; + float floats[16]; + } _val; +}; + +class AnimVariantMap { +public: + + bool lookup(const std::string& key, bool defaultValue) const { + // check triggers first, then map + if (key.empty()) { + return defaultValue; + } else if (_triggers.find(key) != _triggers.end()) { + return true; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getBool() : defaultValue; + } + } + + int lookup(const std::string& key, int defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getInt() : defaultValue; + } + } + + float lookup(const std::string& key, float defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getFloat() : defaultValue; + } + } + + const glm::vec3& lookup(const std::string& key, const glm::vec3& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getVec3() : defaultValue; + } + } + + const glm::quat& lookup(const std::string& key, const glm::quat& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getQuat() : defaultValue; + } + } + + const glm::mat4& lookup(const std::string& key, const glm::mat4& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getMat4() : defaultValue; + } + } + + const std::string& lookup(const std::string& key, const std::string& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getString() : defaultValue; + } + } + + void set(const std::string& key, bool value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, int value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, float value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const glm::vec3& value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const glm::quat& value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const glm::mat4& value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const std::string& value) { _map[key] = AnimVariant(value); } + + void setTrigger(const std::string& key) { _triggers.insert(key); } + void clearTriggers() { _triggers.clear(); } + +protected: + std::map _map; + std::set _triggers; +}; + +#endif // hifi_AnimVariant_h diff --git a/libraries/animation/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp index 1f4c886a06..ad09e0736d 100644 --- a/libraries/animation/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -10,7 +10,7 @@ // #include "AnimationHandle.h" - +#include "AnimationLogging.h" void AnimationHandle::setURL(const QUrl& url) { if (_url != url) { @@ -51,8 +51,8 @@ void AnimationHandle::setMaskedJoints(const QStringList& maskedJoints) { } void AnimationHandle::setRunning(bool running, bool doRestoreJoints) { - if (running && isRunning()) { - // if we're already running, this is the same as a restart + if (running && isRunning() && (getFadePerSecond() >= 0.0f)) { + // if we're already running, this is the same as a restart -- unless we're fading out. setFrameIndex(getFirstFrame()); return; } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index d536bcb608..f2ea922ab7 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -9,30 +9,34 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "Rig.h" + #include #include #include "AnimationHandle.h" #include "AnimationLogging.h" +#include "AnimSkeleton.h" + #include "Rig.h" void Rig::HeadParameters::dump() const { qCDebug(animation, "HeadParameters ="); - qCDebug(animation, " leanSideways = %0.5f", leanSideways); - qCDebug(animation, " leanForward = %0.5f", leanForward); - qCDebug(animation, " torsoTwist = %0.5f", torsoTwist); + qCDebug(animation, " leanSideways = %0.5f", (double)leanSideways); + qCDebug(animation, " leanForward = %0.5f", (double)leanForward); + qCDebug(animation, " torsoTwist = %0.5f", (double)torsoTwist); glm::vec3 axis = glm::axis(localHeadOrientation); float theta = glm::angle(localHeadOrientation); - qCDebug(animation, " localHeadOrientation axis = (%.5f, %.5f, %.5f), theta = %0.5f", axis.x, axis.y, axis.z, theta); + qCDebug(animation, " localHeadOrientation axis = (%.5f, %.5f, %.5f), theta = %0.5f", (double)axis.x, (double)axis.y, (double)axis.z, (double)theta); axis = glm::axis(worldHeadOrientation); theta = glm::angle(worldHeadOrientation); - qCDebug(animation, " worldHeadOrientation axis = (%.5f, %.5f, %.5f), theta = %0.5f", axis.x, axis.y, axis.z, theta); + qCDebug(animation, " worldHeadOrientation axis = (%.5f, %.5f, %.5f), theta = %0.5f", (double)axis.x, (double)axis.y, (double)axis.z, (double)theta); axis = glm::axis(modelRotation); theta = glm::angle(modelRotation); - qCDebug(animation, " modelRotation axis = (%.5f, %.5f, %.5f), theta = %0.5f", axis.x, axis.y, axis.z, theta); - qCDebug(animation, " modelTranslation = (%.5f, %.5f, %.5f)", modelTranslation.x, modelTranslation.y, modelTranslation.z); - qCDebug(animation, " eyeLookAt = (%.5f, %.5f, %.5f)", eyeLookAt.x, eyeLookAt.y, eyeLookAt.z); - qCDebug(animation, " eyeSaccade = (%.5f, %.5f, %.5f)", eyeSaccade.x, eyeSaccade.y, eyeSaccade.z); + qCDebug(animation, " modelRotation axis = (%.5f, %.5f, %.5f), theta = %0.5f", (double)axis.x, (double)axis.y, (double)axis.z, (double)theta); + qCDebug(animation, " modelTranslation = (%.5f, %.5f, %.5f)", (double)modelTranslation.x, (double)modelTranslation.y, (double)modelTranslation.z); + qCDebug(animation, " eyeLookAt = (%.5f, %.5f, %.5f)", (double)eyeLookAt.x, (double)eyeLookAt.y, (double)eyeLookAt.z); + qCDebug(animation, " eyeSaccade = (%.5f, %.5f, %.5f)", (double)eyeSaccade.x, (double)eyeSaccade.y, (double)eyeSaccade.z); qCDebug(animation, " leanJointIndex = %.d", leanJointIndex); qCDebug(animation, " neckJointIndex = %.d", neckJointIndex); qCDebug(animation, " leftEyeJointIndex = %.d", leftEyeJointIndex); @@ -103,7 +107,7 @@ AnimationHandlePointer Rig::addAnimationByRole(const QString& role, const QStrin const QString& base = "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/"; if (role == "walk") { standard = base + "walk_fwd.fbx"; - } else if (role == "backup") { + } else if (role == "backup") { standard = base + "walk_bwd.fbx"; } else if (role == "leftTurn") { standard = base + "turn_left.fbx"; @@ -185,6 +189,12 @@ void Rig::deleteAnimations() { _animationHandles.clear(); } +void Rig::destroyAnimGraph() { + _animSkeleton = nullptr; + _animLoader = nullptr; + _animNode = nullptr; +} + void Rig::initJointStates(QVector states, glm::mat4 rootTransform, int rootJointIndex, int leftHandJointIndex, @@ -406,86 +416,223 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { } void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { - if (!_enableRig) { - return; - } - bool isMoving = false; + glm::vec3 front = worldRotation * IDENTITY_FRONT; - float forwardSpeed = glm::dot(worldVelocity, front); - float rightLateralSpeed = glm::dot(worldVelocity, worldRotation * IDENTITY_RIGHT); - float rightTurningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; - auto updateRole = [&](const QString& role, bool isOn) { - isMoving = isMoving || isOn; - if (isOn) { - if (!isRunningRole(role)) { - qCDebug(animation) << "Rig STARTING" << role; - startAnimationByRole(role); - } + + // It can be more accurate/smooth to use velocity rather than position, + // but some modes (e.g., hmd standing) update position without updating velocity. + // It's very hard to debug hmd standing. (Look down at yourself, or have a second person observe. HMD third person is a bit undefined...) + // So, let's create our own workingVelocity from the worldPosition... + glm::vec3 positionDelta = worldPosition - _lastPosition; + glm::vec3 workingVelocity = positionDelta / deltaTime; + +#if !WANT_DEBUG + // But for smoothest (non-hmd standing) results, go ahead and use velocity: + if (!positionDelta.x && !positionDelta.y && !positionDelta.z) { + workingVelocity = worldVelocity; + } +#endif + + if (_enableAnimGraph) { + + glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity; + float forwardSpeed = glm::dot(localVel, IDENTITY_FRONT); + float lateralSpeed = glm::dot(localVel, IDENTITY_RIGHT); + float turningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; + + // sine wave LFO var for testing. + static float t = 0.0f; + _animVars.set("sine", static_cast(0.5 * sin(t) + 0.5)); + + // default anim vars to notMoving and notTurning + _animVars.set("isMovingForward", false); + _animVars.set("isMovingBackward", false); + _animVars.set("isMovingLeft", false); + _animVars.set("isMovingRight", false); + _animVars.set("isNotMoving", true); + _animVars.set("isTurningLeft", false); + _animVars.set("isTurningRight", false); + _animVars.set("isNotTurning", true); + + const float ANIM_WALK_SPEED = 1.4f; // m/s + _animVars.set("walkTimeScale", glm::clamp(0.5f, 2.0f, glm::length(localVel) / ANIM_WALK_SPEED)); + + const float MOVE_ENTER_SPEED_THRESHOLD = 0.2f; // m/sec + const float MOVE_EXIT_SPEED_THRESHOLD = 0.07f; // m/sec + const float TURN_ENTER_SPEED_THRESHOLD = 0.5f; // rad/sec + const float TURN_EXIT_SPEED_THRESHOLD = 0.2f; // rad/sec + + float moveThresh; + if (_state != RigRole::Move) { + moveThresh = MOVE_ENTER_SPEED_THRESHOLD; } else { - if (isRunningRole(role)) { - qCDebug(animation) << "Rig stopping" << role; - stopAnimationByRole(role); + moveThresh = MOVE_EXIT_SPEED_THRESHOLD; + } + + float turnThresh; + if (_state != RigRole::Turn) { + turnThresh = TURN_ENTER_SPEED_THRESHOLD; + } else { + turnThresh = TURN_EXIT_SPEED_THRESHOLD; + } + + if (glm::length(localVel) > moveThresh) { + if (fabs(forwardSpeed) > 0.5f * fabs(lateralSpeed)) { + if (forwardSpeed > 0.0f) { + // forward + _animVars.set("isMovingForward", true); + _animVars.set("isNotMoving", false); + + } else { + // backward + _animVars.set("isMovingBackward", true); + _animVars.set("isNotMoving", false); + } + } else { + if (lateralSpeed > 0.0f) { + // right + _animVars.set("isMovingRight", true); + _animVars.set("isNotMoving", false); + } else { + // left + _animVars.set("isMovingLeft", true); + _animVars.set("isNotMoving", false); + } + } + _state = RigRole::Move; + } else { + if (fabs(turningSpeed) > turnThresh) { + if (turningSpeed > 0.0f) { + // turning right + _animVars.set("isTurningRight", true); + _animVars.set("isNotTurning", false); + } else { + // turning left + _animVars.set("isTurningLeft", true); + _animVars.set("isNotTurning", false); + } + _state = RigRole::Turn; + } else { + // idle + _state = RigRole::Idle; } } - }; - updateRole("walk", forwardSpeed > 0.01f); - updateRole("backup", forwardSpeed < -0.01f); - bool isTurning = std::abs(rightTurningSpeed) > 0.5f; - updateRole("rightTurn", isTurning && (rightTurningSpeed > 0)); - updateRole("leftTurn", isTurning && (rightTurningSpeed < 0)); - bool isStrafing = !isTurning && (std::abs(rightLateralSpeed) > 0.01f); - updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f)); - updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f)); - updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus. + + t += deltaTime; + } + + if (_enableRig) { + bool isMoving = false; + + glm::vec3 right = worldRotation * IDENTITY_RIGHT; + const float PERCEPTIBLE_DELTA = 0.001f; + const float PERCEPTIBLE_SPEED = 0.1f; + + // Note: Separately, we've arranged for starting/stopping animations by role (as we've done here) to pick up where they've left off when fading, + // so that you wouldn't notice the start/stop if it happens fast enough (e.g., one frame). But the print below would still be noisy. + + float forwardSpeed = glm::dot(workingVelocity, front); + float rightLateralSpeed = glm::dot(workingVelocity, right); + float rightTurningDelta = glm::orientedAngle(front, _lastFront, IDENTITY_UP); + float rightTurningSpeed = rightTurningDelta / deltaTime; + bool isTurning = (std::abs(rightTurningDelta) > PERCEPTIBLE_DELTA) && (std::abs(rightTurningSpeed) > PERCEPTIBLE_SPEED); + bool isStrafing = std::abs(rightLateralSpeed) > PERCEPTIBLE_SPEED; + auto updateRole = [&](const QString& role, bool isOn) { + isMoving = isMoving || isOn; + if (isOn) { + if (!isRunningRole(role)) { + qCDebug(animation) << "Rig STARTING" << role; + startAnimationByRole(role); + + } + } else { + if (isRunningRole(role)) { + qCDebug(animation) << "Rig stopping" << role; + stopAnimationByRole(role); + } + } + }; + updateRole("walk", forwardSpeed > PERCEPTIBLE_SPEED); + updateRole("backup", forwardSpeed < -PERCEPTIBLE_SPEED); + updateRole("rightTurn", isTurning && (rightTurningSpeed > 0.0f)); + updateRole("leftTurn", isTurning && (rightTurningSpeed < 0.0f)); + isStrafing = isStrafing && !isMoving; + updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f)); + updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f)); + updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus. + } + _lastFront = front; _lastPosition = worldPosition; } void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { - - // First normalize the fades so that they sum to 1.0. - // update the fade data in each animation (not normalized as they are an independent propert of animation) - foreach (const AnimationHandlePointer& handle, _runningAnimations) { - float fadePerSecond = handle->getFadePerSecond(); - float fade = handle->getFade(); - if (fadePerSecond != 0.0f) { - fade += fadePerSecond * deltaTime; - if ((0.0f >= fade) || (fade >= 1.0f)) { - fade = glm::clamp(fade, 0.0f, 1.0f); - handle->setFadePerSecond(0.0f); + + if (_enableAnimGraph) { + if (!_animNode) { + return; + } + + // evaluate the animation + AnimNode::Triggers triggersOut; + AnimPoseVec poses = _animNode->evaluate(_animVars, deltaTime, triggersOut); + _animVars.clearTriggers(); + for (auto& trigger : triggersOut) { + _animVars.setTrigger(trigger); + } + + // copy poses into jointStates + const float PRIORITY = 1.0f; + for (size_t i = 0; i < poses.size(); i++) { + setJointRotationInConstrainedFrame((int)i, glm::inverse(_animSkeleton->getRelativeBindPose(i).rot) * poses[i].rot, PRIORITY, false); + } + + } else { + + // First normalize the fades so that they sum to 1.0. + // update the fade data in each animation (not normalized as they are an independent propert of animation) + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + float fadePerSecond = handle->getFadePerSecond(); + float fade = handle->getFade(); + if (fadePerSecond != 0.0f) { + fade += fadePerSecond * deltaTime; + if ((0.0f >= fade) || (fade >= 1.0f)) { + fade = glm::clamp(fade, 0.0f, 1.0f); + handle->setFadePerSecond(0.0f); + } + handle->setFade(fade); + if (fade <= 0.0f) { // stop any finished animations now + handle->setRunning(false, false); // but do not restore joints as it causes a flicker + } } - handle->setFade(fade); - if (fade <= 0.0f) { // stop any finished animations now - handle->setRunning(false, false); // but do not restore joints as it causes a flicker - } - } + } + // sum the remaining fade data + float fadeTotal = 0.0f; + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + fadeTotal += handle->getFade(); + } + float fadeSumSoFar = 0.0f; + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + handle->setPriority(1.0f); + // if no fadeTotal, everyone's (typically just one running) is starting at zero. In that case, blend equally. + float normalizedFade = (fadeTotal != 0.0f) ? (handle->getFade() / fadeTotal) : (1.0f / _runningAnimations.count()); + assert(normalizedFade != 0.0f); + // simulate() will blend each animation result into the result so far, based on the pairwise mix at at each step. + // i.e., slerp the 'mix' distance from the result so far towards this iteration's animation result. + // The formula here for mix is based on the idea that, at each step: + // fadeSum is to normalizedFade, as (1 - mix) is to mix + // i.e., fadeSumSoFar/normalizedFade = (1 - mix)/mix + // Then we solve for mix. + // Sanity check: For the first animation, fadeSum = 0, and the mix will always be 1. + // Sanity check: For equal blending, the formula is equivalent to mix = 1 / nAnimationsSoFar++ + float mix = 1.0f / ((fadeSumSoFar / normalizedFade) + 1.0f); + assert((0.0f <= mix) && (mix <= 1.0f)); + fadeSumSoFar += normalizedFade; + handle->setMix(mix); + handle->simulate(deltaTime); + } } - // sum the remaining fade data - float fadeTotal = 0.0f; - foreach (const AnimationHandlePointer& handle, _runningAnimations) { - fadeTotal += handle->getFade(); - } - float fadeSumSoFar = 0.0f; - foreach (const AnimationHandlePointer& handle, _runningAnimations) { - handle->setPriority(1.0f); - // if no fadeTotal, everyone's (typically just one running) is starting at zero. In that case, blend equally. - float normalizedFade = (fadeTotal != 0.0f) ? (handle->getFade() / fadeTotal) : (1.0f / _runningAnimations.count()); - assert(normalizedFade != 0.0f); - // simulate() will blend each animation result into the result so far, based on the pairwise mix at at each step. - // i.e., slerp the 'mix' distance from the result so far towards this iteration's animation result. - // The formula here for mix is based on the idea that, at each step: - // fadeSum is to normalizedFade, as (1 - mix) is to mix - // i.e., fadeSumSoFar/normalizedFade = (1 - mix)/mix - // Then we solve for mix. - // Sanity check: For the first animation, fadeSum = 0, and the mix will always be 1. - // Sanity check: For equal blending, the formula is equivalent to mix = 1 / nAnimationsSoFar++ - float mix = 1.0f / ((fadeSumSoFar / normalizedFade) + 1.0f); - assert((0.0f <= mix) && (mix <= 1.0f)); - fadeSumSoFar += normalizedFade; - handle->setMix(mix); - handle->simulate(deltaTime); - } - + for (int i = 0; i < _jointStates.size(); i++) { updateJointState(i, rootTransform); } @@ -838,6 +985,7 @@ void Rig::updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& updateEyeJoint(leftEyeIndex, modelTranslation, modelRotation, worldHeadOrientation, lookAtSpot, saccade); updateEyeJoint(rightEyeIndex, modelTranslation, modelRotation, worldHeadOrientation, lookAtSpot, saccade); } + void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) { if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { auto& state = _jointStates[index]; @@ -856,3 +1004,30 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm state.getDefaultRotation(), DEFAULT_PRIORITY); } } + +void Rig::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) { + if (!_enableAnimGraph) { + return; + } + + // convert to std::vector of joints + std::vector joints; + joints.reserve(fbxGeometry.joints.size()); + for (auto& joint : fbxGeometry.joints) { + joints.push_back(joint); + } + + // create skeleton + AnimPose geometryOffset(fbxGeometry.offset); + _animSkeleton = std::make_shared(joints, geometryOffset); + + // load the anim graph + _animLoader.reset(new AnimNodeLoader(url)); + connect(_animLoader.get(), &AnimNodeLoader::success, [this](AnimNode::Pointer nodeIn) { + _animNode = nodeIn; + _animNode->setSkeleton(_animSkeleton); + }); + connect(_animLoader.get(), &AnimNodeLoader::error, [this, url](int error, QString str) { + qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + }); +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 45fee8aa32..0bf0645b4d 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -40,6 +40,9 @@ #include "JointState.h" // We might want to change this (later) to something that doesn't depend on gpu, fbx and model. -HRS +#include "AnimNode.h" +#include "AnimNodeLoader.h" + class AnimationHandle; typedef std::shared_ptr AnimationHandlePointer; @@ -80,6 +83,7 @@ public: bool isRunningRole(const QString& role); // There can be multiple animations per role, so this is more general than isRunningAnimation. const QList& getRunningAnimations() const { return _runningAnimations; } void deleteAnimations(); + void destroyAnimGraph(); const QList& getAnimationHandles() const { return _animationHandles; } void startAnimation(const QString& url, float fps = 30.0f, float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); @@ -155,6 +159,8 @@ public: virtual void updateJointState(int index, glm::mat4 rootTransform) = 0; void setEnableRig(bool isEnabled) { _enableRig = isEnabled; } + void setEnableAnimGraph(bool isEnabled) { _enableAnimGraph = isEnabled; } + bool getEnableAnimGraph() const { return _enableAnimGraph; } void updateFromHeadParameters(const HeadParameters& params); void updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& modelTranslation, const glm::quat& modelRotation, @@ -163,6 +169,11 @@ public: virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, float scale, float priority) = 0; + void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry); + + AnimNode::ConstPointer getAnimNode() const { return _animNode; } + AnimSkeleton::ConstPointer getAnimSkeleton() const { return _animSkeleton; } + protected: void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist); @@ -183,9 +194,21 @@ public: QList _animationHandles; QList _runningAnimations; - bool _enableRig; + bool _enableRig = false; + bool _enableAnimGraph = false; glm::vec3 _lastFront; glm::vec3 _lastPosition; + + std::shared_ptr _animNode; + std::shared_ptr _animSkeleton; + std::unique_ptr _animLoader; + AnimVariantMap _animVars; + enum class RigRole { + Idle = 0, + Turn, + Move + }; + RigRole _state = RigRole::Idle; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 8fd7cb9ce5..716ed5d43e 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -313,7 +313,7 @@ AudioInjector* AudioInjector::playSound(const QString& soundUrl, const float vol QByteArray samples = sound->getByteArray(); if (stretchFactor == 1.0f) { - return playSound(samples, options, NULL); + return playSoundAndDelete(samples, options, NULL); } soxr_io_spec_t spec = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I); @@ -333,9 +333,16 @@ AudioInjector* AudioInjector::playSound(const QString& soundUrl, const float vol qCDebug(audio) << "Unable to resample" << soundUrl << "from" << nInputSamples << "@" << standardRate << "to" << nOutputSamples << "@" << resampledRate; resampled = samples; } - return playSound(resampled, options, NULL); + return playSoundAndDelete(resampled, options, NULL); } +AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface) { + AudioInjector* sound = playSound(buffer, options, localInterface); + sound->triggerDeleteAfterFinish(); + return sound; +} + + AudioInjector* AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface) { QThread* injectorThread = new QThread(); injectorThread->setObjectName("Audio Injector Thread"); diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index d65925b865..0e98fe1682 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -46,6 +46,7 @@ public: void setLocalAudioInterface(AbstractAudioInterface* localAudioInterface) { _localAudioInterface = localAudioInterface; } + static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface); static AudioInjector* playSound(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface); static AudioInjector* playSound(const QString& soundUrl, const float volume, const float stretchFactor, const glm::vec3 position); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index f69ea395c9..1091b299b9 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -31,10 +31,6 @@ quint64 DEFAULT_FILTERED_LOG_EXPIRY = 2 * USECS_PER_SECOND; -// this controls how large a change in joint-rotation must be before the interface sends it to the avatar mixer -const float MIN_ROTATION_DOT = 0.9999999f; - - using namespace std; const glm::vec3 DEFAULT_LOCAL_AABOX_CORNER(-0.5f); @@ -145,7 +141,7 @@ void AvatarData::setHandPosition(const glm::vec3& handPosition) { _handPosition = glm::inverse(getOrientation()) * (handPosition - _position); } -QByteArray AvatarData::toByteArray(bool cullSmallChanges) { +QByteArray AvatarData::toByteArray(bool cullSmallChanges, bool sendAll) { // TODO: DRY this up to a shared method // that can pack any type given the number of bytes // and return the number of bytes to push the pointer @@ -243,11 +239,12 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges) { _lastSentJointData.resize(_jointData.size()); - // foreach (const JointData& data, _jointData) { for (int i=0; i < _jointData.size(); i++) { const JointData& data = _jointData.at(i); - if (_lastSentJointData[i].rotation != data.rotation) { - if (!cullSmallChanges || fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= MIN_ROTATION_DOT) { + if (sendAll || _lastSentJointData[i].rotation != data.rotation) { + if (sendAll || + !cullSmallChanges || + fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= AVATAR_MIN_ROTATION_DOT) { validity |= (1 << validityBit); } } @@ -266,7 +263,6 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges) { const JointData& data = _jointData[ i ]; if (validity & (1 << validityBit)) { destinationBuffer += packOrientationQuatToBytes(destinationBuffer, data.rotation); - _lastSentJointData[i].rotation = data.rotation; } if (++validityBit == BITS_IN_BYTE) { validityBit = 0; @@ -277,6 +273,20 @@ QByteArray AvatarData::toByteArray(bool cullSmallChanges) { return avatarDataByteArray.left(destinationBuffer - startPosition); } +void AvatarData::doneEncoding(bool cullSmallChanges) { + // The server has finished sending this version of the joint-data to other nodes. Update _lastSentJointData. + _lastSentJointData.resize(_jointData.size()); + for (int i = 0; i < _jointData.size(); i ++) { + const JointData& data = _jointData[ i ]; + if (_lastSentJointData[i].rotation != data.rotation) { + if (!cullSmallChanges || + fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= AVATAR_MIN_ROTATION_DOT) { + _lastSentJointData[i].rotation = data.rotation; + } + } + } +} + bool AvatarData::shouldLogError(const quint64& now) { if (now > _errorLogExpiry) { _errorLogExpiry = now + DEFAULT_FILTERED_LOG_EXPIRY; @@ -1081,11 +1091,15 @@ void AvatarData::setJointMappingsFromNetworkReply() { void AvatarData::sendAvatarDataPacket() { auto nodeList = DependencyManager::get(); - - QByteArray avatarByteArray = toByteArray(true); - static uint16_t sequenceNumber = 0; + // about 2% of the time, we send a full update (meaning, we transmit all the joint data), even if nothing has changed. + // this is to guard against a joint moving once, the packet getting lost, and the joint never moving again. + bool sendFullUpdate = randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO; + QByteArray avatarByteArray = toByteArray(true, sendFullUpdate); + doneEncoding(true); + static uint16_t sequenceNumber = 0; + auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size() + sizeof(sequenceNumber)); avatarPacket->writePrimitive(sequenceNumber++); avatarPacket->write(avatarByteArray); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 278ec2047c..af97180cbd 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -111,6 +111,11 @@ const int AVATAR_BILLBOARD_PACKET_SEND_INTERVAL_MSECS = 5000; // See also static AvatarData::defaultFullAvatarModelUrl(). const QString DEFAULT_FULL_AVATAR_MODEL_NAME = QString("Default"); +// how often should we send a full report about joint rotations, even if they haven't changed? +const float AVATAR_SEND_FULL_UPDATE_RATIO = 0.02; +// this controls how large a change in joint-rotation must be before the interface sends it to the avatar mixer +const float AVATAR_MIN_ROTATION_DOT = 0.9999999f; + // Where one's own Avatar begins in the world (will be overwritten if avatar data file is found). // This is the start location in the Sandbox (xyz: 6270, 211, 6000). @@ -171,7 +176,8 @@ public: glm::vec3 getHandPosition() const; void setHandPosition(const glm::vec3& handPosition); - virtual QByteArray toByteArray(bool cullSmallChanges); + virtual QByteArray toByteArray(bool cullSmallChanges, bool sendAll); + virtual void doneEncoding(bool cullSmallChanges); /// \return true if an error should be logged bool shouldLogError(const quint64& now); diff --git a/libraries/avatars/src/HandData.cpp b/libraries/avatars/src/HandData.cpp index 9a9b51c1c8..1a9b6775d3 100644 --- a/libraries/avatars/src/HandData.cpp +++ b/libraries/avatars/src/HandData.cpp @@ -114,12 +114,14 @@ float HandData::getBaseScale() const { } glm::vec3 PalmData::getFingerDirection() const { - const glm::vec3 LOCAL_FINGER_DIRECTION(0.0f, 0.0f, 1.0f); + // finger points along yAxis in hand-frame + const glm::vec3 LOCAL_FINGER_DIRECTION(0.0f, 1.0f, 0.0f); return glm::normalize(_owningHandData->localToWorldDirection(_rawRotation * LOCAL_FINGER_DIRECTION)); } glm::vec3 PalmData::getNormal() const { - const glm::vec3 LOCAL_PALM_DIRECTION(0.0f, -1.0f, 0.0f); + // palm normal points along zAxis in hand-frame + const glm::vec3 LOCAL_PALM_DIRECTION(0.0f, 0.0f, 1.0f); return glm::normalize(_owningHandData->localToWorldDirection(_rawRotation * LOCAL_PALM_DIRECTION)); } diff --git a/libraries/avatars/src/HandData.h b/libraries/avatars/src/HandData.h index 6388c882c7..c87c840399 100644 --- a/libraries/avatars/src/HandData.h +++ b/libraries/avatars/src/HandData.h @@ -64,12 +64,13 @@ public: bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, const PalmData*& collidingPalm) const; + glm::quat getBaseOrientation() const; + friend class AvatarData; protected: AvatarData* _owningAvatarData; std::vector _palms; - glm::quat getBaseOrientation() const; glm::vec3 getBasePosition() const; float getBaseScale() const; @@ -95,6 +96,7 @@ public: void setRawRotation(const glm::quat rawRotation) { _rawRotation = rawRotation; }; glm::quat getRawRotation() const { return _rawRotation; } + glm::quat getRotation() const { return _owningHandData->getBaseOrientation() * _rawRotation; } void setRawPosition(const glm::vec3& pos) { _rawPosition = pos; } void setRawVelocity(const glm::vec3& velocity) { _rawVelocity = velocity; } const glm::vec3& getRawVelocity() const { return _rawVelocity; } @@ -147,6 +149,7 @@ public: glm::vec3 getNormal() const; private: + // unless marked otherwise, these are all in the model-frame glm::quat _rawRotation; glm::vec3 _rawPosition; glm::vec3 _rawVelocity; @@ -156,6 +159,7 @@ private: glm::vec3 _tipPosition; glm::vec3 _tipVelocity; glm::vec3 _totalPenetration; // accumulator for per-frame penetrations + unsigned int _controllerButtons; unsigned int _lastControllerButtons; float _trigger; diff --git a/libraries/display-plugins/src/display-plugins/DisplayPlugin.h b/libraries/display-plugins/src/display-plugins/DisplayPlugin.h index a9220d68f6..86cfabe724 100644 --- a/libraries/display-plugins/src/display-plugins/DisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/DisplayPlugin.h @@ -57,6 +57,11 @@ public: // Rendering support + // Stop requesting renders, but don't do full deactivation + // needed to work around the issues caused by Oculus + // processing messages in the middle of submitFrame + virtual void stop() = 0; + /** * Called by the application before the frame rendering. Can be used for * render timing related calls (for instance, the Oculus begin frame timing @@ -120,6 +125,7 @@ public: virtual void resetSensors() {} virtual float devicePixelRatio() { return 1.0; } + static const QString& MENU_PATH(); signals: void recommendedFramebufferSizeChanged(const QSize & size); diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index e5a96d167e..1f8242f081 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -30,3 +30,4 @@ void NullDisplayPlugin::finishFrame() {} void NullDisplayPlugin::activate() {} void NullDisplayPlugin::deactivate() {} +void NullDisplayPlugin::stop() {} diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index 90e717b5ee..bb1ab2d97f 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -17,6 +17,7 @@ public: void activate() override; void deactivate() override; + void stop() override; virtual glm::uvec2 getRecommendedRenderSize() const override; virtual bool hasFocus() const override; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 4101bebfa8..cfe101e1ab 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -16,7 +16,9 @@ OpenGLDisplayPlugin::OpenGLDisplayPlugin() { connect(&_timer, &QTimer::timeout, this, [&] { - emit requestRender(); + if (_active) { + emit requestRender(); + } }); } @@ -56,10 +58,17 @@ void OpenGLDisplayPlugin::customizeContext() { } void OpenGLDisplayPlugin::activate() { + _active = true; _timer.start(1); } +void OpenGLDisplayPlugin::stop() { + _active = false; + _timer.stop(); +} + void OpenGLDisplayPlugin::deactivate() { + _active = false; _timer.stop(); makeCurrent(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 0dc94b72f5..52715ebde7 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -25,7 +25,7 @@ public: virtual void activate() override; virtual void deactivate() override; - + virtual void stop() override; virtual bool eventFilter(QObject* receiver, QEvent* event) override; virtual void display(GLuint sceneTexture, const glm::uvec2& sceneSize) override; @@ -44,6 +44,7 @@ protected: mutable QTimer _timer; ProgramPtr _program; ShapeWrapperPtr _plane; + bool _active{ false }; bool _vsyncSupported{ false }; }; diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp index ff218987ec..c8d9993e5a 100644 --- a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.cpp @@ -310,8 +310,6 @@ void OculusDisplayPlugin::activate() { // not needed since the structure was zeroed on init, but explicit sceneLayer.ColorTexture[1] = nullptr; - PerformanceTimer::setActive(true); - if (!OVR_SUCCESS(ovr_ConfigureTracking(_hmd, ovrTrackingCap_Orientation | ovrTrackingCap_Position | ovrTrackingCap_MagYawCorrection, 0))) { qFatal("Could not attach to sensor device"); @@ -322,24 +320,19 @@ void OculusDisplayPlugin::activate() { void OculusDisplayPlugin::customizeContext() { WindowOpenGLDisplayPlugin::customizeContext(); #if (OVR_MAJOR_VERSION >= 6) - //_texture = DependencyManager::get()-> - // getImageTexture(PathUtils::resourcesPath() + "/images/cube_texture.png"); - uvec2 mirrorSize = toGlm(_window->geometry().size()); - _mirrorFbo = MirrorFboPtr(new MirrorFramebufferWrapper(_hmd)); - _mirrorFbo->Init(mirrorSize); - _sceneFbo = SwapFboPtr(new SwapFramebufferWrapper(_hmd)); _sceneFbo->Init(getRecommendedRenderSize()); #endif + enableVsync(false); + // Only enable mirroring if we know vsync is disabled + _enableMirror = !isVsyncEnabled(); } void OculusDisplayPlugin::deactivate() { #if (OVR_MAJOR_VERSION >= 6) makeCurrent(); _sceneFbo.reset(); - _mirrorFbo.reset(); doneCurrent(); - PerformanceTimer::setActive(false); WindowOpenGLDisplayPlugin::deactivate(); @@ -350,17 +343,22 @@ void OculusDisplayPlugin::deactivate() { } void OculusDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSize) { - static bool inDisplay = false; - if (inDisplay) { - return; - } - inDisplay = true; #if (OVR_MAJOR_VERSION >= 6) using namespace oglplus; // Need to make sure only the display plugin is responsible for // controlling vsync wglSwapIntervalEXT(0); + // screen mirroring + if (_enableMirror) { + auto windowSize = toGlm(_window->size()); + Context::Viewport(windowSize.x, windowSize.y); + glBindTexture(GL_TEXTURE_2D, finalTexture); + GLenum err = glGetError(); + Q_ASSERT(0 == err); + drawUnitQuad(); + } + _sceneFbo->Bound([&] { auto size = _sceneFbo->size; Context::Viewport(size.x, size.y); @@ -375,23 +373,7 @@ void OculusDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSi }); auto windowSize = toGlm(_window->size()); - - /* - Two alternatives for mirroring to the screen, the first is to copy our own composited - scene to the window framebuffer, before distortion. Note this only works if we're doing - ui compositing ourselves, and not relying on the Oculus SDK compositor (or we don't want - the UI visible in the output window (unlikely). This should be done before - _sceneFbo->Increment or we're be using the wrong texture - */ - _sceneFbo->Bound(Framebuffer::Target::Read, [&] { - glBlitFramebuffer( - 0, 0, _sceneFbo->size.x, _sceneFbo->size.y, - 0, 0, windowSize.x, windowSize.y, - GL_COLOR_BUFFER_BIT, GL_NEAREST); - }); - { - PerformanceTimer("OculusSubmit"); ovrViewScaleDesc viewScaleDesc; viewScaleDesc.HmdSpaceToWorldScaleInMeters = 1.0f; viewScaleDesc.HmdToEyeViewOffset[0] = _eyeOffsets[0]; @@ -405,36 +387,12 @@ void OculusDisplayPlugin::display(GLuint finalTexture, const glm::uvec2& sceneSi } _sceneFbo->Increment(); - /* - The other alternative for mirroring is to use the Oculus mirror texture support, which - will contain the post-distorted and fully composited scene regardless of how many layers - we send. - */ - //auto mirrorSize = _mirrorFbo->size; - //_mirrorFbo->Bound(Framebuffer::Target::Read, [&] { - // Context::BlitFramebuffer( - // 0, mirrorSize.y, mirrorSize.x, 0, - // 0, 0, windowSize.x, windowSize.y, - // BufferSelectBit::ColorBuffer, BlitFilter::Nearest); - //}); - ++_frameIndex; #endif - inDisplay = false; } // Pass input events on to the application bool OculusDisplayPlugin::eventFilter(QObject* receiver, QEvent* event) { -#if (OVR_MAJOR_VERSION >= 6) - if (event->type() == QEvent::Resize) { - QResizeEvent* resizeEvent = static_cast(event); - qDebug() << resizeEvent->size().width() << " x " << resizeEvent->size().height(); - auto newSize = toGlm(resizeEvent->size()); - makeCurrent(); - _mirrorFbo->Resize(newSize); - doneCurrent(); - } -#endif return WindowOpenGLDisplayPlugin::eventFilter(receiver, event); } @@ -444,7 +402,9 @@ bool OculusDisplayPlugin::eventFilter(QObject* receiver, QEvent* event) { otherwise the swapbuffer delay will interefere with the framerate of the headset */ void OculusDisplayPlugin::finishFrame() { - //swapBuffers(); + if (_enableMirror) { + swapBuffers(); + } doneCurrent(); }; diff --git a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h index 42f8d5763f..d30356daa0 100644 --- a/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/oculus/OculusDisplayPlugin.h @@ -58,6 +58,7 @@ private: mat4 _compositeEyeProjections[2]; uvec2 _desiredFramebufferSize; ovrTrackingState _trackingState; + bool _enableMirror{ false }; #if (OVR_MAJOR_VERSION >= 6) ovrHmd _hmd; @@ -70,7 +71,6 @@ private: ovrLayerEyeFov& getSceneLayer(); ovrHmdDesc _hmdDesc; SwapFboPtr _sceneFbo; - MirrorFboPtr _mirrorFbo; ovrLayerEyeFov _sceneLayer; #endif #if (OVR_MAJOR_VERSION == 7) diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index de34451c83..ae3c1f29e2 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -78,7 +78,7 @@ void StereoDisplayPlugin::activate() { } void StereoDisplayPlugin::updateScreen() { - for (int i = 0; i < (int) _screenActions.size(); ++i) { + for (uint32_t i = 0; i < _screenActions.size(); ++i) { if (_screenActions[i]->isChecked()) { CONTAINER->setFullscreen(qApp->screens().at(i)); break; diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 46c4986fa8..5ddee32f88 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -68,6 +68,19 @@ RenderablePolyVoxEntityItem::~RenderablePolyVoxEntityItem() { } +bool isEdged(PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle) { + switch (surfaceStyle) { + case PolyVoxEntityItem::SURFACE_CUBIC: + case PolyVoxEntityItem::SURFACE_MARCHING_CUBES: + return false; + case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: + case PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES: + return true; + } + return false; +} + + void RenderablePolyVoxEntityItem::setVoxelData(QByteArray voxelData) { _voxelDataLock.lockForWrite(); if (_voxelData == voxelData) { @@ -88,10 +101,8 @@ void RenderablePolyVoxEntityItem::setVoxelSurfaceStyle(PolyVoxSurfaceStyle voxel } // if we are switching to or from "edged" we need to force a resize of _volData. - bool wasEdged = (_voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_CUBIC || - _voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES); - bool willBeEdged = (voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_CUBIC || - voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES); + bool wasEdged = isEdged(_voxelSurfaceStyle); + bool willBeEdged = isEdged(voxelSurfaceStyle); if (wasEdged != willBeEdged) { _volDataLock.lockForWrite(); @@ -113,15 +124,10 @@ void RenderablePolyVoxEntityItem::setVoxelSurfaceStyle(PolyVoxSurfaceStyle voxel glm::vec3 RenderablePolyVoxEntityItem::getSurfacePositionAdjustment() const { glm::vec3 scale = getDimensions() / _voxelVolumeSize; // meters / voxel-units - switch (_voxelSurfaceStyle) { - case PolyVoxEntityItem::SURFACE_MARCHING_CUBES: - case PolyVoxEntityItem::SURFACE_CUBIC: - return scale / 2.0f; - case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: - case PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES: - return scale / -2.0f; + if (isEdged(_voxelSurfaceStyle)) { + return scale / -2.0f; } - return glm::vec3(0.0f, 0.0f, 0.0f); + return scale / 2.0f; } @@ -130,7 +136,7 @@ glm::mat4 RenderablePolyVoxEntityItem::voxelToLocalMatrix() const { glm::vec3 center = getCenterPosition(); glm::vec3 position = getPosition(); glm::vec3 positionToCenter = center - position; - positionToCenter -= getDimensions() * glm::vec3(0.5f, 0.5f, 0.5f) - getSurfacePositionAdjustment(); + positionToCenter -= getDimensions() * Vectors::HALF - getSurfacePositionAdjustment(); glm::mat4 centerToCorner = glm::translate(glm::mat4(), positionToCenter); glm::mat4 scaled = glm::scale(centerToCorner, scale); return scaled; @@ -195,6 +201,37 @@ bool RenderablePolyVoxEntityItem::setAll(uint8_t toValue) { } +bool RenderablePolyVoxEntityItem::setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue) { + bool result = false; + if (_locked) { + return result; + } + + int xLow = std::max(std::min((int)roundf(lowPosition.x), (int)roundf(_voxelVolumeSize.x) - 1), 0); + int yLow = std::max(std::min((int)roundf(lowPosition.y), (int)roundf(_voxelVolumeSize.y) - 1), 0); + int zLow = std::max(std::min((int)roundf(lowPosition.z), (int)roundf(_voxelVolumeSize.z) - 1), 0); + int xHigh = std::max(std::min(xLow + (int)roundf(cuboidSize.x), (int)roundf(_voxelVolumeSize.x)), xLow); + int yHigh = std::max(std::min(yLow + (int)roundf(cuboidSize.y), (int)roundf(_voxelVolumeSize.y)), yLow); + int zHigh = std::max(std::min(zLow + (int)roundf(cuboidSize.z), (int)roundf(_voxelVolumeSize.z)), zLow); + + _volDataLock.lockForWrite(); + _volDataDirty = true; + + for (int x = xLow; x < xHigh; x++) { + for (int y = yLow; y < yHigh; y++) { + for (int z = zLow; z < zHigh; z++) { + result |= setVoxelInternal(x, y, z, toValue); + } + } + } + _volDataLock.unlock(); + if (result) { + compressVolumeDataAndSendEditPacket(); + } + return result; +} + + bool RenderablePolyVoxEntityItem::setVoxelInVolume(glm::vec3 position, uint8_t toValue) { if (_locked) { @@ -213,7 +250,6 @@ bool RenderablePolyVoxEntityItem::setSphereInVolume(glm::vec3 center, float radi // This three-level for loop iterates over every voxel in the volume _volDataLock.lockForWrite(); - _volDataDirty = true; for (int z = 0; z < _voxelVolumeSize.z; z++) { for (int y = 0; y < _voxelVolumeSize.y; y++) { for (int x = 0; x < _voxelVolumeSize.x; x++) { @@ -228,20 +264,52 @@ bool RenderablePolyVoxEntityItem::setSphereInVolume(glm::vec3 center, float radi } } } - _volDataLock.unlock(); + if (result) { + _volDataDirty = true; + _volDataLock.unlock(); compressVolumeDataAndSendEditPacket(); + } else { + _volDataLock.unlock(); } return result; } bool RenderablePolyVoxEntityItem::setSphere(glm::vec3 centerWorldCoords, float radiusWorldCoords, uint8_t toValue) { - // glm::vec3 centerVoxelCoords = worldToVoxelCoordinates(centerWorldCoords); - glm::vec4 centerVoxelCoords = worldToVoxelMatrix() * glm::vec4(centerWorldCoords, 1.0f); - glm::vec3 scale = getDimensions() / _voxelVolumeSize; // meters / voxel-units - float scaleY = scale.y; - float radiusVoxelCoords = radiusWorldCoords / scaleY; - return setSphereInVolume(glm::vec3(centerVoxelCoords), radiusVoxelCoords, toValue); + bool result = false; + if (_locked) { + return result; + } + + glm::mat4 vtwMatrix = voxelToWorldMatrix(); + + // This three-level for loop iterates over every voxel in the volume + _volDataLock.lockForWrite(); + for (int z = 0; z < _voxelVolumeSize.z; z++) { + for (int y = 0; y < _voxelVolumeSize.y; y++) { + for (int x = 0; x < _voxelVolumeSize.x; x++) { + // Store our current position as a vector... + glm::vec4 pos(x + 0.5f, y + 0.5f, z + 0.5f, 1.0); // consider voxels cenetered on their coordinates + // convert to world coordinates + glm::vec3 worldPos = glm::vec3(vtwMatrix * pos); + // compute how far the current position is from the center of the volume + float fDistToCenter = glm::distance(worldPos, centerWorldCoords); + // If the current voxel is less than 'radius' units from the center then we set its value + if (fDistToCenter <= radiusWorldCoords) { + result |= setVoxelInternal(x, y, z, toValue); + } + } + } + } + + if (result) { + _volDataDirty = true; + _volDataLock.unlock(); + compressVolumeDataAndSendEditPacket(); + } else { + _volDataLock.unlock(); + } + return result; } class RaycastFunctor @@ -296,7 +364,6 @@ bool RenderablePolyVoxEntityItem::findDetailedRayIntersection(const glm::vec3& o glm::mat4 wtvMatrix = worldToVoxelMatrix(); glm::mat4 vtwMatrix = voxelToWorldMatrix(); - glm::mat4 vtlMatrix = voxelToLocalMatrix(); glm::vec3 normDirection = glm::normalize(direction); // the PolyVox ray intersection code requires a near and far point. @@ -304,67 +371,33 @@ bool RenderablePolyVoxEntityItem::findDetailedRayIntersection(const glm::vec3& o float distanceToEntity = glm::distance(origin, getPosition()); float largestDimension = glm::max(getDimensions().x, getDimensions().y, getDimensions().z) * 2.0f; glm::vec3 farPoint = origin + normDirection * (distanceToEntity + largestDimension); + glm::vec4 originInVoxel = wtvMatrix * glm::vec4(origin, 1.0f); glm::vec4 farInVoxel = wtvMatrix * glm::vec4(farPoint, 1.0f); - glm::vec4 result; + glm::vec4 directionInVoxel = glm::normalize(farInVoxel - originInVoxel); + + glm::vec4 result = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f); PolyVox::RaycastResult raycastResult = doRayCast(originInVoxel, farInVoxel, result); if (raycastResult == PolyVox::RaycastResults::Completed) { // the ray completed its path -- nothing was hit. return false; } - // set up ray tests against each face of the voxel. - glm::vec3 minXPosition = glm::vec3(vtwMatrix * (result + glm::vec4(0.0f, 0.5f, 0.5f, 0.0f))); - glm::vec3 maxXPosition = glm::vec3(vtwMatrix * (result + glm::vec4(1.0f, 0.5f, 0.5f, 0.0f))); - glm::vec3 minYPosition = glm::vec3(vtwMatrix * (result + glm::vec4(0.5f, 0.0f, 0.5f, 0.0f))); - glm::vec3 maxYPosition = glm::vec3(vtwMatrix * (result + glm::vec4(0.5f, 1.0f, 0.5f, 0.0f))); - glm::vec3 minZPosition = glm::vec3(vtwMatrix * (result + glm::vec4(0.5f, 0.5f, 0.0f, 0.0f))); - glm::vec3 maxZPosition = glm::vec3(vtwMatrix * (result + glm::vec4(0.5f, 0.5f, 1.0f, 0.0f))); + glm::vec3 result3 = glm::vec3(result); - glm::vec4 baseDimensions = glm::vec4(1.0, 1.0, 1.0, 0.0); - glm::vec3 worldDimensions = glm::vec3(vtlMatrix * baseDimensions); - glm::vec2 xDimensions = glm::vec2(worldDimensions.z, worldDimensions.y); - glm::vec2 yDimensions = glm::vec2(worldDimensions.x, worldDimensions.z); - glm::vec2 zDimensions = glm::vec2(worldDimensions.x, worldDimensions.y); + AABox voxelBox; + voxelBox += result3 - Vectors::HALF; + voxelBox += result3 + Vectors::HALF; - glm::quat vtwRotation = extractRotation(vtwMatrix); - glm::quat minXRotation = vtwRotation * glm::quat(glm::vec3(0.0f, PI_OVER_TWO, 0.0f)); - glm::quat maxXRotation = vtwRotation * glm::quat(glm::vec3(0.0f, PI_OVER_TWO, 0.0f)); - glm::quat minYRotation = vtwRotation * glm::quat(glm::vec3(PI_OVER_TWO, 0.0f, 0.0f)); - glm::quat maxYRotation = vtwRotation * glm::quat(glm::vec3(PI_OVER_TWO, 0.0f, 0.0f)); - glm::quat minZRotation = vtwRotation * glm::quat(glm::vec3(0.0f, 0.0f, 0.0f)); - glm::quat maxZRotation = vtwRotation * glm::quat(glm::vec3(0.0f, 0.0f, 0.0f)); + float voxelDistance; - float bestDx = FLT_MAX; - bool hit[ 6 ]; - float dx[ 6 ] = {FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX}; + bool hit = voxelBox.findRayIntersection(glm::vec3(originInVoxel), glm::vec3(directionInVoxel), voxelDistance, face); - hit[0] = findRayRectangleIntersection(origin, direction, minXRotation, minXPosition, xDimensions, dx[0]); - hit[1] = findRayRectangleIntersection(origin, direction, maxXRotation, maxXPosition, xDimensions, dx[1]); - hit[2] = findRayRectangleIntersection(origin, direction, minYRotation, minYPosition, yDimensions, dx[2]); - hit[3] = findRayRectangleIntersection(origin, direction, maxYRotation, maxYPosition, yDimensions, dx[3]); - hit[4] = findRayRectangleIntersection(origin, direction, minZRotation, minZPosition, zDimensions, dx[4]); - hit[5] = findRayRectangleIntersection(origin, direction, maxZRotation, maxZPosition, zDimensions, dx[5]); - - bool ok = false; - for (int i = 0; i < 6; i ++) { - if (hit[ i ] && dx[ i ] < bestDx) { - face = (BoxFace)i; - distance = dx[ i ]; - ok = true; - bestDx = dx[ i ]; - } - } - - if (!ok) { - // if the attempt to put the ray against one of the voxel-faces fails, just return the center - glm::vec4 intersectedWorldPosition = vtwMatrix * (result + vec4(0.5f, 0.5f, 0.5f, 0.0f)); - distance = glm::distance(glm::vec3(intersectedWorldPosition), origin); - face = BoxFace::MIN_X_FACE; - } - - return true; + glm::vec4 voxelIntersectionPoint = glm::vec4(glm::vec3(originInVoxel) + glm::vec3(directionInVoxel) * voxelDistance, 1.0); + glm::vec4 intersectionPoint = vtwMatrix * voxelIntersectionPoint; + distance = glm::distance(origin, glm::vec3(intersectionPoint)); + return hit; } @@ -380,7 +413,7 @@ PolyVox::RaycastResult RenderablePolyVoxEntityItem::doRayCast(glm::vec4 originIn _volDataLock.unlock(); // result is in voxel-space coordinates. - result = callback._result - glm::vec4(0.5f, 0.5f, 0.5f, 0.0f); + result = callback._result; return raycastResult; } @@ -402,21 +435,29 @@ bool RenderablePolyVoxEntityItem::isReadyToComputeShape() { } void RenderablePolyVoxEntityItem::computeShapeInfo(ShapeInfo& info) { - _shapeInfoLock.lockForRead(); + QReadLocker(&this->_shapeInfoLock); info = _shapeInfo; - _shapeInfoLock.unlock(); } void RenderablePolyVoxEntityItem::setXTextureURL(QString xTextureURL) { - PolyVoxEntityItem::setXTextureURL(xTextureURL); + if (xTextureURL != _xTextureURL) { + _xTexture.clear(); + PolyVoxEntityItem::setXTextureURL(xTextureURL); + } } void RenderablePolyVoxEntityItem::setYTextureURL(QString yTextureURL) { - PolyVoxEntityItem::setYTextureURL(yTextureURL); + if (yTextureURL != _yTextureURL) { + _yTexture.clear(); + PolyVoxEntityItem::setYTextureURL(yTextureURL); + } } void RenderablePolyVoxEntityItem::setZTextureURL(QString zTextureURL) { - PolyVoxEntityItem::setZTextureURL(zTextureURL); + if (zTextureURL != _zTextureURL) { + _zTexture.clear(); + PolyVoxEntityItem::setZTextureURL(zTextureURL); + } } void RenderablePolyVoxEntityItem::render(RenderArgs* args) { @@ -426,9 +467,12 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { _volDataLock.lockForRead(); if (_volDataDirty) { + _volDataLock.unlock(); getMesh(); + } else { + _volDataLock.unlock(); } - _volDataLock.unlock(); + _meshLock.lockForRead(); model::MeshPointer mesh = _mesh; @@ -543,23 +587,21 @@ namespace render { glm::vec3 RenderablePolyVoxEntityItem::voxelCoordsToWorldCoords(glm::vec3& voxelCoords) const { - return glm::vec3(voxelToWorldMatrix() * glm::vec4(voxelCoords, 1.0f)); + glm::vec3 adjustedCoords; + if (isEdged(_voxelSurfaceStyle)) { + adjustedCoords = voxelCoords + Vectors::HALF; + } else { + adjustedCoords = voxelCoords - Vectors::HALF; + } + return glm::vec3(voxelToWorldMatrix() * glm::vec4(adjustedCoords, 1.0f)); } glm::vec3 RenderablePolyVoxEntityItem::worldCoordsToVoxelCoords(glm::vec3& worldCoords) const { glm::vec3 result = glm::vec3(worldToVoxelMatrix() * glm::vec4(worldCoords, 1.0f)); - switch (_voxelSurfaceStyle) { - case PolyVoxEntityItem::SURFACE_MARCHING_CUBES: - case PolyVoxEntityItem::SURFACE_CUBIC: - result += glm::vec3(0.5f, 0.5f, 0.5f); - break; - case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: - case PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES: - result -= glm::vec3(0.5f, 0.5f, 0.5f); - break; + if (isEdged(_voxelSurfaceStyle)) { + return result - Vectors::HALF; } - - return result; + return result + Vectors::HALF; } glm::vec3 RenderablePolyVoxEntityItem::voxelCoordsToLocalCoords(glm::vec3& voxelCoords) const { @@ -585,8 +627,7 @@ void RenderablePolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize) } _onCount = 0; - if (_voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_CUBIC || - _voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES) { + if (isEdged(_voxelSurfaceStyle)) { // with _EDGED_ we maintain an extra box of voxels around those that the user asked for. This // changes how the surface extractor acts -- mainly it becomes impossible to have holes in the // generated mesh. The non _EDGED_ modes will leave holes in the mesh at the edges of the @@ -598,9 +639,11 @@ void RenderablePolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize) _volData = new PolyVox::SimpleVolume(PolyVox::Region(lowCorner, highCorner)); } else { PolyVox::Vector3DInt32 lowCorner(0, 0, 0); - PolyVox::Vector3DInt32 highCorner(_voxelVolumeSize.x - 1, // -1 because these corners are inclusive - _voxelVolumeSize.y - 1, - _voxelVolumeSize.z - 1); + // these should each have -1 after them, but if we leave layers on the upper-axis faces, + // they act more like I expect. + PolyVox::Vector3DInt32 highCorner(_voxelVolumeSize.x, + _voxelVolumeSize.y, + _voxelVolumeSize.z); _volData = new PolyVox::SimpleVolume(PolyVox::Region(lowCorner, highCorner)); } @@ -613,35 +656,27 @@ void RenderablePolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize) bool RenderablePolyVoxEntityItem::inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, - int x, int y, int z) { + int x, int y, int z) const { // x, y, z are in user voxel-coords, not adjusted-for-edge voxel-coords. - switch (surfaceStyle) { - case PolyVoxEntityItem::SURFACE_MARCHING_CUBES: - case PolyVoxEntityItem::SURFACE_CUBIC: - if (x < 0 || y < 0 || z < 0 || - x >= vol->getWidth() || y >= vol->getHeight() || z >= vol->getDepth()) { - return false; - } - return true; - - case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: - case PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES: - if (x < 0 || y < 0 || z < 0 || - x >= vol->getWidth() - 2 || y >= vol->getHeight() - 2 || z >= vol->getDepth() - 2) { - return false; - } - return true; + if (isEdged(surfaceStyle)) { + if (x < 0 || y < 0 || z < 0 || + x >= vol->getWidth() - 2 || y >= vol->getHeight() - 2 || z >= vol->getDepth() - 2) { + return false; + } + return true; + } else { + if (x < 0 || y < 0 || z < 0 || + x >= vol->getWidth() || y >= vol->getHeight() || z >= vol->getDepth()) { + return false; + } + return true; } - - return false; } uint8_t RenderablePolyVoxEntityItem::getVoxel(int x, int y, int z) { - _volDataLock.lockForRead(); - auto result = getVoxelInternal(x, y, z); - _volDataLock.unlock(); - return result; + QReadLocker(&this->_volDataLock); + return getVoxelInternal(x, y, z); } @@ -650,19 +685,13 @@ uint8_t RenderablePolyVoxEntityItem::getVoxelInternal(int x, int y, int z) { return 0; } - // if _voxelSurfaceStyle is SURFACE_EDGED_CUBIC, we maintain an extra layer of + // if _voxelSurfaceStyle is *_EDGED_*, we maintain an extra layer of // voxels all around the requested voxel space. Having the empty voxels around // the edges changes how the surface extractor behaves. - - uint8_t result; - if (_voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_CUBIC || - _voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES) { - result = _volData->getVoxelAt(x + 1, y + 1, z + 1); - } else { - result = _volData->getVoxelAt(x, y, z); + if (isEdged(_voxelSurfaceStyle)) { + return _volData->getVoxelAt(x + 1, y + 1, z + 1); } - - return result; + return _volData->getVoxelAt(x, y, z); } @@ -675,9 +704,7 @@ bool RenderablePolyVoxEntityItem::setVoxelInternal(int x, int y, int z, uint8_t result = updateOnCount(x, y, z, toValue); - assert(_volData); - if (_voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_CUBIC || - _voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES) { + if (isEdged(_voxelSurfaceStyle)) { _volData->setVoxelAt(x + 1, y + 1, z + 1, toValue); } else { _volData->setVoxelAt(x, y, z, toValue); @@ -710,13 +737,11 @@ bool RenderablePolyVoxEntityItem::updateOnCount(int x, int y, int z, uint8_t toV return false; } - void RenderablePolyVoxEntityItem::decompressVolumeData() { _threadRunning.acquire(); QtConcurrent::run(this, &RenderablePolyVoxEntityItem::decompressVolumeDataAsync); } - // take compressed data and expand it into _volData. void RenderablePolyVoxEntityItem::decompressVolumeDataAsync() { _voxelDataLock.lockForRead(); @@ -777,7 +802,6 @@ void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() { QtConcurrent::run(this, &RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacketAsync); } - // compress the data in _volData and save the results. The compressed form is used during // saves to disk and for transmission over the wire void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacketAsync() { @@ -850,23 +874,157 @@ void RenderablePolyVoxEntityItem::getMesh() { QtConcurrent::run(this, &RenderablePolyVoxEntityItem::getMeshAsync); } +void RenderablePolyVoxEntityItem::clearOutOfDateNeighbors() { + if (_xNNeighborID != UNKNOWN_ENTITY_ID) { + EntityItemPointer currentXNNeighbor = _xNNeighbor.lock(); + if (currentXNNeighbor && currentXNNeighbor->getID() != _xNNeighborID) { + _xNNeighbor.reset(); + } + } + if (_yNNeighborID != UNKNOWN_ENTITY_ID) { + EntityItemPointer currentYNNeighbor = _yNNeighbor.lock(); + if (currentYNNeighbor && currentYNNeighbor->getID() != _yNNeighborID) { + _yNNeighbor.reset(); + } + } + if (_zNNeighborID != UNKNOWN_ENTITY_ID) { + EntityItemPointer currentZNNeighbor = _zNNeighbor.lock(); + if (currentZNNeighbor && currentZNNeighbor->getID() != _zNNeighborID) { + _zNNeighbor.reset(); + } + } + + if (_xPNeighborID != UNKNOWN_ENTITY_ID) { + EntityItemPointer currentXPNeighbor = _xPNeighbor.lock(); + if (currentXPNeighbor && currentXPNeighbor->getID() != _xPNeighborID) { + _xPNeighbor.reset(); + } + } + if (_yPNeighborID != UNKNOWN_ENTITY_ID) { + EntityItemPointer currentYPNeighbor = _yPNeighbor.lock(); + if (currentYPNeighbor && currentYPNeighbor->getID() != _yPNeighborID) { + _yPNeighbor.reset(); + } + } + if (_zPNeighborID != UNKNOWN_ENTITY_ID) { + EntityItemPointer currentZPNeighbor = _zPNeighbor.lock(); + if (currentZPNeighbor && currentZPNeighbor->getID() != _zPNeighborID) { + _zPNeighbor.reset(); + } + } + +} + +void RenderablePolyVoxEntityItem::cacheNeighbors() { + clearOutOfDateNeighbors(); + EntityTreeElement* element = getElement(); + EntityTree* tree = element ? element->getTree() : nullptr; + if (!tree) { + return; + } + + if (_xNNeighborID != UNKNOWN_ENTITY_ID && _xNNeighbor.expired()) { + _xNNeighbor = tree->findEntityByID(_xNNeighborID); + } + if (_yNNeighborID != UNKNOWN_ENTITY_ID && _yNNeighbor.expired()) { + _yNNeighbor = tree->findEntityByID(_yNNeighborID); + } + if (_zNNeighborID != UNKNOWN_ENTITY_ID && _zNNeighbor.expired()) { + _zNNeighbor = tree->findEntityByID(_zNNeighborID); + } + + if (_xPNeighborID != UNKNOWN_ENTITY_ID && _xPNeighbor.expired()) { + _xPNeighbor = tree->findEntityByID(_xPNeighborID); + } + if (_yPNeighborID != UNKNOWN_ENTITY_ID && _yPNeighbor.expired()) { + _yPNeighbor = tree->findEntityByID(_yPNeighborID); + } + if (_zPNeighborID != UNKNOWN_ENTITY_ID && _zPNeighbor.expired()) { + _zPNeighbor = tree->findEntityByID(_zPNeighborID); + } + +} + +void RenderablePolyVoxEntityItem::copyUpperEdgesFromNeighbors() { + if (_voxelSurfaceStyle != PolyVoxEntityItem::SURFACE_MARCHING_CUBES) { + return; + } + + EntityItemPointer currentXPNeighbor = _xPNeighbor.lock(); + EntityItemPointer currentYPNeighbor = _yPNeighbor.lock(); + EntityItemPointer currentZPNeighbor = _zPNeighbor.lock(); + + if (currentXPNeighbor) { + auto polyVoxXPNeighbor = std::dynamic_pointer_cast(currentXPNeighbor); + if (polyVoxXPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { + for (int y = 0; y < _volData->getHeight(); y++) { + for (int z = 0; z < _volData->getDepth(); z++) { + uint8_t neighborValue = polyVoxXPNeighbor->getVoxel(0, y, z); + _volData->setVoxelAt(_volData->getWidth() - 1, y, z, neighborValue); + } + } + } + } + + if (currentYPNeighbor) { + auto polyVoxYPNeighbor = std::dynamic_pointer_cast(currentYPNeighbor); + if (polyVoxYPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { + for (int x = 0; x < _volData->getWidth(); x++) { + for (int z = 0; z < _volData->getDepth(); z++) { + uint8_t neighborValue = polyVoxYPNeighbor->getVoxel(x, 0, z); + _volData->setVoxelAt(x, _volData->getWidth() - 1, z, neighborValue); + } + } + } + } + + if (currentZPNeighbor) { + auto polyVoxZPNeighbor = std::dynamic_pointer_cast(currentZPNeighbor); + if (polyVoxZPNeighbor->getVoxelVolumeSize() == _voxelVolumeSize) { + for (int x = 0; x < _volData->getWidth(); x++) { + for (int y = 0; y < _volData->getHeight(); y++) { + uint8_t neighborValue = polyVoxZPNeighbor->getVoxel(x, y, 0); + _volData->setVoxelAt(x, y, _volData->getDepth() - 1, neighborValue); + } + } + } + } +} void RenderablePolyVoxEntityItem::getMeshAsync() { model::MeshPointer mesh(new model::Mesh()); + cacheNeighbors(); + // A mesh object to hold the result of surface extraction PolyVox::SurfaceMesh polyVoxMesh; _volDataLock.lockForRead(); + if (!_volData) { + _volDataLock.unlock(); + return; + } + copyUpperEdgesFromNeighbors(); + switch (_voxelSurfaceStyle) { - case PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES: + case PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES: { + PolyVox::MarchingCubesSurfaceExtractor> surfaceExtractor + (_volData, _volData->getEnclosingRegion(), &polyVoxMesh); + surfaceExtractor.execute(); + break; + } case PolyVoxEntityItem::SURFACE_MARCHING_CUBES: { PolyVox::MarchingCubesSurfaceExtractor> surfaceExtractor (_volData, _volData->getEnclosingRegion(), &polyVoxMesh); surfaceExtractor.execute(); break; } - case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: + case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: { + PolyVox::CubicSurfaceExtractorWithNormals> surfaceExtractor + (_volData, _volData->getEnclosingRegion(), &polyVoxMesh); + surfaceExtractor.execute(); + break; + } case PolyVoxEntityItem::SURFACE_CUBIC: { PolyVox::CubicSurfaceExtractorWithNormals> surfaceExtractor (_volData, _volData->getEnclosingRegion(), &polyVoxMesh); @@ -908,7 +1066,7 @@ void RenderablePolyVoxEntityItem::getMeshAsync() { _meshLock.unlock(); _volDataDirty = false; _volDataLock.unlock(); - + bonkNeighbors(); _threadRunning.release(); } @@ -925,7 +1083,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorkerAsync() { if (_voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_MARCHING_CUBES || _voxelSurfaceStyle == PolyVoxEntityItem::SURFACE_EDGED_MARCHING_CUBES) { - /* pull each triangle in the mesh into a polyhedron which can be collided with */ + // pull each triangle in the mesh into a polyhedron which can be collided with unsigned int i = 0; _meshLock.lockForRead(); @@ -972,10 +1130,16 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorkerAsync() { } else { unsigned int i = 0; + _volDataLock.lockForRead(); + if (!_volData) { + _volDataLock.unlock(); + return; + } + for (int z = 0; z < _voxelVolumeSize.z; z++) { for (int y = 0; y < _voxelVolumeSize.y; y++) { for (int x = 0; x < _voxelVolumeSize.x; x++) { - if (getVoxel(x, y, z) > 0) { + if (getVoxelInternal(x, y, z) > 0) { if ((x > 0 && getVoxel(x - 1, y, z) > 0) && (y > 0 && getVoxel(x, y - 1, z) > 0) && @@ -1033,6 +1197,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorkerAsync() { } } } + _volDataLock.unlock(); } if (points.isEmpty()) { @@ -1056,3 +1221,93 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorkerAsync() { _threadRunning.release(); return; } + + +void RenderablePolyVoxEntityItem::setXNNeighborID(const EntityItemID& xNNeighborID) { + if (xNNeighborID != _xNNeighborID) { + PolyVoxEntityItem::setXNNeighborID(xNNeighborID); + cacheNeighbors(); + EntityItemPointer currentXNNeighbor = _xNNeighbor.lock(); + if (currentXNNeighbor && currentXNNeighbor->getType() == EntityTypes::PolyVox) { + auto polyVoxXNNeighbor = std::dynamic_pointer_cast(currentXNNeighbor); + polyVoxXNNeighbor->setXPNeighborID(_id); + polyVoxXNNeighbor->rebakeMesh(); + } + } +} + +void RenderablePolyVoxEntityItem::setYNNeighborID(const EntityItemID& yNNeighborID) { + if (yNNeighborID != _yNNeighborID) { + PolyVoxEntityItem::setYNNeighborID(yNNeighborID); + cacheNeighbors(); + EntityItemPointer currentYNNeighbor = _yNNeighbor.lock(); + if (currentYNNeighbor && currentYNNeighbor->getType() == EntityTypes::PolyVox) { + auto polyVoxYNNeighbor = std::dynamic_pointer_cast(currentYNNeighbor); + polyVoxYNNeighbor->setYPNeighborID(_id); + polyVoxYNNeighbor->rebakeMesh(); + } + } +} + +void RenderablePolyVoxEntityItem::setZNNeighborID(const EntityItemID& zNNeighborID) { + if (zNNeighborID != _zNNeighborID) { + PolyVoxEntityItem::setZNNeighborID(zNNeighborID); + cacheNeighbors(); + EntityItemPointer currentZNNeighbor = _yNNeighbor.lock(); + if (currentZNNeighbor && currentZNNeighbor->getType() == EntityTypes::PolyVox) { + auto polyVoxZNNeighbor = std::dynamic_pointer_cast(currentZNNeighbor); + polyVoxZNNeighbor->setZPNeighborID(_id); + polyVoxZNNeighbor->rebakeMesh(); + } + } +} + + +void RenderablePolyVoxEntityItem::setXPNeighborID(const EntityItemID& xPNeighborID) { + if (xPNeighborID != _xPNeighborID) { + PolyVoxEntityItem::setXPNeighborID(xPNeighborID); + rebakeMesh(); + } +} + +void RenderablePolyVoxEntityItem::setYPNeighborID(const EntityItemID& yPNeighborID) { + if (yPNeighborID != _yPNeighborID) { + PolyVoxEntityItem::setYPNeighborID(yPNeighborID); + rebakeMesh(); + } +} + +void RenderablePolyVoxEntityItem::setZPNeighborID(const EntityItemID& zPNeighborID) { + if (zPNeighborID != _zPNeighborID) { + PolyVoxEntityItem::setZPNeighborID(zPNeighborID); + rebakeMesh(); + } +} + + +void RenderablePolyVoxEntityItem::rebakeMesh() { + QReadLocker(&this->_volDataLock); + _volDataDirty = true; +} + +void RenderablePolyVoxEntityItem::bonkNeighbors() { + clearOutOfDateNeighbors(); + cacheNeighbors(); + + EntityItemPointer currentXNNeighbor = _xNNeighbor.lock(); + EntityItemPointer currentYNNeighbor = _yNNeighbor.lock(); + EntityItemPointer currentZNNeighbor = _zNNeighbor.lock(); + + if (currentXNNeighbor && currentXNNeighbor->getType() == EntityTypes::PolyVox) { + auto polyVoxXNNeighbor = std::dynamic_pointer_cast(currentXNNeighbor); + polyVoxXNNeighbor->rebakeMesh(); + } + if (currentYNNeighbor && currentYNNeighbor->getType() == EntityTypes::PolyVox) { + auto polyVoxYNNeighbor = std::dynamic_pointer_cast(currentYNNeighbor); + polyVoxYNNeighbor->rebakeMesh(); + } + if (currentZNNeighbor && currentZNNeighbor->getType() == EntityTypes::PolyVox) { + auto polyVoxZNNeighbor = std::dynamic_pointer_cast(currentZNNeighbor); + polyVoxZNNeighbor->rebakeMesh(); + } +} diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 110e8f8ab4..01578b5e58 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -93,6 +93,7 @@ public: // coords are in world-space virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue); virtual bool setAll(uint8_t toValue); + virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue); virtual void setXTextureURL(QString xTextureURL); virtual void setYTextureURL(QString yTextureURL); @@ -105,6 +106,16 @@ public: std::shared_ptr scene, render::PendingChanges& pendingChanges); + virtual void setXNNeighborID(const EntityItemID& xNNeighborID); + virtual void setYNNeighborID(const EntityItemID& yNNeighborID); + virtual void setZNNeighborID(const EntityItemID& zNNeighborID); + + virtual void setXPNeighborID(const EntityItemID& xPNeighborID); + virtual void setYPNeighborID(const EntityItemID& yPNeighborID); + virtual void setZPNeighborID(const EntityItemID& zPNeighborID); + + virtual void rebakeMesh(); + private: // The PolyVoxEntityItem class has _voxelData which contains dimensions and compressed voxel data. The dimensions // may not match _voxelVolumeSize. @@ -130,7 +141,7 @@ private: int _onCount; // how many non-zero voxels are in _volData bool inUserBounds(const PolyVox::SimpleVolume* vol, PolyVoxEntityItem::PolyVoxSurfaceStyle surfaceStyle, - int x, int y, int z); + int x, int y, int z) const; uint8_t getVoxelInternal(int x, int y, int z); bool setVoxelInternal(int x, int y, int z, uint8_t toValue); bool updateOnCount(int x, int y, int z, uint8_t toValue); @@ -147,6 +158,18 @@ private: void computeShapeInfoWorkerAsync(); QSemaphore _threadRunning{1}; + + // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID + EntityItemWeakPointer _xNNeighbor; // neighor found by going along negative X axis + EntityItemWeakPointer _yNNeighbor; + EntityItemWeakPointer _zNNeighbor; + EntityItemWeakPointer _xPNeighbor; // neighor found by going along positive X axis + EntityItemWeakPointer _yPNeighbor; + EntityItemWeakPointer _zPNeighbor; + void clearOutOfDateNeighbors(); + void cacheNeighbors(); + void copyUpperEdgesFromNeighbors(); + void bonkNeighbors(); }; diff --git a/libraries/entities-renderer/src/polyvox.slf b/libraries/entities-renderer/src/polyvox.slf index 5772a93f7d..c694630770 100644 --- a/libraries/entities-renderer/src/polyvox.slf +++ b/libraries/entities-renderer/src/polyvox.slf @@ -12,16 +12,12 @@ // <@include gpu/Inputs.slh@> - -layout(location = 0) out vec4 _fragColor0; -layout(location = 1) out vec4 _fragColor1; -layout(location = 2) out vec4 _fragColor2; - <@include model/Material.slh@> +<@include DeferredBufferWrite.slh@> in vec3 _normal; in vec4 _position; -in vec4 _inPosition; +in vec4 _worldPosition; uniform sampler2D xMap; uniform sampler2D yMap; @@ -29,12 +25,12 @@ uniform sampler2D zMap; uniform vec3 voxelVolumeSize; void main(void) { - vec3 worldNormal = cross(dFdy(_inPosition.xyz), dFdx(_inPosition.xyz)); + vec3 worldNormal = cross(dFdy(_worldPosition.xyz), dFdx(_worldPosition.xyz)); worldNormal = normalize(worldNormal); - float inPositionX = (_inPosition.x - 0.5) / voxelVolumeSize.x; - float inPositionY = (_inPosition.y - 0.5) / voxelVolumeSize.y; - float inPositionZ = (_inPosition.z - 0.5) / voxelVolumeSize.z; + float inPositionX = (_worldPosition.x - 0.5) / voxelVolumeSize.x; + float inPositionY = (_worldPosition.y - 0.5) / voxelVolumeSize.y; + float inPositionZ = (_worldPosition.z - 0.5) / voxelVolumeSize.z; vec4 xyDiffuse = texture(xMap, vec2(-inPositionX, -inPositionY)); vec4 xzDiffuse = texture(yMap, vec2(-inPositionX, inPositionZ)); @@ -43,12 +39,7 @@ void main(void) { vec3 xyDiffuseScaled = xyDiffuse.rgb * abs(worldNormal.z); vec3 xzDiffuseScaled = xzDiffuse.rgb * abs(worldNormal.y); vec3 yzDiffuseScaled = yzDiffuse.rgb * abs(worldNormal.x); - vec4 diffuse = vec4(xyDiffuseScaled + xzDiffuseScaled + yzDiffuseScaled, 1.0); - Material mat = getMaterial(); - - _fragColor0 = vec4(diffuse.rgb, 0.0); - _fragColor1 = vec4(_normal, 1.0); - _fragColor2 = vec4(getMaterialSpecular(mat), getMaterialShininess(mat) / 128.0); + packDeferredFragment(_normal, 0.0, vec3(diffuse), vec3(0.01, 0.01, 0.01), 10.0); } diff --git a/libraries/entities-renderer/src/polyvox.slv b/libraries/entities-renderer/src/polyvox.slv index 0074993c80..eb8d264a1b 100644 --- a/libraries/entities-renderer/src/polyvox.slv +++ b/libraries/entities-renderer/src/polyvox.slv @@ -17,7 +17,7 @@ <$declareStandardTransform()$> out vec4 _position; -out vec4 _inPosition; +out vec4 _worldPosition; out vec3 _normal; void main(void) { @@ -26,5 +26,5 @@ void main(void) { TransformObject obj = getTransformObject(); <$transformModelToEyeAndClipPos(cam, obj, inPosition, _position, gl_Position)$> <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$> - _inPosition = inPosition; + _worldPosition = inPosition; } diff --git a/libraries/entities/src/EntityItemID.h b/libraries/entities/src/EntityItemID.h index 7f55005a17..765082ef0b 100644 --- a/libraries/entities/src/EntityItemID.h +++ b/libraries/entities/src/EntityItemID.h @@ -32,26 +32,8 @@ public: QScriptValue toScriptValue(QScriptEngine* engine) const; bool isInvalidID() const { return *this == UNKNOWN_ENTITY_ID; } - - // QUuid id; }; -// inline bool operator<(const EntityItemID& a, const EntityItemID& b) { -// return a.id < b.id; -// } - -// inline bool operator==(const EntityItemID& a, const EntityItemID& b) { -// return a.id == b.id; -// } - -// inline bool operator!=(const EntityItemID& a, const EntityItemID& b) { -// return !(a == b); -// } - -// inline uint qHash(const EntityItemID& a, uint seed) { -// return qHash(a.id, seed); -// } - inline QDebug operator<<(QDebug debug, const EntityItemID& id) { debug << "[entity-id:" << id.toString() << "]"; return debug; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5550f10a4a..19521019d0 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -109,6 +109,13 @@ CONSTRUCT_PROPERTY(strokeWidths, QVector()), CONSTRUCT_PROPERTY(xTextureURL, ""), CONSTRUCT_PROPERTY(yTextureURL, ""), CONSTRUCT_PROPERTY(zTextureURL, ""), +CONSTRUCT_PROPERTY(xNNeighborID, UNKNOWN_ENTITY_ID), +CONSTRUCT_PROPERTY(yNNeighborID, UNKNOWN_ENTITY_ID), +CONSTRUCT_PROPERTY(zNNeighborID, UNKNOWN_ENTITY_ID), +CONSTRUCT_PROPERTY(xPNeighborID, UNKNOWN_ENTITY_ID), +CONSTRUCT_PROPERTY(yPNeighborID, UNKNOWN_ENTITY_ID), +CONSTRUCT_PROPERTY(zPNeighborID, UNKNOWN_ENTITY_ID), + _id(UNKNOWN_ENTITY_ID), _idSet(false), @@ -377,6 +384,12 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_X_TEXTURE_URL, xTextureURL); CHECK_PROPERTY_CHANGE(PROP_Y_TEXTURE_URL, yTextureURL); CHECK_PROPERTY_CHANGE(PROP_Z_TEXTURE_URL, zTextureURL); + CHECK_PROPERTY_CHANGE(PROP_X_N_NEIGHBOR_ID, xNNeighborID); + CHECK_PROPERTY_CHANGE(PROP_Y_N_NEIGHBOR_ID, yNNeighborID); + CHECK_PROPERTY_CHANGE(PROP_Z_N_NEIGHBOR_ID, zNNeighborID); + CHECK_PROPERTY_CHANGE(PROP_X_P_NEIGHBOR_ID, xPNeighborID); + CHECK_PROPERTY_CHANGE(PROP_Y_P_NEIGHBOR_ID, yPNeighborID); + CHECK_PROPERTY_CHANGE(PROP_Z_P_NEIGHBOR_ID, zPNeighborID); changedProperties += _stage.getChangedProperties(); changedProperties += _atmosphere.getChangedProperties(); @@ -521,6 +534,14 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(yTextureURL); COPY_PROPERTY_TO_QSCRIPTVALUE(zTextureURL); + COPY_PROPERTY_TO_QSCRIPTVALUE(xNNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(yNNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(zNNeighborID); + + COPY_PROPERTY_TO_QSCRIPTVALUE(xPNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(yPNeighborID); + COPY_PROPERTY_TO_QSCRIPTVALUE(zPNeighborID); + return properties; } @@ -620,6 +641,14 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(yTextureURL, QString, setYTextureURL); COPY_PROPERTY_FROM_QSCRIPTVALUE(zTextureURL, QString, setZTextureURL); + COPY_PROPERTY_FROM_QSCRIPTVALUE(xNNeighborID, EntityItemID, setXNNeighborID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(yNNeighborID, EntityItemID, setYNNeighborID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(zNNeighborID, EntityItemID, setZNNeighborID); + + COPY_PROPERTY_FROM_QSCRIPTVALUE(xPNeighborID, EntityItemID, setXPNeighborID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(yPNeighborID, EntityItemID, setYPNeighborID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(zPNeighborID, EntityItemID, setZPNeighborID); + _lastEdited = usecTimestampNow(); } @@ -852,6 +881,12 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_X_TEXTURE_URL, properties.getXTextureURL()); APPEND_ENTITY_PROPERTY(PROP_Y_TEXTURE_URL, properties.getYTextureURL()); APPEND_ENTITY_PROPERTY(PROP_Z_TEXTURE_URL, properties.getZTextureURL()); + APPEND_ENTITY_PROPERTY(PROP_X_N_NEIGHBOR_ID, properties.getXNNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Y_N_NEIGHBOR_ID, properties.getYNNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Z_N_NEIGHBOR_ID, properties.getZNNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_X_P_NEIGHBOR_ID, properties.getXPNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Y_P_NEIGHBOR_ID, properties.getYPNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Z_P_NEIGHBOR_ID, properties.getZPNeighborID()); } if (properties.getType() == EntityTypes::Line) { @@ -1115,6 +1150,12 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_X_TEXTURE_URL, QString, setXTextureURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_Y_TEXTURE_URL, QString, setYTextureURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_Z_TEXTURE_URL, QString, setZTextureURL); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_X_N_NEIGHBOR_ID, EntityItemID, setXNNeighborID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_Y_N_NEIGHBOR_ID, EntityItemID, setYNNeighborID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_Z_N_NEIGHBOR_ID, EntityItemID, setZNNeighborID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_X_P_NEIGHBOR_ID, EntityItemID, setXPNeighborID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_Y_P_NEIGHBOR_ID, EntityItemID, setYPNeighborID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_Z_P_NEIGHBOR_ID, EntityItemID, setZPNeighborID); } if (properties.getType() == EntityTypes::Line) { @@ -1248,13 +1289,21 @@ void EntityItemProperties::markAllChanged() { _descriptionChanged = true; _faceCameraChanged = true; _actionDataChanged = true; - + _normalsChanged = true; _strokeWidthsChanged = true; _xTextureURLChanged = true; _yTextureURLChanged = true; _zTextureURLChanged = true; + + _xNNeighborIDChanged = true; + _yNNeighborIDChanged = true; + _zNNeighborIDChanged = true; + + _xPNeighborIDChanged = true; + _yPNeighborIDChanged = true; + _zPNeighborIDChanged = true; } /// The maximum bounding cube for the entity, independent of it's rotation. diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index ffc409e933..4fe28362d1 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -161,6 +161,12 @@ public: DEFINE_PROPERTY_REF(PROP_X_TEXTURE_URL, XTextureURL, xTextureURL, QString); DEFINE_PROPERTY_REF(PROP_Y_TEXTURE_URL, YTextureURL, yTextureURL, QString); DEFINE_PROPERTY_REF(PROP_Z_TEXTURE_URL, ZTextureURL, zTextureURL, QString); + DEFINE_PROPERTY_REF(PROP_X_N_NEIGHBOR_ID, XNNeighborID, xNNeighborID, EntityItemID); + DEFINE_PROPERTY_REF(PROP_Y_N_NEIGHBOR_ID, YNNeighborID, yNNeighborID, EntityItemID); + DEFINE_PROPERTY_REF(PROP_Z_N_NEIGHBOR_ID, ZNNeighborID, zNNeighborID, EntityItemID); + DEFINE_PROPERTY_REF(PROP_X_P_NEIGHBOR_ID, XPNeighborID, xPNeighborID, EntityItemID); + DEFINE_PROPERTY_REF(PROP_Y_P_NEIGHBOR_ID, YPNeighborID, yPNeighborID, EntityItemID); + DEFINE_PROPERTY_REF(PROP_Z_P_NEIGHBOR_ID, ZPNeighborID, zPNeighborID, EntityItemID); static QString getBackgroundModeString(BackgroundMode mode); @@ -327,6 +333,12 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, XTextureURL, xTextureURL, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, YTextureURL, yTextureURL, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, ZTextureURL, zTextureURL, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, XNNeighborID, xNNeighborID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, YNNeighborID, yNNeighborID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ZNNeighborID, zNNeighborID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, XPNeighborID, xPNeighborID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, YPNeighborID, yPNeighborID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ZPNeighborID, zPNeighborID, ""); properties.getStage().debugDump(); properties.getAtmosphere().debugDump(); diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index e48be1da8c..a3e31024d1 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -14,6 +14,8 @@ #ifndef hifi_EntityItemPropertiesMacros_h #define hifi_EntityItemPropertiesMacros_h +#include "EntityItemID.h" + #define APPEND_ENTITY_PROPERTY(P,V) \ if (requestedProperties.getHasProperty(P)) { \ LevelDetails propertyLevel = packetData->startLevel(); \ @@ -106,6 +108,9 @@ inline QScriptValue convertScriptValue(QScriptEngine* e, const QByteArray& v) { return QScriptValue(QString(b64)); } +inline QScriptValue convertScriptValue(QScriptEngine* e, const EntityItemID& v) { return QScriptValue(QUuid(v).toString()); } + + #define COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(G,g,P,p) \ if (!skipDefaults || defaultEntityProperties.get##G().get##P() != get##P()) { \ QScriptValue groupProperties = properties.property(#g); \ @@ -143,6 +148,9 @@ inline int int_convertFromScriptValue(const QScriptValue& v, bool& isValid) { re inline bool bool_convertFromScriptValue(const QScriptValue& v, bool& isValid) { isValid = true; return v.toVariant().toBool(); } inline QString QString_convertFromScriptValue(const QScriptValue& v, bool& isValid) { isValid = true; return v.toVariant().toString().trimmed(); } inline QUuid QUuid_convertFromScriptValue(const QScriptValue& v, bool& isValid) { isValid = true; return v.toVariant().toUuid(); } +inline EntityItemID EntityItemID_convertFromScriptValue(const QScriptValue& v, bool& isValid) { isValid = true; return v.toVariant().toUuid(); } + + inline QDateTime QDateTime_convertFromScriptValue(const QScriptValue& v, bool& isValid) { isValid = true; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index abb8241d8f..d929bdbef5 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -124,21 +124,28 @@ enum EntityPropertyList { PROP_FACE_CAMERA, PROP_SCRIPT_TIMESTAMP, - + PROP_ACTION_DATA, - + PROP_X_TEXTURE_URL, // used by PolyVox PROP_Y_TEXTURE_URL, // used by PolyVox PROP_Z_TEXTURE_URL, // used by PolyVox - + // Used by PolyLine entity PROP_NORMALS, PROP_STROKE_WIDTHS, - + // used by particles PROP_VELOCITY_SPREAD, PROP_ACCELERATION_SPREAD, + PROP_X_N_NEIGHBOR_ID, // used by PolyVox + PROP_Y_N_NEIGHBOR_ID, // used by PolyVox + PROP_Z_N_NEIGHBOR_ID, // used by PolyVox + PROP_X_P_NEIGHBOR_ID, // used by PolyVox + PROP_Y_P_NEIGHBOR_ID, // used by PolyVox + PROP_Z_P_NEIGHBOR_ID, // used by PolyVox + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 2fb947634e..a735129a28 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -493,6 +493,13 @@ bool EntityScriptingInterface::setAllVoxels(QUuid entityID, int value) { }); } +bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, + const glm::vec3& cuboidSize, int value) { + return setVoxels(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) { + return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value); + }); +} + bool EntityScriptingInterface::setAllPoints(QUuid entityID, const QVector& points) { EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); if (!entity) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index c642cf88f2..9edf685464 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -122,6 +122,8 @@ public slots: Q_INVOKABLE bool setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value); Q_INVOKABLE bool setVoxel(QUuid entityID, const glm::vec3& position, int value); Q_INVOKABLE bool setAllVoxels(QUuid entityID, int value); + Q_INVOKABLE bool setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, + const glm::vec3& cuboidSize, int value); Q_INVOKABLE bool setAllPoints(QUuid entityID, const QVector& points); Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point); diff --git a/libraries/entities/src/PolyVoxEntityItem.cpp b/libraries/entities/src/PolyVoxEntityItem.cpp index e765afd430..6c53dbfa16 100644 --- a/libraries/entities/src/PolyVoxEntityItem.cpp +++ b/libraries/entities/src/PolyVoxEntityItem.cpp @@ -12,6 +12,7 @@ #include #include +#include #include @@ -56,13 +57,14 @@ PolyVoxEntityItem::PolyVoxEntityItem(const EntityItemID& entityItemID, const Ent _voxelSurfaceStyle(PolyVoxEntityItem::DEFAULT_VOXEL_SURFACE_STYLE), _xTextureURL(PolyVoxEntityItem::DEFAULT_X_TEXTURE_URL), _yTextureURL(PolyVoxEntityItem::DEFAULT_Y_TEXTURE_URL), - _zTextureURL(PolyVoxEntityItem::DEFAULT_Z_TEXTURE_URL) -{ + _zTextureURL(PolyVoxEntityItem::DEFAULT_Z_TEXTURE_URL) { _type = EntityTypes::PolyVox; setProperties(properties); } void PolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize) { + QWriteLocker(&this->_voxelDataLock); + assert((int)_voxelVolumeSize.x == _voxelVolumeSize.x); assert((int)_voxelVolumeSize.y == _voxelVolumeSize.y); assert((int)_voxelVolumeSize.z == _voxelVolumeSize.z); @@ -96,6 +98,12 @@ void PolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize) { } } +const glm::vec3& PolyVoxEntityItem::getVoxelVolumeSize() const { + QWriteLocker locker(&this->_voxelDataLock); + return _voxelVolumeSize; +} + + EntityItemProperties PolyVoxEntityItem::getProperties() const { EntityItemProperties properties = EntityItem::getProperties(); // get the properties from our base class COPY_ENTITY_PROPERTY_TO_PROPERTIES(voxelVolumeSize, getVoxelVolumeSize); @@ -104,6 +112,12 @@ EntityItemProperties PolyVoxEntityItem::getProperties() const { COPY_ENTITY_PROPERTY_TO_PROPERTIES(xTextureURL, getXTextureURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(yTextureURL, getYTextureURL); COPY_ENTITY_PROPERTY_TO_PROPERTIES(zTextureURL, getZTextureURL); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(xNNeighborID, getXNNeighborID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(yNNeighborID, getYNNeighborID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(zNNeighborID, getZNNeighborID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(xPNeighborID, getXPNeighborID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(yPNeighborID, getYPNeighborID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(zPNeighborID, getZPNeighborID); return properties; } @@ -116,6 +130,12 @@ bool PolyVoxEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(xTextureURL, setXTextureURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(yTextureURL, setYTextureURL); SET_ENTITY_PROPERTY_FROM_PROPERTIES(zTextureURL, setZTextureURL); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(xNNeighborID, setXNNeighborID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(yNNeighborID, setYNNeighborID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(zNNeighborID, setZNNeighborID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(xPNeighborID, setXPNeighborID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(yPNeighborID, setYPNeighborID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(zPNeighborID, setZPNeighborID); if (somethingChanged) { bool wantDebug = false; @@ -143,6 +163,12 @@ int PolyVoxEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* dat READ_ENTITY_PROPERTY(PROP_X_TEXTURE_URL, QString, setXTextureURL); READ_ENTITY_PROPERTY(PROP_Y_TEXTURE_URL, QString, setYTextureURL); READ_ENTITY_PROPERTY(PROP_Z_TEXTURE_URL, QString, setZTextureURL); + READ_ENTITY_PROPERTY(PROP_X_N_NEIGHBOR_ID, EntityItemID, setXNNeighborID); + READ_ENTITY_PROPERTY(PROP_Y_N_NEIGHBOR_ID, EntityItemID, setYNNeighborID); + READ_ENTITY_PROPERTY(PROP_Z_N_NEIGHBOR_ID, EntityItemID, setZNNeighborID); + READ_ENTITY_PROPERTY(PROP_X_P_NEIGHBOR_ID, EntityItemID, setXPNeighborID); + READ_ENTITY_PROPERTY(PROP_Y_P_NEIGHBOR_ID, EntityItemID, setYPNeighborID); + READ_ENTITY_PROPERTY(PROP_Z_P_NEIGHBOR_ID, EntityItemID, setZPNeighborID); return bytesRead; } @@ -157,16 +183,22 @@ EntityPropertyFlags PolyVoxEntityItem::getEntityProperties(EncodeBitstreamParams requestedProperties += PROP_X_TEXTURE_URL; requestedProperties += PROP_Y_TEXTURE_URL; requestedProperties += PROP_Z_TEXTURE_URL; + requestedProperties += PROP_X_N_NEIGHBOR_ID; + requestedProperties += PROP_Y_N_NEIGHBOR_ID; + requestedProperties += PROP_Z_N_NEIGHBOR_ID; + requestedProperties += PROP_X_P_NEIGHBOR_ID; + requestedProperties += PROP_Y_P_NEIGHBOR_ID; + requestedProperties += PROP_Z_P_NEIGHBOR_ID; return requestedProperties; } -void PolyVoxEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, +void PolyVoxEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, EntityTreeElementExtraEncodeData* modelTreeElementExtraEncodeData, EntityPropertyFlags& requestedProperties, EntityPropertyFlags& propertyFlags, EntityPropertyFlags& propertiesDidntFit, - int& propertyCount, - OctreeElement::AppendState& appendState) const { + int& propertyCount, + OctreeElement::AppendState& appendState) const { bool successPropertyFits = true; APPEND_ENTITY_PROPERTY(PROP_VOXEL_VOLUME_SIZE, getVoxelVolumeSize()); @@ -175,7 +207,12 @@ void PolyVoxEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeB APPEND_ENTITY_PROPERTY(PROP_X_TEXTURE_URL, getXTextureURL()); APPEND_ENTITY_PROPERTY(PROP_Y_TEXTURE_URL, getYTextureURL()); APPEND_ENTITY_PROPERTY(PROP_Z_TEXTURE_URL, getZTextureURL()); - + APPEND_ENTITY_PROPERTY(PROP_X_N_NEIGHBOR_ID, getXNNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Y_N_NEIGHBOR_ID, getYNNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Z_N_NEIGHBOR_ID, getZNNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_X_P_NEIGHBOR_ID, getXPNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Y_P_NEIGHBOR_ID, getYPNeighborID()); + APPEND_ENTITY_PROPERTY(PROP_Z_P_NEIGHBOR_ID, getZPNeighborID()); } void PolyVoxEntityItem::debugDump() const { @@ -187,15 +224,12 @@ void PolyVoxEntityItem::debugDump() const { } void PolyVoxEntityItem::setVoxelData(QByteArray voxelData) { - _voxelDataLock.lockForWrite(); + QWriteLocker(&this->_voxelDataLock); _voxelData = voxelData; _voxelDataDirty = true; - _voxelDataLock.unlock(); } const QByteArray PolyVoxEntityItem::getVoxelData() const { - _voxelDataLock.lockForRead(); - auto result = _voxelData; - _voxelDataLock.unlock(); - return result; + QReadLocker(&this->_voxelDataLock); + return _voxelData; } diff --git a/libraries/entities/src/PolyVoxEntityItem.h b/libraries/entities/src/PolyVoxEntityItem.h index c84dc9f4c1..20a0646c9b 100644 --- a/libraries/entities/src/PolyVoxEntityItem.h +++ b/libraries/entities/src/PolyVoxEntityItem.h @@ -50,7 +50,7 @@ class PolyVoxEntityItem : public EntityItem { virtual void debugDump() const; virtual void setVoxelVolumeSize(glm::vec3 voxelVolumeSize); - virtual const glm::vec3& getVoxelVolumeSize() const { return _voxelVolumeSize; } + virtual const glm::vec3& getVoxelVolumeSize() const; virtual void setVoxelData(QByteArray voxelData); virtual const QByteArray getVoxelData() const; @@ -85,6 +85,7 @@ class PolyVoxEntityItem : public EntityItem { // coords are in world-space virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue) { return false; } virtual bool setAll(uint8_t toValue) { return false; } + virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value) { return false; } virtual uint8_t getVoxel(int x, int y, int z) { return 0; } virtual bool setVoxel(int x, int y, int z, uint8_t toValue) { return false; } @@ -103,6 +104,28 @@ class PolyVoxEntityItem : public EntityItem { virtual void setZTextureURL(QString zTextureURL) { _zTextureURL = zTextureURL; } virtual const QString& getZTextureURL() const { return _zTextureURL; } + virtual void setXNNeighborID(const EntityItemID& xNNeighborID) { _xNNeighborID = xNNeighborID; } + void setXNNeighborID(const QString& xNNeighborID) { setXNNeighborID(QUuid(xNNeighborID)); } + virtual const EntityItemID& getXNNeighborID() const { return _xNNeighborID; } + virtual void setYNNeighborID(const EntityItemID& yNNeighborID) { _yNNeighborID = yNNeighborID; } + void setYNNeighborID(const QString& yNNeighborID) { setYNNeighborID(QUuid(yNNeighborID)); } + virtual const EntityItemID& getYNNeighborID() const { return _yNNeighborID; } + virtual void setZNNeighborID(const EntityItemID& zNNeighborID) { _zNNeighborID = zNNeighborID; } + void setZNNeighborID(const QString& zNNeighborID) { setZNNeighborID(QUuid(zNNeighborID)); } + virtual const EntityItemID& getZNNeighborID() const { return _zNNeighborID; } + + virtual void setXPNeighborID(const EntityItemID& xPNeighborID) { _xPNeighborID = xPNeighborID; } + void setXPNeighborID(const QString& xPNeighborID) { setXPNeighborID(QUuid(xPNeighborID)); } + virtual const EntityItemID& getXPNeighborID() const { return _xPNeighborID; } + virtual void setYPNeighborID(const EntityItemID& yPNeighborID) { _yPNeighborID = yPNeighborID; } + void setYPNeighborID(const QString& yPNeighborID) { setYPNeighborID(QUuid(yPNeighborID)); } + virtual const EntityItemID& getYPNeighborID() const { return _yPNeighborID; } + virtual void setZPNeighborID(const EntityItemID& zPNeighborID) { _zPNeighborID = zPNeighborID; } + void setZPNeighborID(const QString& zPNeighborID) { setZPNeighborID(QUuid(zPNeighborID)); } + virtual const EntityItemID& getZPNeighborID() const { return _zPNeighborID; } + + virtual void rebakeMesh() {}; + protected: glm::vec3 _voxelVolumeSize; // this is always 3 bytes @@ -116,6 +139,14 @@ class PolyVoxEntityItem : public EntityItem { QString _yTextureURL; QString _zTextureURL; + // for non-edged surface styles, these are used to compute the high-axis edges + EntityItemID _xNNeighborID{UNKNOWN_ENTITY_ID}; + EntityItemID _yNNeighborID{UNKNOWN_ENTITY_ID}; + EntityItemID _zNNeighborID{UNKNOWN_ENTITY_ID}; + + EntityItemID _xPNeighborID{UNKNOWN_ENTITY_ID}; + EntityItemID _yPNeighborID{UNKNOWN_ENTITY_ID}; + EntityItemID _zPNeighborID{UNKNOWN_ENTITY_ID}; }; #endif // hifi_PolyVoxEntityItem_h diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index 8bbe8dafd8..35390a8e44 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1722,7 +1722,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping glm::vec3 rotationOffset; glm::vec3 preRotation, rotation, postRotation; glm::vec3 scale = glm::vec3(1.0f, 1.0f, 1.0f); - glm::vec3 scalePivot, rotationPivot; + glm::vec3 scalePivot, rotationPivot, scaleOffset; bool rotationMinX = false, rotationMinY = false, rotationMinZ = false; bool rotationMaxX = false, rotationMaxY = false, rotationMaxZ = false; glm::vec3 rotationMin, rotationMax; @@ -1771,12 +1771,14 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } else if (property.properties.at(0) == "Lcl Scaling") { scale = getVec3(property.properties, index); + } else if (property.properties.at(0) == "ScalingOffset") { + scaleOffset = getVec3(property.properties, index); + + // NOTE: these rotation limits are stored in degrees (NOT radians) } else if (property.properties.at(0) == "RotationMin") { rotationMin = getVec3(property.properties, index); - } - // NOTE: these rotation limits are stored in degrees (NOT radians) - else if (property.properties.at(0) == "RotationMax") { + } else if (property.properties.at(0) == "RotationMax") { rotationMax = getVec3(property.properties, index); } else if (property.properties.at(0) == "RotationMinX") { @@ -1843,8 +1845,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping model.preRotation = glm::quat(glm::radians(preRotation)); model.rotation = glm::quat(glm::radians(rotation)); model.postRotation = glm::quat(glm::radians(postRotation)); - model.postTransform = glm::translate(-rotationPivot) * glm::translate(scalePivot) * - glm::scale(scale) * glm::translate(-scalePivot); + model.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) * + glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot); // NOTE: angles from the FBX file are in degrees // so we convert them to radians for the FBXModel class model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f, @@ -2306,7 +2308,9 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping break; } } - + + joint.bindTransformFoundInCluster = false; + geometry.joints.append(joint); geometry.jointIndices.insert(model.name, geometry.joints.size()); @@ -2534,7 +2538,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping FBXJoint& joint = geometry.joints[fbxCluster.jointIndex]; joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink)); joint.bindTransform = cluster.transformLink; - + joint.bindTransformFoundInCluster = true; + // update the bind pose extents glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform); geometry.bindExtents.addPoint(bindTranslation); @@ -2557,6 +2562,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping int maxJointIndex = firstFBXCluster.jointIndex; glm::mat4 inverseModelTransform = glm::inverse(modelTransform); if (clusterIDs.size() > 1) { + // this is a multi-mesh joint extracted.mesh.clusterIndices.resize(extracted.mesh.vertices.size()); extracted.mesh.clusterWeights.resize(extracted.mesh.vertices.size()); float maxWeight = 0.0f; @@ -2640,6 +2646,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } } } else { + // this is a single-mesh joint int jointIndex = maxJointIndex; FBXJoint& joint = geometry.joints[jointIndex]; JointShapeInfo& jointShapeInfo = jointShapeInfos[jointIndex]; @@ -2660,6 +2667,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } float radiusScale = extractUniformScale(joint.transform * firstFBXCluster.inverseBindMatrix); + // compute average vertex glm::vec3 averageVertex(0.0f); foreach (const glm::vec3& vertex, extracted.mesh.vertices) { float proj = glm::dot(boneDirection, boneEnd - vertex); @@ -2669,16 +2677,26 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping ++jointShapeInfo.numVertexWeights; averageVertex += vertex; } + + // compute joint's radius int numVertices = extracted.mesh.vertices.size(); jointShapeInfo.numVertices = numVertices; if (numVertices > 0) { + // compute average radius averageVertex /= (float)jointShapeInfo.numVertices; float averageRadius = 0.0f; foreach (const glm::vec3& vertex, extracted.mesh.vertices) { averageRadius += glm::distance(vertex, averageVertex); } - jointShapeInfo.averageRadius = averageRadius * radiusScale; + averageRadius *= radiusScale / (float)jointShapeInfo.numVertices; + + // final radius is minimum of average and weighted + float weightedRadius = jointShapeInfo.sumWeightedRadii / jointShapeInfo.sumVertexWeights; + jointShapeInfo.averageRadius = glm::min(weightedRadius, averageRadius); } + + // clear sumVertexWeights (this flags it as a single-mesh joint for later) + jointShapeInfo.sumVertexWeights = 0.0f; } extracted.mesh.isEye = (maxJointIndex == geometry.leftEyeJointIndex || maxJointIndex == geometry.rightEyeJointIndex); @@ -2714,23 +2732,12 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } if (jointShapeInfo.sumVertexWeights > 0.0f) { + // mutiple meshes contributed to the bone radius and now that all + // contributing meshes are done we can finally compute the boneRadius joint.boneRadius = jointShapeInfo.sumWeightedRadii / jointShapeInfo.sumVertexWeights; - } - - // the joint is "capsule-like" if it had ANY mesh vertices successfully projected onto the bone - // AND its boneRadius is not too close to zero - bool collideLikeCapsule = jointShapeInfo.numVertexWeights > 0 - && glm::length(jointShapeInfo.boneBegin) > EPSILON; - - if (!collideLikeCapsule) { - // this joint's mesh did not successfully project onto the bone axis - // so it isn't "capsule-like" and we need to estimate its radius a different way: - // the average radius to the average point. - if (jointShapeInfo.numVertexWeights == 0 - && jointShapeInfo.numVertices > 0) { - jointShapeInfo.averageRadius /= (float)jointShapeInfo.numVertices; - joint.boneRadius = jointShapeInfo.averageRadius; - } + } else { + // single-mesh joint + joint.boneRadius = jointShapeInfo.averageRadius; } } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 471a9c1777..158b5581c6 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -64,12 +64,18 @@ public: int parentIndex; float distanceToParent; float boneRadius; - glm::vec3 translation; - glm::mat4 preTransform; - glm::quat preRotation; - glm::quat rotation; - glm::quat postRotation; - glm::mat4 postTransform; + + // http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html + + glm::vec3 translation; // T + glm::mat4 preTransform; // Roff * Rp + glm::quat preRotation; // Rpre + glm::quat rotation; // R + glm::quat postRotation; // Rpost + glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1 + + // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) + glm::mat4 transform; glm::vec3 rotationMin; // radians glm::vec3 rotationMax; // radians @@ -78,6 +84,7 @@ public: glm::mat4 bindTransform; QString name; bool isSkeletonJoint; + bool bindTransformFoundInCluster; }; diff --git a/libraries/input-plugins/src/input-plugins/SixenseManager.cpp b/libraries/input-plugins/src/input-plugins/SixenseManager.cpp index 579fddcd63..f090db7959 100644 --- a/libraries/input-plugins/src/input-plugins/SixenseManager.cpp +++ b/libraries/input-plugins/src/input-plugins/SixenseManager.cpp @@ -481,6 +481,7 @@ void SixenseManager::handlePoseEvent(glm::vec3 position, glm::quat rotation, int // Qsh = angleAxis(PI, zAxis) * angleAxis(-PI/2, xAxis) // const glm::vec3 xAxis = glm::vec3(1.0f, 0.0f, 0.0f); + const glm::vec3 yAxis = glm::vec3(0.0f, 1.0f, 0.0f); const glm::vec3 zAxis = glm::vec3(0.0f, 0.0f, 1.0f); const glm::quat sixenseToHand = glm::angleAxis(PI, zAxis) * glm::angleAxis(-PI/2.0f, xAxis); @@ -491,13 +492,15 @@ void SixenseManager::handlePoseEvent(glm::vec3 position, glm::quat rotation, int const glm::quat preOffset = glm::angleAxis(sign * PI / 2.0f, zAxis); // Finally, there is a post-offset (same for both hands) to get the hand's rest orientation - // (fingers forward, palm down) aligned properly in the avatar's model-frame. - const glm::quat postOffset = glm::angleAxis(PI / 2.0f, xAxis); + // (fingers forward, palm down) aligned properly in the avatar's model-frame, + // and then a flip about the yAxis to get into model-frame. + const glm::quat postOffset = glm::angleAxis(PI, yAxis) * glm::angleAxis(PI / 2.0f, xAxis); // The total rotation of the hand uses the formula: // // rotation = postOffset * Qsh^ * (measuredRotation * preOffset) * Qsh // + // TODO: find a shortcut with fewer rotations. rotation = postOffset * glm::inverse(sixenseToHand) * rotation * preOffset * sixenseToHand; _poseStateMap[makeInput(JointChannel(index)).getChannel()] = UserInputMapper::PoseValue(position, rotation); diff --git a/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp b/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp index 082c37a837..5410db11a4 100644 --- a/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp +++ b/libraries/input-plugins/src/input-plugins/ViveControllerManager.cpp @@ -362,18 +362,17 @@ void ViveControllerManager::handlePoseEvent(const mat4& mat, int index) { // // Qoffset = glm::inverse(deltaRotation when hand is posed fingers forward, palm down) // - // An approximate offset for the Vive can be obtained by inpection: + // An approximate offset for the Vive can be obtained by inspection: // // Qoffset = glm::inverse(glm::angleAxis(sign * PI/4.0f, zAxis) * glm::angleAxis(PI/2.0f, xAxis)) // - - // Finally there is another flip around the yAxis to re-align from model to Vive space, so the full equation is: + // So the full equation is: // - // Q = yFlip * combinedMeasurement * viveToHand + // Q = combinedMeasurement * viveToHand // - // Q = yFlip * (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) + // Q = (deltaQ * QOffset) * (yFlip * quarterTurnAboutX) // - // Q = yFlip * (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) + // Q = (deltaQ * inverse(deltaQForAlignedHand)) * (yFlip * quarterTurnAboutX) const glm::quat quarterX = glm::angleAxis(PI / 2.0f, glm::vec3(1.0f, 0.0f, 0.0f)); const glm::quat yFlip = glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); @@ -381,7 +380,7 @@ void ViveControllerManager::handlePoseEvent(const mat4& mat, int index) { const glm::quat signedQuaterZ = glm::angleAxis(sign * PI / 2.0f, glm::vec3(0.0f, 0.0f, 1.0f)); const glm::quat eighthX = glm::angleAxis(PI / 4.0f, glm::vec3(1.0f, 0.0f, 0.0f)); const glm::quat offset = glm::inverse(signedQuaterZ * eighthX); - rotation = yFlip * rotation * offset * yFlip * quarterX; + rotation = rotation * offset * yFlip * quarterX; position += rotation * glm::vec3(0, 0, -CONTROLLER_LENGTH_OFFSET); diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp index 9e0320f6b7..7fc7359330 100644 --- a/libraries/networking/src/FileResourceRequest.cpp +++ b/libraries/networking/src/FileResourceRequest.cpp @@ -16,6 +16,12 @@ void FileResourceRequest::doSend() { QString filename = _url.toLocalFile(); + // sometimes on windows, we see the toLocalFile() return null, + // in this case we will attempt to simply use the url as a string + if (fileName.isEmpty()) { + fileName = url.toString(); + } + QFile file(filename); if (file.exists()) { if (file.open(QFile::ReadOnly)) { diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 28e0bc716a..873bcb695e 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -136,6 +136,7 @@ const PacketVersion VERSION_POLYVOX_TEXTURES = 36; const PacketVersion VERSION_ENTITIES_POLYLINE = 37; const PacketVersion VERSION_OCTREE_CENTERED_ORIGIN = 38; const PacketVersion VERSION_ENTITIES_PARTICLE_MODIFICATIONS = 39; -const PacketVersion VERSION_ENTITIES_PROTOCOL_HEADER_SWAP = 40; +const PacketVersion VERSION_ENTITIES_POLYVOX_NEIGHBORS = 40; +const PacketVersion VERSION_ENTITIES_PROTOCOL_HEADER_SWAP = 41; #endif // hifi_PacketHeaders_h diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp new file mode 100644 index 0000000000..1c0f7e0054 --- /dev/null +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -0,0 +1,430 @@ +// +// AnimDebugDraw.cpp +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "animdebugdraw_vert.h" +#include "animdebugdraw_frag.h" +#include +#include "AbstractViewStateInterface.h" +#include "RenderUtilsLogging.h" +#include "GLMHelpers.h" + +#include "AnimDebugDraw.h" + +struct Vertex { + glm::vec3 pos; + uint32_t rgba; +}; + +class AnimDebugDrawData { +public: + typedef render::Payload Payload; + typedef Payload::DataPointer Pointer; + + AnimDebugDrawData() { + + _vertexFormat = std::make_shared(); + _vertexBuffer = std::make_shared(); + _indexBuffer = std::make_shared(); + + _vertexFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element::VEC3F_XYZ, 0); + _vertexFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element::COLOR_RGBA_32, offsetof(Vertex, rgba)); + } + + void render(RenderArgs* args) { + auto& batch = *args->_batch; + batch.setPipeline(_pipeline); + auto transform = Transform{}; + batch.setModelTransform(transform); + + batch.setInputFormat(_vertexFormat); + batch.setInputBuffer(0, _vertexBuffer, 0, sizeof(Vertex)); + batch.setIndexBuffer(gpu::UINT16, _indexBuffer, 0); + + auto numIndices = _indexBuffer->getSize() / sizeof(uint16_t); + batch.drawIndexed(gpu::LINES, numIndices); + } + + gpu::PipelinePointer _pipeline; + render::ItemID _item; + gpu::Stream::FormatPointer _vertexFormat; + gpu::BufferPointer _vertexBuffer; + gpu::BufferPointer _indexBuffer; +}; + +typedef render::Payload AnimDebugDrawPayload; + +namespace render { + template <> const ItemKey payloadGetKey(const AnimDebugDrawData::Pointer& data) { return ItemKey::Builder::opaqueShape(); } + template <> const Item::Bound payloadGetBound(const AnimDebugDrawData::Pointer& data) { return Item::Bound(); } + template <> void payloadRender(const AnimDebugDrawData::Pointer& data, RenderArgs* args) { + data->render(args); + } +} + +static AnimDebugDraw* instance = nullptr; + +AnimDebugDraw& AnimDebugDraw::getInstance() { + if (!instance) { + instance = new AnimDebugDraw(); + } + return *instance; +} + +static uint32_t toRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + return ((uint32_t)r | (uint32_t)g << 8 | (uint32_t)b << 16 | (uint32_t)a << 24); +} + +static uint32_t toRGBA(const glm::vec4& v) { + return toRGBA(static_cast(v.r * 255.0f), + static_cast(v.g * 255.0f), + static_cast(v.b * 255.0f), + static_cast(v.a * 255.0f)); +} + +gpu::PipelinePointer AnimDebugDraw::_pipeline; + +AnimDebugDraw::AnimDebugDraw() : + _itemID(0) { + + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, + gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, + gpu::State::BLEND_OP_ADD, gpu::State::ONE); + auto vertShader = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(animdebugdraw_vert))); + auto fragShader = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(animdebugdraw_frag))); + auto program = gpu::ShaderPointer(gpu::Shader::createProgram(vertShader, fragShader)); + _pipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); + + _animDebugDrawData = std::make_shared(); + _animDebugDrawPayload = std::make_shared(_animDebugDrawData); + + _animDebugDrawData->_pipeline = _pipeline; + + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + if (scene) { + _itemID = scene->allocateID(); + render::PendingChanges pendingChanges; + pendingChanges.resetItem(_itemID, _animDebugDrawPayload); + scene->enqueuePendingChanges(pendingChanges); + } + + // HACK: add red, green and blue axis at (1,1,1) + _animDebugDrawData->_vertexBuffer->resize(sizeof(Vertex) * 6); + Vertex* data = (Vertex*)_animDebugDrawData->_vertexBuffer->editData(); + + data[0].pos = glm::vec3(1.0, 1.0f, 1.0f); + data[0].rgba = toRGBA(255, 0, 0, 255); + data[1].pos = glm::vec3(2.0, 1.0f, 1.0f); + data[1].rgba = toRGBA(255, 0, 0, 255); + + data[2].pos = glm::vec3(1.0, 1.0f, 1.0f); + data[2].rgba = toRGBA(0, 255, 0, 255); + data[3].pos = glm::vec3(1.0, 2.0f, 1.0f); + data[3].rgba = toRGBA(0, 255, 0, 255); + + data[4].pos = glm::vec3(1.0, 1.0f, 1.0f); + data[4].rgba = toRGBA(0, 0, 255, 255); + data[5].pos = glm::vec3(1.0, 1.0f, 2.0f); + data[5].rgba = toRGBA(0, 0, 255, 255); + + _animDebugDrawData->_indexBuffer->resize(sizeof(uint16_t) * 6); + uint16_t* indices = (uint16_t*)_animDebugDrawData->_indexBuffer->editData(); + for (int i = 0; i < 6; i++) { + indices[i] = i; + } + +} + +AnimDebugDraw::~AnimDebugDraw() { + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + if (scene && _itemID) { + render::PendingChanges pendingChanges; + pendingChanges.removeItem(_itemID); + scene->enqueuePendingChanges(pendingChanges); + } +} + +void AnimDebugDraw::addSkeleton(std::string key, AnimSkeleton::ConstPointer skeleton, const AnimPose& rootPose, const glm::vec4& color) { + _skeletons[key] = SkeletonInfo(skeleton, rootPose, color); +} + +void AnimDebugDraw::removeSkeleton(std::string key) { + _skeletons.erase(key); +} + +void AnimDebugDraw::addAnimNode(std::string key, AnimNode::ConstPointer animNode, const AnimPose& rootPose, const glm::vec4& color) { + _animNodes[key] = AnimNodeInfo(animNode, rootPose, color); +} + +void AnimDebugDraw::removeAnimNode(std::string key) { + _animNodes.erase(key); +} + +static const uint32_t red = toRGBA(255, 0, 0, 255); +static const uint32_t green = toRGBA(0, 255, 0, 255); +static const uint32_t blue = toRGBA(0, 0, 255, 255); + +const int NUM_CIRCLE_SLICES = 24; + +static void addBone(const AnimPose& rootPose, const AnimPose& pose, float radius, Vertex*& v) { + + AnimPose finalPose = rootPose * pose; + glm::vec3 base = rootPose * pose.trans; + + glm::vec3 xRing[NUM_CIRCLE_SLICES + 1]; // one extra for last index. + glm::vec3 yRing[NUM_CIRCLE_SLICES + 1]; + glm::vec3 zRing[NUM_CIRCLE_SLICES + 1]; + const float dTheta = (2.0f * (float)M_PI) / NUM_CIRCLE_SLICES; + for (int i = 0; i < NUM_CIRCLE_SLICES + 1; i++) { + float rCosTheta = radius * cos(dTheta * i); + float rSinTheta = radius * sin(dTheta * i); + xRing[i] = finalPose * glm::vec3(0.0f, rCosTheta, rSinTheta); + yRing[i] = finalPose * glm::vec3(rCosTheta, 0.0f, rSinTheta); + zRing[i] = finalPose * glm::vec3(rCosTheta, rSinTheta, 0.0f); + } + + // x-axis + v->pos = base; + v->rgba = red; + v++; + v->pos = finalPose * glm::vec3(radius * 2.0f, 0.0f, 0.0f); + v->rgba = red; + v++; + + // x-ring + for (int i = 0; i < NUM_CIRCLE_SLICES; i++) { + v->pos = xRing[i]; + v->rgba = red; + v++; + v->pos = xRing[i + 1]; + v->rgba = red; + v++; + } + + // y-axis + v->pos = base; + v->rgba = green; + v++; + v->pos = finalPose * glm::vec3(0.0f, radius * 2.0f, 0.0f); + v->rgba = green; + v++; + + // y-ring + for (int i = 0; i < NUM_CIRCLE_SLICES; i++) { + v->pos = yRing[i]; + v->rgba = green; + v++; + v->pos = yRing[i + 1]; + v->rgba = green; + v++; + } + + // z-axis + v->pos = base; + v->rgba = blue; + v++; + v->pos = finalPose * glm::vec3(0.0f, 0.0f, radius * 2.0f); + v->rgba = blue; + v++; + + // z-ring + for (int i = 0; i < NUM_CIRCLE_SLICES; i++) { + v->pos = zRing[i]; + v->rgba = blue; + v++; + v->pos = zRing[i + 1]; + v->rgba = blue; + v++; + } +} + +static void addLink(const AnimPose& rootPose, const AnimPose& pose, const AnimPose& parentPose, + float radius, const glm::vec4& colorVec, Vertex*& v) { + + uint32_t color = toRGBA(colorVec); + + AnimPose pose0 = rootPose * parentPose; + AnimPose pose1 = rootPose * pose; + + glm::vec3 boneAxisWorld = glm::normalize(pose1.trans - pose0.trans); + glm::vec3 boneAxis0 = glm::normalize(pose0.inverse().xformVector(boneAxisWorld)); + glm::vec3 boneAxis1 = glm::normalize(pose1.inverse().xformVector(boneAxisWorld)); + + glm::vec3 boneBase = pose0 * (boneAxis0 * radius); + glm::vec3 boneTip = pose1 * (boneAxis1 * -radius); + + const int NUM_BASE_CORNERS = 4; + + // make sure there's room between the two bones to draw a nice bone link. + if (glm::dot(boneTip - pose0.trans, boneAxisWorld) > glm::dot(boneBase - pose0.trans, boneAxisWorld)) { + + // there is room, so lets draw a nice bone + + glm::vec3 uAxis, vAxis, wAxis; + generateBasisVectors(boneAxis0, glm::vec3(1, 0, 0), uAxis, vAxis, wAxis); + + glm::vec3 boneBaseCorners[NUM_BASE_CORNERS]; + boneBaseCorners[0] = pose0 * ((uAxis * radius) + (vAxis * radius) + (wAxis * radius)); + boneBaseCorners[1] = pose0 * ((uAxis * radius) - (vAxis * radius) + (wAxis * radius)); + boneBaseCorners[2] = pose0 * ((uAxis * radius) - (vAxis * radius) - (wAxis * radius)); + boneBaseCorners[3] = pose0 * ((uAxis * radius) + (vAxis * radius) - (wAxis * radius)); + + for (int i = 0; i < NUM_BASE_CORNERS; i++) { + v->pos = boneBaseCorners[i]; + v->rgba = color; + v++; + v->pos = boneBaseCorners[(i + 1) % NUM_BASE_CORNERS]; + v->rgba = color; + v++; + } + + for (int i = 0; i < NUM_BASE_CORNERS; i++) { + v->pos = boneBaseCorners[i]; + v->rgba = color; + v++; + v->pos = boneTip; + v->rgba = color; + v++; + } + } else { + // There's no room between the bones to draw the link. + // just draw a line between the two bone centers. + // We add the same line multiple times, so the vertex count is correct. + for (int i = 0; i < NUM_BASE_CORNERS * 2; i++) { + v->pos = pose0.trans; + v->rgba = color; + v++; + v->pos = pose1.trans; + v->rgba = color; + v++; + } + } +} + +void AnimDebugDraw::update() { + + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + if (!scene) { + return; + } + + render::PendingChanges pendingChanges; + pendingChanges.updateItem(_itemID, [&](AnimDebugDrawData& data) { + + const size_t VERTICES_PER_BONE = (6 + (NUM_CIRCLE_SLICES * 2) * 3); + const size_t VERTICES_PER_LINK = 8 * 2; + + const float BONE_RADIUS = 0.0075f; + + // figure out how many verts we will need. + int numVerts = 0; + for (auto&& iter : _skeletons) { + AnimSkeleton::ConstPointer& skeleton = std::get<0>(iter.second); + numVerts += skeleton->getNumJoints() * VERTICES_PER_BONE; + for (int i = 0; i < skeleton->getNumJoints(); i++) { + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + numVerts += VERTICES_PER_LINK; + } + } + } + + for (auto&& iter : _animNodes) { + AnimNode::ConstPointer& animNode = std::get<0>(iter.second); + auto poses = animNode->getPosesInternal(); + numVerts += poses.size() * VERTICES_PER_BONE; + auto skeleton = animNode->getSkeleton(); + for (size_t i = 0; i < poses.size(); i++) { + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + numVerts += VERTICES_PER_LINK; + } + } + } + + data._vertexBuffer->resize(sizeof(Vertex) * numVerts); + Vertex* verts = (Vertex*)data._vertexBuffer->editData(); + Vertex* v = verts; + for (auto&& iter : _skeletons) { + AnimSkeleton::ConstPointer& skeleton = std::get<0>(iter.second); + AnimPose rootPose = std::get<1>(iter.second); + int hipsIndex = skeleton->nameToJointIndex("Hips"); + if (hipsIndex >= 0) { + rootPose.trans -= skeleton->getRelativeBindPose(hipsIndex).trans; + } + glm::vec4 color = std::get<2>(iter.second); + + for (int i = 0; i < skeleton->getNumJoints(); i++) { + AnimPose pose = skeleton->getAbsoluteBindPose(i); + + const float radius = BONE_RADIUS / (pose.scale.x * rootPose.scale.x); + + // draw bone + addBone(rootPose, pose, radius, v); + + // draw link to parent + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + assert(parentIndex < skeleton->getNumJoints()); + AnimPose parentPose = skeleton->getAbsoluteBindPose(parentIndex); + addLink(rootPose, pose, parentPose, radius, color, v); + } + } + } + + for (auto&& iter : _animNodes) { + AnimNode::ConstPointer& animNode = std::get<0>(iter.second); + AnimPose rootPose = std::get<1>(iter.second); + if (animNode->_skeleton) { + int hipsIndex = animNode->_skeleton->nameToJointIndex("Hips"); + if (hipsIndex >= 0) { + rootPose.trans -= animNode->_skeleton->getRelativeBindPose(hipsIndex).trans; + } + } + glm::vec4 color = std::get<2>(iter.second); + + auto poses = animNode->getPosesInternal(); + + auto skeleton = animNode->getSkeleton(); + + std::vector absAnimPose; + absAnimPose.resize(skeleton->getNumJoints()); + + for (size_t i = 0; i < poses.size(); i++) { + + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + absAnimPose[i] = absAnimPose[parentIndex] * poses[i]; + } else { + absAnimPose[i] = poses[i]; + } + + const float radius = BONE_RADIUS / (absAnimPose[i].scale.x * rootPose.scale.x); + addBone(rootPose, absAnimPose[i], radius, v); + + if (parentIndex >= 0) { + assert((size_t)parentIndex < poses.size()); + // draw line to parent + addLink(rootPose, absAnimPose[i], absAnimPose[parentIndex], radius, color, v); + } + } + } + + assert(numVerts == (v - verts)); + + data._indexBuffer->resize(sizeof(uint16_t) * numVerts); + uint16_t* indices = (uint16_t*)data._indexBuffer->editData(); + for (int i = 0; i < numVerts; i++) { + indices[i] = i; + } + }); + scene->enqueuePendingChanges(pendingChanges); +} diff --git a/libraries/render-utils/src/AnimDebugDraw.h b/libraries/render-utils/src/AnimDebugDraw.h new file mode 100644 index 0000000000..489213b80b --- /dev/null +++ b/libraries/render-utils/src/AnimDebugDraw.h @@ -0,0 +1,56 @@ +// +// AnimDebugDraw.h +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimDebugDraw_h +#define hifi_AnimDebugDraw_h + +#include + +#include "render/Scene.h" +#include "gpu/Pipeline.h" +#include "AnimNode.h" +#include "AnimSkeleton.h" + +class AnimDebugDrawData; +typedef render::Payload AnimDebugDrawPayload; + +class AnimDebugDraw { +public: + static AnimDebugDraw& getInstance(); + + AnimDebugDraw(); + ~AnimDebugDraw(); + + void addSkeleton(std::string key, AnimSkeleton::ConstPointer skeleton, const AnimPose& rootPose, const glm::vec4& color); + void removeSkeleton(std::string key); + + void addAnimNode(std::string key, AnimNode::ConstPointer animNode, const AnimPose& rootPose, const glm::vec4& color); + void removeAnimNode(std::string key); + + void update(); + +protected: + std::shared_ptr _animDebugDrawData; + std::shared_ptr _animDebugDrawPayload; + render::ItemID _itemID; + + static gpu::PipelinePointer _pipeline; + + typedef std::tuple SkeletonInfo; + typedef std::tuple AnimNodeInfo; + + std::unordered_map _skeletons; + std::unordered_map _animNodes; + + // no copies + AnimDebugDraw(const AnimDebugDraw&) = delete; + AnimDebugDraw& operator=(const AnimDebugDraw&) = delete; +}; + +#endif // hifi_AnimDebugDraw diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp new file mode 100644 index 0000000000..0e362bfb49 --- /dev/null +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -0,0 +1,165 @@ +// +// AntialiasingEffect.cpp +// libraries/render-utils/src/ +// +// Created by Raffi Bedikian on 8/30/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include + +#include +#include +#include + +#include "gpu/StandardShaderLib.h" +#include "AntialiasingEffect.h" +#include "TextureCache.h" +#include "FramebufferCache.h" +#include "DependencyManager.h" +#include "ViewFrustum.h" +#include "GeometryCache.h" + +#include "fxaa_vert.h" +#include "fxaa_frag.h" +#include "fxaa_blend_frag.h" + + +Antialiasing::Antialiasing() { +} + +const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { + if (!_antialiasingPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(fxaa_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(fxaa_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("colorTexture"), 0)); + + gpu::Shader::makeProgram(*program, slotBindings); + + _texcoordOffsetLoc = program->getUniforms().findLocation("texcoordOffset"); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(false, false, gpu::LESS_EQUAL); + + // Link the antialiasing FBO to texture + _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, + DependencyManager::get()->getFrameBufferSize().width(), DependencyManager::get()->getFrameBufferSize().height())); + auto format = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + auto width = _antialiasingBuffer->getWidth(); + auto height = _antialiasingBuffer->getHeight(); + auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); + _antialiasingTexture = gpu::TexturePointer(gpu::Texture::create2D(format, width, height, defaultSampler)); + + // Good to go add the brand new pipeline + _antialiasingPipeline.reset(gpu::Pipeline::create(program, state)); + } + + int w = DependencyManager::get()->getFrameBufferSize().width(); + int h = DependencyManager::get()->getFrameBufferSize().height(); + if (w != _antialiasingBuffer->getWidth() || h != _antialiasingBuffer->getHeight()) { + _antialiasingBuffer->resize(w, h); + } + + return _antialiasingPipeline; +} + +const gpu::PipelinePointer& Antialiasing::getBlendPipeline() { + if (!_blendPipeline) { + auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(fxaa_vert))); + auto ps = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(fxaa_blend_frag))); + gpu::ShaderPointer program = gpu::ShaderPointer(gpu::Shader::createProgram(vs, ps)); + + gpu::Shader::BindingSet slotBindings; + slotBindings.insert(gpu::Shader::Binding(std::string("colorTexture"), 0)); + + gpu::Shader::makeProgram(*program, slotBindings); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + + state->setDepthTest(false, false, gpu::LESS_EQUAL); + + // Good to go add the brand new pipeline + _blendPipeline.reset(gpu::Pipeline::create(program, state)); + } + return _blendPipeline; +} + +void Antialiasing::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext) { + assert(renderContext->args); + assert(renderContext->args->_viewFrustum); + + if (renderContext->args->_renderMode == RenderArgs::MIRROR_RENDER_MODE) { + return; + } + + gpu::Batch batch; + + batch.enableStereo(false); + + RenderArgs* args = renderContext->args; + + auto framebufferCache = DependencyManager::get(); + QSize framebufferSize = framebufferCache->getFrameBufferSize(); + float fbWidth = framebufferSize.width(); + float fbHeight = framebufferSize.height(); + float sMin = args->_viewport.x / fbWidth; + float sWidth = args->_viewport.z / fbWidth; + float tMin = args->_viewport.y / fbHeight; + float tHeight = args->_viewport.w / fbHeight; + + glm::mat4 projMat; + Transform viewMat; + args->_viewFrustum->evalProjectionMatrix(projMat); + args->_viewFrustum->evalViewTransform(viewMat); + batch.setProjectionTransform(projMat); + batch.setViewTransform(viewMat); + batch.setModelTransform(Transform()); + + // FXAA step + getAntialiasingPipeline(); + batch.setResourceTexture(0, framebufferCache->getPrimaryColorTexture()); + _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); + batch.setFramebuffer(_antialiasingBuffer); + batch.setPipeline(getAntialiasingPipeline()); + + // initialize the view-space unpacking uniforms using frustum data + float left, right, bottom, top, nearVal, farVal; + glm::vec4 nearClipPlane, farClipPlane; + + args->_viewFrustum->computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); + + float depthScale = (farVal - nearVal) / farVal; + float nearScale = -1.0f / nearVal; + float depthTexCoordScaleS = (right - left) * nearScale / sWidth; + float depthTexCoordScaleT = (top - bottom) * nearScale / tHeight; + float depthTexCoordOffsetS = left * nearScale - sMin * depthTexCoordScaleS; + float depthTexCoordOffsetT = bottom * nearScale - tMin * depthTexCoordScaleT; + + batch._glUniform2f(_texcoordOffsetLoc, 1.0 / fbWidth, 1.0 / fbHeight); + + glm::vec4 color(0.0f, 0.0f, 0.0f, 1.0f); + glm::vec2 bottomLeft(-1.0f, -1.0f); + glm::vec2 topRight(1.0f, 1.0f); + glm::vec2 texCoordTopLeft(0.0f, 0.0f); + glm::vec2 texCoordBottomRight(1.0f, 1.0f); + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + + // Blend step + getBlendPipeline(); + batch.setResourceTexture(0, _antialiasingTexture); + batch.setFramebuffer(framebufferCache->getPrimaryFramebuffer()); + batch.setPipeline(getBlendPipeline()); + + DependencyManager::get()->renderQuad(batch, bottomLeft, topRight, texCoordTopLeft, texCoordBottomRight, color); + + // Ready to render + args->_context->render((batch)); +} diff --git a/libraries/render-utils/src/AntialiasingEffect.h b/libraries/render-utils/src/AntialiasingEffect.h new file mode 100644 index 0000000000..c7cce4cb15 --- /dev/null +++ b/libraries/render-utils/src/AntialiasingEffect.h @@ -0,0 +1,44 @@ +// +// AntialiasingEffect.h +// libraries/render-utils/src/ +// +// Created by Raffi Bedikian on 8/30/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AntialiasingEffect_h +#define hifi_AntialiasingEffect_h + +#include + +#include "render/DrawTask.h" + +class Antialiasing { +public: + + Antialiasing(); + + void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); + typedef render::Job::Model JobModel; + + const gpu::PipelinePointer& getAntialiasingPipeline(); + const gpu::PipelinePointer& getBlendPipeline(); + +private: + + // Uniforms for AA + gpu::int32 _texcoordOffsetLoc; + + gpu::FramebufferPointer _antialiasingBuffer; + + gpu::TexturePointer _antialiasingTexture; + + gpu::PipelinePointer _antialiasingPipeline; + gpu::PipelinePointer _blendPipeline; + +}; + +#endif // hifi_AntialiasingEffect_h diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 158f9aaa17..d8e3225a73 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1416,6 +1416,7 @@ void Model::deleteGeometry() { _rig->clearJointStates(); _meshStates.clear(); _rig->deleteAnimations(); + _rig->destroyAnimGraph(); _blendedBlendshapeCoefficients.clear(); } diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index ca3f87f53f..137294c5b6 100755 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -23,6 +23,7 @@ #include "render/DrawStatus.h" #include "AmbientOcclusionEffect.h" +#include "AntialiasingEffect.h" #include "overlay3D_vert.h" #include "overlay3D_frag.h" @@ -88,6 +89,11 @@ RenderDeferredTask::RenderDeferredTask() : Task() { _jobs.back().setEnabled(false); _occlusionJobIndex = _jobs.size() - 1; + _jobs.push_back(Job(new Antialiasing::JobModel("Antialiasing"))); + + _jobs.back().setEnabled(false); + _antialiasingJobIndex = _jobs.size() - 1; + _jobs.push_back(Job(new FetchItems::JobModel("FetchTransparent", FetchItems( ItemFilter::Builder::transparentShape().withoutLayered(), @@ -146,6 +152,8 @@ void RenderDeferredTask::run(const SceneContextPointer& sceneContext, const Rend // TODO: turn on/off AO through menu item setOcclusionStatus(renderContext->_occlusionStatus); + setAntialiasingStatus(renderContext->_fxaaStatus); + renderContext->args->_context->syncCache(); for (auto job : _jobs) { diff --git a/libraries/render-utils/src/RenderDeferredTask.h b/libraries/render-utils/src/RenderDeferredTask.h index 0041f5d9aa..8366a2665d 100755 --- a/libraries/render-utils/src/RenderDeferredTask.h +++ b/libraries/render-utils/src/RenderDeferredTask.h @@ -91,6 +91,11 @@ public: void setOcclusionStatus(bool draw) { if (_occlusionJobIndex >= 0) { _jobs[_occlusionJobIndex].setEnabled(draw); } } bool doOcclusionStatus() const { if (_occlusionJobIndex >= 0) { return _jobs[_occlusionJobIndex].isEnabled(); } else { return false; } } + int _antialiasingJobIndex = -1; + + void setAntialiasingStatus(bool draw) { if (_antialiasingJobIndex >= 0) { _jobs[_antialiasingJobIndex].setEnabled(draw); } } + bool doAntialiasingStatus() const { if (_antialiasingJobIndex >= 0) { return _jobs[_antialiasingJobIndex].isEnabled(); } else { return false; } } + virtual void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext); diff --git a/libraries/render-utils/src/animdebugdraw.slf b/libraries/render-utils/src/animdebugdraw.slf new file mode 100644 index 0000000000..8a3aca055e --- /dev/null +++ b/libraries/render-utils/src/animdebugdraw.slf @@ -0,0 +1,19 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// unlit untextured fragment shader +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +in vec4 _color; + +out vec4 _fragColor; + +void main(void) { + _fragColor = _color; +} diff --git a/libraries/render-utils/src/animdebugdraw.slv b/libraries/render-utils/src/animdebugdraw.slv new file mode 100644 index 0000000000..f3117714b0 --- /dev/null +++ b/libraries/render-utils/src/animdebugdraw.slv @@ -0,0 +1,26 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// unlit untextured vertex shader +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> + +<@include gpu/Transform.slh@> + +<$declareStandardTransform()$> + +out vec4 _color; + +void main(void) { + // pass along the diffuse color + _color = inColor.rgba; + + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> +} diff --git a/libraries/render-utils/src/fxaa.slf b/libraries/render-utils/src/fxaa.slf new file mode 100644 index 0000000000..d1c50b2c58 --- /dev/null +++ b/libraries/render-utils/src/fxaa.slf @@ -0,0 +1,94 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// fxaa.frag +// fragment shader +// +// Created by Raffi Bedikian on 8/30/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// FXAA shader, GLSL code adapted from: +// http://horde3d.org/wiki/index.php5?title=Shading_Technique_-_FXAA +// Whitepaper describing the technique: +// http://developer.download.nvidia.com/assets/gamedev/files/sdk/11/FXAA_WhitePaper.pdf + +#ifdef GL_ES +precision mediump float; +precision mediump int; +#endif + +uniform sampler2D colorTexture; +uniform vec2 texcoordOffset; + +in vec2 varTexcoord; +out vec4 outFragColor; + +void main() { + // filter width limit for dependent "two-tap" texture samples + float FXAA_SPAN_MAX = 8.0; + + // local contrast multiplier for performing AA + // higher = sharper, but setting this value too high will cause near-vertical and near-horizontal edges to fail + // see "fxaaQualityEdgeThreshold" + float FXAA_REDUCE_MUL = 1.0 / 8.0; + + // luminance threshold for processing dark colors + // see "fxaaQualityEdgeThresholdMin" + float FXAA_REDUCE_MIN = 1.0 / 128.0; + + // fetch raw RGB values for nearby locations + // sampling pattern is "five on a die" (each diagonal direction and the center) + // computing the coordinates for these texture reads could be moved to the vertex shader for speed if needed + vec3 rgbNW = texture2D(colorTexture, varTexcoord + (vec2(-1.0, -1.0) * texcoordOffset)).xyz; + vec3 rgbNE = texture2D(colorTexture, varTexcoord + (vec2(+1.0, -1.0) * texcoordOffset)).xyz; + vec3 rgbSW = texture2D(colorTexture, varTexcoord + (vec2(-1.0, +1.0) * texcoordOffset)).xyz; + vec3 rgbSE = texture2D(colorTexture, varTexcoord + (vec2(+1.0, +1.0) * texcoordOffset)).xyz; + vec3 rgbM = texture2D(colorTexture, varTexcoord).xyz; + + // convert RGB values to luminance + vec3 luma = vec3(0.299, 0.587, 0.114); + float lumaNW = dot(rgbNW, luma); + float lumaNE = dot(rgbNE, luma); + float lumaSW = dot(rgbSW, luma); + float lumaSE = dot(rgbSE, luma); + float lumaM = dot( rgbM, luma); + + // luma range of local neighborhood + float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE))); + float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE))); + + // direction perpendicular to local luma gradient + vec2 dir; + dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE)); + dir.y = ((lumaNW + lumaSW) - (lumaNE + lumaSE)); + + // compute clamped direction offset for additional "two-tap" samples + // longer vector = blurry, shorter vector = sharp + float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN); + float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); + dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), + max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), dir * rcpDirMin)) * texcoordOffset; + + // perform additional texture sampling perpendicular to gradient + vec3 rgbA = (1.0 / 2.0) * ( + texture2D(colorTexture, varTexcoord + dir * (1.0 / 3.0 - 0.5)).xyz + + texture2D(colorTexture, varTexcoord + dir * (2.0 / 3.0 - 0.5)).xyz); + vec3 rgbB = rgbA * (1.0 / 2.0) + (1.0 / 4.0) * ( + texture2D(colorTexture, varTexcoord + dir * (0.0 / 3.0 - 0.5)).xyz + + texture2D(colorTexture, varTexcoord + dir * (3.0 / 3.0 - 0.5)).xyz); + float lumaB = dot(rgbB, luma); + + // compare luma of new samples to the luma range of the original neighborhood + // if the new samples exceed this range, just use the first two samples instead of all four + if (lumaB < lumaMin || lumaB > lumaMax) { + outFragColor.xyz=rgbA; + } else { + outFragColor.xyz=rgbB; + } + outFragColor.a = 1.0; +} diff --git a/libraries/render-utils/src/fxaa.slv b/libraries/render-utils/src/fxaa.slv new file mode 100644 index 0000000000..35a96ceb24 --- /dev/null +++ b/libraries/render-utils/src/fxaa.slv @@ -0,0 +1,26 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// fxaa.vert +// vertex shader +// +// Created by Raffi Bedikian on 8/30/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> + +<@include gpu/Transform.slh@> + +<$declareStandardTransform()$> + +out vec2 varTexcoord; + +void main(void) { + varTexcoord = inTexCoord0.xy; + gl_Position = inPosition; +} diff --git a/libraries/render-utils/src/fxaa_blend.slf b/libraries/render-utils/src/fxaa_blend.slf new file mode 100644 index 0000000000..d5819cc9a6 --- /dev/null +++ b/libraries/render-utils/src/fxaa_blend.slf @@ -0,0 +1,24 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// fxaa_blend.frag +// fragment shader +// +// Created by Raffi Bedikian on 8/30/15 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include DeferredBufferWrite.slh@> + +in vec2 varTexcoord; +out vec4 outFragColor; + +uniform sampler2D colorTexture; + +void main(void) { + outFragColor = texture(colorTexture, varTexcoord); +} diff --git a/libraries/render-utils/src/simple.slv b/libraries/render-utils/src/simple.slv index 99f404eaec..b22bb36d83 100644 --- a/libraries/render-utils/src/simple.slv +++ b/libraries/render-utils/src/simple.slv @@ -23,10 +23,12 @@ out vec3 _normal; out vec3 _color; out vec2 _texCoord0; +out vec4 _position; void main(void) { _color = inColor.rgb; _texCoord0 = inTexCoord0.st; + _position = inPosition; // standard transform TransformCamera cam = getTransformCamera(); diff --git a/libraries/render/src/render/Engine.h b/libraries/render/src/render/Engine.h index 8d096c5e15..5da7956b22 100644 --- a/libraries/render/src/render/Engine.h +++ b/libraries/render/src/render/Engine.h @@ -53,6 +53,7 @@ public: bool _drawHitEffect = false; bool _occlusionStatus = false; + bool _fxaaStatus = false; RenderContext() {} }; diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp index 46ef05e65a..ad3f422813 100644 --- a/libraries/script-engine/src/BatchLoader.cpp +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -34,6 +34,7 @@ void BatchLoader::start() { } _started = true; + for (const auto& url : _urls) { auto request = ResourceManager::createResourceRequest(this, url); if (!request) { diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 9cd659dd5d..14a6573415 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -18,6 +18,26 @@ #include "ScriptEngineLogging.h" #include "Quat.h" +quat Quat::normalize(const glm::quat& q) { + return glm::normalize(q); +} + +glm::quat Quat::rotationBetween(const glm::vec3& v1, const glm::vec3& v2) { + return ::rotationBetween(v1, v2); +} + +glm::quat Quat::lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up) { + return glm::quat_cast(glm::lookAt(eye, center, up)); +} + +glm::quat Quat::lookAtSimple(const glm::vec3& eye, const glm::vec3& center) { + auto dir = glm::normalize(center - eye); + // if the direction is nearly aligned with the Y axis, then use the X axis for 'up' + if (dir.x < 0.001f && dir.z < 0.001f) { + return lookAt(eye, center, Vectors::UNIT_X); + } + return lookAt(eye, center, Vectors::UNIT_Y); +} glm::quat Quat::multiply(const glm::quat& q1, const glm::quat& q2) { return q1 * q2; diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 1f130c57c7..543c93401f 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -25,6 +25,10 @@ class Quat : public QObject { public slots: glm::quat multiply(const glm::quat& q1, const glm::quat& q2); + glm::quat normalize(const glm::quat& q); + glm::quat lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up); + glm::quat lookAtSimple(const glm::vec3& eye, const glm::vec3& center); + glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); glm::quat fromVec3Degrees(const glm::vec3& vec3); // degrees glm::quat fromVec3Radians(const glm::vec3& vec3); // radians glm::quat fromPitchYawRollDegrees(float pitch, float yaw, float roll); // degrees diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index aa395b1b06..eeff6c15f0 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -599,7 +599,8 @@ void ScriptEngine::run() { / (1000 * 1000)) + 0.5); const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t); - QByteArray avatarByteArray = _avatarData->toByteArray(true); + QByteArray avatarByteArray = _avatarData->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); + _avatarData->doneEncoding(true); auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size()); avatarPacket->write(avatarByteArray); diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp index 4ed16b2ef0..69961bfd8e 100644 --- a/libraries/script-engine/src/Vec3.cpp +++ b/libraries/script-engine/src/Vec3.cpp @@ -13,66 +13,26 @@ #include +#include + #include "ScriptEngineLogging.h" #include "NumericalConstants.h" #include "Vec3.h" -glm::vec3 Vec3::reflect(const glm::vec3& v1, const glm::vec3& v2) { - return glm::reflect(v1, v2); -} -glm::vec3 Vec3::cross(const glm::vec3& v1, const glm::vec3& v2) { - return glm::cross(v1,v2); -} - -float Vec3::dot(const glm::vec3& v1, const glm::vec3& v2) { - return glm::dot(v1,v2); -} - -glm::vec3 Vec3::multiply(const glm::vec3& v1, float f) { - return v1 * f; -} - -glm::vec3 Vec3::multiply(float f, const glm::vec3& v1) { - 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); -} - -float Vec3::distance(const glm::vec3& v1, const glm::vec3& v2) { - return glm::distance(v1, v2); -} float Vec3::orientedAngle(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3) { - return glm::degrees(glm::orientedAngle(glm::normalize(v1), glm::normalize(v2), glm::normalize(v3))); + float radians = glm::orientedAngle(glm::normalize(v1), glm::normalize(v2), glm::normalize(v3)); + return glm::degrees(radians); } -glm::vec3 Vec3::normalize(const glm::vec3& v) { - return glm::normalize(v); -} - -glm::vec3 Vec3::mix(const glm::vec3& v1, const glm::vec3& v2, float m) { - return glm::mix(v1, v2, m); -} void Vec3::print(const QString& lable, const glm::vec3& v) { qCDebug(scriptengine) << qPrintable(lable) << v.x << "," << v.y << "," << v.z; } -bool Vec3::equal(const glm::vec3& v1, const glm::vec3& v2) { - return v1 == v2; +bool Vec3::withinEpsilon(const glm::vec3& v1, const glm::vec3& v2, float epsilon) { + float distanceSquared = glm::length2(v1 - v2); + return (epsilon*epsilon) >= distanceSquared; } glm::vec3 Vec3::toPolar(const glm::vec3& v) { diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index 82062ca80d..b05e729a49 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -11,40 +11,59 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#pragma once #ifndef hifi_Vec3_h #define hifi_Vec3_h -#include -#include +#include +#include -#include -#include +#include "GLMHelpers.h" /// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API class Vec3 : public QObject { Q_OBJECT public slots: - glm::vec3 reflect(const glm::vec3& v1, const glm::vec3& v2); - glm::vec3 cross(const glm::vec3& v1, const glm::vec3& v2); - float dot(const glm::vec3& v1, const glm::vec3& v2); - glm::vec3 multiply(const glm::vec3& v1, float f); - glm::vec3 multiply(float, const glm::vec3& v1); - 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); - float distance(const glm::vec3& v1, const glm::vec3& v2); + glm::vec3 reflect(const glm::vec3& v1, const glm::vec3& v2) { return glm::reflect(v1, v2); } + glm::vec3 cross(const glm::vec3& v1, const glm::vec3& v2) { return glm::cross(v1, v2); } + float dot(const glm::vec3& v1, const glm::vec3& v2) { return glm::dot(v1, v2); } + glm::vec3 multiply(const glm::vec3& v1, float f) { return v1 * f; } + glm::vec3 multiply(float f, const glm::vec3& v1) { return v1 * f; } + glm::vec3 multiplyQbyV(const glm::quat& q, const glm::vec3& v) { return q * v; } + glm::vec3 sum(const glm::vec3& v1, const glm::vec3& v2) { return v1 + v2; } + glm::vec3 subtract(const glm::vec3& v1, const glm::vec3& v2) { return v1 - v2; } + float length(const glm::vec3& v) { return glm::length(v); } + float distance(const glm::vec3& v1, const glm::vec3& v2) { return glm::distance(v1, v2); } float orientedAngle(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3); - glm::vec3 normalize(const glm::vec3& v); - glm::vec3 mix(const glm::vec3& v1, const glm::vec3& v2, float m); - void print(const QString& lable, const glm::vec3& v); - bool equal(const glm::vec3& v1, const glm::vec3& v2); + glm::vec3 normalize(const glm::vec3& v) { return glm::normalize(v); }; + glm::vec3 mix(const glm::vec3& v1, const glm::vec3& v2, float m) { return glm::mix(v1, v2, m); } + void print(const QString& label, const glm::vec3& v); + bool equal(const glm::vec3& v1, const glm::vec3& v2) { return v1 == v2; } + bool withinEpsilon(const glm::vec3& v1, const glm::vec3& v2, float epsilon); + // FIXME misnamed, should be 'spherical' or 'euler' depending on the implementation glm::vec3 toPolar(const glm::vec3& v); glm::vec3 fromPolar(const glm::vec3& polar); glm::vec3 fromPolar(float elevation, float azimuth); + const glm::vec3& UNIT_X() { return Vectors::UNIT_X; } + const glm::vec3& UNIT_Y() { return Vectors::UNIT_Y; } + const glm::vec3& UNIT_Z() { return Vectors::UNIT_Z; } + const glm::vec3& UNIT_NEG_X() { return Vectors::UNIT_NEG_X; } + const glm::vec3& UNIT_NEG_Y() { return Vectors::UNIT_NEG_Y; } + const glm::vec3& UNIT_NEG_Z() { return Vectors::UNIT_NEG_Z; } + const glm::vec3& UNIT_XY() { return Vectors::UNIT_XY; } + const glm::vec3& UNIT_XZ() { return Vectors::UNIT_XZ; } + const glm::vec3& UNIT_YZ() { return Vectors::UNIT_YZ; } + const glm::vec3& UNIT_XYZ() { return Vectors::UNIT_XYZ; } + const glm::vec3& FLOAT_MAX() { return Vectors::MAX; } + const glm::vec3& FLOAT_MIN() { return Vectors::MIN; } + const glm::vec3& ZERO() { return Vectors::ZERO; } + const glm::vec3& ONE() { return Vectors::ONE; } + const glm::vec3& TWO() { return Vectors::TWO; } + const glm::vec3& HALF() { return Vectors::HALF; } + const glm::vec3& RIGHT() { return Vectors::RIGHT; } + const glm::vec3& UP() { return Vectors::UNIT_X; } + const glm::vec3& FRONT() { return Vectors::FRONT; } }; - - #endif // hifi_Vec3_h diff --git a/libraries/shared/src/Finally.h b/libraries/shared/src/Finally.h new file mode 100644 index 0000000000..59e8be0228 --- /dev/null +++ b/libraries/shared/src/Finally.h @@ -0,0 +1,27 @@ +// +// Created by Bradley Austin Davis on 2015/09/01 +// Copyright 2013-2105 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// Simulates a java finally block by executing a lambda when an instance leaves +// scope + +#include + +#pragma once +#ifndef hifi_Finally_h +#define hifi_Finally_h + +class Finally { +public: + template + Finally(F f) : _f(f) {} + ~Finally() { _f(); } +private: + std::function _f; +}; + +#endif diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 4ca8ed330b..7d56157e53 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -27,6 +27,8 @@ const vec3 Vectors::MAX{ FLT_MAX }; const vec3 Vectors::MIN{ -FLT_MAX }; const vec3 Vectors::ZERO{ 0.0f }; const vec3 Vectors::ONE{ 1.0f }; +const vec3 Vectors::TWO{ 2.0f }; +const vec3 Vectors::HALF{ 0.5f }; const vec3& Vectors::RIGHT = Vectors::UNIT_X; const vec3& Vectors::UP = Vectors::UNIT_Y; const vec3& Vectors::FRONT = Vectors::UNIT_NEG_Z; @@ -406,3 +408,22 @@ glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p) { glm::vec4 temp = m * glm::vec4(p, 1.0f); return glm::vec3(temp.x / temp.w, temp.y / temp.w, temp.z / temp.w); } + +glm::vec3 transformVector(const glm::mat4& m, const glm::vec3& v) { + glm::mat3 rot(m); + return glm::inverse(glm::transpose(rot)) * v; +} + +void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& secondaryAxis, + glm::vec3& uAxisOut, glm::vec3& vAxisOut, glm::vec3& wAxisOut) { + + uAxisOut = glm::normalize(primaryAxis); + wAxisOut = glm::cross(uAxisOut, secondaryAxis); + if (glm::length(wAxisOut) > 0.0f) { + wAxisOut = glm::normalize(wAxisOut); + } else { + wAxisOut = glm::normalize(glm::cross(uAxisOut, glm::vec3(0, 1, 0))); + } + vAxisOut = glm::cross(wAxisOut, uAxisOut); +} + diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 4b03ed2525..6683088306 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -64,12 +64,13 @@ public: static const vec3 UNIT_XY; static const vec3 UNIT_XZ; static const vec3 UNIT_YZ; - static const vec3 UNIT_ZX; static const vec3 UNIT_XYZ; static const vec3 MAX; static const vec3 MIN; static const vec3 ZERO; static const vec3 ONE; + static const vec3 TWO; + static const vec3 HALF; static const vec3& RIGHT; static const vec3& UP; static const vec3& FRONT; @@ -176,9 +177,35 @@ T toNormalizedDeviceScale(const T& value, const T& size) { #define PITCH(euler) euler.x #define ROLL(euler) euler.z +// vec2 lerp - linear interpolate +template +glm::detail::tvec2 lerp(const glm::detail::tvec2& x, const glm::detail::tvec2& y, T a) { + return x * (T(1) - a) + (y * a); +} + +// vec3 lerp - linear interpolate +template +glm::detail::tvec3 lerp(const glm::detail::tvec3& x, const glm::detail::tvec3& y, T a) { + return x * (T(1) - a) + (y * a); +} + +// vec4 lerp - linear interpolate +template +glm::detail::tvec4 lerp(const glm::detail::tvec4& x, const glm::detail::tvec4& y, T a) { + return x * (T(1) - a) + (y * a); +} + glm::mat4 createMatFromQuatAndPos(const glm::quat& q, const glm::vec3& p); glm::quat cancelOutRollAndPitch(const glm::quat& q); glm::mat4 cancelOutRollAndPitch(const glm::mat4& m); glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p); +glm::vec3 transformVector(const glm::mat4& m, const glm::vec3& v); + +// Calculate an orthogonal basis from a primary and secondary axis. +// The uAxis, vAxis & wAxis will form an orthognal basis. +// The primary axis will be the uAxis. +// The vAxis will be as close as possible to to the secondary axis. +void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& secondaryAxis, + glm::vec3& uAxisOut, glm::vec3& vAxisOut, glm::vec3& wAxisOut); #endif // hifi_GLMHelpers_h diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index c4e05a68fb..b2389f4db6 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -26,8 +26,7 @@ static int quatMetaTypeId = qRegisterMetaType(); static int xColorMetaTypeId = qRegisterMetaType(); static int pickRayMetaTypeId = qRegisterMetaType(); static int collisionMetaTypeId = qRegisterMetaType(); - - +static int qMapURLStringMetaTypeId = qRegisterMetaType>(); void registerMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, vec4toScriptValue, vec4FromScriptValue); @@ -197,9 +196,23 @@ QScriptValue xColorToScriptValue(QScriptEngine *engine, const xColor& color) { } void xColorFromScriptValue(const QScriptValue &object, xColor& color) { - color.red = object.property("red").toVariant().toInt(); - color.green = object.property("green").toVariant().toInt(); - color.blue = object.property("blue").toVariant().toInt(); + if (!object.isValid()) { + return; + } + if (object.isNumber()) { + color.red = color.green = color.blue = (uint8_t)object.toUInt32(); + } else if (object.isString()) { + QColor qcolor(object.toString()); + if (qcolor.isValid()) { + color.red = (uint8_t)qcolor.red(); + color.blue = (uint8_t)qcolor.blue(); + color.green = (uint8_t)qcolor.green(); + } + } else { + color.red = object.property("red").toVariant().toInt(); + color.green = object.property("green").toVariant().toInt(); + color.blue = object.property("blue").toVariant().toInt(); + } } QScriptValue qColorToScriptValue(QScriptEngine* engine, const QColor& color) { diff --git a/libraries/shared/src/Transform.h b/libraries/shared/src/Transform.h index 3064e18471..699eb8ef0a 100644 --- a/libraries/shared/src/Transform.h +++ b/libraries/shared/src/Transform.h @@ -129,6 +129,7 @@ public: static Transform& inverseMult(Transform& result, const Transform& left, const Transform& right); Vec4 transform(const Vec4& pos) const; + Vec3 transform(const Vec3& pos) const; protected: @@ -504,6 +505,12 @@ inline Transform::Vec4 Transform::transform(const Vec4& pos) const { return m * pos; } +inline Transform::Vec3 Transform::transform(const Vec3& pos) const { + Mat4 m; + getMatrix(m); + Vec4 result = m * Vec4(pos, 1.0f); + return Vec3(result.x / result.w, result.y / result.w, result.z / result.w); +} inline Transform::Mat4& Transform::getCachedMatrix(Transform::Mat4& result) const { updateCache(); diff --git a/tests/animation/CMakeLists.txt b/tests/animation/CMakeLists.txt index 2e9dbc9424..a66e391f69 100644 --- a/tests/animation/CMakeLists.txt +++ b/tests/animation/CMakeLists.txt @@ -1,7 +1,7 @@ # Declare dependencies macro (setup_testcase_dependencies) # link in the shared libraries - link_hifi_libraries(shared animation gpu fbx model) + link_hifi_libraries(shared animation gpu fbx model networking) copy_dlls_beside_windows_executable() endmacro () diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp new file mode 100644 index 0000000000..806560d96f --- /dev/null +++ b/tests/animation/src/AnimTests.cpp @@ -0,0 +1,225 @@ +// +// AnimTests.cpp +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimTests.h" +#include "AnimNodeLoader.h" +#include "AnimClip.h" +#include "AnimBlendLinear.h" +#include "AnimationLogging.h" +#include "AnimVariant.h" + +#include <../QTestExtensions.h> + +QTEST_MAIN(AnimTests) + +const float EPSILON = 0.001f; + +void AnimTests::initTestCase() { + auto animationCache = DependencyManager::set(); + auto resourceCacheSharedItems = DependencyManager::set(); +} + +void AnimTests::cleanupTestCase() { + DependencyManager::destroy(); +} + +void AnimTests::testClipInternalState() { + std::string id = "my anim clip"; + std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; + float startFrame = 2.0f; + float endFrame = 20.0f; + float timeScale = 1.1f; + bool loopFlag = true; + + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + + QVERIFY(clip.getID() == id); + QVERIFY(clip.getType() == AnimNode::Type::Clip); + + QVERIFY(clip._url == url); + QVERIFY(clip._startFrame == startFrame); + QVERIFY(clip._endFrame == endFrame); + QVERIFY(clip._timeScale == timeScale); + QVERIFY(clip._loopFlag == loopFlag); +} + +static float framesToSec(float secs) { + const float FRAMES_PER_SECOND = 30.0f; + return secs / FRAMES_PER_SECOND; +} + +void AnimTests::testClipEvaulate() { + std::string id = "myClipNode"; + std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; + float startFrame = 2.0f; + float endFrame = 22.0f; + float timeScale = 1.0f; + float loopFlag = true; + + auto vars = AnimVariantMap(); + vars.set("FalseVar", false); + + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + + AnimNode::Triggers triggers; + clip.evaluate(vars, framesToSec(10.0f), triggers); + QCOMPARE_WITH_ABS_ERROR(clip._frame, 12.0f, EPSILON); + + // does it loop? + triggers.clear(); + clip.evaluate(vars, framesToSec(11.0f), triggers); + QCOMPARE_WITH_ABS_ERROR(clip._frame, 3.0f, EPSILON); + + // did we receive a loop trigger? + QVERIFY(std::find(triggers.begin(), triggers.end(), "myClipNodeOnLoop") != triggers.end()); + + // does it pause at end? + triggers.clear(); + clip.setLoopFlagVar("FalseVar"); + clip.evaluate(vars, framesToSec(20.0f), triggers); + QCOMPARE_WITH_ABS_ERROR(clip._frame, 22.0f, EPSILON); + + // did we receive a done trigger? + QVERIFY(std::find(triggers.begin(), triggers.end(), "myClipNodeOnDone") != triggers.end()); +} + +void AnimTests::testClipEvaulateWithVars() { + std::string id = "myClipNode"; + std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; + float startFrame = 2.0f; + float endFrame = 22.0f; + float timeScale = 1.0f; + float loopFlag = true; + + float startFrame2 = 22.0f; + float endFrame2 = 100.0f; + float timeScale2 = 1.2f; + bool loopFlag2 = false; + + auto vars = AnimVariantMap(); + vars.set("startFrame2", startFrame2); + vars.set("endFrame2", endFrame2); + vars.set("timeScale2", timeScale2); + vars.set("loopFlag2", loopFlag2); + + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + clip.setStartFrameVar("startFrame2"); + clip.setEndFrameVar("endFrame2"); + clip.setTimeScaleVar("timeScale2"); + clip.setLoopFlagVar("loopFlag2"); + + AnimNode::Triggers triggers; + clip.evaluate(vars, framesToSec(0.1f), triggers); + + // verify that the values from the AnimVariantMap made it into the clipNode's + // internal state + QVERIFY(clip._startFrame == startFrame2); + QVERIFY(clip._endFrame == endFrame2); + QVERIFY(clip._timeScale == timeScale2); + QVERIFY(clip._loopFlag == loopFlag2); +} + +void AnimTests::testLoader() { + auto url = QUrl("https://gist.githubusercontent.com/hyperlogic/857129fe04567cbe670f/raw/8ba57a8f0a76f88b39a11f77f8d9df04af9cec95/test.json"); + AnimNodeLoader loader(url); + + const int timeout = 1000; + QEventLoop loop; + QTimer timer; + timer.setInterval(timeout); + timer.setSingleShot(true); + + AnimNode::Pointer node = nullptr; + connect(&loader, &AnimNodeLoader::success, [&](AnimNode::Pointer nodeIn) { node = nodeIn; }); + + loop.connect(&loader, SIGNAL(success(AnimNode::Pointer)), SLOT(quit())); + loop.connect(&loader, SIGNAL(error(int, QString)), SLOT(quit())); + loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + timer.start(); + loop.exec(); + + QVERIFY((bool)node); + + QVERIFY(node->getID() == "blend"); + QVERIFY(node->getType() == AnimNode::Type::BlendLinear); + + QVERIFY((bool)node); + QVERIFY(node->getID() == "blend"); + QVERIFY(node->getType() == AnimNode::Type::BlendLinear); + + auto blend = std::static_pointer_cast(node); + QVERIFY(blend->_alpha == 0.5f); + + QVERIFY(node->getChildCount() == 3); + + std::shared_ptr nodes[3] = { node->getChild(0), node->getChild(1), node->getChild(2) }; + + QVERIFY(nodes[0]->getID() == "test01"); + QVERIFY(nodes[0]->getChildCount() == 0); + QVERIFY(nodes[1]->getID() == "test02"); + QVERIFY(nodes[1]->getChildCount() == 0); + QVERIFY(nodes[2]->getID() == "test03"); + QVERIFY(nodes[2]->getChildCount() == 0); + + auto test01 = std::static_pointer_cast(nodes[0]); + QVERIFY(test01->_url == "test01.fbx"); + QVERIFY(test01->_startFrame == 1.0f); + QVERIFY(test01->_endFrame == 20.0f); + QVERIFY(test01->_timeScale == 1.0f); + QVERIFY(test01->_loopFlag == false); + + auto test02 = std::static_pointer_cast(nodes[1]); + QVERIFY(test02->_url == "test02.fbx"); + QVERIFY(test02->_startFrame == 2.0f); + QVERIFY(test02->_endFrame == 21.0f); + QVERIFY(test02->_timeScale == 0.9f); + QVERIFY(test02->_loopFlag == true); +} + +void AnimTests::testVariant() { + auto defaultVar = AnimVariant(); + auto boolVar = AnimVariant(true); + auto intVar = AnimVariant(1); + auto floatVar = AnimVariant(1.0f); + auto vec3Var = AnimVariant(glm::vec3(1.0f, 2.0f, 3.0f)); + auto quatVar = AnimVariant(glm::quat(1.0f, 2.0f, 3.0f, 4.0f)); + auto mat4Var = AnimVariant(glm::mat4(glm::vec4(1.0f, 2.0f, 3.0f, 4.0f), + glm::vec4(5.0f, 6.0f, 7.0f, 8.0f), + glm::vec4(9.0f, 10.0f, 11.0f, 12.0f), + glm::vec4(13.0f, 14.0f, 15.0f, 16.0f))); + QVERIFY(defaultVar.isBool()); + QVERIFY(defaultVar.getBool() == false); + + QVERIFY(boolVar.isBool()); + QVERIFY(boolVar.getBool() == true); + + QVERIFY(intVar.isInt()); + QVERIFY(intVar.getInt() == 1); + + QVERIFY(floatVar.isFloat()); + QVERIFY(floatVar.getFloat() == 1.0f); + + QVERIFY(vec3Var.isVec3()); + auto v = vec3Var.getVec3(); + QVERIFY(v.x == 1.0f); + QVERIFY(v.y == 2.0f); + QVERIFY(v.z == 3.0f); + + QVERIFY(quatVar.isQuat()); + auto q = quatVar.getQuat(); + QVERIFY(q.w == 1.0f); + QVERIFY(q.x == 2.0f); + QVERIFY(q.y == 3.0f); + QVERIFY(q.z == 4.0f); + + QVERIFY(mat4Var.isMat4()); + auto m = mat4Var.getMat4(); + QVERIFY(m[0].x == 1.0f); + QVERIFY(m[3].w == 16.0f); +} diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h new file mode 100644 index 0000000000..e667444657 --- /dev/null +++ b/tests/animation/src/AnimTests.h @@ -0,0 +1,28 @@ +// +// AnimTests.h +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimTests_h +#define hifi_AnimTests_h + +#include +#include + +class AnimTests : public QObject { + Q_OBJECT +private slots: + void initTestCase(); + void cleanupTestCase(); + void testClipInternalState(); + void testClipEvaulate(); + void testClipEvaulateWithVars(); + void testLoader(); + void testVariant(); +}; + +#endif // hifi_AnimTests_h diff --git a/tests/animation/src/RigTests.cpp b/tests/animation/src/RigTests.cpp index b0e0a53ee5..ff457ff804 100644 --- a/tests/animation/src/RigTests.cpp +++ b/tests/animation/src/RigTests.cpp @@ -78,24 +78,25 @@ void RigTests::initTestCase() { #ifdef FROM_FILE QFile file(FROM_FILE); QCOMPARE(file.open(QIODevice::ReadOnly), true); - FBXGeometry geometry = readFBX(file.readAll(), QVariantHash()); + FBXGeometry* geometry = readFBX(file.readAll(), QVariantHash()); #else QUrl fbxUrl("https://s3.amazonaws.com/hifi-public/models/skeletons/Zack/Zack.fbx"); QNetworkReply* reply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QCOMPARE(fbxHttpCode, 200); - FBXGeometry geometry = readFBX(reply->readAll(), QVariantHash()); + FBXGeometry* geometry = readFBX(reply->readAll(), QVariantHash()); #endif - + QVERIFY((bool)geometry); + QVector jointStates; - for (int i = 0; i < geometry.joints.size(); ++i) { - JointState state(geometry.joints[i]); + for (int i = 0; i < geometry->joints.size(); ++i) { + JointState state(geometry->joints[i]); jointStates.append(state); } _rig = std::make_shared(); _rig->initJointStates(jointStates, glm::mat4(), 0, 41, 40, 39, 17, 16, 15); // FIXME? get by name? do we really want to exclude the shoulder blades? - std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl; + std::cout << "Rig is ready " << geometry->joints.count() << " joints " << std::endl; reportAll(_rig); } diff --git a/tests/animation/src/data/avatar.json b/tests/animation/src/data/avatar.json new file mode 100644 index 0000000000..24967979ea --- /dev/null +++ b/tests/animation/src/data/avatar.json @@ -0,0 +1,191 @@ +{ + "version": "1.0", + "root": { + "id": "root", + "type": "stateMachine", + "data": { + "currentState": "idle", + "states": [ + { + "id": "idle", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkFwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkBwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" } + ] + } + ] + }, + "children": [ + { + "id": "idle", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "walkBwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx", + "startFrame": 0.0, + "endFrame": 37.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "turnLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 28.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "turnRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } +} diff --git a/tests/animation/src/data/test.json b/tests/animation/src/data/test.json new file mode 100644 index 0000000000..765617fa2f --- /dev/null +++ b/tests/animation/src/data/test.json @@ -0,0 +1,48 @@ +{ + "version": "1.0", + "root": { + "id": "blend", + "type": "blendLinear", + "data": { + "alpha": 0.5 + }, + "children": [ + { + "id": "test01", + "type": "clip", + "data": { + "url": "test01.fbx", + "startFrame": 1.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "test02", + "type": "clip", + "data": { + "url": "test02.fbx", + "startFrame": 2.0, + "endFrame": 21.0, + "timeScale": 0.9, + "loopFlag": true + }, + "children": [] + }, + { + "id": "test03", + "type": "clip", + "data": { + "url": "test03.fbx", + "startFrame": 3.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + } +}