Scripted SimpleObject Plug-ins

A SimpleObject is a kind of geometry primitive that is defined by a tri-mesh such as, a box, sphere, cone, and so on. In other words, a simpleObject plugin is similar to all the Standard and Extended primitives in 3ds Max.

In scripting a SimpleObject plug-in, you need to supply a handler for building this mesh and 3ds Max automatically handles the scene display, hit-testing, ray intersection, rendering, modifier applicability, and others.

A scripted SimpleObject plug-in is declared by specifying the <superclass> as simpleObject .

Scripted SimpleObject plug-ins require a create tool.

A SimpleObject plug-in must not perform any scene related actions such as, creating scene nodes or building modifier stacks. It must only adjust its parameters in the mouse tool handlers and construct a mesh in the buildMesh handler. Attempting to create scene nodes or applying modifiers in a SimpleObject plug-in will cause all kinds off strange interactions with the in-progress scene node creation.

A SimpleObject plug-in has a predefined local variable mesh . This variable contains the underlying mesh of the object being created. The mesh local variable is accessible in any of the plug-in's handlers, but is typically only built in the buildMesh handler. You can either modify the existing TriMesh value in place using the TriMesh methods, or construct a new TriMesh value and assign it to the mesh plug-in local variable. The TriMesh methods and properties are described in Editable Mesh.

SCRIPT:

   plugin simpleObject tower_plugin_def
   name:"Tower"
   classID:#(145345,543211)
   category:"Scripted Primitives"
   (
     parameters main rollout:params
     (
       height type:#worldUnits ui:height default:0
       width type:#worldUnits ui:width default:0
       depth type:#worldUnits ui:depth default:0
     )
     rollout params "Two Faces Parameters"
     (
       spinner height "Height" type:#worldunits range:[-1000,1000,0]
       spinner width "Width" type:#worldunits range:[-1000,1000,0]
       spinner depth "Depth" type:#worldunits range:[-1000,1000,0]
     )
     on buildMesh do
     (
       setMesh mesh \
       verts:#([0,0,0],[width,0,0],[width,depth,0],[0,depth,0]) \
       faces:#([3,2,1], [1,4,3])
       extrudeFace mesh #(1,2) (height * 0.5) 40 dir:#common
       extrudeFace mesh #(1,2) (height * 0.5) 50 dir:#common
     )
     tool create
     (
       on mousePoint click do
         case click of
         (
           1: nodeTM.translation = gridPoint
           3: #stop
          )
       on mouseMove click do
         case click of
         (
           2: (width = gridDist.x; depth = gridDist.y)
           3: height = gridDist.z
         )
     )
   )

In this script a new primitive called Tower is defined. It constructs a very simple tower form; the first drag sets the base and the second drag sets the height. It contains three animatable rollout parameters: height , width , and depth , which set the object's basic bounds. The key components here are the create tool and the on buildMesh handler, and they work together in a simple way. The create tool sets the values of the parameters and the on buildMesh handler constructs a mesh based on the parameter values.

The create tool can also access and set the position of the node that is being created to contain the new object through the Matrix3 value in the pre-defined mouse tool local variable nodeTM . In this case, the position portion of the node's TM is set to the initial mouse down world position. The on mouseMove handler sets the width and depth during click 2 and the height during click 3.

The on buildMesh handler constructs the desired mesh in the TriMesh value in the pre-defined plug-in local variable mesh . Typically, it does this by building the mesh from scratch each time the handler is called. In the above example, the mesh is initially set to a simple two-face rectangular plane (using setMesh() ), and then the two faces are extruded up and scaled-down twice to get the simple tower.

The mesh plug-in local variable is accessible in any of the plug-in's handlers, but is typically built only in the on buildMesh handler. You can either modify the existing TriMesh value in place using the TriMesh methods, or construct a new TriMesh value and assign it to the mesh plug-in local variable. The TriMesh created must be valid or a 3ds Max crash might occur. All face, vertex, material ID, and texture vertex arrays allocated must be filled in the handler.

There are three additional event handlers that might be implemented for a scripted SimpleObject:

on OKtoDisplay do <boolean_expr>

If the OKtoDisplay event handler is implemented, <boolean_expr> must return true or false depending on whether it is OK to display the current mesh. This is useful in situations where a mesh might be in some degenerate state for certain parameter settings and must not be displayed in the viewports. The default implementation is true . Note that empty meshes are OK to display.

on hasUVW do <boolean_expr>

If the hasUVW event handler is implemented, <boolean_expr> must return true or false depending on whether UVW coordinates are present on the mesh. In many primitive objects, a Generate Mapping Coords checkbox is provided for the user to control UVW coordinate generation and the hasUVW handler expression returns the state of this checkbox. The default implementation returns true or false depending on whether the mesh has texture vertices or not. At present, only a single map channel is supported.

on setGenUVW <onOff> do <expr>

The setGenUVW event handler is called when 3ds Max wants the plug-in to automatically generate mapping coordinates. This happens when you first render an object that has a material applied, but not mapping coordinates. It is called with a switch argument <onOff> , which is true or false to turn on or turn off the mapping coordinates. If your plug-in is managing mapping coordinates, it must implement this handler and generate mapping coordinates when called with a true argument.

EXAMPLE

   plugin simpleObject TexturePlane
   name:"TexturePlane"
   classID:#(0x19a75166, 0x83e3a1c7)
   category:"MAXScript Examples"
   (
   parameters main rollout:params
   (
     Size type:#worldUnits ui:size default:0
     genMapCoords type:#boolean ui:gen_Map_Coords default:false animatable:false
     u_tile type:#float default:1.0 ui:u_step
     v_tile type:#float default:1.0 ui:v_step
   )
   rollout params "TexturePlane"
   (
     group "Geometry"
     (
       spinner size "Size:" type:#worldunits range:[0,1000,1]
     )
     group "Mapping"
     (
       spinner u_step "U Tiling" scale:0.01
       spinner v_step "V Tiling" scale:0.01
       checkbox gen_Map_Coords "Generate Mapping Coords."
     )
   ) --end roll

   --When Max wants to know whether the object has UV coords,
   --return the value stored in genMapCoords to inform it of
   --the current UV sate of the plug-in.

   on hasUVW do genMapCoords 

   --When 3ds Max tells the plug-in to turn on UV coordinates 
   --(for example when applying a texture 
   --with "Show Map In Viewport" checked), 
   --this handler will automagically check the checkbutton 
   --connected to our parameter called genMapCoords. 
   --Also, when the user changes the checkbox, 
   --the variable will tell the builMesh to generate coordinates. 
   --Note that the handler will override the manual settings, 
   --for example if a texture has "Show Map In Viewport" enabled, 
   --you cannot actually uncheck the "Generate Mapping Coords." 
   --checkbox with the mouse! 

   on setGenUVW bool do genMapCoords = bool 

   on buildMesh do ( 
     --Generate a simple Quad 
     local vertex_array = #() 
     append vertex_array [-size/2,-size/2,0] 
     append vertex_array [size/2,-size/2,0] 
     append vertex_array [size/2,size/2,0] 
     append vertex_array [-size/2,size/2,0] 
     face_array = #( [1,2,3], [1,3,4] ) 
     setMesh mesh verts:vertex_array faces:face_array 
     --If the checkbox was enabled and we want 
     --coordinates to be generated... 

     if genMapCoords then ( --define the texture vertices 
       local uvw_array = #() append uvw_array [0.0,0.0,0.0] append uvw_array [u_tile ,0.0,0.0] append uvw_array [u_tile ,v_tile ,0.0] append uvw_array [0.0,v_tile ,0.0] 
       --set the number of vertices 
       setNumTVerts mesh uvw_array.count 
       --set all vertices 
       for v = 1 to uvw_array.count do setTVert mesh v uvw_array[v] 
       --build texture faces 
       buildTVFaces mesh false 
       --set all texture faces (in this case using the mesh faces 
       --since we happen to have a one-to-one correspondence 
       --between vertices and texture vertices.) 
       --This is not always the case though... 
       for f = 1 to face_array.count do setTVFace mesh f face_array[f] ) 
   )--end on buildMesh 
   tool create 
   ( 
     on mousePoint click do 
     case click of 
     ( 
       1: ( nodeTM.translation = gridPoint ) 
       2: ( #stop) 
     ) 
     on mouseMove click do 
     case click of 
     ( 
       2: (Size= abs gridDist.x) 
     ) 
     ) 
   )--end plugin 

Here is another example that creates a mesh by first building other temporary objects and using mesh booleans to build the final mesh:

SCRIPT

   plugin simpleObject squareTube
   name:"SquareTube"
   classID:#(63445,55332)
   category:"Scripted Primitives"
   (
   local box1, box2
   parameters main rollout:params
   (
     length type:#worldUnits ui:length default:1E-3
     width type:#worldUnits ui:width default:1E-3
     height type:#worldUnits ui:height default:1E-3
   )
   rollout params "SquareTube"
   (
     spinner height "Height" type:#worldUnits range:[1E-3,1E9,1E-3]
     spinner width "Width" type:#worldUnits range:[1E-3,1E9,1E-3]
     spinner length "Length" type:#worldUnits range:[-1E9,1E9,1E-3]
   )
   on buildMesh do
   (
     if box1 == undefined do
       (box1 = createInstance box; box2 = createInstance box)
     box1.height = height; box2.height = height
     box1.width = width; box2.width = width * 2
     box1.length = length; box2.length = length * 2
     mesh = box2.mesh - box1.mesh
   )
   tool create
   (
     on mousePoint click do
       case click of
       (
         1: nodeTM.translation = gridPoint
         3: #stop
       )
     on mouseMove click do
       case click of
       (
         2: (width = abs gridDist.x; length = abs gridDist.y)
         3: height = gridDist.z
       )
     )
   )

In this example, the parameters and rollouts are handled in a similar manner to the first example, but the buildMesh handler generates the mesh in an indirect way. The final object is in the form of a rectangular pipe, a box with a box hole through the middle. Two plug-in locals ( box1 and box2 ) are made to contain Box base objects whose size parameters are set according to the plug-in's parameters ( box2 is the outer box and box1 is the hole). The final mesh is made by boolean subtraction of box1 from box2 . In this case, a separate new mesh is created and assigned to the mesh plug-in local variable, in contrast to the first example that modified the object's original mesh directly. Either method is okay.

The createInstance() method is used to directly create the box base objects. This method creates the geometry associated with the specified object, but does not create a node. This method is used since SimpleObject plug-in must not perform any scene related actions such as, creating scene nodes or building modifier stacks.