Grab script hotspot work

* Updated grab/equip logic to use sphere vs sphere tests, instead of sphere vs entity bounding box.
* Added debug flag for visualizing grab spheres.
* hotspot overlays are now updated as the objects they are attached to move.
* You can now use the search beam to near grab large objects, as well as the sphere sphere test.
* Optimized EntityPropertyCache to make a single call to Entities.getEntityProperties instead of three.
* Moved grab script options from the "Developer > Hands" menu to the "Developer > Grab Script" menu.
This commit is contained in:
Anthony J. Thibault 2016-06-23 15:36:47 -07:00
parent 2a82dddc2b
commit fc42a3aef5

View file

@ -10,10 +10,9 @@
// //
// Distributed under the Apache License, Version 2.0. // Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
/*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr, Xform */ /*global print, MyAvatar, Entities, AnimationCache, SoundCache, Scene, Camera, Overlays, Audio, HMD, AvatarList, AvatarManager, Controller, UndoStack, Window, Account, GlobalServices, Script, ScriptDiscoveryService, LODManager, Menu, Vec3, Quat, AudioDevice, Paths, Clipboard, Settings, XMLHttpRequest, Reticle, Messages, setEntityCustomData, getEntityCustomData, vec3toStr */
Script.include("/~/system/libraries/utils.js"); Script.include("/~/system/libraries/utils.js");
Script.include("../libraries/Xform.js");
// //
// add lines where the hand ray picking is happening // add lines where the hand ray picking is happening
@ -38,6 +37,9 @@ var THUMB_ON_VALUE = 0.5;
var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move. var HAND_HEAD_MIX_RATIO = 0.0; // 0 = only use hands for search/move. 1 = only use head for search/move.
var PICK_WITH_HAND_RAY = true; var PICK_WITH_HAND_RAY = true;
var DRAW_GRAB_SPHERES = false;
var DRAW_HAND_SPHERES = false;
var DROP_WITHOUT_SHAKE = false; var DROP_WITHOUT_SHAKE = false;
// //
@ -68,16 +70,20 @@ var LINE_ENTITY_DIMENSIONS = {
var LINE_LENGTH = 500; var LINE_LENGTH = 500;
var PICK_MAX_DISTANCE = 500; // max length of pick-ray var PICK_MAX_DISTANCE = 500; // max length of pick-ray
// //
// near grabbing // near grabbing
// //
var GRAB_RADIUS = 0.06; // if the ray misses but an object is this close, it will still be selected var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping.
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
var NEAR_PICK_MAX_DISTANCE = 0.3; // max length of pick-ray for close grabbing to be selected
var NEAR_GRAB_RADIUS = 0.15; // radius used for palm vs object for near grabbing.
var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for near grabbing.
var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object
var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed
var SHOW_GRAB_SPHERE = false; // draw a green sphere to show the grab search position and size
var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds
// //
@ -254,7 +260,8 @@ function restore2DMode() {
} }
} }
// constructor // EntityPropertiesCache is a helper class that contains a cache of entity properties.
// the hope is to prevent excess calls to Entity.getEntityProperties()
function EntityPropertiesCache() { function EntityPropertiesCache() {
this.cache = {}; this.cache = {};
} }
@ -265,34 +272,55 @@ EntityPropertiesCache.prototype.findEntities = function(position, radius) {
var entities = Entities.findEntities(position, radius); var entities = Entities.findEntities(position, radius);
var _this = this; var _this = this;
entities.forEach(function (x) { entities.forEach(function (x) {
_this.addEntity(x); _this.updateEntity(x);
}); });
}; };
EntityPropertiesCache.prototype.addEntity = function(entityID) { EntityPropertiesCache.prototype.updateEntity = function(entityID) {
var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES); var props = Entities.getEntityProperties(entityID, GRABBABLE_PROPERTIES);
var grabbableProps = getEntityCustomData(GRABBABLE_DATA_KEY, entityID, DEFAULT_GRABBABLE_DATA);
var grabProps = getEntityCustomData(GRAB_USER_DATA_KEY, entityID, {}); // convert props.userData from a string to an object.
var wearableProps = getEntityCustomData("wearable", entityID, {}); var userData = {};
this.cache[entityID] = { props: props, grabbableProps: grabbableProps, grabProps: grabProps, wearableProps: wearableProps }; if (props.userData) {
try {
userData = JSON.parse(props.userData);
} catch(err) {
print("WARNING: malformed userData on " + entityID + ", name = " + props.name + ", error = " + err);
}
}
props.userData = userData;
this.cache[entityID] = props;
}; };
EntityPropertiesCache.prototype.getEntities = function() { EntityPropertiesCache.prototype.getEntities = function() {
return Object.keys(this.cache); return Object.keys(this.cache);
} }
EntityPropertiesCache.prototype.getProps = function(entityID) { EntityPropertiesCache.prototype.getProps = function(entityID) {
var obj = this.cache[entityID] var obj = this.cache[entityID]
return obj ? obj.props : undefined; return obj ? obj : undefined;
}; };
EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) { EntityPropertiesCache.prototype.getGrabbableProps = function(entityID) {
var obj = this.cache[entityID] var props = this.cache[entityID];
return obj ? obj.grabbableProps : undefined; if (props) {
return props.userData.grabbableKey ? props.userData.grabbableKey : DEFAULT_GRABBABLE_DATA;
} else {
return undefined;
}
}; };
EntityPropertiesCache.prototype.getGrabProps = function(entityID) { EntityPropertiesCache.prototype.getGrabProps = function(entityID) {
var obj = this.cache[entityID] var props = this.cache[entityID];
return obj ? obj.grabProps : undefined; if (props) {
return props.userData.grabKey ? props.userData.grabKey : {};
} else {
return undefined;
}
}; };
EntityPropertiesCache.prototype.getWearableProps = function(entityID) { EntityPropertiesCache.prototype.getWearableProps = function(entityID) {
var obj = this.cache[entityID] var props = this.cache[entityID];
return obj ? obj.wearableProps : undefined; if (props) {
return props.userData.wearable ? props.userData.wearable : {};
} else {
return undefined;
}
}; };
function MyController(hand) { function MyController(hand) {
@ -323,7 +351,6 @@ function MyController(hand) {
//for visualizations //for visualizations
this.overlayLine = null; this.overlayLine = null;
this.particleBeamObject = null; this.particleBeamObject = null;
this.grabSphere = null;
//for lights //for lights
this.spotlight = null; this.spotlight = null;
@ -384,7 +411,7 @@ function MyController(hand) {
} }
this.setState = function(newState, reason) { this.setState = function(newState, reason) {
this.grabSphereOff();
if (WANT_DEBUG || WANT_DEBUG_STATE) { if (WANT_DEBUG || WANT_DEBUG_STATE) {
var oldStateName = stateToName(this.state); var oldStateName = stateToName(this.state);
var newStateName = stateToName(newState); var newStateName = stateToName(newState);
@ -491,39 +518,6 @@ function MyController(hand) {
} }
} }
this.grabSphereOn = function() {
var color = {red: 0, green: 255, blue: 0};
if (this.grabSphere === null) {
var sphereProperties = {
position: this.getHandPosition(),
size: GRAB_RADIUS*2,
color: color,
alpha: 0.1,
solid: true,
ignoreRayIntersection: true,
drawInFront: true, // Even when burried inside of something, show it.
visible: true
}
this.grabSphere = Overlays.addOverlay("sphere", sphereProperties);
} else {
Overlays.editOverlay(this.grabSphere, {
position: this.getHandPosition(),
size: GRAB_RADIUS*2,
color: color,
alpha: 0.1,
solid: true,
visible: true
});
}
}
this.grabSphereOff = function() {
if (this.grabSphere !== null) {
Overlays.deleteOverlay(this.grabSphere);
this.grabSphere = null;
}
};
this.overlayLineOn = function(closePoint, farPoint, color) { this.overlayLineOn = function(closePoint, farPoint, color) {
if (this.overlayLine === null) { if (this.overlayLine === null) {
var lineProperties = { var lineProperties = {
@ -873,51 +867,123 @@ function MyController(hand) {
} }
}; };
this.searchEnter = function() { this.createHotspots = function () {
this.equipHotspotOverlays = [];
var HAND_SPHERE_COLOR = { red: 90, green: 255, blue: 90 };
var HAND_SPHERE_ALPHA = 0.7;
var HAND_SPHERE_RADIUS = 0.01;
var EQUIP_SPHERE_COLOR = { red: 90, green: 255, blue: 90 };
var EQUIP_SPHERE_ALPHA = 0.3;
var NEAR_SPHERE_COLOR = { red: 90, green: 90, blue: 255 };
var NEAR_SPHERE_ALPHA = 0.1;
this.hotspotOverlays = [];
if (DRAW_HAND_SPHERES) {
// add tiny spheres around the palm.
var handPosition = this.getHandPosition();
var overlay = Overlays.addOverlay("sphere", {
position: handPosition,
size: HAND_SPHERE_RADIUS * 2,
color: HAND_SPHERE_COLOR,
alpha: HAND_SPHERE_ALPHA,
solid: true,
visible: true,
ignoreRayIntersection: true,
drawInFront: false
});
this.hotspotOverlays.push({
entityID: undefined,
overlay: overlay,
type: "hand"
});
}
// find entities near the avatar that might be equipable. // find entities near the avatar that might be equipable.
var entities = Entities.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE); this.entityPropertyCache.clear();
var i, l = entities.length; this.entityPropertyCache.findEntities(MyAvatar.position, HOTSPOT_DRAW_DISTANCE);
for (i = 0; i < l; i++) {
var props = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES);
// does this entity have an attach point?
var wearableData = getEntityCustomData("wearable", entities[i], undefined);
if (wearableData && wearableData.joints) {
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
if (wearableData.joints[handJointName]) {
var handOffsetPos = wearableData.joints[handJointName][0];
var handOffsetRot = wearableData.joints[handJointName][1];
var handOffsetXform = new Xform(handOffsetRot, handOffsetPos); var _this = this;
var objectXform = new Xform(props.rotation, props.position); this.entityPropertyCache.getEntities().forEach(function (entityID) {
var overlayXform = Xform.mul(objectXform, handOffsetXform.inv()); if (_this.entityIsEquippableWithoutDistanceCheck(entityID)) {
var props = _this.entityPropertyCache.getProps(entityID);
// draw the hotspot overlay = Overlays.addOverlay("sphere", {
this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", { rotation: props.rotation,
rotation: overlayXform.rot, position: props.position,
position: overlayXform.pos, size: EQUIP_RADIUS * 2,
size: 0.2, color: EQUIP_SPHERE_COLOR,
color: { red: 90, green: 255, blue: 90 }, alpha: EQUIP_SPHERE_ALPHA,
alpha: 0.7, solid: true,
solid: true, visible: true,
visible: true, ignoreRayIntersection: true,
ignoreRayIntersection: true, drawInFront: false
drawInFront: false });
}));
} _this.hotspotOverlays.push({
entityID: entityID,
overlay: overlay,
type: "equip"
});
} }
}
if (DRAW_GRAB_SPHERES && _this.entityIsGrabbable(entityID)) {
var props = _this.entityPropertyCache.getProps(entityID);
overlay = Overlays.addOverlay("sphere", {
rotation: props.rotation,
position: props.position,
size: NEAR_GRAB_RADIUS * 2,
color: NEAR_SPHERE_COLOR,
alpha: NEAR_SPHERE_ALPHA,
solid: true,
visible: true,
ignoreRayIntersection: true,
drawInFront: false
});
_this.hotspotOverlays.push({
entityID: entityID,
overlay: overlay,
type: "near"
});
}
});
};
this.updateHotspots = function() {
var _this = this;
this.hotspotOverlays.forEach(function (overlayInfo) {
if (overlayInfo.type === "hand") {
Overlays.editOverlay(overlayInfo.overlay, { position: _this.getHandPosition() });
} else if (overlayInfo.type === "equip") {
_this.entityPropertyCache.updateEntity(overlayInfo.entityID);
var props = _this.entityPropertyCache.getProps(overlayInfo.entityID);
Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation });
} else if (overlayInfo.type === "near") {
_this.entityPropertyCache.updateEntity(overlayInfo.entityID);
var props = _this.entityPropertyCache.getProps(overlayInfo.entityID);
Overlays.editOverlay(overlayInfo.overlay, { position: props.position, rotation: props.rotation });
}
});
};
this.destroyHotspots = function() {
this.hotspotOverlays.forEach(function (overlayInfo) {
Overlays.deleteOverlay(overlayInfo.overlay);
});
this.hotspotOverlays = [];
};
this.searchEnter = function() {
this.createHotspots();
}; };
this.searchExit = function() { this.searchExit = function() {
this.destroyHotspots();
// delete all equip hotspots
var i, l = this.equipHotspotOverlays.length;
for (i = 0; i < l; i++) {
Overlays.deleteOverlay(this.equipHotspotOverlays[i]);
}
this.equipHotspotOverlays = [];
}; };
/// ///
@ -984,7 +1050,13 @@ function MyController(hand) {
return grabbableProps && grabbableProps.wantsTrigger; return grabbableProps && grabbableProps.wantsTrigger;
}; };
this.entityIsEquippable = function (entityID, handPosition) { this.entityIsEquippableWithoutDistanceCheck = function (entityID) {
var props = this.entityPropertyCache.getProps(entityID);
var handPosition = props.position;
return this.entityIsEquippableWithDistanceCheck(entityID, handPosition);
};
this.entityIsEquippableWithDistanceCheck = function (entityID, handPosition) {
var props = this.entityPropertyCache.getProps(entityID); var props = this.entityPropertyCache.getProps(entityID);
var distance = Vec3.distance(props.position, handPosition); var distance = Vec3.distance(props.position, handPosition);
var grabProps = this.entityPropertyCache.getGrabProps(entityID); var grabProps = this.entityPropertyCache.getGrabProps(entityID);
@ -998,9 +1070,9 @@ function MyController(hand) {
return false; return false;
} }
if (distance > NEAR_PICK_MAX_DISTANCE) { if (distance > EQUIP_RADIUS) {
if (debug) { if (debug) {
print("equip is skipping '" + props.name + "': too far away."); print("equip is skipping '" + props.name + "': too far away, " + distance + " meters");
} }
return false; return false;
} }
@ -1113,7 +1185,7 @@ function MyController(hand) {
return true; return true;
}; };
this.entityIsNearGrabbable = function(entityID, handPosition) { this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) {
if (!this.entityIsGrabbable(entityID)) { if (!this.entityIsGrabbable(entityID)) {
return false; return false;
@ -1123,7 +1195,7 @@ function MyController(hand) {
var distance = Vec3.distance(props.position, handPosition); var distance = Vec3.distance(props.position, handPosition);
var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME); var debug = (WANT_DEBUG_SEARCH_NAME && props.name === WANT_DEBUG_SEARCH_NAME);
if (distance > NEAR_PICK_MAX_DISTANCE) { if (distance > maxDistance) {
// too far away, don't grab // too far away, don't grab
if (debug) { if (debug) {
print(" grab is skipping '" + props.name + "': too far away."); print(" grab is skipping '" + props.name + "': too far away.");
@ -1138,6 +1210,8 @@ function MyController(hand) {
var _this = this; var _this = this;
var name; var name;
this.updateHotspots();
this.grabbedEntity = null; this.grabbedEntity = null;
this.isInitialGrab = false; this.isInitialGrab = false;
this.shouldResetParentOnRelease = false; this.shouldResetParentOnRelease = false;
@ -1151,16 +1225,12 @@ function MyController(hand) {
var handPosition = this.getHandPosition(); var handPosition = this.getHandPosition();
if (SHOW_GRAB_SPHERE) {
this.grabSphereOn();
}
this.entityPropertyCache.clear(); this.entityPropertyCache.clear();
this.entityPropertyCache.findEntities(handPosition, GRAB_RADIUS); this.entityPropertyCache.findEntities(handPosition, NEAR_GRAB_RADIUS);
var candidateEntities = this.entityPropertyCache.getEntities(); var candidateEntities = this.entityPropertyCache.getEntities();
var equippableEntities = candidateEntities.filter(function (entity) { var equippableEntities = candidateEntities.filter(function (entity) {
return _this.entityIsEquippable(entity, handPosition); return _this.entityIsEquippableWithDistanceCheck(entity, handPosition);
}); });
var entity; var entity;
@ -1181,16 +1251,19 @@ function MyController(hand) {
} }
} }
var grabbableEntities = candidateEntities.filter(function (entity) {
return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_RADIUS);
});
var rayPickInfo = this.calcRayPickInfo(this.hand); var rayPickInfo = this.calcRayPickInfo(this.hand);
this.intersectionDistance = rayPickInfo.distance; this.intersectionDistance = rayPickInfo.distance;
if (rayPickInfo.entityID) {
candidateEntities.push(rayPickInfo.entityID);
this.entityPropertyCache.addEntity(rayPickInfo.entityID);
}
var grabbableEntities = candidateEntities.filter(function (entity) { if (rayPickInfo.entityID) {
return _this.entityIsNearGrabbable(entity, handPosition); this.entityPropertyCache.updateEntity(rayPickInfo.entityID);
}); if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) {
grabbableEntities.push(rayPickInfo.entityID);
}
}
if (grabbableEntities.length > 0) { if (grabbableEntities.length > 0) {
// sort by distance // sort by distance
@ -1732,11 +1805,11 @@ function MyController(hand) {
this.lastUnequipCheckTime = now; this.lastUnequipCheckTime = now;
if (props.parentID == MyAvatar.sessionUUID && if (props.parentID == MyAvatar.sessionUUID &&
Vec3.length(props.localPosition) > NEAR_PICK_MAX_DISTANCE * 2.0) { Vec3.length(props.localPosition) > NEAR_GRAB_PICK_RADIUS * 2.0) {
var handPosition = this.getHandPosition(); var handPosition = this.getHandPosition();
// the center of the equipped object being far from the hand isn't enough to autoequip -- we also // the center of the equipped object being far from the hand isn't enough to autoequip -- we also
// need to fail the findEntities test. // need to fail the findEntities test.
var nearPickedCandidateEntities = Entities.findEntities(handPosition, GRAB_RADIUS); var nearPickedCandidateEntities = Entities.findEntities(handPosition, EQUIP_RADIUS);
if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) {
// for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip. // for whatever reason, the held/equipped entity has been pulled away. ungrab or unequip.
print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." + print("handControllerGrab -- autoreleasing held or equipped item because it is far from hand." +
@ -2182,17 +2255,32 @@ function cleanup() {
Script.scriptEnding.connect(cleanup); Script.scriptEnding.connect(cleanup);
Script.update.connect(update); Script.update.connect(update);
if (!Menu.menuExists("Developer > Grab Script")) {
Menu.addMenu("Developer > Grab Script");
}
Menu.addMenuItem({ Menu.addMenuItem({
menuName: "Developer > Hands", menuName: "Developer > Grab Script",
menuItemName: "Drop Without Shake", menuItemName: "Drop Without Shake",
isCheckable: true, isCheckable: true,
isChecked: DROP_WITHOUT_SHAKE isChecked: DROP_WITHOUT_SHAKE
}); });
Menu.addMenuItem({
menuName: "Developer > Grab Script",
menuItemName: "Draw Grab Spheres",
isCheckable: true,
isChecked: DRAW_GRAB_SPHERES
});
function handleMenuItemEvent(menuItem) { function handleMenuItemEvent(menuItem) {
if (menuItem === "Drop Without Shake") { if (menuItem === "Drop Without Shake") {
DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake"); DROP_WITHOUT_SHAKE = Menu.isOptionChecked("Drop Without Shake");
} }
if (menuItem === "Draw Grab Spheres") {
DRAW_GRAB_SPHERES = Menu.isOptionChecked("Draw Grab Spheres");
DRAW_HAND_SPHERES = DRAW_GRAB_SPHERES;
}
} }
Menu.menuItemEvent.connect(handleMenuItemEvent); Menu.menuItemEvent.connect(handleMenuItemEvent);