Scripts

Some Very Useful Scripts

For the last couple of years I’ve tried to maintain a “master list” of useful scripts for Cheetah 3D (I’ve even contributed a couple myself). This list is posted on the forums here: https://www.cheetah3d.com/forum/index.php?threads/5752/ but I’ve included the current list as of writing below.

Note: needs checking in Cheetah 3D 8.1.1: none of these scripts has a forum report from 8.x, and most date from Cheetah 3D 5–6. The Cheetah 3D scripting knowledge base catalog lists what forum users reported for each one.

Basic Interface Enhancements

Modeling

Animation

Texturing and UV-Mapping

Rendering

Particles

Using Scripts

The first thing you need to know about scripts is where to “install” them. This is especially tricky as of Lion because Apple has hidden user Library folders.

The simplest option is to use Go > Go To Folder in Finder to go to this folder:

~/Library/Application Support/Cheetah3D/

Which subfolder does a script go in?

For some reason Cheetah 3D requires scripts to be put in specific sub-folders despite the fact that (a) human beings cannot easily tell what kind of script a given script file is and (b) Cheetah 3D probably can. Go figure. If it's not obvious from the script's documentation or internal comments what kind of script it is, here's how you can probably tell by looking through it:

AI draft: generated from the Cheetah 3D 8.1.1 manual and the Cheetah 3D scripting knowledge base; not yet reviewed by the author.

The subfolders are Macro, Tool, Tag, Polygonobj, Splineobj, and Particle, and the folder decides the script type: a script in the wrong folder fails silently, throws misleading errors, or crashes Cheetah 3D.

End of AI draft.

This is not completely foolproof. It's possible to create a Particle script that never calls addParticle, for example, but it would be kind of weird. In most cases, what a script is supposed to do should be documented.

When must you relaunch Cheetah 3D?

AI draft: generated from the Cheetah 3D 8.1.1 app and the Cheetah 3D scripting knowledge base; not yet reviewed by the author.

If you’re working on a script you do not need to relaunch Cheetah 3D for it to see changes to the script (but you may need to select the script anew from the Script menu). To make Cheetah 3D pick up a newly added script, choose Update Menu at the bottom of the Tools > Script menu, or quit and relaunch Cheetah 3D.

End of AI draft.

Note: needs checking in Cheetah 3D 8.1.1: the manual (Executing a Script) still says to restart, but the app has an “Update Menu” item; confirm it finds new scripts.

Be careful when coding scripts. It is quite possible for a script to seize up Cheetah 3D and require a force quit. (Command-option escape to force quit, in case you didn’t know. You can also click and hold on an application’s icon in the dock.)

Assigning Hotkeys to Scripts

Once you’ve loaded a script, you can assign a hotkey to it (just as you would anything else). This is extremely useful for some of the more frequently needed scripts. E.g. when doing character animation, you might want to hotkey Pose Utilities.

Writing Scripts

To create a new script simply fire up your favorite text editor and create a new JavaScript (.js) document in the appropriate folder.

The best way to do it is probably to open up your Cheetah 3D folder inside Application Support — ~/Library/Application Support/Cheetah3D/ — and duplicate a script of the type you want to write, name it what you want, and then launch/relaunch Cheetah 3D. (TextEdit in plain text mode works fine, but it’s very basic — if you don’t have a serious programmer’s text editor, I recommend TextWrangler from barebones.com — it’s free and very powerful.)

To write Cheetah 3D scripts requires that you know some JavaScript. Teaching JavaScript is beyond the scope of this book, but there are many good resources for learning JavaScript (many would recommend Douglas Crockford’s JavaScript: The Good Parts and his videos at http://yuiblog.com/crockford). If you’ve done any programming you can probably follow along without knowing much about JavaScript specifically.

AI draft: generated from the Cheetah 3D scripting knowledge base README; not yet reviewed by the author.

Getting an AI Assistant to Write Scripts for You

These days you don't have to be a programmer to get a useful script. AI chat assistants (ChatGPT, Claude, Gemini, and so on) are good at JavaScript but know very little about Cheetah 3D, so on their own they write scripts that look right but don't work. The Cheetah 3D scripting knowledge base fixes this: it collects the Cheetah 3D 8.1.1 scripting API, the example scripts that ship with the app, and about twenty years of lessons from the forum into one file you hand to the AI.

Start a new chat and begin your message with:

Read https://raw.githubusercontent.com/tonioloewald/c3d-scripting-kb/main/dist/c3d-context-short.md and follow it.

Then, in the same message, describe what you want in Cheetah 3D terms: what it acts on (the selection, the whole scene, or a new object), what it should do, which settings you want to control, and your Cheetah 3D version. The AI will tell you the script type and which folder to put it in. Save the script as a plain-text .js file, install it, and test it on a copy of your scene. If something goes wrong, paste the Console output back into the chat.

The same approach works for fixing an old forum script that has stopped working, or adding a feature to one. The knowledge base also has a catalog of the scripts posted on the forum, with what users reported about whether they work.

End of AI draft.

Basics

The simplest kind of script in Cheetah 3D is a Macro script. A Macro script looks like this:

function main(doc){
// do stuff here
}

A comment in JavaScript is anything between /* and */ or anything following // until the end of that line. So “// do stuff here” is a comment.

When someone selects a macro script from the Tools > Script > Macro Script menu Cheetah 3D loads the script and calls its main function, passing it the variable doc, which contains a representation of your Cheetah 3D document.

If we look through the online help for documentation on the Document object (which is what doc is going to be) we can see it has some useful looking properties and methods. A nice simple method is render(). Let’s try that.

function main(doc){
doc.render();
}

If you try running this script it will do exactly what you expect. Well, kind of. Unlike clicking the Render icon in the toolbar you get no visual feedback until the render is complete.

Still, it’s not like this is a terribly useful script. Let’s try something a little more advanced.

function main(doc){
doc.addObject( BOX );
}

This, again, does what you’d expect. The documentation also stipulates that addObject returns an object — the thing you added. Let’s do something with it.

function main(doc){
var box = doc.addObject( BOX );
box.setParameter( "position", new Vec3D( 0, 0.5, 0 ) );
}

This macro creates a new box and then moves it up 0.5 units. (Ever frustrated by how the default box is centered on the origin rather than sitting neatly on the XZ plane? This script is your answer!)

Debugging Scripts — The Console

If you right-click on the header of any panel in a Cheetah 3D window you’ll discover that one of the options is for a console panel. I recommend adding a console panel somewhere to one of your layouts if you want to do any scripting.

My default layout has a console tab tucked in with the object browser — ready whenever I need it.

If you want your script to say something to you (“Hey, I got this far without crashing”) then Cheetah 3D provides a print() function.

function main(doc){
print( "hello world");
}

Hmm, maybe I should have started with this!

You can also generate alerts using the OS.messageBox() function.

function main(doc){
OS.messageBox("Title","Here is my message!");
}

Feeling Your Way Around

Cheetah 3D’s documentation is sometimes lacking, especially with respect to scripting, but it does offer some introspection tools to allow you to figure things out for yourself. One of the sample Macro scripts — Parameter Info.js — gives you a rough idea of how to do this, and forum scripting expert tg_jp has improved on it with this more general version:

function main(doc) {
var obj = doc.selectedObject();
print('---- Object Info ---');
if ( obj ) {
print('-- object type:'+obj.type());
var info = obj.parameterInfo();
for(var j=0;j<info.length;j++) {
print(info[j][0] +" [" + info[j][1] + "]:"+obj.getParameter(info[j][0]));
}
var tcount = obj.tagCount();
for (var i = 0;i < tcount;++i) {
var tag = obj.tagAtIndex(i);
info = tag.parameterInfo();
print('-- tag '+i+' type:'+tag.type());
for(var j=0;j<info.length;j++){
print(info[j][0] + " [" + info[j][1] + "]:"+tag.getParameter(info[j][0]));
}
if (tag.type() == 102) { // material tag.
var material = doc.materialAtIndex(tag.linkedToMaterial());
print('-- linked Material --');
info = material.parameterInfo();
for(var j=0;j<info.length;j++){
print(info[j][0] + " [" + info[j][1] + "]:"+material.getParameter(info[j][0]));
}
}
}
}
}

This script, when saved as a Macro Script, will document all the properties of any selected object.

If you wrap the core functionality of this script in a utility function, I’ll call it describe(), then you get something like this:

function describe( obj ){
if ( obj ) {
print('-- object type:'+obj.type());
var info = obj.parameterInfo();
for(var j=0;j<info.length;j++) {
print(info[j][0] +" [" + info[j][1] + "]:"+obj.getParameter(info[j][0]));
}
var tcount = obj.tagCount();
for (var i = 0;i < tcount;++i) {
var tag = obj.tagAtIndex(i);
info = tag.parameterInfo();
print('-- tag '+i+' type:'+tag.type());
for(var j=0;j<info.length;j++){
print(info[j][0] + " [" + info[j][1] + "]:"+tag.getParameter(info[j][0]));
}
if (tag.type() == 102) { // material tag.
var material = doc.materialAtIndex(tag.linkedToMaterial());
print('-- linked Material --');
info = material.parameterInfo();
for(var j=0;j<info.length;j++){
print(info[j][0] + " [" + info[j][1] + "]:"+material.getParameter(info[j][0]));
}
}
}
}
}

Now if I’m trying to figure out what properties a given object has, I can simply describe() it and its properties will be listed in the console.

This function has saved my bacon many times — e.g. when trying to script a Particle System the Random Seed property isn’t documented — how do you get at it? Using getParameter(“Random Seed”) doesn’t work. If you open the .jas file you’ll discover it’s called “particlesSeed” in the XML. But “particlesSeed” doesn’t work either (nor does “Particle Seed”). Finally, selecting a particle system and using this script reveals a property called “seed” of type “Int”. Eureka!

---- Object Info ---
-- object type:81
scriptName [String]:Scripts/Particle/Particle City.js
locked [Bool]:0
...
rotorder [Int]:6
name [String]:Procedural City
order [Int]:1
seed [Int]:12345
displayType [Int]:3
...
editorActive [Bool]:1
renderActive [Bool]:1
editorShading [Int]:0

Some Useful Techniques

Well, you’re pretty much on your own now. The best way to create new scripts is to look at the example scripts provided with Cheetah 3D, as well as those on the forums, and use them as a starting point. Before I send you off to swim with the sharks, here are a few useful techniques I’ve picked up.

Iterating Across the Scene Graph

This simple tool illustrates probably the single most useful pattern for writing macros and tool scripts.

First of all, it shows how to build a tool user interface.

The set_smooth function is called by the user interface button (the name of the function the button will call is passed as a parameter when creating the button).

The set_smooth function uses the target utility function to figure out if it’s going to act on the scene root or the selection (based on UI settings).

It then defines a simple function fn that does what it wants to do for a given object and calls the iterate_on_objects utility function to apply this simple function to the object returned by the target function and — if the user has opted for recursing children — everything below it in the scene graph (i.e. the Object Browser).

Note: requires updating for Cheetah 3D 8.1.1: doc.selectedObject() returns the hidden scene root (not null) when nothing is selected, so “Selection Only” then acts on the whole scene; 8.1.1 also has doc.selectedObjects() for multiple selections (KB skill/SKILL.md rule 5; API: Document). Tool scripts should also call recordParametersForUndo() before changing objects, or the changes bypass Undo (KB knowledge/gotchas.md).

// build the tool UI
function buildUI(tool){
tool.addParameterBool("Selection Only", true, false, true, false, false);
tool.addParameterBool("Recurse Children", true, false, true, false, false);
tool.addParameterSeparator("Smoothing");
tool.addParameterFloat("Angle",15.0,0.0,90.0,true,true);
tool.addParameterButton("Set Smooth Angle","OK","set_smooth");
}
function set_smooth(tool){
var angle = tool.getParameter("Angle");
var fn = function( obj ){
if( obj.type() == POLYGONOBJ ){
obj.setParameter("normalAngle", angle);
}
}
iterate_on_objects( tool, target(tool), fn );
tool.document().redrawAll();
}
function target(tool){
var doc = tool.document();
if( tool.getParameter("Selection Only") ){
return doc.selectedObject();
} else {
return doc.root();
}
}
// applies a function to an object and then, if user has selected recurse,
// recursively iterates across that object's children
function iterate_on_objects( tool, obj, fn ){
fn( obj );
if( tool.getParameter("Recurse Children") ){
for( var i=0; i<obj.childCount(); i++ ){
iterate_on_objects( tool, obj.childAtIndex(i), fn );
}
}
}

Iterating across the parts of an editable mesh

Using the same framework from the previous example, here’s a tool for flipping normals. You can flip every normal in your entire scene with a single button press using this handy script!

But the flip_normals function shows how to iterate across components of an editable mesh (a.k.a. POLYGONOBJ). Also note how it uses some tricks to force Cheetah 3D to recalculate normals and then forces the scene to be redrawn when it’s done.

// build the tool UI
function buildUI(tool){
tool.addParameterBool("Selection Only", true, false, true, false, false);
tool.addParameterBool("Recurse Children", true, false, true, false, false);
tool.addParameterSeparator("Normals");
tool.addParameterButton("Flip Normals","OK","flip_normals");
} 
function flip_normals(tool){
var fn = function( obj ){
if( obj.type() == POLYGONOBJ ){
var c = obj.core();
for( var i=0; i<c.polygonCount(); i++ ){
c.flipWinding( i );
}
// Force recalculation of normals
obj.setParameter("normalAngle", obj.getParameter("normalAngle"));
}
}
iterate_on_objects( tool, target(tool), fn );
tool.document().redrawAll();
}
function target(tool){
var doc = tool.document();
if( tool.getParameter("Selection Only") ){
return doc.selectedObject();
} else {
return doc.root();
}
}
// applies a function to an object and then, if user has selected recurse,
// recursively iterates across that object's children
function iterate_on_objects( tool, obj, fn ){
fn( obj );
if( tool.getParameter("Recurse Children") ){
for( var i=0; i<obj.childCount(); i++ ){
iterate_on_objects( tool, obj.childAtIndex(i), fn );
}
}
}

Pseudo-Random Numbers

One of the useful features of Cheetah 3D’s Particle System objects is that they are driven by a pseudo-random number generator for which you explicitly provide a seed value (which defaults to 12345). This means that a particle system will deterministically produce results based on that seed value. The effects seem random, but they’re completely predictable and reproducible. This means that if you’ve tweaked your particle settings to produce a pleasing result it won’t suddenly do something different when you render it (or the next time you load the file).

But what if you’re writing your own Particle System? Cheetah 3D doesn’t provide access to the pseudo-random number generator it uses internally. Here’s the current version of my Particle City script, a Particle System specifically designed to lay out buildings on a street grid. Note the Pseudo-random number generator! Also note how I obtain the seed value that Cheetah 3D automatically provides as part of any Particle System interface (so I don’t even need to create my own).

If you want to create procedural meshes (e.g. like the Relief object) you can make sure they will produce deterministic results based on a seed value using this pseudo-random number generator, or another like it.

//
// Particle City
// ©2010-12 Tonio Loewald
//
function buildUI(obj){
obj.setParameter("name","Procedural City");
obj.addParameterFloat( "Grid Spacing", 1,0.001,100,true, true);
obj.addParameterInt("Width",10,3,100,true,true);
obj.addParameterInt("Depth",10,3,100,true,true);
obj.addParameterInt("Block Size",2,1,5,true,true);
obj.addParameterFloat("Street Width",0.3,0.01,1,true,true);
obj.addParameterFloat("Floor Height",0.05,0.001,1,true,true);
obj.addParameterInt("Max Floor Reduction",3,0,100,true,true);
obj.addParameterFloat("Density",0.7,0,1,true,true);
}
function buildObject(obj){
var core = obj.core();
var gridSpacing = obj.getParameter("Grid Spacing");
var width = obj.getParameter("Width");
var depth = obj.getParameter("Depth");
var blockSize = obj.getParameter("Block Size");
var streetWidth = obj.getParameter("Street Width");
var maxFloors = obj.getParameter("Max Floor Reduction");
var floorHeight = obj.getParameter("Floor Height");
var density = obj.getParameter("Density");
// Pseudo-random number generator
var pseudoRandom = {
seed : obj.getParameter("seed"),
value : function(){
this.seed = ( this.seed * 9301 + 49297) % 233280;
return this.seed / 233280;
},
range : function( min, max ){
return Math.floor( this.value() * (max - min + 1) + min );
}
}
var w = (width - 1) * gridSpacing + Math.floor( width / blockSize ) * streetWidth;
var d = (depth - 1) * gridSpacing + Math.floor( depth / blockSize ) * streetWidth;
for( var x = 0; x < width; x++ ){
for( var z = 0; z < depth; z++ ){
if( pseudoRandom.value() < density ){
var bx = x * gridSpacing - w * 0.5 + Math.floor( x / blockSize ) * streetWidth;
var bz = z * gridSpacing - d * 0.5 + Math.floor( z / blockSize ) * streetWidth;
var  p = core.addParticle();
// drop each building by a number of floors
var h = pseudoRandom.range( 0, maxFloors ) * floorHeight;
p.setPosition( new Vec3D(bx, -h, bz) );
// rotate each building by a multiple of 90°
var r = pseudoRandom.range( 0, 3 ) * 90;
p.setRotation( new Vec3D( 0, r, 0 ) );
}
}
}
}