All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.onemillionworlds.tamarin.vrhands.BoundHand Maven / Gradle / Ivy

There is a newer version: 2.6.1
Show newest version
package com.onemillionworlds.tamarin.vrhands;

import com.jme3.anim.Armature;
import com.jme3.anim.Joint;
import com.jme3.anim.SkinningControl;
import com.jme3.asset.AssetManager;
import com.jme3.bounding.BoundingSphere;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Line;
import com.jme3.scene.shape.Sphere;
import com.onemillionworlds.tamarin.actions.HandSide;
import com.onemillionworlds.tamarin.actions.OpenXrActionState;
import com.onemillionworlds.tamarin.actions.actionprofile.ActionHandle;
import com.onemillionworlds.tamarin.actions.state.BonePose;
import com.onemillionworlds.tamarin.actions.state.BooleanActionState;
import com.onemillionworlds.tamarin.actions.state.FloatActionState;
import com.onemillionworlds.tamarin.debug.TamarinDebugOverlayState;
import com.onemillionworlds.tamarin.handskeleton.HandJoint;
import com.onemillionworlds.tamarin.math.RotationalVelocity;
import com.onemillionworlds.tamarin.vrhands.functions.BoundHandFunction;
import com.onemillionworlds.tamarin.vrhands.functions.ClimbSupport;
import com.onemillionworlds.tamarin.vrhands.functions.FunctionRegistration;
import com.onemillionworlds.tamarin.vrhands.functions.GrabPickingFunction;
import com.onemillionworlds.tamarin.vrhands.functions.LemurClickFunction;
import com.onemillionworlds.tamarin.vrhands.functions.PickMarkerFunction;
import com.onemillionworlds.tamarin.vrhands.functions.PressFunction;
import com.onemillionworlds.tamarin.vrhands.grabbing.AbstractGrabControl;
import com.onemillionworlds.tamarin.vrhands.touching.AbstractTouchControl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;

@SuppressWarnings("unused")
public abstract class BoundHand {
    public static final String DEFAULT_HAND_TEXTURE = "Tamarin/Textures/basicHands_pinStripe.png";
    public static final String DEFAULT_HAND_MODEL_LEFT = "Tamarin/Models/basicHands_left.j3o";
    public static final String DEFAULT_HAND_MODEL_RIGHT = "Tamarin/Models/basicHands_right.j3o";
    public static final float FINGER_PICK_SPHERE_RADIUS = 0.0075F;
    private static boolean lemurCheckedAvailable = false;
    private static boolean lemurIsAvailable = false;
    /**
     * Placed on GEOMETRIES that should not be picked by the hand (either grab or point pick)
     */
    public static String NO_PICK = "TAMARIN_NO_PICK";
    /**
     * Placed on nodes where if a pick is done on a child it will stop looking higher in the parent chain for
     * grab controls or similar.
     */
    public static final String TAMARIN_STOP_BUBBLING = "TAMARIN_STOP_BUBBLING";
    private final Node rawOpenXrPosition = new Node();
    private final Node geometryNode = new Node();
    /**
     * Returns a node that will update with the hands position and rotation.
     * 

* The exact position and rotation of this node depends on the pose type specified for the hand: *

* If the pose is AIM them it will be just in front and above the hand with +X pointing in the direction a held weapon * would fire with Y pointing upwards and Z pointing to the right (when the hands are held with the palms * facing each other) *

* If the pose is GRIP them it will be away from the palm, with +X pointing in the direction a held weapon * would fire with Y pointing upwards and Z pointing to the right (when the hands are held with the palms * facing each other) *

* This node has an orientation such that x aligns with the grip direction (like you are holding a sword, and Z * pointing to the right (when the hands are held with the palms facing each other). *

* This is an ideal node for things like picking lines, which can be put in the x direction *

* Note that the (0,0,0) position is just in front of the thumb, not the centre of the hand. *

* This node is primarily used for picking, but if you want a node to attach to that only cares about the bulk * hand position */ private final Node handNode_xPointing = new Node(); /** * A hand node, with +z pointing in the direction of the bulk hand (if an AIM pose). This is used primarily for direct lemur interactions */ private final Node handNode_zPointing = new Node(); private final Node pickLineNode = new Node(); /** * This is a node that sits on the palm (but near the fingers) such that with the hands held so the palms face each other * +X faces forwards (along the line of the fingers) the +Y points upwards and +Z points to the right. * It is live updated based on the skeleton positions so its exact relations to other nodes may change as * the node moves. */ private final Node palmNode_xPointing = new Node(); /** * This is a node that sits near on the tip of the index finger whose +x points out way from the index * finger (and Y point up and Z points right if the hands are held with palms facing each other. This node sits inside * the finger such that a sphere would touch the skin of the finger in 5 directions. * The exact distance from the skin can vary (it is reported by OpenXr) but for bound hand's perspective it is considered * to be {@link BoundHand#FINGER_PICK_SPHERE_RADIUS} away from the skin. */ private final Node indexFingerTip_xPointing = new Node(); private final Node debugPointsNode = new Node(); /** * A node at the wrist. *

* With hands held flat with thumbs facing each other +x going to the right, +y goes upwards and +z goes towards the player */ private final Node wristNode = new Node(); /** * The hand will be detached from the scene graph and will no longer receive updates */ public abstract void unbindHand(); private final ActionHandle handPoseActionName; private final ActionHandle skeletonActionName; private Armature armature; private final AssetManager assetManager; /** * Left or right hand */ private final HandSide handSide; private final OpenXrActionState vrState; /** * Debug points add markers onto the hands where the bone positions are, and their directions */ private boolean debugPoints = false; private float baseSkinDepth = 0.02F; /** * The velocity (in world coordinates) that the hand is currently moving at */ private Vector3f velocity_world = new Vector3f(); /** * The rotational velocity (in world coordinates) that the hand is currently rotating at */ private RotationalVelocity rotationalVelocity_world = new RotationalVelocity(new Vector3f()); /** * When doing a palm pick spheres of this radius are created above the palm */ private float palmPickSphereRadius = 0.02F; /** * When doing a palm pick these are the points where spheres are formed to detect if the palm is against anything. * They are relative to the palm node (in the {@link BoundHand#getPalmNode}'s coordinate system) */ private List palmPickPoints; private final List functions = new CopyOnWriteArrayList<>(); /** * A pointing arrangement is when the index finger is mostly straight and the ring ringer is not. *

* This is the sort of hand position that indicates pressing buttons with the index finger */ public boolean handPointing = false; private final HandJoint middleProximalName = HandJoint.MIDDLE_PROXIMAL_EXT; private final HandJoint middleMetacarpalName = HandJoint.MIDDLE_METACARPAL_EXT; private final HandJoint indexMetacarpalName = HandJoint.INDEX_METACARPAL_EXT; private final HandJoint indexEndName = HandJoint.INDEX_TIP_EXT; private final HandJoint index2Name = HandJoint.INDEX_INTERMEDIATE_EXT; private final HandJoint index1Name = HandJoint.INDEX_PROXIMAL_EXT; private final HandJoint ringMetacarpalName = HandJoint.RING_METACARPAL_EXT; private final HandJoint ringEndName = HandJoint.RING_TIP_EXT; private final HandJoint ring2Name = HandJoint.RING_INTERMEDIATE_EXT; private final HandJoint ring1Name = HandJoint.RING_PROXIMAL_EXT; private final HandJoint wristName = HandJoint.WRIST_EXT; public BoundHand(OpenXrActionState vrState, ActionHandle handPoseActionName, ActionHandle skeletonActionName, Spatial handGeometry, Armature armature, AssetManager assetManager, HandSide handSide) { this.vrState = Objects.requireNonNull(vrState); this.geometryNode.attachChild(handGeometry); this.handPoseActionName = handPoseActionName; this.skeletonActionName = skeletonActionName; this.armature = armature; this.assetManager = assetManager; this.handSide = handSide; this.rawOpenXrPosition.attachChild(handNode_xPointing); this.rawOpenXrPosition.attachChild(debugPointsNode); float outOfPalm = (handSide == HandSide.LEFT ? 1 : -1); this.palmPickPoints = List.of(new Vector3f(0, 0, outOfPalm * (0.01F + palmPickSphereRadius)), new Vector3f(0.02F, -0.03F, outOfPalm * (0.01F + palmPickSphereRadius)), new Vector3f(0.03F, 0.03F, outOfPalm * (0.005F + palmPickSphereRadius)), new Vector3f(-0.03F, 0, outOfPalm * (0.01F + palmPickSphereRadius))); searchForGeometry(handGeometry).forEach(g -> g.setUserData(NO_PICK, true)); Quaternion naturalRotation = new Quaternion(); naturalRotation.fromAngleAxis(-0.5F * FastMath.PI, Vector3f.UNIT_X); Quaternion zToXRotation = new Quaternion(); zToXRotation.fromAngleAxis(0.5F * FastMath.PI, Vector3f.UNIT_Z); Quaternion rotateAxes = new Quaternion(); rotateAxes.fromAngleAxis(0.5F * FastMath.PI, Vector3f.UNIT_X); handNode_xPointing.setLocalRotation(naturalRotation.mult(zToXRotation).mult(rotateAxes)); Quaternion xPointingToZPointing = new Quaternion(); xPointingToZPointing.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y); handNode_zPointing.setLocalRotation(xPointingToZPointing); handNode_xPointing.attachChild(handNode_zPointing); rawOpenXrPosition.attachChild(palmNode_xPointing); rawOpenXrPosition.attachChild(geometryNode); rawOpenXrPosition.attachChild(indexFingerTip_xPointing); rawOpenXrPosition.attachChild(wristNode); handNode_xPointing.attachChild(pickLineNode); addFunction(new ClimbSupport()); } /** * @param handGeometry A spatial that will be updated with the hand geometry, it must have a SkinningControl * at its top level */ public void updateHandGeometryAndControlledArmature(Spatial handGeometry) { geometryNode.detachAllChildren(); geometryNode.attachChild(handGeometry); searchForGeometry(handGeometry).forEach(g -> g.setUserData(NO_PICK, true)); SkinningControl skinningControl = handGeometry.getControl(SkinningControl.class); if (skinningControl == null) { throw new IllegalArgumentException("Hand geometry must have a SkinningControl"); } this.armature = skinningControl.getArmature(); } /** * Returns a node that will update with the hands position and rotation. If you are dealing with raw bone positions * they are in this coordinate system. *

* Note that this is in the orientation that OpenVR provides which isn't very helpful * (it doesn't map well to the direction the hand is pointing for example) *

* You probably don't want this and probably want {@link BoundHand#getHandNode_xPointing} * which has a more natural rotation. Unless you are dealing with raw bone positions, which are in this coordinate * system. * * @return the raw hand node */ public Node getRawOpenVrNode() { return rawOpenXrPosition; } /** * Returns a node that will update with the hands position and rotation. *

* This node has an orientation such that x aligns with the hands pointing direction, Y pointing upwards and Z * pointing to the right, as defined by the middle finger metacarpal bone. Its X,Y and Z are likely to be in similar * directions to those of {@link BoundHand#getHandNode_xPointing()} but not precisely * @return the palm node */ public Node getPalmNode() { return palmNode_xPointing; } /** * The material that will be applied to any geometries within the spatial provided as the hand * @param material the material */ public void setMaterial(Material material) { searchForGeometry(geometryNode).forEach(g -> g.setMaterial(material)); } /** * Adds the requested * @param function the functionality to add (things like grabbing, clicking etc are implemented as BoundHandFunctions * @return a method that if called will remove the function */ public FunctionRegistration addFunction(BoundHandFunction function) { function.onBind(this, vrState.getStateManager()); functions.add(function); return () -> removeFunction(function); } /** * Note that this function can behave oddly when multiple copies of the same BoundHandFunction type are * registered. It is better to use the Runnable returned by addFunction to remove functions */ public void removeFunction(Class functionToRemove) { BoundHandFunction function = functions.stream().filter(f -> f.getClass().equals(functionToRemove)).findFirst().orElse(null); removeFunction(function); } public void removeFunction(BoundHandFunction functionToRemove) { if (functionToRemove != null) { functionToRemove.onUnbind(this, vrState.getStateManager()); functions.remove(functionToRemove); } } public Optional getFunctionOpt(Class function) { //noinspection unchecked return functions.stream().filter(f -> f.getClass().equals(function)).map(f -> (T) f).findFirst(); } public T getFunction(Class function) { return getFunctionOpt(function).orElseThrow(); } protected void update(float timeSlice, Map boneStances) { if (debugPoints) { debugPointsNode.detachAllChildren(); debugPointsNode.attachChild(armatureToNodes(getArmature(), ColorRGBA.Red)); } updatePalm(timeSlice, boneStances); updateFingerTips(boneStances); updateWrist(boneStances); functions.forEach(f -> f.update(timeSlice, this, vrState.getStateManager())); updatePointingState(boneStances); } /** * Updates the hand to check if it's in a pointing arrangement; * fist with index finger outstretched, like when pressing a button */ private void updatePointingState(Map boneStances) { BonePose indexEnd = boneStances.get(indexEndName); BonePose index2 = boneStances.get(index2Name); BonePose index1 = boneStances.get(index1Name); BonePose indexMeta = boneStances.get(indexMetacarpalName); BonePose ringEnd = boneStances.get(ringEndName); BonePose ring2 = boneStances.get(ring2Name); BonePose ring1 = boneStances.get(ring1Name); BonePose ringMeta = boneStances.get(ringMetacarpalName); if (notNull(indexEnd, index2, index1, ringEnd, ring2, ring1, indexMeta, ringMeta)) { float ringFingerAlignment = ringEnd.position().subtract(ring2.position()).normalizeLocal().dot(ring1.position().subtract(ringMeta.position()).normalizeLocal()); float indexFingerAlignment = indexEnd.position().subtract(index2.position()).normalizeLocal().dot(index1.position().subtract(indexMeta.position()).normalizeLocal()); handPointing = indexFingerAlignment > 0.85 && ringFingerAlignment < 0.8; } } private void updateWrist(Map boneStances) { BonePose wrist = boneStances.get(wristName); if (wrist != null) { wristNode.setLocalTranslation(wrist.position()); wristNode.setLocalRotation(wrist.orientation()); } } private void updatePalm(float timeSlice, Map boneStances) { //the palm node is put at the position between the finger_middle_0_l bone and finger_middle_meta_l, but with the // rotation of the finger_middle_meta_l bone. This gives roughly the position of a grab point, with a sensible rotation BonePose metacarpel = boneStances.get(middleMetacarpalName); BonePose proximal = boneStances.get(middleProximalName); if (metacarpel != null) { Quaternion coordinateStandardisingRotation = new Quaternion(); if (handSide == HandSide.LEFT) { coordinateStandardisingRotation.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y); coordinateStandardisingRotation.multLocal(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X)); } else { coordinateStandardisingRotation.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y); coordinateStandardisingRotation.multLocal(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X)); } palmNode_xPointing.setLocalTranslation(proximal.position().add(metacarpel.position()).multLocal(0.5F)); palmNode_xPointing.setLocalRotation(metacarpel.orientation().mult(coordinateStandardisingRotation)); } } private void updateFingerTips(Map boneStances) { BonePose indexFingerTip = boneStances.get(indexEndName); if (indexFingerTip != null) { indexFingerTip_xPointing.setLocalTranslation(indexFingerTip.position()); Quaternion rotation = indexFingerTip.orientation(); Quaternion coordinateStandardisingRotation = new Quaternion(); if (handSide == HandSide.LEFT) { coordinateStandardisingRotation.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y); coordinateStandardisingRotation.multLocal(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X)); } else { coordinateStandardisingRotation.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y); coordinateStandardisingRotation.multLocal(new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X)); } indexFingerTip_xPointing.setLocalRotation(rotation.mult(coordinateStandardisingRotation)); } } /** * Picks using a small sphere at the index finger tip (to catch if the index finger has been plunged into something) * @param nodeToPickAgainst the node that contains geometries to be picked from * @return the results */ public CollisionResults pickIndexFingerTip(Spatial nodeToPickAgainst) { CollisionResults results = new CollisionResults(); BoundingSphere sphere = new BoundingSphere(FINGER_PICK_SPHERE_RADIUS, indexFingerTip_xPointing.getWorldTranslation()); nodeToPickAgainst.collideWith(sphere, results); return results; } /** * Picks from a point just in front of the thumb (the point the getHandNode_zPointing() is at) in the direction * out away from the hand. *

* Note that the geometry of the hand itself may be the first result from the pick but more reasonable pick results * will follow. (These will have NO_PICK = true as userdata which can be used to ignore them) * * @param nodeToPickAgainst node that is the parent of all things that can be picked. Probably the root node */ public CollisionResults pickBulkHand(Node nodeToPickAgainst) { Vector3f pickOrigin = getHandNode_xPointing().getWorldTranslation(); Vector3f pickingPoint = getHandNode_xPointing().localToWorld(new Vector3f(1, 0, 0), null); Vector3f pickingVector = pickingPoint.subtract(pickOrigin); pickingVector.normalizeLocal(); CollisionResults results = new CollisionResults(); Ray ray = new Ray(pickOrigin, pickingVector); nodeToPickAgainst.collideWith(ray, results); return results; } /** * This will set the hand to do a pick in the same direction as {@link BoundHand#pickBulkHand}/lemur clicks * and place a marker (by default a white sphere) at the point where the pick hits a geometry. This gives the * player an indication what they would pick it they clicked now; think of it like a mouse pointer in 3d space *

* Run the returned runnable to remove the pick marker */ public FunctionRegistration setPickMarkerContinuous(Node nodeToPickAgainst) { return addFunction(new PickMarkerFunction(nodeToPickAgainst)); } /** * Picks from roughly the centre of the palm out from the palm (i.e. for the left hand it points right) *

* This can be used to detect what the palm is pointing at (which is rarely useful to be honest) *

* Note that the geometry of the hand itself may be the first result from the pick but more reasonable pick results * will follow. (These will have NO_PICK = true as userdata which can be used to ignore them) *

* deprecated as intend to move towards the pickPalmSpheres method * * @param nodeToPickAgainst node that is the parent of all things that can be picked. Probably the root node */ @Deprecated public CollisionResults pickPalm(Node nodeToPickAgainst) { return pickPalm(nodeToPickAgainst, Vector3f.ZERO); } /** * Picks (using a series of bounding shapes) just beyond the palm. Unlike pickPalm it does not pick out to an * infinite range but uses a series of spheres *

* This can be useful to use picking to determine what the player wishes to grab */ public CollisionResults pickGrab(Node nodeToPickAgainst) { Vector3f worldPickLocation = new Vector3f(); CollisionResults overallResults = new CollisionResults(); for (Vector3f pickPoint : palmPickPoints) { worldPickLocation = getPalmNode().localToWorld(pickPoint, worldPickLocation); CollisionResults results = new CollisionResults(); BoundingSphere sphere = new BoundingSphere(palmPickSphereRadius, worldPickLocation); nodeToPickAgainst.collideWith(sphere, results); for (int i = 0; i < results.size(); i++) { CollisionResult result = results.getCollision(i); if (!Boolean.TRUE.equals(result.getGeometry().getUserData(NO_PICK))) { overallResults.addCollision(result); } } } return overallResults; } /** * Picks outward away from the palm palmRelativePosition to {@link BoundHand#getPalmNode()}. * Note that x is towards the fingers, y is up and z is right (whether +z is the palm direction depends on if this * is the left or right hand) *

* deprecated as intend to move towards the pickPalmSpheres method * * @param nodeToPickAgainst node that is the parent of all things that can be picked. Probably the root node * @param palmRelativePosition the position for the pick line to start */ @Deprecated public CollisionResults pickPalm(Node nodeToPickAgainst, Vector3f palmRelativePosition) { Vector3f pickOrigin = getPalmNode().localToWorld(palmRelativePosition, null); Vector3f pickingPoint = getPalmNode().localToWorld(palmRelativePosition.add(0, 0, handSide == HandSide.LEFT ? 1 : -1), null); Vector3f pickingVector = pickingPoint.subtract(pickOrigin); CollisionResults results = new CollisionResults(); Ray ray = new Ray(pickOrigin, pickingVector); nodeToPickAgainst.collideWith(ray, results); return results; } /** * Will start rendering the positions of the bones (note if all is well they will be inside the hands, so not really * visible (doing this is not performant, debug only) * Deprecated. Use {@link TamarinDebugOverlayState} instead */ @Deprecated public void debugArmature() { this.debugPoints = true; } /** * Adds a debug line in the direction the hand would use for picking or clicking * Deprecated. Use {@link TamarinDebugOverlayState} instead */ @Deprecated public void debugPointingPickLine() { handNode_xPointing.attachChild(microLine(ColorRGBA.Green, new Vector3f(0.25F, 0, 0))); indexFingerTip_xPointing.attachChild(microLine(ColorRGBA.Green, new Vector3f(0.25F, 0, 0))); } /** * Adds green (x), yellow (y) and red (x) lines indicating the coordinate system of the node * {@link BoundHand#getHandNode_xPointing()} * Deprecated. Use {@link TamarinDebugOverlayState} instead */ @Deprecated public void debugHandNodeXPointingCoordinateSystem() { handNode_xPointing.attachChild(microLine(ColorRGBA.Green, new Vector3f(0.25F, 0, 0))); handNode_xPointing.attachChild(microLine(ColorRGBA.Yellow, new Vector3f(0, 0.15F, 0))); handNode_xPointing.attachChild(microLine(ColorRGBA.Red, new Vector3f(0, 0.0F, 0.1F))); } /** * Adds green (x), yellow (y) and red (x) lines indicating the coordinate system of the node * {@link BoundHand#getHandNode_xPointing()} * * Deprecated. Use {@link TamarinDebugOverlayState} instead (Although it doesn't actually support this particular point * it does support the getHandNode_xPointing one) */ @Deprecated public void debugHandNodeZPointingCoordinateSystem() { handNode_zPointing.attachChild(microLine(ColorRGBA.Green, new Vector3f(0.15F, 0, 0))); handNode_zPointing.attachChild(microLine(ColorRGBA.Yellow, new Vector3f(0, 0.15F, 0))); handNode_zPointing.attachChild(microLine(ColorRGBA.Red, new Vector3f(0, 0.0F, 0.25F))); } /** * Adds green (x), yellow (y) and red (x) lines indicating the coordinate system of the node * {@link BoundHand#getPalmNode()} * Deprecated. Sort of replaced by {@link TamarinDebugOverlayState} but really the palmNode_xPointing coordinate system * is niche */ @Deprecated public void debugPalmCoordinateSystem() { palmNode_xPointing.attachChild(microLine(ColorRGBA.Green, new Vector3f(0.25F, 0, 0))); palmNode_xPointing.attachChild(microLine(ColorRGBA.Yellow, new Vector3f(0, 0.15F, 0))); palmNode_xPointing.attachChild(microLine(ColorRGBA.Red, new Vector3f(0, 0.0F, 0.1F))); } /** * spheres showing the current grab points for the palm. *

* The first (index zero) points are bright, the last points (high index) are dark * {@link BoundHand#getPalmNode()} * Deprecated. Use {@link TamarinDebugOverlayState} instead */ @Deprecated public FunctionRegistration debugPalmGrabPoints() { List addedPoints = new ArrayList<>(); int index = 0; for (Vector3f grabPoint : this.palmPickPoints) { float brightness = ((float) this.palmPickPoints.size() - index) / this.palmPickPoints.size(); index++; Geometry sphere = sphere(new ColorRGBA(brightness, brightness, brightness, 1), grabPoint, palmPickSphereRadius); addedPoints.add(sphere); palmNode_xPointing.attachChild(sphere); } return () -> addedPoints.forEach(Spatial::removeFromParent); } /** * Returns a world position that a spatial's holdCentre should be at. * @param distanceFromSkin how far from the skin the hold position should be (provided so differing sized objects make sense) */ public Vector3f getHoldPosition(float distanceFromSkin) { return palmNode_xPointing.localToWorld(new Vector3f(0, 0, (handSide == HandSide.LEFT ? 1 : -1) * (baseSkinDepth + distanceFromSkin)), null); } /** * The rotation that should be applied to objects currently being held by this hand */ public Quaternion getHoldRotation() { return getPalmNode().getWorldRotation(); } /** * When a grab action is specified (action in the openVr action manifest sense of the word) then periodically * (see {@link GrabPickingFunction#setGrabEvery}) if the action is true then a grab pick will occur and if the pick finds * any spatials with a control of type {@link AbstractGrabControl} then it will grab them. Equally, once bound if the * grab action is released then it will unbind from them. *

* The action can be non hand specific as the hand restricts the action to only the hand this BoundHand represents *

* The grab action can be either an analog or digital action *

* Use the {@link FunctionRegistration} to end the function when done * * @param grabAction the openVr action name to use to decide if the hand is grabbing * @param nodeToPickAgainst the node to scan for items to grab (probably the root node) */ public FunctionRegistration setGrabAction(ActionHandle grabAction, Node nodeToPickAgainst) { GrabPickingFunction grabPickingFunction = new GrabPickingFunction(grabAction, nodeToPickAgainst); return addFunction(grabPickingFunction); } public void clearGrabAction() { removeFunction(GrabPickingFunction.class); } /** * Will bind an action (see actions manifest) against a lemur click (picks against the * passed node). *

* This requires lemur to be on the class path (or else you'll get an exception). *

* It kind of "fakes" a click. So it's only limited in what it does. It tracks up the parents of things it hits looking * for things which are a button, or have a MouseEventControl. If it finds one it clicks that, then returns. *

* Its worth noting that the MouseButtonEvents will not have meaningful x,y coordinates *

* More advanced functionality, like receiving on click on nothing events can be obtained by using the {@link BoundHand#addFunction(BoundHandFunction)} * method and adding a configured LemurClickFunction *

* NOTE: at present only a single action can be picked against (but potentially many nodes) at a time and old click actions will be deregistered. * However, that restriction may be lifted in later versions so old actions should be explicitly removed for forwards * compatibility *

* Use the {@link FunctionRegistration} to end the function when done * * @param clickAction the action (see action manifest) that will trigger a click, can be a vector1 or a digital action. * @param nodesToPickAgainst The node(s) that is picked against to look for lemur UIs * @return a Runnable that if called will end the click action */ public FunctionRegistration setClickAction_lemurSupport(ActionHandle clickAction, Node... nodesToPickAgainst) { assertLemurAvailable(); clearClickAction_lemurSupport(); //the reason for this is the way that with many nodes dominance becomes a problem, if attached as several ClickActions (would probably be fine if bound to different buttons) return addFunction(new LemurClickFunction(clickAction, nodesToPickAgainst)); } /** * Will continuously look for touches between the node and the index finger tip. Will trigger lemur buttons etc (if * lemur is available) and Tamarin {@link AbstractTouchControl}. Filters out accidental double taps and only fires * when the fingertip first touches (I.e. putting your finger on a button will only generate a single event, not * continuous events while the finger remains in contact) *

* Use the {@link FunctionRegistration} to end the function when done * * @param nodeToScanForTouches the note to scan for contact with the fingertip * @param requireFingerPointing if the scan should only occur if the hand is in a pointing arrangement * (index finger outstretched, other fingers curled) * @param vibrateActionOnTouch the action name of the vibration binding (e.g. "/actions/main/out/haptic"). Can be null for no vibrate * @param vibrateOnTouchIntensity how hard the vibration response is. Should be between 0 (none) and 1 (lots) */ public FunctionRegistration setFingerTipPressDetection(Node nodeToScanForTouches, boolean requireFingerPointing, ActionHandle vibrateActionOnTouch, float vibrateOnTouchIntensity) { return addFunction(new PressFunction(nodeToScanForTouches, requireFingerPointing, vibrateActionOnTouch, vibrateOnTouchIntensity)); } /** * Clears the click action. */ public void clearClickAction_lemurSupport() { removeFunction(LemurClickFunction.class); } /** * Set the depth between the centre of the palm and the skin of the palm used by the hand model you have bound. *

* This ensures that held objects are flush against the palm * @param baseSkinDepth a value in meters (0.02 is a good example of the right kind of size) */ public void setBaseSkinDepth(float baseSkinDepth) { this.baseSkinDepth = baseSkinDepth; } /** * Broadly similar to attaching a geometry to {@link BoundHand#getHandNode_xPointing()} but it will get special * handling to ensure it doesn't block lemur picks (and will all get the {@link BoundHand#NO_PICK} label on all its * geometries which may make it easier to avoid when using manual picking. Also it can be easily removed with the *

* Use the {@link FunctionRegistration} to end the function when done *

* {@link BoundHand#removePickLine()} method * @param spatial the pick line (+X should be in the direction of the pick line) * @return a FunctionRegistration that will remove the pick line */ public FunctionRegistration attachPickLine(Spatial spatial) { searchForGeometry(spatial).forEach(g -> g.setUserData(NO_PICK, true)); pickLineNode.attachChild(spatial); return spatial::removeFromParent; } public void removePickLine() { pickLineNode.detachAllChildren(); } protected void updateVelocityData(Vector3f velocity_world, RotationalVelocity rotationalVelocity_world) { this.velocity_world = velocity_world; this.rotationalVelocity_world = rotationalVelocity_world; } private static Collection searchForGeometry(Spatial spatial) { if (spatial instanceof Geometry) { return List.of((Geometry) spatial); } else if (spatial instanceof Node) { List geometries = new ArrayList<>(); for (Spatial child : ((Node) spatial).getChildren()) { geometries.addAll(searchForGeometry(child)); } return geometries; } else { throw new RuntimeException("Could not find skinable model"); } } private Spatial armatureToNodes(Armature armature, ColorRGBA colorRGBA) { return jointToNode(armature.getRoots()[0], colorRGBA); } private Spatial jointToNode(Joint joint, ColorRGBA colorRGBA) { Node node = new Node(); node.setLocalTranslation(joint.getLocalTranslation()); node.setLocalRotation(joint.getLocalRotation()); node.attachChild(microBox(colorRGBA)); node.attachChild(microLine(colorRGBA)); for (Joint child : joint.getChildren()) { node.attachChild(jointToNode(child, colorRGBA)); } return node; } private Geometry microBox(ColorRGBA colorRGBA) { Box b = new Box(0.002F, 0.002F, 0.002F); Geometry geom = new Geometry("debugHandBox", b); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", colorRGBA); geom.setMaterial(mat); geom.setUserData(NO_PICK, true); return geom; } private Geometry sphere(ColorRGBA colorRGBA, Vector3f position, float radius) { Sphere b = new Sphere(10, 10, radius); Geometry geom = new Geometry("debugHandSphere", b); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", colorRGBA); geom.setMaterial(mat); geom.setUserData(NO_PICK, true); geom.setLocalTranslation(position); return geom; } private Geometry microLine(ColorRGBA colorRGBA) { return microLine(colorRGBA, new Vector3f(0.015F, 0, 0)); } private Geometry microLine(ColorRGBA colorRGBA, Vector3f vector) { Line line = new Line(new Vector3f(0, 0, 0), vector); Geometry geometry = new Geometry("debugHandLine", line); Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); material.getAdditionalRenderState().setLineWidth(5); material.setColor("Color", colorRGBA); geometry.setMaterial(material); geometry.setUserData(NO_PICK, true); return geometry; } /** * Given a picking result returns the first result that is not marked as being {@link BoundHand#NO_PICK} * @param collisionResults the collision result * @return the first hit */ public static Optional firstNonSkippedHit(CollisionResults collisionResults) { for (CollisionResult hit : collisionResults) { if (!Boolean.TRUE.equals(hit.getGeometry().getUserData(NO_PICK))) { return Optional.of(hit); } } return Optional.empty(); } /** * Returns the direction the hand is pointing (i.e. the direction the fingers would point if they aren't curled) * @return the bulkPointingDirection */ public Vector3f getBulkPointingDirection() { return handNode_xPointing.getWorldRotation().mult(Vector3f.UNIT_X); } @Override public String toString() { return handSide.name() + " hand"; } public static void assertLemurAvailable() { if (!isLemurAvailable()) { throw new RuntimeException("Lemur not available on class path. Lemur required for methods named _lemurSupport or classes with Lemur in name"); } } public static boolean isLemurAvailable() { if (!lemurCheckedAvailable) { try { Class.forName("com.simsilica.lemur.Button"); lemurIsAvailable = true; } catch (Throwable ex) { lemurIsAvailable = false; } lemurCheckedAvailable = true; } return lemurIsAvailable; } public static boolean notNull(Object... objects) { for (Object o : objects) { if (o == null) { return false; } } return true; } /** * Gets the current state of the action (abstract version of a button press) generated by this hand only. *

* This is called for analog style actions (most commonly joysticks, but button pressure can also be mapped in analog). *

* This method is commonly called when it is important which hand the action is found on. For example an "in universe" * joystick that has a hat control might (while you are holding it) bind to the on-controller hat, but only on the hand * holding it *

* Note that restrictToInput only restricts, it must still be bound to the input you want to receive the input from in * the action manifest default bindings. *

* @param actionHandle the handle for the action (just an object with the set name and action name) * @return the AnalogActionState that has details on if the state has changed, what the state is etc. */ public FloatActionState getFloatActionState(ActionHandle actionHandle) { return vrState.getFloatActionState(actionHandle, getHandSide().restrictToInputString); } /** * Triggers a haptic action (aka a vibration) restricted to just one input (e.g. left or right hand). *

* This method is typically used to bind the haptic to both hands then decide at run time which hand to sent to * *

*/ public void triggerHapticAction(Haptic haptic) { vrState.triggerHapticAction(haptic.actionHandle(), haptic.duration(), haptic.frequency(), haptic.amplitude(), getHandSide().restrictToInputString); } /** * Triggers a haptic action (aka a vibration) restricted to just one input (e.g. left or right hand). *

* This method is typically used to bind the haptic to both hands then decide at run time which hand to sent to * *

* @param actionHandle the handle for the action (just an object with the set name and action name) * @param duration how long in seconds the * @param frequency in cycles per second * @param amplitude between 0 and 1 */ public void triggerHapticAction(ActionHandle actionHandle, float duration, float frequency, float amplitude) { vrState.triggerHapticAction(actionHandle, duration, frequency, amplitude, getHandSide().restrictToInputString); } /** * Gets the current state of the action (abstract version of a button press). *

* This is called for digital style actions (a button is pressed, or not) *

* This method is commonly called when it is important which hand the action is found on. For example while * holding a weapon a button may be bound to "eject magazine" to allow you to load a new one, but that would only * want to take effect on the hand that is holding the weapon *

* Note that this action must still be bound in the action manifest against this hand it to receive the input * * @param actionHandle the handle for the action (just an object with the set name and action name) * @return the DigitalActionState that has details on if the state has changed, what the state is etc. */ public BooleanActionState getBooleanActionState(ActionHandle actionHandle) { return vrState.getBooleanActionState(actionHandle, getHandSide().restrictToInputString); } /** * Returns a node that will update with the hands position and rotation. *

* The exact position and rotation of this node depends on the pose type specified for the hand: *

* If the pose is AIM them it will be just in front and above the hand with +X pointing in the direction a held weapon * would fire with Y pointing upwards and Z pointing to the right (when the hands are held with the palms * facing each other) *

* If the pose is GRIP them it will be away from the palm, with +X pointing in the direction a held weapon * would fire with Y pointing upwards and Z pointing to the right (when the hands are held with the palms * facing each other) *

* This node has an orientation such that x aligns with the grip direction (like you are holding a sword, and Z * pointing to the right (when the hands are held with the palms facing each other). *

* This is an ideal node for things like picking lines, which can be put in the x direction *

* Note that the (0,0,0) position is just in front of the thumb, not the centre of the hand. *

* This node is primarily used for picking, but if you want a node to attach to that only cares about the bulk * hand position */ public Node getHandNode_xPointing() { return this.handNode_xPointing; } /** * A hand node, with +z pointing in the direction of the bulk hand (if an AIM pose). This is used primarily for direct lemur interactions */ public Node getHandNode_zPointing() { return this.handNode_zPointing; } /** * This is a node that sits near on the tip of the index finger whose +x points out way from the index * finger (and Y point up and Z points right if the hands are held with palms facing each other. This node sits inside * the finger such that a sphere would touch the skin of the finger in 5 directions. * The exact distance from the skin can vary (it is reported by OpenXr) but for bound hand's perspective it is considered * to be {@link BoundHand#FINGER_PICK_SPHERE_RADIUS} away from the skin. */ public Node getIndexFingerTip_xPointing() { return this.indexFingerTip_xPointing; } /** * A node at the wrist. *

* With hands held flat with thumbs facing each other +x going to the right, +y goes upwards and +z goes towards the player */ public Node getWristNode() { return this.wristNode; } public ActionHandle getHandPoseActionName() { return this.handPoseActionName; } public ActionHandle getSkeletonActionName() { return this.skeletonActionName; } public Armature getArmature() { return this.armature; } public AssetManager getAssetManager() { return this.assetManager; } /** * Left or right hand */ public HandSide getHandSide() { return this.handSide; } /** * The velocity (in world coordinates) that the hand is currently moving at */ public Vector3f getVelocity_world() { return this.velocity_world; } /** * The rotational velocity (in world coordinates) that the hand is currently rotating at */ public RotationalVelocity getRotationalVelocity_world() { return this.rotationalVelocity_world; } /** * When doing a palm pick spheres of this radius are created above the palm */ public float getPalmPickSphereRadius() { return this.palmPickSphereRadius; } /** * When doing a palm pick spheres of this radius are created above the palm */ public void setPalmPickSphereRadius(final float palmPickSphereRadius) { this.palmPickSphereRadius = palmPickSphereRadius; } /** * When doing a palm pick these are the points where spheres are formed to detect if the palm is against anything. * They are relative to the palm node (in the {@link BoundHand#getPalmNode}'s coordinate system) */ public void setPalmPickPoints(final List palmPickPoints) { this.palmPickPoints = palmPickPoints; } /** * When doing a palm pick these are the points where spheres are formed to detect if the palm is against anything. * They are relative to the palm node (in the {@link BoundHand#getPalmNode}'s coordinate system) */ public List getPalmPickPoints() { return this.palmPickPoints; } /** * A pointing arrangement is when the index finger is mostly straight and the ring ringer is not. *

* This is the sort of hand position that indicates pressing buttons with the index finger */ public boolean isHandPointing() { return this.handPointing; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy