Last active
December 16, 2021 14:48
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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