Home Technical Talk

3DS maxscript autorig

polycounter lvl 9
Offline / Send Message
tigerheart polycounter lvl 9
Hi,
A friend of mine told me that it is easier to learn how to code with a project. Now I have a project, and i don't know how to code. So here begin my journey on maxscript.

My project: For some time, i used to make a relatively easy rig for my character,(i don't like CAT for some reason) And i managed to make it pretty solid. But it still take almost a day per character to put it in place. So I want to write a code wich will set it up for me.

Right now, I manage to write the setup of the basic bones which will be use with the skin modifier. But in order to keep the scene clean, i'd like to move all the bones in a new layer. That's where i'm stuck.

As I said I don't know how to code, and i'm asking for help
/*create base*/
boneSys.createBone [0,0,0] [0,-30,0] [1,0,0]
select $Bone001
$.name = "base"
boneSys.createBone [0,-30,0] [0,-40,0] [1,0,0]
select $Bone001
$.name = "nub_base"
$.parent = $base

/*select all bones*/
select (for b in objects where classof b == boneGeometry collect b)

/* create new layer and store bones */
Layer = LayerManager.newLayerFromName "MC_bones"

So here a part of what I have so far:
a bone and his nub
then a selection of all bones
then the creation of a new layer.

Now how can i make all the bone to go ine the new layer?
Or how can i create a new layer, select it and create the bone inside?

Thanks for reading

Replies

  • Mark Dygert
    You will probably want to read up on the layer manager in the maxscript help files.
    http://docs.autodesk.com/3DSMAX/16/ENU/MAXScript-Help/index.html?url=files/GUID-78B79975-7BA5-4A03-8FEF-27E78D14B575.htm,topicNumber=d30e259872

    You will need to define the layer probably with "getLayerFromName" and then use ".addnode" to put whatever on that layer. The code could look something like this...
    CurSel = Selection as array [COLOR="Green"]--Create an array from selection[/COLOR]
    Layer =  LayerManager.getLayerFromName "MyLayerName" [COLOR="green"]--Change MyLayerName to the actual name of your layer[/COLOR] 
    for i=1 to CurSel.count do ([COLOR="green"]--a for loop to select each object and then put it on a layer.[/COLOR]
    	select CurSel[i] 
    	Layer.addNode $
    )
    
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    Thanks for the quick and efficient answer :D
    I did read the doc, and search over google , but as i said, i really started programming like 5 hour ago... So it is still hard to figure out what all the stuff mean or how to use it.
    Like in your piece of code, i have am not sur to understand:
    "Selection as array"
    "Layer.addNode $"
    and why the second line start with "layer ="
    I'll try to figure it out^^
  • Mark Dygert
    Layer.addNode $
    In this case the word Layer is more than just a word in the lines above I told it that Layer = "get Layer From this Name: MyLayerName" So anytime it sees the word layer it will go looking for a layer with that specific name.

    Then maxscript has a function called ".addnode" which adds whatever comes next to the layer. In this case what comes after .addNode is $ which means selection to maxscript.
    So the line Layer.addnode $ means:
    Get this specific layer name and add selection to it.

    CurSel = Selection as array
    This means take whatever is selected and create a group of objects (technically called an array) called "CurSel" (CURrent SELection). It could be called Bob if you wanted to it doesn't really matter.

    So now instead of having to know the name of every bone you now have a group of things from a selection. Which is handy because a literal script with hard coded names will only work if the names match. What if you didn't want to write the name of every bone or what if the names or number of bones were not the same from rig to rig? That's a lot of typing the same thing over and over again.

    So now you don't need to know the exact name and hard code that into your script you can select the first thing in the array by calling "CurSel[1]".
    Select Cursel[1] --Grabs the first object
    Layer.addNode $ --Adds it to the layer

    Select CurSel[2] --Grab the second object
    Layer.addNode $ --Adds it to the layer

    Select CurSel[3] --Grab the second object
    Layer.addNode $ --Adds it to the layer

    Select CurSel[4] --Grab the second object
    Layer.addNode $ --Adds it to the layer

    and so on...

    Note 1: If you really wanted to find out the name of the first object you could do:
    CurSel[1].name
    
    But it's not really necessary, because you can just say "get the first thing in the array" CurSel[1].

    Note 2: You can also do things like "count" the things in an array to find out how many items there are. CurSel.count = Number of items in the array. So if you wanted to count how many things you have selected you can do:
    CurSel = Selection as Array
    Print Cursel.count
    

    But handling each thing (CurSel[1], CurSel[2] ect...) one at a time, turns into a huge list of the exact same commands over and over again. If you wanted to change the commands you have to edit a bunch of different entries. But once we have an array you can do things like "the for-loop", which tells it to count the items in the array and do a list of commands to each thing. So now you have a short list of commands to edit, yeah!

    For i=1 Anytime you see an "i" insert an item number. "CurSel" is CurSel[1] then CurSel[2] ect...

    to CurSel.count We are only going to do the following commands for the number of things in the array. You could say "to 100" but it will do it 100 times even if you have 5 or 200 things. Its smarter to only do it the number of times you need.

    do ( ) Anything inside the parentheses gets treated as a group of commands.

    select CurSel{i} Select the items in CurSel, as the for loop counts replace i with 1 and when it runs out of commands switch it to 2 and run through the same commands.

    Layer.addNode $ Layer means get a specific layer and add whatever is selected.

    The commands are done so it goes back to the top of the list grabs the next item and goes through the commands again.

    Confusing at first, and I hope I didn't confuse you more, but its how a lot of scripts operate and it's a really fast way to do a lot of commands to a large group of objects.
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    wow, thanks for the detailed explanation, i wasn't hopping for this much^^ it's perfect.
    Right now I have my basic bone structure generated by one button. Tomorow i'll start handling all the contraints with another button^^ Thanks again
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    Hi, i'm stuck again...
    Right now I have a bone generator, and a part of script that create some controler for the legs and hip...
    until now, each action was hardcoded but i am now working on the spine. I have 3 spine bone named spine1,spine2,spine3, wich will receive the same kind of controler... Time to try to make a loop. Here is what i coded:
    Circle radius:20 isSelected:on  --create circle controller
    $.name = "Ctrl_spine1"
    $'Ctrl_spine1'.pivot = [0,0,-5]
    spine1pos = $spine1.pos  --get coordinate
    $.pos = spine1pos  --move circle to coordinate
    
    	hipRot = orientation_constraint()
    	hipRot.appendTarget $'Ctrl_spine1' 50.0
    	$'spine1'.rotation.controller=hipRot
    	$'spine1'.rotation.controller.relative = on  --set up contraint
    
    
    I edited the message to make it easier.
    I want to do the same thing with spine2 and spine3 but i can't figure out a working loop
  • Mrfred
    Offline / Send Message
    Mrfred polycounter lvl 4
    	for o in #($spine1,$spine2,$spine3) do
    	(
    		local ctrl = Circle radius:20 isSelected:off name:("ctrl"+o.name)
    		ctrl.pivot = [0,0,-5]
    		ctrl.pos   = o.transform.pos 
    		o.rotation.controller = orientation_constraint()
    		o.rotation.controller.appendTarget ctrl 50
    		o.rotation.controller.relative = true
    	)
    

    you do not need to use ' ' after $ if there's no space in the name. You would only need them for something like $'spine 1' $spine1 is good enough ;)

    rather than trying to select an object or keeping it selected after his creation you could assign him to a variable when you create it (like I did with the ctrl variable)


    how to do a loop:
    there's a few way to start a loop
    you can either do something like:
    for o = 1 to 10 do (
        print o
    )
    
    or
    for o in 1 to 10 do (
        print o
    )
    

    or
    (
       local array = #("this","is","an","array")
       for o in 1 to array.count do (
           print array[o] -- o is now the index of a given element inside our array
       )
    )
    

    or
    (
       local array = #("this","is","an","array")
       for o in array  do (
           print o  -- we will print every element in the array
       )
    )
    
    or
    (
       for o in #("this","is","an","array") do (
           print o  -- we will print every element in the array
       )
    )
    

    sometime you need to work with an index sometime you don't... but the index will always work. It's up to you.

    Good luck ;)
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    Hi Mrfred, thanks for your help
    This "("ctrl"+o.name)" is actually the part i couldn't guess.
    As for the ', i use them just to be sure there is no error since some of the name i use have space.

    I have a couple follow up question, why is there a "local" before circle? and does the letter i use as index matter? you use "o" while Mark Dygert above used "i"...

    Thanks anyway
  • Mrfred
    Offline / Send Message
    Mrfred polycounter lvl 4
    tigerheart wrote: »
    I have a couple follow up question, why is there a "local" before circle?

    local and global variable... a local variable inside the scope of the ( ) they're in while global variable can be accessed anytime. I suggest to stick with local as much as you can.

    ie:
    (
    local test
    )
    print test 
    
    this will give an undefined error as the variable test is not defined outside the ()
    (
    local test
    print test
    )
    
    works fine
    (
    global test
    )
    print test 
    

    works fine
    tigerheart wrote: »


    and does the letter i use as index matter? you use "o" while Mark Dygert above used "i"...

    it doesn't matter you could even use a word... like foo, pizza or whatever
    ie: for polycount in #("aa","bb") do print polycount
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    Thanks, indeed i did read that on the doc and just didn't recognise it when i saw it ^^'
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    Hi,thanks to your advice, i have a wonderfull button who happend a bone structure to my scene, and another one who create control over with ik and other contraint. Witch is pretty much what i wanted.
    There is one last thing i would like to do, but can't think of a method :
    When you create the bone structure, depending on the character, it might be out of place. You can still go in bone tool to adjust that, but if your character is symetrical, it might be easier to miror the position of the bone name *.L over those named *.R .

    This one might be a bit tricky for a newbie like me but If you have an answer, i would like to try

    thanks
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    I up here, in case someone with an answer haven't seen it, but i'm still looking for a solution to this mirror problem. thanks
  • Noors
    Offline / Send Message
    Noors greentooth
    --get all Right Bones
    sBones = for obj in $* where (classof obj == BoneGeometry) and (matchpattern obj.name pattern:"*.R" ignoreCase:false) collect obj
    
    --for each right bone
    for sBone in sBones do
    (
        -- get the right bone position
        bPos = sBone.pos
        -- find name of the left bone
        s1= ".R"
        s2= ".L"
        tBoneName = substituteString sBone.name s1 s2
        
        --get left bone and move it
        tBone = getnodebyname tBoneName             
        tBone.pos = [-bPos.x , bPos.y , bPos.z ]
      
    )
    
    That should be a start. Didn't test it. That's probably not the way to move bones tho. I think you have to move their pivot.
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    Hi noors, it seems to work but i guess there is something i didn't think through
    code.JPG
    I have no idea how I should proceed...
  • Noors
    Offline / Send Message
    Noors greentooth
    yeah, we need to move the bones with affect pivot only but via maxscript, so they stay well oriented and with the good length. I didn't find how to make that actually.
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    they are parented indeed^^ don't worry, it's just for me anyways, so no big deal...
  • tigerheart
    Offline / Send Message
    tigerheart polycounter lvl 9
    I have been seeking help on CG society too, but the logic behind the script might not be enough.
    code3.png
    code4.png

    the script on the left work unless you moved them with the bone tool. I don't know why yet, but if there is no solution, i'll work my around another method
  • S-S
    Offline / Send Message
    S-S polycounter lvl 18
    Hi there tigerheart!

    Interesting thread. It's been long time since I've touched something similar, but here it goes. There are some points to consider and obvious omissions in you script seen in last image you posted:

    1. I don't see the script even working; there is no copy made for the other side.

    2. There are no bone specific properties set. Node (anykind of node for that matter, not just bone representation) must be using $.boneEnable set to true, otherwise you don't see proper (bone) behavior at all.

    3. Mirror axis, looks quite OK to me, but then again it's been long time since I've done anything similar.
    You end up with non-negative scaled bones - which is good. Then again, remember that you won't end up with "mirror rotations" during animation time. Maybe someone with proper knowledge can refresh if this was even possible with max bones. Might be that I remember this incorrectly.

    4. Axis setup - If you draw a left arm from top down view, you notice that bone length in max is always along x-axis (no matter what), z-axis will be projecting out of outside of side of arm. Y-axis will be the one facing in different direction on right side compared to left side in this case.

    5. Hierarchy. I don't see any parentation happening. In general, you'd have to parent bones. Store parent for each node, then do loop of create bones, then a loop of parenting bones.
    However, here you can cheat, double click the root bone, then your selection will contain bones in hierarchical order. This would only work for an arm or a leg, not for a full rig if hierarchy is broken - but this is OK for testing your script on a limb.

    6. Structure of script. Overall, I'd first collect all the properties of source object, then proceed to copying, and then apply these settings to a node on mirrored side.


    P.S.
    I made a quick hacks to your script. Not everything you need though. Search for posts by Eek and Paul Neale on CGSociety. They know their rigging stuff. Good luck!

    *EDIT* I hope this won't cause any headaches - I didn't test mirroring that much, only with couple of poses, I accidentally flipped position of mirrorMatrix and ref.transform (I have fixed it in code below now).

    *EDIT* sorry for edits - fixed lots of typos and such (long week, too little sleep)
    fn mirrorBone ref obj =
    (
    	mirrorMatrix =scaleMatrix[1,-1,1]
    	flipMatrix = scaleMatrix[-1,1,1]
    	obj.transform = mirrorMatrix * ref.transform * flipMatrix
    )
    
    -- Collect all bones
    sBones = for obj in selection where  (classOf obj == BoneGeometry) collect obj
    
    
    for sBone in sBones do 
    (
    	-- Side Suffixes
    	s1 =".L"
    	s2 =".R"
    	
    	-- Get parent 
    	sBoneParent = sBone.parent
    	
    	-- If parent exists, get name
    	if (sBoneParent != null) do tBoneParentName = substituteString sBoneParent.name s1 s2
    	
    	-- Create a copy
    	tBone = copy sBone
    	
    	-- Mirror bone
    	mirrorBone sBone tBone
    	
    	-- Set name
    	tBoneName = substituteString sBone.name s1 s2
    	tBone.name = tBoneName
    	
    	-- If bone had a parent, parent it to corresponding parent
    	if (sBoneParent != null) do
    		tBone.parent = getNodeByName tBoneParentName
    		
    	-- Set bone property - otherwise it wont behave like a bone, try this in Bone Tools panel
    	tBone.boneEnable=true
    )
    
Sign In or Register to comment.