IC Python API:Local Move
- Main article: RL Python Samples.
This article will go over the proper way to read an object's true local-space position and create an avenue to modify it.
Local vs World Space
The main difference between local and world space coordinates is that local-space has directionality while in world-space, orientation does not matter whatsoever. For example, when an object is translated 100 meters in the foward-axis and rotated 45° in the up-axis then it's forward-axis remains 100 while the side-axis remains zero under world-space. However, under local-space, the foward-axis becomes 70.71 and the side-axis becomes -70.71. This is because, under local-space, orientation has a direct affect on the positional values due to the realignment of the axes.
Usually the local-axes alignments are decided at the completion of the 3D model, hence, they are derived from the world coordinates. Which can be changed when the said object is transformed and then baked (reset to zero).
Necessary Modules
Besides the fundamental Reallusion Python module, we'll also need Pyside2 and os to read the QT UI file and build the user interface. We'll also need a global variable to store the various designated objects in the scene.
import RLPy
import os
from PySide2 import *
from PySide2.shiboken2 import wrapInstance
Local Position
In order to display the correct local position of an object we can't rely on iClone's RLPy.RINode.LocalTransform() because everything in an iClone scene is considered as belonging to the global transform-space. This has the tendency to align local-space to world-space coordinates because, technically, it's always parented. In order to get the true local position of an object, we'll need a special function:
def local_position(obj):
# New matrix for the transform-space
transform = obj.WorldTransform()
parent_matrix = transform.Matrix()
parent_matrix.SetTranslate(RLPy.RVector3.ZERO)
# Get local-space position by multiplying world-space with the inverse transform-space
relative_matrix = transform.Matrix() * parent_matrix.Inverse()
return relative_matrix.GetTranslate()
Local to World Translate
We'll also need a function to transform local-space to world-space coordinates so we can drive an object's translation with local-space coordinates, much like iClone's Local Move.
def local_to_world_translate(obj, local_pos):
# New matrix for the transform-space
transform = obj.WorldTransform()
transform_matrix = transform.Matrix()
transform_matrix.SetTranslate(RLPy.RVector3.ZERO)
# New matrix for local-space position
local_matrix = RLPy.RMatrix4()
local_matrix.MakeIdentity()
local_matrix.SetTranslate(local_pos)
# Get world-space position by multiplying local-space with the transform-space
world_matrix = local_matrix * transform_matrix
return world_matrix.GetTranslate()
Local Move
Now, we'll need a function for local transformation where we can just set the local-space coordinates to drive an object in world-space. This function hinges on the local_to_world_transform function mentioned above.
def local_move():
items = RLPy.RScene.GetSelectedObjects()
if len(items) > 0:
local_position = RLPy.RVector3(widget.moveX.value(), widget.moveY.value(), widget.moveZ.value())
world_position = local_to_world_translate(items[0], local_position)
current_time = RLPy.RGlobal.GetTime()
# Set positional keys
t_control = items[0].GetControl("Transform")
t_data_block = t_control.GetDataBlock()
t_data_block.SetData("Position/PositionX", current_time, RLPy.RVariant(world_position.x))
t_data_block.SetData("Position/PositionY", current_time, RLPy.RVariant(world_position.y))
t_data_block.SetData("Position/PositionZ", current_time, RLPy.RVariant(world_position.z))
# Force update iClone native UI
RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() + RLPy.RTime(1))
RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() - RLPy.RTime(1))
Event Callbacks
We'll also need a RLPy.REventCallback class to update our user interface.
class EventCallback(RLPy.REventCallback):
def __init__(self):
RLPy.REventCallback.__init__(self)
def OnObjectDataChanged(self):
update_ui()
def OnCurrentTimeChanged(self, fTime):
update_ui()
def OnObjectAdded(self):
update_ui()
def OnObjectDeleted(self):
update_ui()
def OnObjectSelectionChanged(self):
update_ui()
Dialog Event Callbacks
In order to clean up the aforementioned event callbacks, we'll need to unregister it when the window is closed.
class DialogEventCallback(RLPy.RDialogCallback):
def __init__(self):
RLPy.RDialogCallback.__init__(self)
def OnDialogHide(self):
RLPy.REventHandler.UnregisterCallback(event_callback_id)
return True
Updating the UI
We'll need function for updating our custom user interface.
def update_ui():
items = RLPy.RScene.GetSelectedObjects()
local_pos = RLPy.RVector3.ZERO
if len(items) > 0:
local_pos = local_position(items[0])
for ind, val in enumerate([widget.moveX, widget.moveY, widget.moveZ]):
val.blockSignals(True)
val.setValue(local_pos[ind])
val.blockSignals(False)
Notice that we block the signals on the qt widgets so we don't get a double feedback, and we unblock it when the parameters have been changed.
Building the UI
We'll need to load the configured QT UI file. You can download Local_Move.ui here -make sure this UI file is placed in the same script directory. We'll also need to connect the custom user controls with relevant methods mentioned above and register our event callbacks.
dialog_window = RLPy.RUi.CreateRDialog()
dialog_window.SetWindowTitle("Local Transform")
# Create Pyside layout for RDialog
dialog = wrapInstance(int(dialog_window.GetWindow()), QtWidgets.QDialog)
dialog.setFixedWidth(350)
# Read and set the QT ui file from the script location
qt_ui_file = QtCore.QFile(os.path.dirname(__file__) + "/Local_Move.ui")
qt_ui_file.open(QtCore.QFile.ReadOnly)
widget = QtUiTools.QUiLoader().load(qt_ui_file)
qt_ui_file.close()
dialog.layout().addWidget(widget)
# Connect the UI controls
widget.moveX.valueChanged.connect(local_move)
widget.moveY.valueChanged.connect(local_move)
widget.moveZ.valueChanged.connect(local_move)
# Register Events
event_callback = EventCallback()
event_callback_id = RLPy.REventHandler.RegisterCallback(event_callback)
dialog_event_callback = DialogEventCallback()
dialog_window.RegisterEventCallback(dialog_event_callback)
dialog_window.Show()
update_ui()
Everything Put Together
You can paste the following code into a PY file and load it into iClone: Script > Load Python.
import RLPy
import os
from PySide2 import *
from PySide2.shiboken2 import wrapInstance
def local_position(obj):
# New matrix for the transform-space
transform = obj.WorldTransform()
parent_matrix = transform.Matrix()
parent_matrix.SetTranslate(RLPy.RVector3.ZERO)
# Get local-space position by multiplying world-space with the inverse transform-space
relative_matrix = transform.Matrix() * parent_matrix.Inverse()
return relative_matrix.GetTranslate()
def local_to_world_translate(obj, local_pos):
# New matrix for the transform-space
transform = obj.WorldTransform()
transform_matrix = transform.Matrix()
transform_matrix.SetTranslate(RLPy.RVector3.ZERO)
# New matrix for local-space position
local_matrix = RLPy.RMatrix4()
local_matrix.MakeIdentity()
local_matrix.SetTranslate(local_pos)
# Get world-space position by multiplying local-space with the transform-space
world_matrix = local_matrix * transform_matrix
return world_matrix.GetTranslate()
def local_move():
items = RLPy.RScene.GetSelectedObjects()
if len(items) > 0:
local_position = RLPy.RVector3(widget.moveX.value(), widget.moveY.value(), widget.moveZ.value())
world_position = local_to_world_translate(items[0], local_position)
current_time = RLPy.RGlobal.GetTime()
# Set positional keys
t_control = items[0].GetControl("Transform")
t_data_block = t_control.GetDataBlock()
t_data_block.SetData("Position/PositionX", current_time, RLPy.RVariant(world_position.x))
t_data_block.SetData("Position/PositionY", current_time, RLPy.RVariant(world_position.y))
t_data_block.SetData("Position/PositionZ", current_time, RLPy.RVariant(world_position.z))
# Force update iClone native UI
RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() + RLPy.RTime(1))
RLPy.RGlobal.SetTime(RLPy.RGlobal.GetTime() - RLPy.RTime(1))
class EventCallback(RLPy.REventCallback):
def __init__(self):
RLPy.REventCallback.__init__(self)
def OnObjectDataChanged(self):
update_ui()
def OnCurrentTimeChanged(self, fTime):
update_ui()
def OnObjectAdded(self):
update_ui()
def OnObjectDeleted(self):
update_ui()
def OnObjectSelectionChanged(self):
update_ui()
class DialogEventCallback(RLPy.RDialogCallback):
def __init__(self):
RLPy.RDialogCallback.__init__(self)
def OnDialogHide(self):
RLPy.REventHandler.UnregisterCallback(event_callback_id)
return True
def update_ui():
items = RLPy.RScene.GetSelectedObjects()
local_pos = RLPy.RVector3.ZERO
if len(items) > 0:
local_pos = local_position(items[0])
for ind, val in enumerate([widget.moveX, widget.moveY, widget.moveZ]):
val.blockSignals(True)
val.setValue(local_pos[ind])
val.blockSignals(False)
dialog_window = RLPy.RUi.CreateRDialog()
dialog_window.SetWindowTitle("Local Transform")
# Create Pyside layout for RDialog
dialog = wrapInstance(int(dialog_window.GetWindow()), QtWidgets.QDialog)
dialog.setFixedWidth(350)
# Read and set the QT ui file from the script location
qt_ui_file = QtCore.QFile(os.path.dirname(__file__) + "/Local_Move.ui")
qt_ui_file.open(QtCore.QFile.ReadOnly)
widget = QtUiTools.QUiLoader().load(qt_ui_file)
qt_ui_file.close()
dialog.layout().addWidget(widget)
# Connect the UI controls
widget.moveX.valueChanged.connect(local_move)
widget.moveY.valueChanged.connect(local_move)
widget.moveZ.valueChanged.connect(local_move)
# Register Events
event_callback = EventCallback()
event_callback_id = RLPy.REventHandler.RegisterCallback(event_callback)
dialog_event_callback = DialogEventCallback()
dialog_window.RegisterEventCallback(dialog_event_callback)
dialog_window.Show()
update_ui()
APIs Used
You can research the following references for the APIs deployed in this code.
- RLPy.RMatrix4()
- RLPy.RScene.GetSelectedObjects()
- RLPy.RVector3()
- RLPy.RGlobal.GetTime()
- RLPy.RVariant()
- RLPy.RGlobal.SetTime()
- RLPy.RTime()
- RLPy.REventCallback.__init__()
- RLPy.RDialogCallback.__init__()
- RLPy.REventHandler.UnregisterCallback()
- RLPy.RUi.CreateRDialog()
- RLPy.REventHandler.RegisterCallback()