Skip to content

Instantly share code, notes, and snippets.

@mattcox
Last active December 16, 2021 14:48
Show Gist options
  • Save mattcox/59c394ccbe06c02a5787 to your computer and use it in GitHub Desktop.
Save mattcox/59c394ccbe06c02a5787 to your computer and use it in GitHub Desktop.
This example plugin for modo 701, shows how to create a surface force in Python. The force will read a mesh, get the closest position on that mesh and find the normal at that position. A force will be created along the normal vector of the surface. The result is a force that pushes particles and dynamic objects away from the surface.
#python
'''
Surface Force
This example plugin for modo 701, shows how to create a surface force in
Python. The force will read a mesh, get the closest position on that mesh
and find the normal at that position. A force will be created along the
normal vector of the surface. The result is a force that pushes particles
and dynamic objects away from the surface.
This plugin example demonstrates a few different concepts; how to implement
a custom item, with custom channels. How to implement a custom graph, control
connections to that graph and also read those connections. How to implement
a force that can be used to drive particle and dynamics simulations. Finally,
how to implement a modifier that reacts to changes in channel values to drive
the force.
'''
'''
Import the required modules.
'''
import lx
import lxifc
import lxu.vector
'''
Define the package name and the various channel names. There's no reason to
do this, other than it makes it easier to update the channel names or item
name without hunting through our code to find every time we've used it.
'''
PACKAGE_NAME = 'force.surface'
GRAPH_NAME = 'meshes'
GRAPH_USERNAME = 'Meshes'
CHAN_THRESHOLD = 'Threshold'
'''
Implement the Schematic class and functions. The Schematic class will inherit
from SchematicConnection. The various functions define the name of the graph,
which items are allowed to connect to each other using the graph and the type
of connections the graph supports; multiple, single, ordered...etc.
'''
class Schematic(lxifc.SchematicConnection):
def __init__(self):
scn_svc = lx.service.Scene()
def schm_ItemFlags(self, item):
'''
The ItemFlags function defines the type of connection that the graph
will support. The types are Single, Multiple, Ordered and Reversed.
The type of connection is often dependent on the item type being
connected to. For example, you may wish to allow multiple items to
connect through this graph, to an item of one type, but only allow a
single item to be connected using this graph to another type of item.
In our case, we only want to allow single items to be connected. We'll
do a simple test of the item type and if it matches our surface force
item, we'll permit a single connection to be made. Otherwise, disallow
all connections through this graph. This has the effect of limiting
connections to only our surface force.
'''
item = lx.object.Item(item)
if(item.Type() == self.scn_svc.ItemTypeLookup(PACKAGE_NAME)):
return lx.symbol.fSCON_SINGLE
else:
return 0
def schm_AllowConnect(self, item_from, item_to):
'''
The AllowConnect function allows you to control whether individual
connections are allowed or not. In the schematic, when you drag from
an item to your graph input, this function will be queried. If it
returns True, the connection link will turn Green and the items will
be connected, if this functions returns False, the connection link
will turn Red and the items will not be connected.
We only want to allow mesh items to be connected to our Surface Force,
so we'll simply check if the item that is trying to connect has a
mesh channel or not.
'''
mesh_type = self.scn_svc.ItemTypeLookup(lx.symbol.sITYPE_MESH)
meshInst_type = self.scn_svc.ItemTypeLookup(lx.symbol.sITYPE_MESH)
item_from = lx.object.Item(item_from)
if(item_from.TestType(mesh_type) == True or item_from.TestType(meshInst_type) == True):
return lx.symbol.e_TRUE
else:
return lx.symbol.e_FALSE
def schm_GraphName(self):
'''
As you'd expect, the GraphName function simply allows you to specify
the name of the graph.
'''
return GRAPH_NAME
'''
Implement the Item Package and Instance. These classes represent the item
that the user adds to the scene. It'll be added to the item list and be used
to hold channel values that are read by our Modifier.
'''
class Instance(lxifc.PackageInstance, lxifc.ViewItem3D):
'''
We don't actually need to implement any functions here, however the class
needs to exist. By inheriting from ViewItem3D, we will override any GL
drawing for this item, preventing it from drawing a locator shape in
the viewport.
'''
pass
class Package(lxifc.Package, lxifc.ChannelUI):
def pkg_SetupChannels(self, addChan):
'''
The SetupChannels function is where we add any extra channels to our
custom item. We are going to be adding a single channel to our item,
called "Threshold". This channel will be used by the force to control
the radius to look for our mesh surface.
'''
addChan = lx.object.AddChannel(addChan)
addChan.NewChannel(CHAN_THRESHOLD, lx.symbol.sTYPE_DISTANCE)
addChan.SetDefault(0.0, 0)
def pkg_Attach(self):
'''
The Attach function is called to create a new Instance of our item
in the scene. We simply return an instance of our Instance class.
'''
return Instance()
def pkg_TestInterface(self, guid):
'''
The TestInterface function is called for every potential interface
that the Instance could inherit from. We need to return true when
queried for any interface that it does inherit from. We do this
by comparing the GUID that is being passed as an argument against
the GUID of the interfaces that we know our Instance inherits from.
As our Instance inherits from PackageInstance and ViewItem3D, we
need to return True for both of them.
'''
return (lx.service.GUID().Compare(guid, lx.symbol.u_PACKAGEINSTANCE) == 0) or (lx.service.GUID().Compare(guid, lx.symbol.u_VIEWITEM3D) == 0)
def cui_UIHints(self,channelName,hints):
'''
The UIHints method is provided by the ChannelUI interface that our
Package class inherits from. This allows us to specify various
hints for a channel, such as Min and Max. Ideally, we'd do this when
creating the channel using the SetHints() method, however, that's
currently impossible in Python.
'''
hints = lx.object.UIHints(hints)
if channelName == CHAN_THRESHOLD:
hints.MinFloat(0.0)
'''
Implement the Force class. This class will be spawned by our modifier, to
create a force vector that will be read by our simulation.
This is where the majority of the calculations for our force take place. The
modifier (implemented below) will read the channels and pass the values to
the force. The force should simply take whatever value it's been passed and
use it to calculate the force.
'''
class Force(lxifc.Force):
def __init__(self):
'''
Our force requires three channel values to be able to calculate the
force vector; Firstly, it requires the mesh, which we will read to
get the closest point on the surface. We also use the threshold
channel on our item to get the maximum threshold to search for that
surface position. Finally, we use the world transform matrix of the
mesh item, so we can compensate for any item transforms on the mesh.
'''
self.val_svc = lx.service.Value()
self.chan_mesh = lx.object.Mesh()
self.chan_threshold = 0.0
self.chan_xfrm = lx.object.Matrix()
'''
To speed up evaluation, we calculate the inverse matrix and the rotation
element of the transform matrix in the mod_Evaluate method and then pass
it to the Force class when we instance it.
'''
self.xfrm_val_inverse = lx.object.Value()
self.xfrm_matrix_inverse = lx.object.Matrix()
self.xfrm_val = lx.object.Value()
self.xfrm_matrix = lx.object.Matrix()
def force_Flags(self):
'''
The Flags function is used to describe the force. This allows us to
specify any extra data that is needed to compute the force. For
example, if we returned lx.symbol.fFORCE_MASS, we'd then be able
to read the mass of the object we are applying the force to.
As we only need the particle positions, we'll simply return 0.
'''
return 0
def force_Force(self, pos):
'''
This function is where we calculate the force itself. We are given
a position vector and we will return a force vector.
The function that is implemented here depends on the value returned
in the Flags function. As we returned 0, we will implement the basic
Force function, which passes us a single position vector. However,
if we'd implemented fFORCE_VELOCITY for example, we'd need to
implement the ForceV() function instead, which passes the Velocity
as an argument. See the wiki for the various definitions of these
functions.
'''
return_value = (0.0,0.0,0.0)
'''
First, we want to check that the mesh channel is valid. If it's not
then we skip whatever is below and simply return a force of zero.
'''
if self.chan_mesh.test() == False:
return return_value
'''
We want to find the closest point on the mesh surface. To do this,
we will use the Closest() method on the Polygon interface. We'll use
the PolygonAccessor() on the Mesh object to get the Polygon
interface. This may seem a bit confusing, as we are not getting a
specific polygon here, instead, we are simply getting an interface
that allows us to work with any polygons on the mesh.
'''
polygon = self.chan_mesh.PolygonAccessor()
if polygon.test() == False:
return return_value
'''
Before we can read the closest position on the surface, we need to
compensate for any item transforms on the mesh item. To do this, we
will multiply our search position vector by the inverse of the mesh
transform matrix.
'''
pos = self.xfrm_matrix_inverse.MultiplyVector(pos)
'''
Now we are ready to perform the lookup to find the Closest position
on the mesh. For this, we will specify a maximum distance to search.
If we find the surface within the range specified, the function will
return True, along with the Position, Normal and Distance to the
closest point. If we fail to find a position on the surface, the
function will return False.
Note: If the threshold is 0, then the function will ignore it.
'''
polygon_closest = polygon.Closest(self.chan_threshold,pos)
if polygon_closest[0] == False:
return return_value
polygon_hitPos = polygon_closest[1]
polygon_hitNrm = polygon_closest[2]
polygon_hitDst = polygon_closest[3]
'''
Our force vector is going to be the normal of the surface. Rather
than simply outputting the vector, we want to scale the vector by
a weight. We could calculate this weight in a number of ways; using
a vertex map for example. In our case, we are going to falloff the
strength by smoothing the value over the range 0 -> threshold. If
the threshold is 0 (infinity), we will use a constant weight of 1.0.
'''
if self.chan_threshold > 0:
hitDst_range = polygon_hitDst/self.chan_threshold
weight = 1.0-((3.0-2.0*hitDst_range) * hitDst_range * hitDst_range)
else:
weight = 1.0
'''
Before we output the normal, we need to compensate for the world
transforms of the mesh item. To do this, we simply multiply the
normal vector by the mesh transform matrix. So the force strength
isn't affected by the mesh item scale, we also normalize the
resulting matrix.
'''
polygon_hitNrm = self.xfrm_matrix.MultiplyVector(polygon_hitNrm)
polygon_hitNrm = lxu.vector.normalize(polygon_hitNrm)
'''
Finally, we'll multiply the force vector by the weight value.
'''
return_value = lxu.vector.scale(polygon_hitNrm,weight)
'''
Return the force vector.
'''
return return_value
'''
Implement the modifier. The modifier is created when our surface force item
is created. It's job is to read the input channels and calculate the value
of the output channels. In our case, we're going to read the mesh channel and
transform channels on a mesh item, and the threshold channel on the surface
force item and write to the Force channel of the surface force item.
'''
class Modifier(lxifc.Modifier):
def __init__(self, item, eval):
'''
In the modifier constructor, we are going to define what channels
we need to read and write. We also need to get an Attributes
interface that our Evaluate() function can use to read the channel
values.
'''
self.attr = lx.object.Attributes(eval)
item = lx.object.Item(item)
self.mesh_connected = False
self.graph_revCount = 0
'''
Channels that we add are accessed using an index. We get the index
of the first channel we add, then the second channel can be accessed
by just adding 1 to channel index.
We want to add the force channel from the surface force item first.
As we will be writing a value to this channel, we will set it's mode
to Write. The force channel is automatically added to the item when
we specify that it's supertype is a force (defined below).
'''
self.index = eval.AddChannelName(item, lx.symbol.sICHAN_FORCE_FORCE, lx.symbol.fECHAN_WRITE)
'''
We want to read the threshold channel on the surface force item. In
this case, we only want to read the channel and not write to it. So
we set the mode to read only.
'''
eval.AddChannelName(item, CHAN_THRESHOLD, lx.symbol.fECHAN_READ)
'''
As we wish to read channels from a different item in the scene, we
need a simple way to specify which item that is. To do this, we'll
simply do a reverse lookup on the Mesh graph that we implemented
above. Then if the user connects a mesh to the mesh input on our
surface force item, we can easily read the required channels from
that item.
'''
scene = item.Context()
self.graph = lx.object.ItemGraph(scene.GraphLookup(GRAPH_NAME))
if self.graph.RevCount(item) > 0:
'''
As our mesh graph only supports a single connection, we'll simply
get the first item connected to it.
'''
input_item = self.graph.RevByIndex(item, 0)
'''
We want to add two channels from the mesh item, the mesh channel
and the world transform matrix. Both are set to read only.
'''
eval.AddChannelName(input_item, lx.symbol.sICHAN_MESH_MESH, lx.symbol.fECHAN_READ)
eval.AddChannelName(input_item, lx.symbol.sICHAN_XFRMCORE_WORLDMATRIX, lx.symbol.fECHAN_READ)
'''
Now that we have the channels from the mesh, we'll also set the
value of the mesh_connected variable, this will allow us to
easily query if a mesh is connected in the rest of the modifier,
without expensive graph lookups. As our constructor will be called
whenever our graph changes (defined below), we can be pretty
confident that the mesh_connected variable will remain up to date.
'''
self.mesh_connected = True
'''
Finally, want to set the value of the graph_revCount variable. This
will be used in the Test() method to check if the graph has changed.
'''
self.graph_revCount = self.graph.RevCount(item)
def mod_Evaluate(self):
'''
The Evaluate function is called whenever an output channel is read by
another modifier. Here we use the input channels to calculate the
correct output channel value.
In our case, we are going to read the input channels and pass the
value to our Force class. We will also set the force channel object
to be our Force class.
'''
'''
Create an instance of the Force class.
'''
force = Force()
'''
Read the input channels, we only want to read the mesh item channels
if a mesh item is connected, so we will query the variable and then
read the values if required.
'''
force.chan_threshold = self.attr.GetFlt(self.index+1)
if self.mesh_connected == True:
'''
The mesh channel is stored as a MeshFilter when reading the mesh
channel from a modifier. Therefore, we need the read the channel
and get the Mesh object from the MeshFilter, before passing it
to force.
'''
mesh_filt = lx.object.MeshFilter(self.attr.Value(self.index+2,0))
force.chan_mesh = mesh_filt.Generate()
'''
Read the world transform channel. We read this as a value object
and then cast to a Matrix object.
'''
force.chan_xfrm = lx.object.Matrix(self.attr.Value(self.index+3,0))
'''
Our force items are going to need to manipulate the matrix channel
value. We can't manipulate the matrix value directly, as it's a
read only channel. We want to create two new matrix channels, one will
store the rotation rows and columns in the transform matrix and the
other will store the inverse of the transformation matrix.
'''
force.xfrm_val_inverse = force.val_svc.CreateValue("matrix4")
force.xfrm_matrix_inverse = lx.object.Matrix(force.xfrm_val_inverse)
force.xfrm_matrix_inverse.Set4(force.chan_xfrm.Get4())
force.xfrm_matrix_inverse.Invert()
force.xfrm_val = force.val_svc.CreateValue("matrix4")
force.xfrm_matrix = lx.object.Matrix(force.xfrm_val)
force.xfrm_matrix.Set4(force.chan_xfrm.Get4())
force.xfrm_matrix.SetOffset((0.0,0.0,0.0))
'''
Finally, we want to set the writeable force channel object to be
our Force class. This means that when the force channel is evaluated
by a simulation, it'll use the Force class to calculate the force.
'''
val_ref = lx.object.ValueReference(self.attr.Value(self.index, 1))
val_ref.SetObject(force)
def mod_Test(self, item, index):
'''
When a change has been made to the Graphs that a modifier uses
(defined below), the modifier can become invalid and need recreating.
for example, if the user deletes the input mesh item, we need to
recreate the modifier so that it doesn't try to read those channels.
However, this could be unnecessary, as a graph change elsewhere may
not affect this particular modifier instance, so we may not need to
recreate it.
The Test method allows us to choose whether we need to recreate the
modifier or not, it will be called whenever the Mesh input graph
changes. Returning false will recreate the modifier, returning True
will do nothing.
To perform the test, we will simply check if the number of input
items on this graph to our item has changed, if it has, we will
recreate the modifier.
'''
graph_revCount = self.graph.RevCount(item)
if graph_revCount != self.graph_revCount:
self.graph_revCount = graph_revCount
return False
'''
Individual Modifiers are spawned from a single EvalModifier. So our
EvalModifier iterates through all of the Surface Force items in the scene
and creates a new modifier for each item.
'''
class EvalModifier(lxifc.EvalModifier):
def __init__(self):
'''
Lookup the surface force item type so that we can easily check items
in the various EvalModifier functions.
'''
scn_svc = lx.service.Scene()
self.itemType = scn_svc.ItemTypeLookup(PACKAGE_NAME)
def eval_Reset(self,scene):
'''
The Reset function stores the number of Surface Force items in the
scene and the initial index.
'''
self.scene = lx.object.Scene(scene)
self.index = 0
self.count = self.scene.ItemCount(self.itemType)
def eval_Next(self):
'''
The Next function is called repeatedly until we return 0, this allows
us to loop over the Surface Force items in the scene. We increment the
index variable and for each item, we return the item object that should
have a modifier attached and the key channel for the modifier.
'''
if self.index >= self.count:
'''
There are no Surface Force items left in the scene, tell the
Modifier to stop enumerating through them.
'''
return (0,0)
item = self.scene.ItemByIndex(self.itemType, self.index)
self.index += 1
return (item,0)
def eval_Alloc(self,item,index,eval):
'''
For each item, we allocate a Modifier.
'''
return Modifier(item, lx.object.Evaluation(eval))
'''
Bless and initialize all of the servers.
'''
'''
For the graph, we want to define a username for the graph in the tags.
'''
tags = { lx.symbol.sSRV_USERNAME: GRAPH_USERNAME }
lx.bless(Schematic, GRAPH_NAME, tags)
'''
For the modifier, we need to define two things. Firstly, we need to tell the
modifier want kind of items it should be attached to. Secondly, we need to
tell the modifier what graphs it should watch for changes, to potentially
invalidate the modifier.
'''
tags = { lx.symbol.sMOD_TYPELIST: PACKAGE_NAME, lx.symbol.sMOD_GRAPHLIST: GRAPH_NAME }
lx.bless(EvalModifier, PACKAGE_NAME, tags)
'''
Finally, for the Surface Force item package, we want to define it's supertype
as Force, this will add a Force channel to the item. We also want to add our
custom graph to this item - this will mean that if the user adds the item to
the schematic, they will see our "Meshes" graph as an input.
'''
tags = { lx.symbol.sPKG_SUPERTYPE: lx.symbol.sITYPE_FORCE, lx.symbol.sPKG_GRAPHS: GRAPH_NAME }
lx.bless(Package, PACKAGE_NAME, tags)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment