mirror of
https://github.com/overte-org/overte.git
synced 2025-04-25 21:36:12 +02:00
441 lines
17 KiB
JavaScript
441 lines
17 KiB
JavaScript
// hydraGrab.js
|
|
// examples
|
|
//
|
|
// Created by Eric Levin on 9/2/15
|
|
// Copyright 2015 High Fidelity, Inc.
|
|
//
|
|
// Grabs physically moveable entities with hydra-like controllers; it works for either near or far objects.
|
|
//
|
|
// 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");
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
//
|
|
// these tune time-averaging and "on" value for analog trigger
|
|
//
|
|
|
|
var TRIGGER_SMOOTH_RATIO = 0.1; // 0.0 disables smoothing of trigger value
|
|
var TRIGGER_ON_VALUE = 0.2;
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
//
|
|
// distant manipulation
|
|
//
|
|
|
|
var DISTANCE_HOLDING_RADIUS_FACTOR = 5; // multiplied by distance between hand and object
|
|
var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
|
|
var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did
|
|
var NO_INTERSECT_COLOR = {red: 10, green: 10, blue: 255}; // line color when pick misses
|
|
var INTERSECT_COLOR = {red: 250, green: 10, blue: 10}; // line color when pick hits
|
|
var LINE_ENTITY_DIMENSIONS = {x: 1000, y: 1000, z: 1000};
|
|
var LINE_LENGTH = 500;
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
//
|
|
// near grabbing
|
|
//
|
|
|
|
var GRAB_RADIUS = 0.3; // if the ray misses but an object is this close, it will still be selected
|
|
var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
|
|
var NEAR_GRABBING_VELOCITY_SMOOTH_RATIO = 1.0; // adjust time-averaging of held object's velocity. 1.0 to disable.
|
|
var NEAR_PICK_MAX_DISTANCE = 0.6; // max length of pick-ray for close grabbing to be selected
|
|
var RELEASE_VELOCITY_MULTIPLIER = 1.5; // affects throwing things
|
|
|
|
/////////////////////////////////////////////////////////////////
|
|
//
|
|
// other constants
|
|
//
|
|
|
|
var RIGHT_HAND = 1;
|
|
var LEFT_HAND = 0;
|
|
|
|
var ZERO_VEC = {x: 0, y: 0, z: 0};
|
|
var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}";
|
|
var MSEC_PER_SEC = 1000.0;
|
|
|
|
// these control how long an abandoned pointer line will hang around
|
|
var startTime = Date.now();
|
|
var LIFETIME = 10;
|
|
|
|
// states for the state machine
|
|
var STATE_SEARCHING = 0;
|
|
var STATE_DISTANCE_HOLDING = 1;
|
|
var STATE_CONTINUE_DISTANCE_HOLDING = 2;
|
|
var STATE_NEAR_GRABBING = 3;
|
|
var STATE_CONTINUE_NEAR_GRABBING = 4;
|
|
var STATE_RELEASE = 5;
|
|
|
|
var GRAB_USER_DATA_KEY = "grabKey";
|
|
|
|
function controller(hand, triggerAction) {
|
|
this.hand = hand;
|
|
if (this.hand === RIGHT_HAND) {
|
|
this.getHandPosition = MyAvatar.getRightPalmPosition;
|
|
this.getHandRotation = MyAvatar.getRightPalmRotation;
|
|
} else {
|
|
this.getHandPosition = MyAvatar.getLeftPalmPosition;
|
|
this.getHandRotation = MyAvatar.getLeftPalmRotation;
|
|
}
|
|
this.triggerAction = triggerAction;
|
|
this.palm = 2 * hand;
|
|
// this.tip = 2 * hand + 1; // unused, but I'm leaving this here for fear it will be needed
|
|
|
|
this.actionID = null; // action this script created...
|
|
this.grabbedEntity = null; // on this entity.
|
|
this.grabbedVelocity = ZERO_VEC; // rolling average of held object's velocity
|
|
this.state = 0;
|
|
this.pointer = null; // entity-id of line object
|
|
this.triggerValue = 0; // rolling average of trigger value
|
|
|
|
this.update = function() {
|
|
switch(this.state) {
|
|
case STATE_SEARCHING:
|
|
this.search();
|
|
break;
|
|
case STATE_DISTANCE_HOLDING:
|
|
this.distanceHolding();
|
|
break;
|
|
case STATE_CONTINUE_DISTANCE_HOLDING:
|
|
this.continueDistanceHolding();
|
|
break;
|
|
case STATE_NEAR_GRABBING:
|
|
this.nearGrabbing();
|
|
break;
|
|
case STATE_CONTINUE_NEAR_GRABBING:
|
|
this.continueNearGrabbing();
|
|
break;
|
|
case STATE_RELEASE:
|
|
this.release();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
this.lineOn = function(closePoint, farPoint, color) {
|
|
// draw a line
|
|
if (this.pointer == null) {
|
|
this.pointer = Entities.addEntity({
|
|
type: "Line",
|
|
name: "pointer",
|
|
dimensions: LINE_ENTITY_DIMENSIONS,
|
|
visible: true,
|
|
position: closePoint,
|
|
linePoints: [ ZERO_VEC, farPoint ],
|
|
color: color,
|
|
lifetime: LIFETIME
|
|
});
|
|
} else {
|
|
Entities.editEntity(this.pointer, {
|
|
position: closePoint,
|
|
linePoints: [ ZERO_VEC, farPoint ],
|
|
color: color,
|
|
lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
this.lineOff = function() {
|
|
if (this.pointer != null) {
|
|
Entities.deleteEntity(this.pointer);
|
|
}
|
|
this.pointer = null;
|
|
}
|
|
|
|
|
|
this.triggerSmoothedSqueezed = function() {
|
|
var triggerValue = Controller.getActionValue(this.triggerAction);
|
|
// smooth out trigger value
|
|
this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) +
|
|
(triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO));
|
|
return this.triggerValue > TRIGGER_ON_VALUE;
|
|
}
|
|
|
|
|
|
this.triggerSqueezed = function() {
|
|
var triggerValue = Controller.getActionValue(this.triggerAction);
|
|
return triggerValue > TRIGGER_ON_VALUE;
|
|
}
|
|
|
|
|
|
this.search = function() {
|
|
if (!this.triggerSmoothedSqueezed()) {
|
|
this.state = STATE_RELEASE;
|
|
return;
|
|
}
|
|
|
|
// the trigger is being pressed, do a ray test
|
|
var handPosition = this.getHandPosition();
|
|
var pickRay = {origin: handPosition, direction: Quat.getUp(this.getHandRotation())};
|
|
var intersection = Entities.findRayIntersection(pickRay, true);
|
|
if (intersection.intersects &&
|
|
intersection.properties.collisionsWillMove === 1 &&
|
|
intersection.properties.locked === 0) {
|
|
// the ray is intersecting something we can move.
|
|
var handControllerPosition = Controller.getSpatialControlPosition(this.palm);
|
|
var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection);
|
|
this.grabbedEntity = intersection.entityID;
|
|
if (intersectionDistance < NEAR_PICK_MAX_DISTANCE) {
|
|
// the hand is very close to the intersected object. go into close-grabbing mode.
|
|
this.state = STATE_NEAR_GRABBING;
|
|
} else {
|
|
// the hand is far from the intersected object. go into distance-holding mode
|
|
this.state = STATE_DISTANCE_HOLDING;
|
|
this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR);
|
|
}
|
|
} else {
|
|
// forward ray test failed, try sphere test.
|
|
var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS);
|
|
var minDistance = GRAB_RADIUS;
|
|
var grabbedEntity = null;
|
|
for (var i = 0; i < nearbyEntities.length; i++) {
|
|
var props = Entities.getEntityProperties(nearbyEntities[i]);
|
|
var distance = Vec3.distance(props.position, handPosition);
|
|
if (distance < minDistance && props.name !== "pointer" &&
|
|
props.collisionsWillMove === 1 &&
|
|
props.locked === 0) {
|
|
this.grabbedEntity = nearbyEntities[i];
|
|
minDistance = distance;
|
|
}
|
|
}
|
|
if (this.grabbedEntity === null) {
|
|
this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR);
|
|
} else {
|
|
this.state = STATE_NEAR_GRABBING;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
this.distanceHolding = function() {
|
|
var handControllerPosition = Controller.getSpatialControlPosition(this.palm);
|
|
var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm));
|
|
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position","rotation"]);
|
|
|
|
// add the action and initialize some variables
|
|
this.currentObjectPosition = grabbedProperties.position;
|
|
this.currentObjectRotation = grabbedProperties.rotation;
|
|
this.currentObjectTime = Date.now();
|
|
this.handPreviousPosition = handControllerPosition;
|
|
this.handPreviousRotation = handRotation;
|
|
|
|
this.actionID = Entities.addAction("spring", this.grabbedEntity, {
|
|
targetPosition: this.currentObjectPosition,
|
|
linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME,
|
|
targetRotation: this.currentObjectRotation,
|
|
angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME
|
|
});
|
|
if (this.actionID == NULL_ACTION_ID) {
|
|
this.actionID = null;
|
|
}
|
|
|
|
if (this.actionID != null) {
|
|
this.state = STATE_CONTINUE_DISTANCE_HOLDING;
|
|
this.activateEntity(this.grabbedEntity);
|
|
Entities.callEntityMethod(this.grabbedEntity, "startDistantGrab");
|
|
|
|
if (this.hand === RIGHT_HAND) {
|
|
Entities.callEntityMethod(this.grabbedEntity, "setRightHand");
|
|
} else {
|
|
Entities.callEntityMethod(this.grabbedEntity, "setLeftHand");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
this.continueDistanceHolding = function() {
|
|
if (!this.triggerSmoothedSqueezed()) {
|
|
this.state = STATE_RELEASE;
|
|
return;
|
|
}
|
|
|
|
var handPosition = this.getHandPosition();
|
|
var handControllerPosition = Controller.getSpatialControlPosition(this.palm);
|
|
var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm));
|
|
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]);
|
|
|
|
this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR);
|
|
|
|
// the action was set up on a previous call. update the targets.
|
|
var radius = Math.max(Vec3.distance(this.currentObjectPosition,
|
|
handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR,
|
|
DISTANCE_HOLDING_RADIUS_FACTOR);
|
|
|
|
var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition);
|
|
this.handPreviousPosition = handControllerPosition;
|
|
var superHandMoved = Vec3.multiply(handMoved, radius);
|
|
|
|
var newObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved);
|
|
var deltaPosition = Vec3.subtract(newObjectPosition, this.currentObjectPosition); // meters
|
|
var now = Date.now();
|
|
var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds
|
|
this.computeReleaseVelocity(deltaPosition, deltaTime, false);
|
|
|
|
this.currentObjectPosition = newObjectPosition;
|
|
this.currentObjectTime = now;
|
|
|
|
// this doubles hand rotation
|
|
var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation,
|
|
DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR),
|
|
Quat.inverse(this.handPreviousRotation));
|
|
this.handPreviousRotation = handRotation;
|
|
this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
|
|
|
|
Entities.callEntityMethod(this.grabbedEntity, "continueDistantGrab");
|
|
|
|
Entities.updateAction(this.grabbedEntity, this.actionID, {
|
|
targetPosition: this.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME,
|
|
targetRotation: this.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME
|
|
});
|
|
}
|
|
|
|
|
|
this.nearGrabbing = function() {
|
|
if (!this.triggerSmoothedSqueezed()) {
|
|
this.state = STATE_RELEASE;
|
|
return;
|
|
}
|
|
|
|
this.lineOff();
|
|
|
|
this.activateEntity(this.grabbedEntity);
|
|
|
|
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]);
|
|
|
|
var handRotation = this.getHandRotation();
|
|
var handPosition = this.getHandPosition();
|
|
|
|
var objectRotation = grabbedProperties.rotation;
|
|
var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation);
|
|
|
|
currentObjectPosition = grabbedProperties.position;
|
|
var offset = Vec3.subtract(currentObjectPosition, handPosition);
|
|
var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset);
|
|
|
|
this.actionID = Entities.addAction("hold", this.grabbedEntity, {
|
|
hand: this.hand == RIGHT_HAND ? "right" : "left",
|
|
timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
|
|
relativePosition: offsetPosition,
|
|
relativeRotation: offsetRotation
|
|
});
|
|
if (this.actionID == NULL_ACTION_ID) {
|
|
this.actionID = null;
|
|
} else {
|
|
this.state = STATE_CONTINUE_NEAR_GRABBING;
|
|
Entities.callEntityMethod(this.grabbedEntity, "startNearGrab");
|
|
if (this.hand === RIGHT_HAND) {
|
|
Entities.callEntityMethod(this.grabbedEntity, "setRightHand");
|
|
} else {
|
|
Entities.callEntityMethod(this.grabbedEntity, "setLeftHand");
|
|
}
|
|
}
|
|
|
|
this.currentHandControllerPosition = Controller.getSpatialControlPosition(this.palm);
|
|
this.currentObjectTime = Date.now();
|
|
}
|
|
|
|
|
|
this.continueNearGrabbing = function() {
|
|
if (!this.triggerSmoothedSqueezed()) {
|
|
this.state = STATE_RELEASE;
|
|
return;
|
|
}
|
|
|
|
// keep track of the measured velocity of the held object
|
|
var handControllerPosition = Controller.getSpatialControlPosition(this.palm);
|
|
var now = Date.now();
|
|
|
|
var deltaPosition = Vec3.subtract(handControllerPosition, this.currentHandControllerPosition); // meters
|
|
var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds
|
|
this.computeReleaseVelocity(deltaPosition, deltaTime, true);
|
|
|
|
this.currentHandControllerPosition = handControllerPosition;
|
|
this.currentObjectTime = now;
|
|
Entities.callEntityMethod(this.grabbedEntity, "continueNearGrab");
|
|
}
|
|
|
|
|
|
this.computeReleaseVelocity = function(deltaPosition, deltaTime, useMultiplier) {
|
|
if (deltaTime > 0.0 && !vec3equal(deltaPosition, ZERO_VEC)) {
|
|
var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime);
|
|
// don't update grabbedVelocity if the trigger is off. the smoothing of the trigger
|
|
// value would otherwise give the held object time to slow down.
|
|
if (this.triggerSqueezed()) {
|
|
this.grabbedVelocity =
|
|
Vec3.sum(Vec3.multiply(this.grabbedVelocity,
|
|
(1.0 - NEAR_GRABBING_VELOCITY_SMOOTH_RATIO)),
|
|
Vec3.multiply(grabbedVelocity, NEAR_GRABBING_VELOCITY_SMOOTH_RATIO));
|
|
}
|
|
|
|
if (useMultiplier) {
|
|
this.grabbedVelocity = Vec3.multiply(this.grabbedVelocity, RELEASE_VELOCITY_MULTIPLIER);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
this.release = function() {
|
|
this.lineOff();
|
|
|
|
if (this.grabbedEntity != null && this.actionID != null) {
|
|
Entities.deleteAction(this.grabbedEntity, this.actionID);
|
|
Entities.callEntityMethod(this.grabbedEntity, "releaseGrab");
|
|
}
|
|
|
|
// the action will tend to quickly bring an object's velocity to zero. now that
|
|
// the action is gone, set the objects velocity to something the holder might expect.
|
|
Entities.editEntity(this.grabbedEntity, {velocity: this.grabbedVelocity});
|
|
this.deactivateEntity(this.grabbedEntity);
|
|
|
|
this.grabbedVelocity = ZERO_VEC;
|
|
this.grabbedEntity = null;
|
|
this.actionID = null;
|
|
this.state = STATE_SEARCHING;
|
|
}
|
|
|
|
|
|
this.cleanup = function() {
|
|
release();
|
|
}
|
|
|
|
this.activateEntity = function(entity) {
|
|
var data = {
|
|
activated: true,
|
|
avatarId: MyAvatar.sessionUUID
|
|
};
|
|
setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data);
|
|
}
|
|
|
|
this.deactivateEntity = function(entity) {
|
|
var data = {
|
|
activated: false,
|
|
avatarId: null
|
|
};
|
|
setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data);
|
|
}
|
|
}
|
|
|
|
|
|
var rightController = new controller(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK"));
|
|
var leftController = new controller(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK"));
|
|
|
|
|
|
function update() {
|
|
rightController.update();
|
|
leftController.update();
|
|
}
|
|
|
|
|
|
function cleanup() {
|
|
rightController.cleanup();
|
|
leftController.cleanup();
|
|
}
|
|
|
|
|
|
Script.scriptEnding.connect(cleanup);
|
|
Script.update.connect(update)
|