Scale geometry in VR by using controllers

This is the script that is part of the example scene ObjectScaling.vpb. The default pointer interaction is used to implement the scaling. For this, the device actions of the pointer are connected to methods of the GeometryScaler class. Further information of the connection to default interactions can be found in the example “Connect to signals of device actions”.

A geometry can be selected with the ray of the pointer, by pressing the trigger completely down. Then a ray on the second controller is activated. Scaling is done by targeting the same object with the second controller and also pressing the trigger completely down. When the controllers are moved, the scale of the geometry is adjusted. The scaling can be stopped by releasing the trigger on one of the controllers or if one of the rays does not intersect with the geometry anymore.

vr/scaleGeometry.py
  1# © 2024 Autodesk, Inc. All rights reserved.
  2
  3class GeometryScaler:
  4    def __init__(self):
  5        
  6        # Init all the class variables
  7        # Two devices are needed. The major device is the one that starts the whole scaling procedure and selects the object
  8        self.majorDevice = vrdVRDevice()        
  9        self.secondaryDevice = vrdVRDevice()
 10        # Flags that indicate the current state
 11        self.objectSelected = False        
 12        self.isSelecting = False
 13        self.isScaling = False
 14        # Picked node
 15        self.hitNode = vrdNode()
 16        # Hitpoints of the controller rays
 17        self.hitPoint1 = PySide6.QtGui.QVector3D(0.0, 0.0, 0.0)
 18        self.hitPoint2 = PySide6.QtGui.QVector3D(0.0, 0.0, 0.0)
 19        # Distance of hitpoints, when the scaling starts
 20        self.initialDistance = 0.0
 21
 22        # Get the default pointer actions ...
 23        pointer = vrDeviceService.getInteraction("Pointer")
 24        self.startSelectionAction = pointer.getControllerAction("prepare")
 25        self.selectAction = pointer.getControllerAction("start")
 26        self.unselectAction = pointer.getControllerAction("execute")
 27        self.stopSelectionAction = pointer.getControllerAction("abort")
 28        # ... and connect the corresponding methods
 29        self.startSelectionAction.signal().triggered.connect(self.startSelection)
 30        self.selectAction.signal().triggered.connect(self.selectElement)
 31        self.unselectAction.signal().triggered.connect(self.unselectElement)
 32        self.stopSelectionAction.signal().triggered.connect(self.stopSelection)        
 33
 34        # Get the controllers for easy access
 35        self.leftController = vrDeviceService.getVRDevice("left-controller")
 36        self.rightController = vrDeviceService.getVRDevice("right-controller")
 37
 38
 39    def startSelection(self, action, device):
 40        # Check if the state and device is correct        
 41        if self.objectSelected or self.isSelecting or self.isSecondaryDevice(device):
 42            return
 43
 44        # Update which device is major and secondary device
 45        self.updateDevices(device.getName())
 46        # Set current state
 47        self.isSelecting = True        
 48
 49    def selectElement(self, action, device):
 50        # If the major device selects a node it is marked for scaling,
 51        # if the secondary device selects a node, it actually starts the scaling.
 52        if self.isMajorDevice(device):
 53            self.markNodeForScaling(device)                    
 54        elif self.isSecondaryDevice(device):
 55            self.startScaling(device)        
 56
 57    def markNodeForScaling(self, device):
 58        # Check if the state is correct
 59        if not self.isSelecting:
 60            return
 61
 62        # Intersect the pick ray with the scene
 63        intersection = device.pick()
 64        if not intersection.hasHit():
 65            return
 66
 67        # Assign what actually has been intersected
 68        self.hitNode = intersection.getNode()
 69        self.hitPoint1 = intersection.getPoint()        
 70        self.isSelecting = False
 71        self.objectSelected = True
 72
 73        # Activate the ray on the secondary device, which is needed for scaling
 74        self.secondaryDevice.enableRay("controllerhandle")        
 75
 76    def startScaling(self, device):
 77        # Check if the state is correct                
 78        if not self.objectSelected:
 79            return
 80
 81        # Intersect the pick ray of the secondary device with the scene
 82        intersection = self.secondaryDevice.pick()
 83        if not intersection.hasHit():
 84            return
 85        
 86        # Check if both rays intersect with the same node
 87        node = intersection.getNode()
 88        if node.getObjectId() != self.hitNode.getObjectId():
 89            return
 90
 91        # Get the hintpoint and calculate the initial distance
 92        self.hitPoint2 = intersection.getPoint()
 93        self.initialDistance = self.hitPoint1.distanceToPoint(self.hitPoint2)
 94        # Get the current scale of the node
 95        self.initialScale = getTransformNodeScale(self.hitNode)
 96        
 97        # Connect the actual scaling method here
 98        self.majorDevice.signal().moved.connect(self.scale)        
 99
100    def unselectElement(self, action, device):
101        # Reset everything        
102        self.stopScaling()
103        self.objectSelected = False
104        self.hitNode = vrdNode()
105        self.secondaryDevice.disableRay()        
106
107    def stopScaling(self):    
108        # Check the state    
109        if not self.isScaling:
110            return
111
112        # Disconnect after scaling    
113        self.majorDevice.signal().moved.disconnect(self.scale)  
114        # Update the state
115        self.isScaling = False                     
116
117    def stopSelection(self, action, device):                
118        if not self.isMajorDevice(device):
119            return
120
121        # Update the state
122        self.isSelecting = False
123        # Reset devices
124        self.majorDevice = vrdVRDevice()
125        self.secondaryDevice = vrdVRDevice()                
126
127    def isMajorDevice(self, device):                
128        return device.getName() == self.majorDevice.getName()
129
130    def isSecondaryDevice(self, device):
131        return device.getName() == self.secondaryDevice.getName()
132
133    def updateDevices(self, majorName):
134        # Update by name, which device is the major device and which is secondary
135        if majorName == self.leftController.getName():
136            self.majorDevice = self.leftController
137            self.secondaryDevice = self.rightController
138        else:
139            self.majorDevice = self.rightController
140            self.secondaryDevice = self.leftController            
141
142    def scale(self, device):
143        intersection1 = self.majorDevice.pick()
144        intersection2 = self.secondaryDevice.pick()
145
146        self.hitPoint1 = intersection1.getPoint()
147        self.hitPoint2 = intersection2.getPoint()
148
149        nodeId1 = intersection1.getNode().getObjectId()
150        nodeId2 = intersection2.getNode().getObjectId()        
151
152        # Check if both rays intersect with the same node
153        if nodeId1 != self.hitNode.getObjectId() or nodeId2 != self.hitNode.getObjectId():
154            self.majorDevice.signal().moved.disconnect(self.scale)
155            self.isScaling = False
156            return
157
158        # Update state
159        self.isScaling = True
160        
161        # Calculate the scale factor depending on the distance of the two hitpoints
162        distance = self.hitPoint1.distanceToPoint(self.hitPoint2)
163        scaleFactor = max(min(distance / self.initialDistance, 5.0), 0.2)
164        scaleX = scaleFactor * self.initialScale.x()
165        scaleY = scaleFactor * self.initialScale.y()
166        scaleZ = scaleFactor * self.initialScale.z()
167
168        # Scale node        
169        setTransformNodeScale(self.hitNode, scaleX, scaleY, scaleZ)        
170
171scaler = GeometryScaler()