Creating controller joysticks in Maxscript - Part 1

Where I work, I deal with facial rigging for characters. The setup of bones, animating the faces, and the loading and saving of facial animations isn’t the easiest thing in the world, and Max doesn’t seem to provide an easy way to load facial animation from one facial setup to another.

What you can do however is bypass the actual bones by performing all the animation on a set of controllers, and then saving their animation. Since the controllers add a layer of abstraction, you can essentially transfer animation from almost any facial rig to any other, as long as you have the same controllers. I decided to use Joystick controllers, since they give a nice visual representation of the animation.

So, how to go about this? Since it’s something that is going to be used a lot, scripting it makes sense. I’ve scripted dozens of workflow tools at work, so I knew the benefits (especially when dealing with potentially hundreds of models). I’ve not built anything quite so complex before however, and my Maxscripting knowledge is still quite low, so I knew I’d have to learn new techniques. Fortunately when I got really stuck I was able to get some help from Maxscript forums such as CGTalk and Autodesk Area.

I broke it down into two distinct areas - building the controllers, and linking the controllers to the bones. This post concerns the joysticks - how I built them, and what I did to try and make them extendable for my future use, or for other people. I’ll built the joysticks in a function, so that I can call it with a few commands and get a custom joystick out the other end.

200711241517

I wanted 3 types of joysticks - Square, Vertical and Horizontal, but they are all the same really, just with different dimensions. They are simply a rounded box with a controller circle and a caption, all done via max shapes.

Creating a rectangle in Maxscript is simple:

Rectangle length:10 width:10

But that’s not quite good enough. I don’t want the joysticks to be 10×10 units, especially since I want to be able to create 3 shapes. Also, I’m working at a very small scale at the minute, but may want to change the size of the joysticks at any time. This is where variables come in - some are kept inside the function, some are passed in. Also, I define a universal scale multiplier so that people can make their joysticks 10 times bigger, or 50 times bigger, or whatever they wish

-- A universal scaler for different sized joysticks
global sizemulti = 1

-- Function that builds a joystick
fn createJoystick jsn jstyle jpos = ()

When I call the function, I pass in three variables: jsn is the joystick name, used to give all its parts a name, and to add the caption; jstyle is the style, where 1 is square, 2 is vertical and 3 is horizontal; finally jpos is the position I want the joystick to initially appear at on screen.

When we get inside the function, I declare some variables that will allow me to draw all the parts of the joystick. I multiply these by my size multiplier (which being 1 in my script does nothing).

local jsl = conjsl = 0.1 * sizemulti as float		-- bounding box length and constraint
local jsw = conjsw = 0.1 * sizemulti as float	-- bounding box width and constraint
local jscor = 0.01 * sizemulti as float 		-- rounded corners
local jscir = 0.01 * sizemulti as float			-- and the circle size
local jscap = 0.02 * sizemulti as float		-- caption text size
local jstxsp = jsl - 0.03 * sizemulti as float		-- caption text offset

With these variables, drawing the parts is easy - lets draw the rectangle and give it some nice rounded corners.

Rectangle length:jsl width:jsw cornerRadius:jscor position:jpos name: ("JSB_" + jsn)

Just a couple of other things - Max draws shapes in a top down view, so we can use transform to change that. Also, lets set the colour of the shape, and also assign this rectangle to a variables called jsbox to make it easy to select later. Please note that the >>> are added here for formatting only, to show where the line was too long for the website. Where you see these, the two lines should be one long line. Let’s continue!

-- create bounding box for the joystick, keep it selected
jsbox = Rectangle length:jsl width:jsw cornerRadius:jscor position:jpos name: ("JSB_" + jsn) >>>
>>> wirecolor:(color 250 230 100) transform:(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0]) isselected:on

Phew! That looks much harder than it actually was, just to get a nice rectangle on the screen, but it makes the following bit much easier. I’m going to skip a few of the steps here, and show you how I created the circle and the text - it should all make sense based on the stuff above. I just create a couple of shapes, parent them to the box (so they move when it does), and set their initial position relative to the box.

The >>> indicates a line split over two for formatting purposes.

-- create bounding box for the joystick, keep it selected
jsbox = Rectangle length:jsl width:jsw cornerRadius:jscor position:jpos name: ("JSB_" + jsn) >>>
>>>wirecolor:(color 250 230 100) transform:(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0]) isselected:on

– add the caption, parent it to the bounding box
jscaption = Text text: jsn size: jscap wirecolor:(color 20 20 255) name: (”JSCaption_” + jsn)
 >>(linebreak here) transform:(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0])
jscaption.parent = jsbox
in coordsys parent jscaption.position = [0,jstxsp,0] 

– and create the circle controller, parent it to the bounding box
jscircle = Circle radius: (jscir) name: (”JS_Circle_” + jsn) wirecolor:(color 20 20 255)
 >>(linebreak here) transform:(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0])
jscircle.parent = jsbox
in coordsys parent jscircle.position = [0,0,0]

That builds a nice square joystick, but you’ll remember I wanted to be able to build 1 dimensional joysticks too that were only for horizontal or for vertical movement. This is actually simple, all I need to to is to change the width or the height of the bounding box before I draw it. I already pass in a variable called jstyle which indicates the type of joystick I want, so lets deal with that next.

As I mentioned, the variable jstyle is a number, where 1 is square, 2 is vertical and 3 is horizontal, so I can use an if statement to alter a few variables if it’s not square.

-- The size of the bounding box will depend on the style chosen, so adjust variables now
if jstyle == 2 then
(
	-- Vertical style stick
	jsw = 0.02 as float
	jscor = 0.01 as float
	conjsw=0
)

else if jstyle == 3 then
(
	-- Horizontal style stick
	jsl = 0.02 as float
	jscor = 0.01 as float
	conjsl=0
)

Quite simple - if the jstyle variable == 2, then it’s a vertical joystick, so set the width of the box to be narrow, and set to width constraint conjsw to be 0 - you can’t move the controller side to side. Obviously the horizontal stick uses the same method, but length instead of width.

At this stage I was happy - I could draw joysticks on the screen, and make them look how I wanted. But I ran into my first problem - how to stop the controller circles leaving the rectangle? This was going to be hard. I had a look around the Maxscript docs, but got stuck, then posted on CGTalk, whereupon I was recommend to try the float_limit() command. After some head banking, I eventually got it to work.

The float_limit command is essentially just that - a limit you can put on a floating point number. You create a float_limit object, assign it to a controller, then tell it what the upper and lower limits are. With my square controllers I’d need an upper and lower limit for both X and Y, and I’ve already explained that I’m treating my horizontal and vertical controllers in the same way as my square ones, and I’m simply setting their length or width to 0.

Limiting the controllers is based on the size of the bounding box, so the upper limit is 1/2 the size of the box, and the lower limit is also half the size (but a negative number). So, the code to limit my controllers.

xfl=float_limit()
jscircle.pos.controller.x_position.controller=xfl
paramWire.connect jsbox.baseObject[#width] xfl.limits[#upper_limit] ((conjsw/2) as string)
paramWire.connect jsbox.baseObject[#width] xfl.limits[#lower_limit] ((-conjsw/2) as string)

yfl=float_limit()
jscircle.pos.controller.y_position.controller=yfl
paramWire.connect jsbox.baseObject[#length] yfl.limits[#upper_limit] ((conjsl/2) as string)
paramWire.connect jsbox.baseObject[#length] yfl.limits[#lower_limit] ((-conjsl/2) as string)

That’s all the parts of the joystick working together, but it’s all in a function because I plan to call the function in script to create as many joysticks as I need, placing them where I want them. However, most people will want a nice little interface, so I decided that I’d knock up a quick one. This is basic, but does the job, even if just to test the building of joysticks.

200711251354


The code is VERY simple - a box to type in the name, a joystick style, and a button to build it. They all get created at 1,0,2 in world space in this UI, but the joystick is automatically selected so that you can move it.

rollout crig "Control Rig builder" width:163 height:175
(
	-- UI here
	group "Create New Joystick"
	(
	Edittext jsn "Joystick Name: "
	RadioButtons jstype labels:#("Square", "Vertical", "Horizontal")
	Button crjstick "Create Joystick"
	)

	on crjstick pressed do
	(	-- call the joystick creation function with the UI data
	jsname = jsn.text as string
	jstyle = jstype.state as integer
	jpos = [1,0,2] — the position on screen. This can overridden later, perhaps by mouse click.
		seljoy = createJoystick jsname jstyle jpos
	)

) — rollout end

createDialog crig 250 400

So, thats was all the parts, I hope you found it useful. In the next part, I’ll discuss linking the controllers to bones, and even to other controllers. I’ve finally managed to get it all working, but need to tidy up the code.

I’ll leave you with the full script:

-- Generic Joystick creation script with test UI.
-- Rick Stirling
-- November 2007

-- A universal scaler for different sized joysticks
global sizemulti = 1

-- Function that builds a joystick
fn createJoystick jsn jstyle jpos =
(
    -- setup our default sizes
    local jsl = conjsl = 0.1 * sizemulti as float -- bounding box length
    local jsw = conjsw = 0.1 * sizemulti as float  -- bounding box width
    local jscor = 0.01 * sizemulti as float -- rounded corners
    local jscir = 0.01 * sizemulti as float-- and the circle size
    local jscap = 0.02 * sizemulti as float-- caption text size
    local jstxsp = jsl - 0.03 * sizemulti as float-- caption text offset

-- The size of the bounding box will depend on the style chosen, so adjust variables now
	if jstyle == 2 then
	(
	-- Vertical style stick
	jsw = 0.02 as float
	jscor = 0.01 as float
	conjsw=0
	)

		else if jstyle == 3 then
	(
	-- Horizontal style stick
	jsl = 0.02 as float
	jscor = 0.01 as float
	conjsl=0
	)

	-- create bounding box for the joystick, keep it selected
        jsbox = Rectangle length:jsl width:jsw cornerRadius:jscor position:jpos name: ("JSB_" + jsn) >>>
 >>> wirecolor:(color 250 230 100)
 >>(linebreak here) transform:(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0]) isselected:on

	– add the caption, parent it to the bounding box
	jscaption = Text text: jsn size: jscap wirecolor:(color 20 20 255) name: (”JSCaption_” + jsn) transform:(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0])
	jscaption.parent = jsbox
	in coordsys parent jscaption.position = [0,jstxsp,0]
			– and create the circle controller, parent it to the bounding box
	jscircle = Circle radius: (jscir) name: (”JS_Circle_” + jsn) wirecolor:(color 20 20 255)
 >>(linebreak here) transform:(matrix3 [1,0,0] [0,0,1] [0,-1,0] [0,0,0])
	jscircle.parent = jsbox
	in coordsys parent jscircle.position = [0,0,0]

		– now that we have the controller bits built, we need to limit the controller	– circle to the bounding box edges.
	– use float limits based on cgtalk ideas

		xfl=float_limit()
	jscircle.pos.controller.x_position.controller=xfl
	paramWire.connect jsbox.baseObject[#width] xfl.limits[#upper_limit] ((conjsw/2) as string)
	paramWire.connect jsbox.baseObject[#width] xfl.limits[#lower_limit] ((-conjsw/2) as string)

		yfl=float_limit()
	jscircle.pos.controller.y_position.controller=yfl
	paramWire.connect jsbox.baseObject[#length] yfl.limits[#upper_limit] ((conjsl/2) as string)
	paramWire.connect jsbox.baseObject[#length] yfl.limits[#lower_limit] ((-conjsl/2) as string)

	return jsbox
)

rollout crig “Control Rig builder” width:163 height:175
(	

	– UI here
	group “Create New Joystick”
	(
	Edittext jsn “Joystick Name: ”
	RadioButtons jstype labels:#(”Square”, “Vertical”, “Horizontal”)
	Button crjstick “Create Joystick”
	)

	on crjstick pressed do
	(
		– call the joystick creation function with the UI data
	jsname = jsn.text as string
	jstyle = jstype.state as integer
	jpos = [1,0,2] — the position on screen. This can overridden later, perhaps by mouse click.
		seljoy = createJoystick jsname jstyle jpos
	)

) — rollout end

createDialog crig 250 400

One final thing to note - my sticks return a range of -0.05 to +0.05, so you’ll want to normalise them to return a value from -1 to +1.

-- normalise joystick
-- get the size of the boundingbox, create a normalise multiplier
fn joystick_normalise stickcontrol =
(
nmx = 1/($.parent.width)*2
nmy = 1/($.parent.length)*2

xyra = [nmx, nmy]
return xyra
)

Thoughts on this?