Basic Data Types
The two most basic data types in ZScript are numbers and strings. You type in numbers without any special syntax; 5, 3.1415, etc. Strings are sequences of characters, and you enter them with surrounding quotes; “This is a ZScript string”.
In addition, ZScript has two more advanced types, arrays and memory blocks. We’ll get to those later in the document.
ZScript is a very simple language, in syntactic terms. You can basically have four kinds of constructs.
- An expression is just some sort of mathematical or logical operation, such as
x + 3 * y
x + (3 * y).
- A ZScript command is a name and sequence of arguments enclosed in parentheses and separated with comments:
[commandName, arg1, arg2, . . . ]
- A block is just a list of commands that are not separated with commas:
[commandName1, . . .] [commandName2, . . .] [commandName3, . . .]
- Anything after a // (unless it’s in a string) is a comment. ZScript will ignore it, but you can put in whatever you want, as an aid to understanding or remembering how your program works. Putting in comments is a good idea—it’s amazing how a program that seemed as clear as day when you were writing it can look like complete gibberish a month later, if you don’t comment it.
Here’s a simple example of comments:
// The number of items we'll be using, may change as the program executes. [VarDef, gNumOfItems, 5] [VarDef, gMaxItems, 20] // This is the maximum value that 'gNumOfItems' can be set to.<br>
- The commands in a block are executed in order when executed in a ZScript. Blocks can be appear inside certain other ZScript commands.
ZScript variables can occur in two places; at the top (global) level of a script, or inside a routine definition (discussed below). Variables defined at the global level are accessible anywhere in the script, including in routines. Variables defined inside a routine are accessible only inside that routine.
ZScript uses two commands to define and set variables, and one command to get the value of variables:
[VarDef, variableName, initialValue] is used at the top level to declare a variable and assign it an initial value.
[VarDef, x, 5]
defines a numeric variable
x with a starting value of 5, while
[VarDef, s, "Hello"]
defines a string variable with initial value of “Hello”. There are a couple of things to remember about VarDef that will make your life much easier, and reduce the number of bugs in your programs:
- Always make sure to give an initial value when using VarDef. For one things, it’s good programming practice. More importantly, and variable defined without an intial argument will automatically be a numeric variable, and if you try to use it as a string, you’ll get some strange results.
- VarDef is always used at the top level of a ZScript, never within a routine definition.
- Finally, since all variables defined with VarDef are defined at the top level and are therefore global variables, it’s good programming to start all such variables with a ‘g’, to make sure you don’t accidentally use a global variable within a routine. So, define [VarDef, gX, 5], not [VarDef, x, 5].
[VarSet, variableName, initialValue] is used to set the value of a variable, but is used in two different ways:
- If the variable is a global variable (defined at the top level with VarDef), then the VarSet sets the value of that global variable, even if used inside a routine.
- If the variable is not a global variable and the VarSet is used inside a routine definition, then the it defines and sets that variable name, but only in that routine. This allows you to have variable in different routines that have the same name, but that don’t interfere with one another. It’s also a big reason to start your global variables with ‘g’ (and not start your routine variables with ‘g’): following that convention means you’ll never accidentally use a global variable as if it were a routine variable.
[Var, variableName] is used to get the value of a variable. For example,
[VarSet, x, [Var, y] + 5]
will set x to a value 5 greater than y. Note: Normally, you don’t need to use Var explicitly; the previous example could have been written as
[VarSet, x, y + 5]
. However, there are times when it’s ambiguous as to whether your name refers to a variable or something else. In such cases, use Var, or its shorthand form,
(which is the equivalent of
. If you find strange things happening in your script that shouldn’t be happening, you might want to try using # on your variables.
One important thing to note about variables is that they don’t necessarily persist through an entire ZBrush session. Several things can cause the current script to be ‘flushed’; loading another script, certain manipulations of the window, and so on. When a script is flushed, all of its variables are destroyed, and are then recreated when the script is next loaded. (Buttons the script creates in the ZBrush palettes aren’t flushed, so they can be used to reload a script.)
This means that you can’t store a value in a global variable and expect it to stay around as the user works in ZBrush. Instead, you’ll used memory blocks to store data persistently. These are discussed in a later section.
ZScript provides for arrays of strings or numbers. The declarations are simple:
[VarDef, gValues(9), 3]
creates an array ‘gValues’ of 9 numbers, each initialized to 3.
[VarDef, gWords(5), "Hello"]
creates a three element array of strings, each initialized to “Hello”. Array element are accessed starting from 0; myArray(0) is the first element of myArray, myArray(1) is the second element, and so forth. (This is standard use for arrays in most programming languages.)
Loops and Conditionals
ZScript has two built in control flow constructs, Loop and and an If…Then…Else… statement (that doesn’t use the Then or Else keywords). A loop looks like this:
[Loop, numberOfTimes, command1 command2 . . . lastCommand , optionalVariable]
Note that the commands don’t have commas between them; they’re a block. The optional variable can be used if you need to know the number of times the loop has executed, in the code in the loop. It has to be separated from the loop block by a comma. I like to put the comma at the beginning of the line containing the variable, to make it easy to identify the variable. If you don’t put in a loop variable, then don’t put in the comma.
The If…Then…Else… statement looks like this:
// Find the absolute value of a number, and make a note in the notebar as to whether the // number was positive or negative [If, x < 0 , // Then... [VarSet, x, -x] [NoteBar, "x was negative."] , // Else... [NoteBar, "x was positive."] ]
There is a comma between the If part and the Then part, and another comma between the Then part and the Else part. The commands in the Then and Else sections form blocks, so there are no commas between them.
Note that the // Then and // Else text in the example are just comments; they don’t have to be there. However, I think that including them makes the code clearer. I also like putting the commas at the start of the first line of the Then and the Else blocks, as them makes it easier to distinguish between the If, Then, and Else sections. But you can use whatever format you like.
You can define custom routines in zscripts, so that code you use over and over needs to be written only once. The form for a routine definition is
[RoutineDef, routineName , command1 command2 . . . lastCommand , arg1, arg3, . . . ,lastArg ]
The arguments at the end of the definition can be used to pass values into the routine definition. Note that there is a comma before each argument, but there are no commas between the commands; they form a block.
Note: If your routine doesn’t have any arguments, then don’t put any commas before the last bracket.
Here’s a small script that defines a routine that calculates the factorial of its argument (storing it in a global variable), and then calculates and displays the factorial of 5.
[RoutineDef, factorial , [VarSet, output, 1] [Loop, input , [VarSet, output, output * i] , i ] [VarSet, gResult, output] , input ] [VarDef, gResult, 0] [RoutineCall, factorial, 5] [NoteBar, gResult]
An important thing about custom routines is they can’t return values directly. Instead, you have to return values by assigning them to variables passed in. Here is a more flexible way to calculate factorials, since it doesn’t require the factorial routine to assign to a predetermined global variable.
[RoutineDef, factorial , [VarSet, output, 1] [Loop, input , [VarSet, output, output * i] , i ] [VarSet, result, output] // The calculated output is passed back via the 'result' variable. , input, result ] [VarDef, gResult, 0] [RoutineCall, factorial, 5, gResult] [NoteBar, gResult]
A memory block is simply a contiguous area of bytes in ZBrush’s memory that has been allocated and named using a ZScript command. Memory blocks are persistent through a ZBrush session, so you’ll use them if you write scripts that need to ‘remember’ settings over a long period of time. Memory blocks can also be used to share values between ZScripts. Finally, memory blocks are used to save data to files, and then to load that data at a later time.
There are three commands that can be used to create memory blocks.
- [MemCreate, blockName, blockSize, optionalFill]
- Creates a memory block with name blockName, blockSize bytes in size, and initially filled with the value given by optionalFill. For example, [MemCreate, “myBlock”, 128, 0] creates a block named “myBlock”, 128 bytes in size, with each byte set to 0. I suggest you always give a fill value—it can avoid some potentially hard-to-find bugs in your scripts.
- [MemCreateFromFile, blockName, fileName, optionalOffset]
- Reads in a file and creates a memory block from it. The optionalOffset can be used to skip that number of bytes from the beginning of the file, but is normally not given, in which case it defaults to 0.
[MemCreateFromFile, "myBlock", "datafile.zdt"]creates a memory block from the file “datafile.zdt”, which is presumably a file you’ve previously created. Note that the suffix of the file is always given. You can also use a variable instead of a fileName.
- [MVarDef, blockName, numberOfNumbers, optionalFill]
- A convenient way of creating memory blocks that will hold up to numberOfNumbers numbers. A number occupies four bytes, so
[MVarDef, "myNumbers", 7, 0]does exactly the same thing as
[MemCreate, "myNumbers", 28, 0].
For more information on these commands, see the ZScript Command Reference.
To output the contents of a memory block to a note interface for review using the following code:
[NoteIButton,MemBlock:(name of memory block),,0,1,30,100,420,315,,0xc0c0c0,0xf0f0f0,1,1]
Converting Memory Blocks To Variables
If we start with a Memory block called MemBlock, simply use the following code to convert it to a variable where 1 is the offset of the MemBlock you want to read:
To convert a MemBlock that is a string into a Variable you must “read” it into a variable. Use the MemReadString command.
These are the string handling routines callable from script. The maximum string size is 255 characters.
- [StrAsk, Optional initial string, Optional title]
- Asks the user to input a string. Returns the text typed by user or an empty string if canceled.
Example: [StrAsk, “preview_file.prv”, “Preview Save File”]
- [StrExtract, Input string, Start character index (0=left), End character index (0=left)]
- Returns specified portion of the input string.
- [StrFind, find this string, in this string, Optional start search index (default=0)]
- Locate a string within a string, returning the starting index of the 1st string within the 2nd string, or returns -1 if nothing was found.
- [StrFromAsc, Input Ascii value]
- Returns the character for the specified numeric Ascii value.
- [StrLength, inputString]
- Returns the number of characters in the input string.
- [StrLower, inputString]
- Returns the lowercase version of the input string.
- [StrMerge, Str1, Str2, Opt3, Opt4, Opt5, Opt6, Opt7, Opt8, Opt9, Opt10, Opt11, Opt12]
- Combines two (or more) strings or numbers into one string. Note: the result string will not exceed 255 characters in length.
Example: [StrMerge, “Polygon Count: “, polyCount, ” Edge Count: “, edgeCount]
Working With Files
To extract the system path of the last saved item use the following code:
//save the file [ipress,Tool:Save As] //define the variable [vardef,path,"Empty"] //set the path variable with the location of the tool just saved [varset,path,[FileNameExtract, [FileNameGetLastUsed], 7]] //the note will print it to screen. Slashes do not display in notes but don't worry. //They're stored in the variable. [note,[var,path]] //This section will save the path to disk by creating a memory block, // writing info into it and then saving a text file from it. [memcreate,MemBlockPath,200,32] [MemWriteString, MemBlockPath, [var,path]] [MemSaveToFile, MemBlockPath, 'location to save information']
Get Basic Information
How do I get the current alpha, tool etc when I am in a script?
|Current Alpha Number||[IGet, Alpha:ItemInfo]|
|Current Alpha’s Name||[IGetTitle, Alpha: Current Alpha]|
|Current Material Number||[IGet, Material:ItemInfo]|
|Current Material’s Name||[IGetTitle, Material:Current Material]|
|Current Texture Number||[IGet, Texture:ItemInfo]|
|Current Texture’s Name||[IGetTitle, Texture:Current Texture]|
|Texture Height||[IGet, Texture:Height]|
|Texture Width||[IGet, Texture:Width]|
|Current Tool Number||[IGet, Tool:ItemInfo]|
|Current Tool’s Name||[IGetTitle, Tool:ItemInfo]|
|Current Tool’s Full Path||[IGetTitle, Tool:Current Tool]|
|ZBrush Version||[ZBrushInfo, 0]|
Creating Controls and User Interfaces
There are two ways you can write user interfaces with ZScript. You can create new controls in the ZBrush palettes, or you can create a temporary dialog containing its own controls; the user will interact with the dialog, and then dismiss it after doing what they wanted. We’ll start by talking about adding control items to the ZBrush palettes.
Adding Controls to Palettes
Controls can be added to ZBrush’s palettes with various ZScript commands. However, you can’t put new controls directly into a palette; you’ll have to create your own subpalette first, and then put controls into that subpalette. The best way of showing this is with an example:
// Create a new subpalette called "MatPack", in the "Material" palette. [ISubPalette,"Material:MatPack"] // Put a new button called "MaterialPack2" in the new subpalette [IButton ,"Material:MatPack:MaterialPack2" // Path to and name of the button. ,"Press to open Material Pack 2" // Popup help. , // CODE TO EXECUTE WHEN BUTTON PRESSED. [RoutineCall,CreatePreview] ]
Notice that the
IButton command includes code that will be executed whenever the button is pressed. Details of these and other control-related commands are included in the ZScript Command Reference.
If you need a more complex user interface, you can create your own dialogs.
Dialogs can control text, buttons, and pictures. In addition, they can give feedback as the mouse moves around in the dialog, even if the user is not pressing any buttons. For example, in the picture above, the mouse cursor (though it doesn’t show up in the screenshot) is on top of the checkerboard style material in the bottom row of thumbnails. As a result, the edges of that thumbnail are highlighted in white.
After your script brings up a dialog, the user can press on buttons in the dialog. Your script will handle these button presses (redrawing the dialog each time) and, at some point (for example, when the user presses the Exit button) will quit the script, automatically removing the dialog. This whole process is normally called the event loop, and looks like this:
- Draw the dialog.
- Wait for a mouse click on a button.
- When the mouse click happens, perform the action associated with the clicked button.
- If the script didn’t exit, then go back to step 1.
So all that you’re really doing is just, “Draw, wait, handle button click, do it all again until finished”. Depending on how much your buttons do, the loop to handle this can get pretty large, but this simple process is always what it’s doing underneath.
Every control in a dialog is a button, created using the
NoteIButton command. Even the dialog window is created with
NoteIButton; it just happens to be a big button that has no text or image, and is always disabled.
Buttons are numbered in the order in which they are drawn. So, the main dialog window (which is drawn first) will be button 1; the next button drawn will be button 2; and so on. This numbering is restarted every time the dialog is drawn, always beginning with 1. If controls overlap on the screen, then the one drawn last will be ‘on top’ of previous ones, which is why the main window is always ‘behind’ the other dialog controls.
A Simple Custom Dialog
The example below, when loaded and invoked in ZBrush (by pressing the “Zplugin:Countit:Countit” button) shows this small dialog:
Pressing on the numbered button advances the number by one, and pressing Exit returns you to ZBrush.
[VarDef, count, 0] [RoutineDef, CreatePreview, [IShowActions,0] [IFreeze, //'IFreeze' prevents the user from seeing any of the following actions // until they are done. //=============THE MAIN EVENT LOOP=========== // This loop continually executes while the plugin is running. It does two main // things: // 1) It draws the interface on the screen. // 2) Then, when the user clicks a button, it processes that click. // These two things are done over and over again until the user presses // the 'Exit' button. [Loop, 100000, // Loop 'forever' // --------- USER INTERFACE CREATION SECTION -------- // This section creates the MaterialPack interface. // It is executed when MaterialPack starts up // and after EVERY mouse click the user makes, until the plugin exits. // Create the main (background) window of the interface. It // doesn't do anything--it's just for looks. Since this is the first control // created, it will be the 'furthest back'--all of the other controls will // appear drawn on top of it. // // IMPORTANT: Controls are identified by the order in which they were created, // counting from 1. So this will be control 1. [NoteIButton , //label , //icon , //pressed ,1 //disabled ,1 //hoffset ,1 //voffset , // width , //height , //button color , //text color , 0 //button opacity , //text opacity , //icon opacity ] // CONTROL 2: The button showing the current count. [NoteIButton , count // text ,,,,,,, // unused arguments, ZBrush will use default values , 0x000000 // button color , ,1,1,1 // opacities ] // CONTROL 3: EXIT Button [NoteIButton, "Exit" ,,,,,,,, 0x000000,, 1,1,1] // CONTROL 4: The Help button. This is an inactive button; // it just displays a line of help. [NoteIButton, "Click on the numbered button to increment it." // label ,, 0 // pressed status , 1 // disabled status ,,,,, ,0x101010 // color ,0,1,1 // opacities ] // --------- EVENT HANDLING SECTION -------- // Get index of the button that was clicked. [VarSet, userSelect, [Note,""]] // --- Space or Return key --- // IF the user pressed the space or return key, exit. [If, (userSelect==2), [VarSet, count, count+1] ] // --- 'Exit' Button --- // IF the user clicked on the 'Exit' button, THEN exit the plugin. [If,(userSelect==3), [Exit]] ] // END LOOP; the main event loop ] // END IFreeze ] // END ROUTINE CreatePreview // CREATE a subpalette, and a button in that subpalette. [ISubPalette,"Zplugin:Countit",2] [IButton ,"Zplugin:Countit:Countit","Click here to bring up the Countit dialog" , // CODE TO EXECUTE WHEN BUTTON PRESSED. [IShowActions,0] [RoutineCall,CreatePreview] ,,152,,,.125 ]