resolve conflicts on merge with upstream/master

This commit is contained in:
Stephen Birarda 2015-09-04 12:31:46 -06:00
commit ed19987b7d
135 changed files with 7522 additions and 1884 deletions
README.md
assignment-client/src/avatars
domain-server/src
examples
interface/src
libraries

View file

@ -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

View file

@ -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<AvatarMixerClientData*>(otherNode->getLinkedData());
MutexTryLocker lock(otherNodeData->getMutex());
if (!lock.isLocked()) {
return;
}
AvatarData& otherAvatar = otherNodeData->getAvatar();
otherAvatar.doneEncoding(false);
});
_lastFrameTimestamp = QDateTime::currentMSecsSinceEpoch();
}

View file

@ -11,6 +11,8 @@
#include "NodeConnectionData.h"
#include <QtCore/QDataStream>
NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, const HifiSockAddr& senderSockAddr,
bool isConnectRequest) {
NodeConnectionData newHeader;

View file

@ -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);
function cleanup() {
rightController.cleanup();
leftController.cleanup();
}
Script.scriptEnding.connect(cleanup);
Script.update.connect(update)
Controller.actionEvent.connect(onActionEvent);

View file

@ -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);

View file

@ -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");

View file

@ -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();
})

View file

@ -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();
})

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="../style.css">
<style>
.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;
}
</style>
</head>
<body>
<div class="container"><span>Add</span></div>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="../style.css">
<style>
.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;
}
</style>
</head>
<body>
<div class="container"><span>Delete</span></div>
</body>
</html>

View file

@ -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;
}

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="../style.css">
<style>
.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;
}
</style>
</head>
<body>
<div class="container"><span>Move</span></div>
</body>
</html>

View file

@ -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 = {};
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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",
}

View file

@ -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) ];

View file

@ -0,0 +1,6 @@
InvisibleWand = function() {
}
InvisibleWand.prototype = Object.create( ModelBase.prototype );

View file

@ -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() {
}

View file

@ -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);
}

View file

@ -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";

View file

@ -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"

View file

@ -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);
}
}
// 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);
}

View file

@ -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);
});
}

View file

@ -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);

311
examples/toys/magBalls.js Normal file
View file

@ -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];
}
}

View file

@ -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() {
}

View file

@ -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";

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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]);

View file

@ -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");
}

View file

@ -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);
}
}
}

View file

@ -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) ];

View file

@ -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();
}

View file

@ -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;

View file

@ -65,6 +65,7 @@
#include <EntityScriptingInterface.h>
#include <ErrorDialog.h>
#include <Finally.h>
#include <FramebufferCache.h>
#include <gpu/Batch.h>
#include <gpu/Context.h>
@ -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<OffscreenUi>();
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<DialogsManager>()->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<DialogsManager>()->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();
}

View file

@ -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;
};

View file

@ -10,16 +10,39 @@
//
#include "FileLogger.h"
#include "HifiSockAddr.h"
#include <FileUtils.h>
#include <QDateTime>
#include <QFile>
#include <QDir>
#include <QDesktopServices>
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 <QtCore/QDateTime>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtGui/QDesktopServices>
#include <NumericalConstants.h>
#include <FileUtils.h>
#include <SharedUtil.h>
#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() {

View file

@ -27,7 +27,7 @@ public:
virtual void locateLog() override;
private:
QString _fileName;
const QString _fileName;
friend class FilePersistThread;
};

View file

@ -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,

View file

@ -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";

View file

@ -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();
}

View file

@ -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<AvatarManager>()->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);

View file

@ -33,6 +33,7 @@
#include <SharedUtil.h>
#include <TextRenderer3D.h>
#include <UserActivityLogger.h>
#include <AnimDebugDraw.h>
#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) {

View file

@ -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<int> _headBoneSet;
RigPointer _rig;
bool _prevShouldDrawHead;
bool _enableDebugDrawBindPose = false;
bool _enableDebugDrawAnimPose = false;
};
#endif // hifi_MyAvatar_h

View file

@ -86,7 +86,7 @@ void SkeletonModel::initJointStates(QVector<JointState> 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<glm::mat4> 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<QString> endEffectors;
endEffectors.push_back("RightHand");
endEffectors.push_back("LeftHand");
endEffectors.push_back("RightFoot");
endEffectors.push_back("LeftFoot");
QVector<QString> 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<int> 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);
}

View file

@ -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

View file

@ -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());

View file

@ -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<LocalModelsOverlay>(Application::getInstance()->getEntityClipboardRenderer());
} else if (type == ModelOverlay::TYPE) {
thisOverlay = std::make_shared<ModelOverlay>();
} else if (type == Web3DOverlay::TYPE) {
thisOverlay = std::make_shared<Web3DOverlay>();
}
if (thisOverlay) {

View file

@ -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 <QtScript/QScriptValue>
#include <QtGui/QOpenGLContext>
#include <QtQuick/QQuickItem>
#include <DeferredLightingEffect.h>
#include <DependencyManager.h>
#include <GeometryCache.h>
#include <GeometryUtil.h>
#include <TextureCache.h>
#include <PathUtils.h>
#include <gpu/Batch.h>
#include <RegisteredMetaTypes.h>
#include <AbstractViewStateInterface.h>
#include <OffscreenQmlSurface.h>
// #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<TextureCache>()->getWhiteTexture());
}
batch.setModelTransform(transform);
DependencyManager::get<DeferredLightingEffect>()->bindSimpleProgram(batch, true, false, false, true);
DependencyManager::get<GeometryCache>()->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);
}

View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -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<AnimationCache>();
_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<FBXJoint>& animJoints = geom.joints;
std::vector<int> 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;
}

View file

@ -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 <string>
#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<AnimPoseVec> _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

View file

@ -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 <string>
#include <vector>
#include <memory>
#include <cassert>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#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<AnimNode>;
using ConstPointer = std::shared_ptr<const AnimNode>;
using Triggers = std::vector<std::string>;
friend class AnimDebugDraw;
friend void buildChildMap(std::map<std::string, Pointer>& 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<AnimNode::Pointer> _children;
AnimSkeleton::ConstPointer _skeleton;
// no copies
AnimNode(const AnimNode&) = delete;
AnimNode& operator=(const AnimNode&) = delete;
};
#endif // hifi_AnimNode_h

View file

@ -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 <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QFile>
#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<int>(AnimNode::Type::NumTypes);
for (int i = 0; i < NUM_TYPES; i++ ) {
AnimNode::Type type = static_cast<AnimNode::Type>(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<AnimClip>(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<AnimBlendLinear>(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<AnimOverlay>(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<AnimStateMachine>(id.toStdString());
return node;
}
void buildChildMap(std::map<std::string, AnimNode::Pointer>& map, AnimNode::Pointer node) {
for ( auto child : node->_children ) {
map.insert(std::pair<std::string, AnimNode::Pointer>(child->_id, child));
}
}
bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl) {
auto smNode = std::static_pointer_cast<AnimStateMachine>(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<std::string, AnimNode::Pointer> childMap;
buildChildMap(childMap, node);
// first pass parse all the states and build up the state and transition map.
using StringPair = std::pair<std::string, std::string>;
using TransitionMap = std::multimap<AnimStateMachine::State::Pointer, StringPair>;
TransitionMap transitionMap;
using StateMap = std::map<std::string, AnimStateMachine::State::Pointer>;
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<AnimStateMachine::State>(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");
}

View file

@ -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 <memory>
#include <QString>
#include <QUrl>
#include <QNetworkReply>
#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

View file

@ -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 <queue>
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 <typename Func>
void for_each_child_joint(AnimSkeleton::ConstPointer skeleton, int startJoint, Func f) {
std::queue<int> 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);
}

View file

@ -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<float> _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

View file

@ -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 <glm/gtx/transform.hpp>
#include <glm/gtc/quaternion.hpp>
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<glm::mat4>(*this) * static_cast<glm::mat4>(rhs));
}
AnimPose AnimPose::inverse() const {
return AnimPose(glm::inverse(static_cast<glm::mat4>(*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<FBXJoint>& 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

View file

@ -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 <vector>
#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<AnimPose>;
class AnimSkeleton {
public:
using Pointer = std::shared_ptr<AnimSkeleton>;
using ConstPointer = std::shared_ptr<const AnimSkeleton>;
AnimSkeleton(const std::vector<FBXJoint>& 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<FBXJoint> _joints;
AnimPoseVec _absoluteBindPoses;
AnimPoseVec _relativeBindPoses;
// no copies
AnimSkeleton(const AnimSkeleton&) = delete;
AnimSkeleton& operator=(const AnimSkeleton&) = delete;
};
#endif

View file

@ -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;
}

View file

@ -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 <string>
#include <vector>
#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<State>;
using ConstPointer = std::shared_ptr<const State>;
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<Transition> _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<State::Pointer> _states;
std::string _currentStateVar;
private:
// no copies
AnimStateMachine(const AnimStateMachine&) = delete;
AnimStateMachine& operator=(const AnimStateMachine&) = delete;
};
#endif // hifi_AnimStateMachine_h

View file

@ -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);
}
}

View file

@ -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

View file

@ -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 <cassert>
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <map>
#include <set>
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<glm::vec3*>(&_val) = value; }
AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast<glm::quat*>(&_val) = value; }
AnimVariant(const glm::mat4& value) : _type(Type::Mat4) { *reinterpret_cast<glm::mat4*>(&_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<glm::vec3*>(&_val) = value; }
void setQuat(const glm::quat& value) { assert(_type == Type::Quat); *reinterpret_cast<glm::quat*>(&_val) = value; }
void setMat4(const glm::mat4& value) { assert(_type == Type::Mat4); *reinterpret_cast<glm::mat4*>(&_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<const glm::vec3*>(&_val); }
const glm::quat& getQuat() const { assert(_type == Type::Quat); return *reinterpret_cast<const glm::quat*>(&_val); }
const glm::mat4& getMat4() const { assert(_type == Type::Mat4); return *reinterpret_cast<const glm::mat4*>(&_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<std::string, AnimVariant> _map;
std::set<std::string> _triggers;
};
#endif // hifi_AnimVariant_h

View file

@ -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;
}

View file

@ -9,30 +9,34 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "Rig.h"
#include <glm/gtx/vector_angle.hpp>
#include <queue>
#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<JointState> 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<float>(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<FBXJoint> 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<AnimSkeleton>(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;
});
}

View file

@ -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<AnimationHandle> 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<AnimationHandlePointer>& getRunningAnimations() const { return _runningAnimations; }
void deleteAnimations();
void destroyAnimGraph();
const QList<AnimationHandlePointer>& 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<AnimationHandlePointer> _animationHandles;
QList<AnimationHandlePointer> _runningAnimations;
bool _enableRig;
bool _enableRig = false;
bool _enableAnimGraph = false;
glm::vec3 _lastFront;
glm::vec3 _lastPosition;
std::shared_ptr<AnimNode> _animNode;
std::shared_ptr<AnimSkeleton> _animSkeleton;
std::unique_ptr<AnimNodeLoader> _animLoader;
AnimVariantMap _animVars;
enum class RigRole {
Idle = 0,
Turn,
Move
};
RigRole _state = RigRole::Idle;
};
#endif /* defined(__hifi__Rig__) */

View file

@ -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");

View file

@ -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);

View file

@ -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<NodeList>();
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);

View file

@ -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);

View file

@ -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));
}

View file

@ -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<PalmData> _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;

View file

@ -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);

View file

@ -30,3 +30,4 @@ void NullDisplayPlugin::finishFrame() {}
void NullDisplayPlugin::activate() {}
void NullDisplayPlugin::deactivate() {}
void NullDisplayPlugin::stop() {}

View file

@ -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;

View file

@ -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();

View file

@ -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 };
};

View file

@ -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<TextureCache>()->
// 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<QResizeEvent*>(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();
};

View file

@ -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)

View file

@ -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;

View file

@ -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<uint8_t>(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<uint8_t>(PolyVox::Region(lowCorner, highCorner));
}
@ -613,35 +656,27 @@ void RenderablePolyVoxEntityItem::setVoxelVolumeSize(glm::vec3 voxelVolumeSize)
bool RenderablePolyVoxEntityItem::inUserBounds(const PolyVox::SimpleVolume<uint8_t>* 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<RenderablePolyVoxEntityItem>(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<RenderablePolyVoxEntityItem>(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<RenderablePolyVoxEntityItem>(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<PolyVox::PositionMaterialNormal> 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<PolyVox::SimpleVolume<uint8_t>> surfaceExtractor
(_volData, _volData->getEnclosingRegion(), &polyVoxMesh);
surfaceExtractor.execute();
break;
}
case PolyVoxEntityItem::SURFACE_MARCHING_CUBES: {
PolyVox::MarchingCubesSurfaceExtractor<PolyVox::SimpleVolume<uint8_t>> surfaceExtractor
(_volData, _volData->getEnclosingRegion(), &polyVoxMesh);
surfaceExtractor.execute();
break;
}
case PolyVoxEntityItem::SURFACE_EDGED_CUBIC:
case PolyVoxEntityItem::SURFACE_EDGED_CUBIC: {
PolyVox::CubicSurfaceExtractorWithNormals<PolyVox::SimpleVolume<uint8_t>> surfaceExtractor
(_volData, _volData->getEnclosingRegion(), &polyVoxMesh);
surfaceExtractor.execute();
break;
}
case PolyVoxEntityItem::SURFACE_CUBIC: {
PolyVox::CubicSurfaceExtractorWithNormals<PolyVox::SimpleVolume<uint8_t>> 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<RenderablePolyVoxEntityItem>(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<RenderablePolyVoxEntityItem>(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<RenderablePolyVoxEntityItem>(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<RenderablePolyVoxEntityItem>(currentXNNeighbor);
polyVoxXNNeighbor->rebakeMesh();
}
if (currentYNNeighbor && currentYNNeighbor->getType() == EntityTypes::PolyVox) {
auto polyVoxYNNeighbor = std::dynamic_pointer_cast<RenderablePolyVoxEntityItem>(currentYNNeighbor);
polyVoxYNNeighbor->rebakeMesh();
}
if (currentZNNeighbor && currentZNNeighbor->getType() == EntityTypes::PolyVox) {
auto polyVoxZNNeighbor = std::dynamic_pointer_cast<RenderablePolyVoxEntityItem>(currentZNNeighbor);
polyVoxZNNeighbor->rebakeMesh();
}
}

View file

@ -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<render::Scene> 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<uint8_t>* 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();
};

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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;

View file

@ -109,6 +109,13 @@ CONSTRUCT_PROPERTY(strokeWidths, QVector<float>()),
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.

View file

@ -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();

View file

@ -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;

View file

@ -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,

View file

@ -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<glm::vec3>& points) {
EntityItemPointer entity = static_cast<EntityItemPointer>(_entityTree->findEntityByEntityItemID(entityID));
if (!entity) {

View file

@ -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<glm::vec3>& points);
Q_INVOKABLE bool appendPoint(QUuid entityID, const glm::vec3& point);

View file

@ -12,6 +12,7 @@
#include <QByteArray>
#include <QDebug>
#include <QWriteLocker>
#include <ByteCountCoding.h>
@ -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;
}

View file

@ -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

View file

@ -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());

View file

@ -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;
};

Some files were not shown because too many files have changed in this diff Show more