Scripted SimpleObject Plug-ins

A SimpleObject is a kind of geometry primitive object that is defined by a tri-mesh, such as 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 supply a handler for building this mesh and 3ds Max automatically handles scene display, hit-testing, ray intersection, rendering, modifier applicability, etc.

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 should basically only be adjusting its parameters in the mouse tool handlers and constructing 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 , that 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 fairly simple way. The create tool sets the values of the parameters and the on buildMesh handler constructs a mesh based on those 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 example above, the mesh is initially set to a simple, two-face rectangular plane (via setMesh() ) and then the 2 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 only built 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 may occur. All face, vertex, material ID, and texture vertex arrays allocated must be filled in the handler.

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

on OKtoDisplay do <boolean_expr>

If the OKtoDisplay event handler is implemented, <boolean_expr> should 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 particular parameter settings and so should not be displayed in the viewports. The default implementation is true . Note, empty meshes are OK to display.

on hasUVW do <boolean_expr>

If the hasUVW event handler is implemented, <boolean_expr> should 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 would return the state of this checkbox. The default implementation returns true or false depending on whether the mesh has texture vertices present 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, as 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 off the mapping coordinates. If your plug-in is managing mapping coordinates, it should 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's 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, 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 which modified the object's original mesh directly. Either technique is OK.

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.

See Also