Two Handed Weapon Manipulation

This is gonna be a long one I guess. For context what I’m trying to create is a 2 handed gun system where you can grab onto the handle of the weapon with your primary hand and somewhere else on the weapon, like the foregrip with your second hand and aim the weapon, release the primary hand and do something like pull a priming handle, load a round, or cycle a bolt, then grab back onto the weapon and be the one that is driving the upward rotation of the aim, as well as have the primary functionality of performing activate etc. The latter part I haven’t gotten to yet and I have a plan for that, I’m not super worried about it. The former part is the issue, when I let go and grab back on the now “first selector” is the secondary hand on the grip. This means the secondary hand is the one that can activate the weapon, but also it is in control of the upward rotation when grabbing back on with the primary hand onto the handle. Likewise the same issue occurs if the first hand to grab onto the weapon is the foregrip one, then you grab onto the handle with the other. I know that upward rotation comes from the XRGeneralGrabTransformer but I’m not sure how to modify it to have it know about my primary/secondary hand stuff.

What I have right now:
I created my own class called GrabPoint, which has a bool property defining if it’s the primary grab point (the weapon handle). I put these on the weapon wherever I want them to be grabbable, I enable the isPrimaryGrabPoint on the handle.
I have extended the XRGrabInteractable using dynamic attach points and the script has a list of GrabPoints. I’m overriding the InitializeDynamicAttachPose function to find the closest grab point from the list and essentially snap the dynamicAttachTransform that position and rotation. This works well for the grabbing on with both hands, and releasing with the primary and grabbing back on. The issue is when grabbing back on with the primary hand, it is not the first selecting interactor. I have a bad workaround where I force the other hand to let go and grab back on using a coroutine and interactionManager.SelectExit/interactionManager.SelectEnter. This works kind of but it can have bad consequences if the primary hand is rotated at a weird angle when I grab back onto the weapon’s handle, it basically causes the weapon to reorient toward the aiming direction of the hand and causes the second hand to kind of latch onto space instead of properly latching onto the grabpoint position/rotation via the reinitialize dynamic attach points every time property as well as the overriden InitializeDynamicAttachPose() function.

I have a few thoughts about how this could be handled but I’m not really getting anywhere it feels like. My first thought was to deselect and reselect as I’m doing now, but another issue besides if you grab at a weird angle is it jerks a little bit as it deselects and reselects and it doesn’t look great. I was hoping there would be some way of enforcing which hand is the first selecting interactor upon selecting again with the primary hand. Another option I guess is coming up with an override for the XRGeneralGrabTransformer to do basically the TwoHandedRotationMode FirstHandDirectedTowardsSecondHand, but it would actually be something like PrimaryHandDirectedTowardsSecondaryHand, so it’s not based on order but based on the custom property.

Any ideas or direction?

What you’re doing is quite custom and I think I have a better idea for how to address this.

Create a custom Base Interactable that intercepts your grab events.
Create two dummy proxy interactors that recreate data from the interactors. (Using the input readers it’s quite possible to have custom data go in for input).

On a seperate interaction layer, you can put the actual interactable that you would be able to select with the normal interactors.

With this setup you can control the grab order and remap input and positioning to match your custom attach point needs. Hopefully this helps reduce how much custom overriding you have to do for grab interactable and the general grab transformer.

First of all thanks for the reply.

Can you elaborate some? I’m not getting what you are saying. I definitely would love to have little to no overriding.

A custom base interactable that intercepts the grab events, so implements OnSelectEnter and OnSelectExit, what would it do with the intercepted events? Where would this script live?

Two dummy proxy interactors, so in my case a couple of Direct Interactors, these are copying data from the normal Direct Interactors on the actual Left Controller and Right Controller in this instance, what data exactly? Position/Rotation/Inputs? Where do these proxy interactors go? I also don’t know what you mean by input readers and custom data go in through input.

Separate Interaction Layer the actual interactable that I want to select with my normal interactors (direct interactors on my controllers), why?

How can I control the grab order and remap input?

Sorry if that’s a lot of questions, I am trying to understand but I can’t figure out how this proposed solution would work.

I’ll answer some more, but I think what will help you understand more is our XRI 3.0 talk from GDC. It should help you better get a handle on how all this works.

Thanks I’ll check it out

I went ahead and updated to the XRI 3.0.3, and I watched the video but I’m still a bit lost on what you were talking about.

Ok so I’ve written up some of the code to help you understand what I mean. I haven’t had time to test it, but it should work.

Here’s how I would setup the rig for this interactable.
9849438--1417929--upload_2024-5-22_12-17-58.png

The Custom Pickup root is just to group things.

The idea is that your dummy interactable receives interaction events from the real interactors in your rig.
Then, the dummy interactable will forward input to the interactors as you see fit. In my example the first grab will activate interactor0, and the second grab will activate interactor1.

In XRI 3.0, an interactor only needs 3 things:

  1. A pose, which is used by interactables

  2. A select input value, which determines whether or not select can happen

  3. A valid target which select is applied to.

  4. The Dummy interactable will move the transform of the proxy interactor. Because it’s a proxy, it doesn’t actually have to map to the real interactor driving things.

  5. When the dummy is selected, it will grab the corresponding interactor and set it’s input driver to selecting = true, and false when select ends.

  6. The dummy interactable holds a reference to the actual target interactable, and will set the valid target of each proxy interactor accordingly when select starts.

Each of the interactors look like this:

They’re a basic interactor with a binding to a custom input reader called “Input Driver” that controls when select is active. These are the interactors that will actually select your target object.

The Dummy interactable will look like this:
9849438--1417926--upload_2024-5-22_12-17-39.png

Here’s the code I wrote to make link all this together:
9849438--1417932--upload_2024-5-22_12-18-28.png

Hope this helps!

using System.Collections.Generic;
using Unity.XR.CoreUtils;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;

namespace UnityEngine.XR.Interaction.Toolkit.Samples.CustomInteractions
{
    public class DummyInteractable : XRBaseInteractable
    {
        [SerializeField]
        ProxyInteractor m_Interactor0;

        [SerializeField]
        ProxyInteractor m_Interactor1;

        [SerializeField]
        XRBaseInteractable m_TargetInteractable;

        Dictionary<IXRInteractor, ProxyInteractor> m_ProxyInteractorsMap = new Dictionary<IXRInteractor, ProxyInteractor>();
       
        protected override void OnSelectEntered(SelectEnterEventArgs args)
        {
            base.OnSelectEntered(args);
           
            int numInteractors = interactorsSelecting.Count;
            var targetInteractor = numInteractors == 1 ? m_Interactor0 : m_Interactor1;
           
            // Set target
            targetInteractor.SetTargetInteractable(m_TargetInteractable);
           
            // Map interactor
            m_ProxyInteractorsMap[args.interactorObject] = targetInteractor;
           
            // Activate input
            targetInteractor.selectInputDriver.SetPerformed(true);
        }

        protected override void OnSelectExited(SelectExitEventArgs args)
        {
            base.OnSelectExited(args);
           
            // Release input
            if(m_ProxyInteractorsMap.TryGetValue(args.interactorObject, out var proxyInteractor))
            {
                proxyInteractor.selectInputDriver.SetPerformed(false);
                proxyInteractor.SetTargetInteractable(default);
            }
        }

        public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
        {
            base.ProcessInteractable(updatePhase);
           
            // Update poses
            foreach (var pair in m_ProxyInteractorsMap)
            {
                Pose targetPose = new Pose(pair.Key.transform.position, pair.Key.transform.rotation);
                pair.Value.transform.SetWorldPose(targetPose);
            }
        }
    }
}
using UnityEngine.XR.Interaction.Toolkit.Inputs.Readers;

namespace UnityEngine.XR.Interaction.Toolkit.Samples.CustomInteractions
{
    public class InputDriver : MonoBehaviour, IXRInputButtonReader
    {
        bool m_IsPerformed;
        bool m_WasPerformedThisFrame;
        bool m_WasCompletedThisFrame;

        float m_PressAmount = -1f;
       
        public void SetPressAmount(float value)
        {
            m_PressAmount = value;
        }
       
        public void SetPerformed(bool value)
        {
            m_WasCompletedThisFrame = !value && m_IsPerformed;
            m_WasPerformedThisFrame = value && !m_IsPerformed;
            m_IsPerformed = value;
        }
       
        public float ReadValue()
        {
            if(m_PressAmount < 0)
                return m_IsPerformed ? 1f : 0f;
            return m_PressAmount;
        }

        public bool TryReadValue(out float value)
        {
            value = ReadValue();
            return true;
        }

        public bool ReadIsPerformed()
        {
            return m_IsPerformed;
        }

        public bool ReadWasPerformedThisFrame()
        {
            return m_WasPerformedThisFrame;
        }

        public bool ReadWasCompletedThisFrame()
        {
            return m_WasCompletedThisFrame;
        }
    }
}
using System.Collections.Generic;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;

namespace UnityEngine.XR.Interaction.Toolkit.Samples.CustomInteractions
{
    public class ProxyInteractor : XRBaseInputInteractor
    {
        IXRInteractable m_TargetInteractable;

        [SerializeField]
        InputDriver m_SelectInputDriver;

        public InputDriver selectInputDriver => m_SelectInputDriver;
       
        public void SetTargetInteractable(IXRInteractable targetInteractable)
        {
            m_TargetInteractable = targetInteractable;
        }
       
        public override void GetValidTargets(List<IXRInteractable> targets)
        {
            base.GetValidTargets(targets);
            targets.Clear();
            if (m_TargetInteractable != null)
                targets.Add(m_TargetInteractable);
        }
    }
}

9849438--1417920--upload_2024-5-22_12-9-14.png

Thanks for the help, I’m very grateful but I’m still not getting it.

I am not sure exactly how this would allow me to accomplish the two handed holding system I’m after either.

As of now I am also starting to think a custom grab transformer will be needed anyway because with the default XRGeneralGrabTransformer you can rotate the gun in very weird ways by rolling your wrist. From the comments in the code I guess that’s intended but it doesn’t make wielding a gun feel very right when you can turn it over like that. I’m thinking I might need to write one that does the pointing from the primary hand to the second hand but with the up from the primary hand purely based on the hand’s up axis.

Back on the topic of your proposed solution:
I don’t see the dummy interactable ever unmapping interactors to proxy interactors, meaning the first grabbing interactor will always map to the same proxy forever after. Aside from that I’m not really sure where my dummy interactable is supposed to exist, attached to the interactable I want to grab?

I tried having them as two separate objects as in your explanation and I can pick up my real interactable by selecting the dummy sure, but the dummy just stays where it is and I still have the issue of when I grab with both hands, if I release with the primary the grab interactable ends up being primarily interacted with by the second hand, making it basically the same as before, except now when I grab back on with my first hand, and then let go with my first hand both hands let go.

Sorry to keep asking for further clarification but I still don’t understand.

The proxy interaction idea I proposed was mainly to make it easier to decouple the affect that controller input has on the motion of your gun. I didn’t put the code in to do this, but the dummy interactable should probably match the transform of the object it’s meant to substitute interaction for.

The idea is the interactors work with the dummy, which can reposition the proxy interactors as you see fit (handling which hand grabs first for instance), and then those interactors move the actual object.

The problem you’re describing with the grab transformer is honestly I bug I think. By crossing the right vector of the interactor0 to ensure it’s stable, the gun should stop wobbling left to right. I’m gonna try and fix it and put the code here when I resolve it.

Two handed interaction that doesn’t break from gimbal lock can be a huge challenge, but in any case a custom transformer specifically designed for guns is not the worst idea.

Just a follow up. I think in the end, what you want to do is just focus on making a good custom transformer that suits your purposes.

The proxy system doesn’t matter in that case because you can just use the interactor transforms however you see fit. That said it would help if you wanted to apply additional transform smoothing. Some games do this to make handling smoother.

I would be curious now to setup a custom transformer for a gun. There are a number of constraints you’d want to add and also you’d want to ensure it works well during manual reloading and such. A lot of the general grab transformer’s complexity exists to handle dynamic attach transforms and permit scaling and other complex generic interactions.

I did try that out with the dummy interactable being parented to the actual interactable, with it on a different interaction layer as you suggested. So the actual interactable is not interactable from the controllers directly but the dummy is, it seems to be forwarding on as you had expected but yeah the repositioning of the proxy interactors and stuff wasn’t very clear to me how to accomplish that.

Yeah I noticed that the there is a lot of logic in the transformer around scaling and stuff. I found that the logic for transforming the motion two handed gun handling is fairly straight forward and you probably understand why this is the case better than I do but when I am using the interactor’s attachTransform for the up when calculating the Quaternion.LookRotation I get the exact same problem where rolling the wrist causes it to get out of wack with the controller’s up, but if I just use the interactor’s up it works the way I want it to. I’ve been trying to write a custom transformer that does exactly this but I haven’t been able to so far.

For now what I have is a modernization of this video:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;

public class MultipleInteractorsGrabInteractable : XRGrabInteractable {

    [SerializeField]
    List<XRSimpleInteractable> extraGrabPoints = new List<XRSimpleInteractable>();

    IXRSelectInteractor alternateInteractor;

    Quaternion initialAttachRotation;

    [SerializeField]
    bool snapToSecondHand = true;
    Quaternion initialOffsetRotation;

    public enum TwoHandRotationType { None, First, Second }

    [SerializeField]
    TwoHandRotationType rotationType = TwoHandRotationType.First;

    private void Start() {
        foreach (var grabPoint in extraGrabPoints) {
            grabPoint.selectEntered.AddListener(OnSecondHandGrab);
            grabPoint.selectExited.AddListener(OnSecondHandRelease);
        }
    }

    public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase) {
        if (alternateInteractor != null && interactorsSelecting.Count > 0) {
            if (snapToSecondHand) {
                interactorsSelecting[0].GetAttachTransform(this).rotation = GetTwoHandRotation();
            } else {
                interactorsSelecting[0].GetAttachTransform(this).rotation = GetTwoHandRotation() * initialOffsetRotation;
            }
           
        }
        base.ProcessInteractable(updatePhase);
    }

    protected override void OnSelectEntered(SelectEnterEventArgs args) {
        base.OnSelectEntered(args);
        alternateInteractor = null;
        initialAttachRotation = interactorsSelecting[0].GetAttachTransform(this).localRotation;
    }

    protected override void OnSelectExited(SelectExitEventArgs args) {
        //This doesn't seem needed anymore
        //Debug.Log($"select exited interactorsSelecting.Count = {interactorsSelecting.Count}");
        //if (interactorsSelecting.Count > 0) {
        //    interactorsSelecting[0].GetAttachTransform(this).localRotation = initialAttachRotation;
        //}
        base.OnSelectExited(args);
    }

    private void OnSecondHandGrab(SelectEnterEventArgs args) {
        alternateInteractor = args.interactorObject;
        initialOffsetRotation = Quaternion.Inverse(GetTwoHandRotation()) * interactorsSelecting[0].GetAttachTransform(this).rotation;
    }

    private void OnSecondHandRelease(SelectExitEventArgs args) {
        alternateInteractor = null;
    }

    public override bool IsSelectableBy(IXRSelectInteractor interactor) {
        bool isAlreadyGrabbed = interactorsSelecting.Count > 0 && !interactorsSelecting.Contains(interactor);
        return base.IsSelectableBy(interactor) && !isAlreadyGrabbed;
    }

    Quaternion GetTwoHandRotation() {
        Quaternion targetRotation = default;
        if (rotationType == TwoHandRotationType.None) {
            targetRotation = Quaternion.LookRotation(alternateInteractor.transform.position - interactorsSelecting[0].transform.position);
        } else if (rotationType == TwoHandRotationType.First) {
            targetRotation = Quaternion.LookRotation(alternateInteractor.transform.position - interactorsSelecting[0].transform.position, interactorsSelecting[0].transform.up);
        } else if (rotationType == TwoHandRotationType.Second) {
            targetRotation = Quaternion.LookRotation(alternateInteractor.transform.position - interactorsSelecting[0].transform.position, alternateInteractor.transform.up);
        }
        return targetRotation;
    }

}

The main issues with doing it this way is it seems to be overriding the transformer system or bypassing it maybe, not really sure. One benefit of this approach is that the weapon is only being selected by the handle interactor and that means the gun can’t be controlled by the off hand basically no matter what, this can be seen as a negative thing though if there were for example something you wanted your offhand to interact with the weapon but not your primary hand, like if you had a flashlight at the end of the gun you wanted to turn on, but an easy circumvention for that is just to have the flashlight be a separate interactable… At any rate, the other issue is it means that when the main hand releases it drops the whole thing. Along with not resetting the aim to the one hand when releasing the second hand. I’m sure it’s solvable but I’m pretty sure this isn’t the route I want to go down. Thinking on it now this is kind of similar to the proxy interactors a little bit. I could foreseeably set up another XRSimpleInteractor for the actual handle and then they would essentially be the proxies for the actual interactable. Then I just need to get rid of the processInteractable here and replace it with a custom gun transformer that does the same stuff. It’s still… lacking a little though.

Here’s my other attempt which thus far has gotten the closest but still needs a custom gun transformer:


Attach Points can be primary or not

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;

public class GrabInteractableWithGrabPoints : XRGrabInteractable {
    [SerializeField]
    List<GrabPoint> grabPoints = new List<GrabPoint>();

    protected override void OnSelectExited(SelectExitEventArgs args) {
        Debug.Log($"Select Exited {args.interactorObject} {args.interactableObject}");
        GrabPointTracker tracker = args.interactorObject.transform.GetComponentInParent<GrabPointTracker>();
        tracker.GrabPoint = null;
        base.OnSelectExited(args);
    }

    protected override void InitializeDynamicAttachPose(IXRSelectInteractor interactor, Transform dynamicAttachTransform) {
        //base.InitializeDynamicAttachPose(interactor, dynamicAttachTransform);
        var interactorAttachTransform = interactor.GetAttachTransform(this);
        GrabPointTracker tracker = interactor.transform.GetComponentInParent<GrabPointTracker>();
        var position = interactorAttachTransform.position;
        var rotation = interactorAttachTransform.rotation;

        GrabPoint closestGrabPoint = GetClosestGrabPoint(position);
        if (closestGrabPoint != null) {
            position = closestGrabPoint.transform.position;
            if (closestGrabPoint.IsPrimaryAttachPoint) {
                rotation = closestGrabPoint.transform.rotation;
            }
            tracker.GrabPoint = closestGrabPoint;
        }

        dynamicAttachTransform.SetPositionAndRotation(position, rotation);
    }

    GrabPoint GetClosestGrabPoint(Vector3 interactorPosition) {
        GrabPoint closestGrabPoint = null;
        float distance = float.MaxValue;
        foreach (var grabPoint in grabPoints) {
            var currentDistance = (grabPoint.transform.position - interactorPosition).sqrMagnitude;
            if (currentDistance < distance) {
                closestGrabPoint = grabPoint;
                distance = currentDistance;
            }
        }
        return closestGrabPoint;
    }
}
using UnityEngine;

public class GrabPoint : MonoBehaviour {
    [SerializeField]
    private bool isPrimaryAttachPoint = false;
    public bool IsPrimaryAttachPoint { get => isPrimaryAttachPoint; set => isPrimaryAttachPoint = value; }
}

This way works extremely close to what I want where I can grab the gun with either hand, which locks onto the handle (if the handle is closest to the interactor, I want to change it so it’s where the near-far interactor cast hits on the interactable but as it works now is also basically fine). Then when grabbing with the second hand near the foregrip it works as expected. Releasing the foregrip reorients the weapon to the handle attach point aiming which is desireable, regrabbing the foregrip and releasing the handle allows normal manipulation of the gun as if you were holding only the foregrip like turning it and angling it however you feel fit, then regrabbing the handle makes it point where it is supposed to (except for the upward direction issue mentioned before). The only issue is if you do release and regrab the handle, again the order of interactors is incorrect, and also releasing the handle would result in being able to fire the gun with the foregrip hand which I admit will require some more effort in telling the controllers what they are allowed to do based on the attached grabpoint.

Another separate issue that is kind of related to my original workaround of release and regrab for the foregrip hand is that when I do that manually, like if I do the action of releasing and regrabbing the foregrip it doesn’t actually seem to reposition correctly, I can end up with my attachTransform being pretty far away from the actual foregrip transform.

Side NOTE: I’m only using GetComponentInParent temporarily, if I continue down this path I’ll make that available through subclassing.

I feel like it all comes back to not being able to control the order of interactors technically, I haven’t tried it yet because it says in the docs that the interactorsSelecting is read only, but I am wondering if I manually swapped the order to the one I wanted based on if they were the primary attach point or not. I’m sure it would just break a bunch of logic to try that.

I tried using a few other things like the XRDualGrabFreeTransformer and such but I’ve had some pretty weird results with it. I’ve had weird results all over the place trying out different things here and there like when I’m writing my own transformer…

Thanks for writing out your thought process!

Just a thought but what might be interesting to try is to enable dynamic attach on the actual interactable, and then use the proxy interactors to grab the object at a position that would match the grab points you’d manually set with the attach transforms. You can use your grab point logic to position these the interactors before using the input driver to start the interaction.

This would prevent whipping around on release and might help some of your issues.

The twist issue you in the general grab transformer is a result of trying to avoid behaving any gimbal lock. What you said as a fix of just using the controller up causes issues when the handle direction formed with the second controller moved more than 90 degrees away. I think there’s a way to stop the roll rotation with a cross product though.

You’re right though that order controller will always be an issue. I’ll think on this and hopefully make an option to keep the interactor list from resetting the order if you release and regrab.

I’m incredibly close to the desired behavior, OnSelectEnter I decided to try modifying the interactorsSelecting to ensure that the primary attach point would always be index 0. This seems to work just fine:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;

public class GrabInteractableWithGrabPoints : XRGrabInteractable {
    [SerializeField]
    List<GrabPoint> grabPoints = new List<GrabPoint>();

    protected override void OnSelectEntered(SelectEnterEventArgs args) {
        base.OnSelectEntered(args);
        GrabPointTracker tracker = args.interactorObject.transform.GetComponentInParent<GrabPointTracker>();
        if (tracker.GrabPoint.IsPrimaryAttachPoint) {
            interactorsSelecting.Remove(args.interactorObject);
            interactorsSelecting.Insert(0, args.interactorObject);
        }
    }

    protected override void OnSelectExited(SelectExitEventArgs args) {
        GrabPointTracker tracker = args.interactorObject.transform.GetComponentInParent<GrabPointTracker>();
        tracker.GrabPoint.isGrabbed = false;
        tracker.GrabPoint = null;
        base.OnSelectExited(args);
    }

    protected override void InitializeDynamicAttachPose(IXRSelectInteractor interactor, Transform dynamicAttachTransform) {
        //base.InitializeDynamicAttachPose(interactor, dynamicAttachTransform);
        var interactorAttachTransform = interactor.GetAttachTransform(this);
        GrabPointTracker tracker = interactor.transform.GetComponentInParent<GrabPointTracker>();
        var position = interactorAttachTransform.position;
        var rotation = interactorAttachTransform.rotation;

        if (tracker.GrabPoint == null) {
            GrabPoint closestGrabPoint = GetClosestGrabPoint(position);
            if (closestGrabPoint != null) {
                position = closestGrabPoint.transform.position;
                if (closestGrabPoint.IsPrimaryAttachPoint) {
                    rotation = closestGrabPoint.transform.rotation;
                }
                tracker.GrabPoint = closestGrabPoint;
                tracker.GrabPoint.isGrabbed = true;
            }
        } else { //if the hand is already holding a grab point, just reinit against that
            GrabPoint alreadyHeldPoint = tracker.GrabPoint;
            position = alreadyHeldPoint.transform.position;
            if (alreadyHeldPoint.IsPrimaryAttachPoint) {
                rotation = alreadyHeldPoint.transform.rotation;
            }
        }

        dynamicAttachTransform.SetPositionAndRotation(position, rotation);
    }

    GrabPoint GetClosestGrabPoint(Vector3 interactorPosition) {
        GrabPoint closestGrabPoint = null;
        float distance = float.MaxValue;
        foreach (var grabPoint in grabPoints) {
            var currentDistance = (grabPoint.transform.position - interactorPosition).sqrMagnitude;
            if (currentDistance < distance && !grabPoint.isGrabbed) {
                closestGrabPoint = grabPoint;
                distance = currentDistance;
            }
        }
        return closestGrabPoint;
    }
}
using UnityEngine;

public class GrabPoint : MonoBehaviour {
    [SerializeField]
    private bool isPrimaryAttachPoint = false;
    public bool IsPrimaryAttachPoint { get => isPrimaryAttachPoint; set => isPrimaryAttachPoint = value; }

    public bool isGrabbed = false;
}
using UnityEngine;

public class GrabPointTracker : MonoBehaviour {
    [SerializeField]
    GrabPoint grabPoint;

    public GrabPoint GrabPoint { get => grabPoint; set => grabPoint = value; }
}

I’m using an XRDualGrabFreeTransformer with both options selected as First:

There are a couple weird things with this transformer though. When I am holding the weapon with two hands the actual position of the foregrip hand is offset from where it should be, slightly below where it should be, when I let go of the primary attach point it slides to the right holding position, when I grab back on with the primary attach point it again slides to a slight offset. The other thing is I can technically grab the handle upside down or right side up, it doesn’t have the issue of allowing wrist rolling to get out of wack with up, but it does let you choose upside down or right side up as the up when you first grab. Not the worst thing.

I’m not really sure why the grab slides up. In this video I forgot to make sure you couldn’t grab the same point twice but that’s fixed now, that happens around 0:25, but I’ve fixed it since (and updated the code snippet above)

Glad you’re improving your results here! I’m 100% sure what’s happening with the grab point sliding, but I did find a fix for the wrist rolling drift that was nagging you. I’m going to try and get this added to an upcoming release, but you can modify your general grab transformer locally with this change.

Replace this section in the “ComputeAdjustedInteractorPose” function.

            if (m_TwoHandedRotationMode == TwoHandedRotationMode.FirstHandDirectedTowardsSecondHand)
                {
                    // Use the fallback axis as the 'up' direction for the LookRotation
                    Vector3 newHandleBarNormalized = newHandleBar.normalized;
                   
                    // Use the last calculated rotation to compute a temporally coherent basline up vector
                    var baselineUp = m_LastHandleBarLocalRotation * Vector3.up;
                   
                    var startUpVector = m_StartHandleBarLookRotation * Vector3.up;
                   
                    // After enough rotation, the baseline up vector will start to drift away from the original vector, causing undesirable output rotations.
                    // To prevent this, we interpolate between the baseline up vector and the original up vector based on the dot product between the two, to gradually restore it without causing sudden jumps.
                    float dot = Vector3.Dot(startUpVector, baselineUp);
                    float sign = Mathf.Sign(dot);
                    Vector3 newUpVector = Vector3.Lerp(baselineUp, sign * startUpVector, Mathf.Abs(dot) * 0.5f);
                   
                    // Compute the new handle bar rotation
                    Quaternion newHandleBarLocalRotation = Quaternion.LookRotation(newHandleBarNormalized, newUpVector);
                   
                    // Store the last handle bar rotation for the next frame
                    m_LastHandleBarLocalRotation = newHandleBarLocalRotation;

                    // Compute the rotation difference
                    Quaternion rotationDiff = newHandleBarLocalRotation * m_InverseStartHandleBarLookRotation;

                    // Update the rotation
                    newRotation = interactor0Transform.rotation * rotationDiff;
                }

Yup that solves the wrist rolling partially, you can still roll your wrist extremely to end up holding the handle upside down but it’s vastly better than before, with that it’s basically the same as the XRDualGrabFreeTransformer. Though switching back from the XRDualGrabFreeTransformer to this Modified XRGeneralGrabTransformer has brought up yet another issue. One I didn’t mention before because at that point I had already switch to the XRDualGrabFreeTransformer but also I’m sure the problem could be due to the fact that I’m still using my custom dynamic attach point system or that I’m swapping around the order of the interactorsSelecting but I’ll record a video anyway for posterity I guess…

Basically when I let go with my primary hand and regrab with my primary hand it’s somehow getting the position of the foregrip for the regrab point. I verified through logs that when I’m regrabbing with the primary hand it is getting the correct GrabPoint (the one belonging to the handle), but I’m not sure why it’s attaching to the wrong position. I have a feeling it has something to do with the shuffling of interactorsSelecting order…

Like it could be reusing the attach transform or something somehow.

Yeah I’m not 100% sure what’s going on there but it could be down to your overriding of the dynamic attach function. I’ll try and look into the ordering issue that will hopefully make this override business unnecessary, some time next week. That change will be longer to merge in though because it involves exposing more options and will need to wait until 3.1 is ready.

Yeah I mean there’s no guarantees of any functionality when I’m overriding stuff, but I have noticed that the XRDualGrabFreeTransformer works very consistently when grabbing and releasing really quickly, it ALWAYS snaps onto the same spot (which is slightly off of where the actual dynamic attach is) but it always gets the same spot. So if I grab and release and grab and release rapidly with my forward hand, it always attaches to the same spot. When I do the same with XRGeneralGrabTransformer it attaches to the right spot initially but due to rotating the weapon back to the interactor0’s position/rotation if I rapidly grab and release with my off hand it’s like every time it reattaches after the first the actual anchor slips away from where it should be.

I’d say normally with dynamic attach points that wouldn’t matter but I bet if you use snap to collider volume it might.

I’m currently testing where the dynamic attach point and stuff is, with my system, it seems to always be exactly in the right spot but I’m about to test out where the attach point of the hand is.

I tested with dynamic attach points on a standard XRGrabInteractable, two handed grabbing and releasing quickly and standard XRGeneralGrabTransformer and yeah there isn’t any issues of it snapping onto the wrong point. Definitely has something to do with my custom grab point and when you release the second hand the grabInteractable rotating back to the Pose of the first hand, then grabbing back on with the second hand while that’s happening very shortly after.

I’ve currently got some custom code to make the attach points visible on the interactor and interactables and they all seem to be in the right spot, but when using XRDualGrabTransformer I just see that the attach points are not lining up properly for the secondary hand. I guess there might be some further customization I can do to properly get them to align.

The general grab transformer does a lot of caching of offsets for position and rotation. It works very differently from the dual grab transformer.

It’s more there for legacy purposes, so it may receive less testing than the general grab transformer. If you do find a bug in it without your attach point customization, it might be worth reporting it in the editor.