The Array Looping Cook Book

By - - 3ds Max

Note: The interface in this tutorial applies to MCG 2016/2017. The interface in MCG 2018 has been revised to a new node naming scheme.

If you’re coming from a scripting or a programming background, you may have spent some time in the MCG operator list searching for the “for loop”. While there is no specific operator called “for loop” in MCG by default, there is a wide variety of operators which behave like specialized versions of the for loop. In this post, we’ll be taking a tour of those operators, and we’ll be looking at common patterns you can apply to create and iterate on arrays.

In the screenshots below, the ParseIntArray, PrintWithLabel, PrintArrayWithLabel, ArrayOfPseudoRandomFloats, and PartialSum compounds were created during the development of this tutorial, so you won’t find them in your MCG operator list by default. To obtain these compounds, install the MCGArrayTests.mcg tool at the bottom of this post.

Note: The MaxScript code in the looping examples below only illustrates the behavior of the operators, and is not a direct translation of the implementation of those MCG operators. In fact, MCG graphs compile down to .NET bytecode, which in most cases runs faster than MaxScript.


Count

Let’s start with the simplest one of them all: Count. The Count operator returns the number of items in the array.

-- Count
A = #(1, 6, 12, 33, 54)
A.count -- => 5


ArrayOf

The ArrayOf operator creates an array of size “n” which contains repeated copies of the supplied value. In the example below, we’re using the ArrayOf operator to create an array containing five copies of the vector [0,0,1].

-- ArrayOf
function ArrayOf x n = (
result = #()
for i = 0 to (n-1) do (
append result x
)
return result
)

ArrayOf [0,0,1] 5 -- => #([0,0,1], [0,0,1], [0,0,1], [0,0,1], [0,0,1])


ArrayOfFunction - Creating an Array of Random Values

If you want to generate an array of random values, use the graph construction below, which makes use of the ArrayOfFunction operator. This operator creates an array by repeating the given function “n” times. The “Bind” operator in this graph forces the PseudoRandomFloat operator to use the same random number generator, instead of recreating a new generator on every loop. We explain the “Bind” operator in more detail in The Low Poly Modifier - Part 2.

-- ArrayOfFunction
function ArrayOfFunction f n = (
result = #()
for i = 0 to (n-1) do (
tmp = f()
append result tmp
)
return result
)

-- Set the random seed value and create a function which generates a new random
-- value on every call.
seed 1234.0
function myRandomFunction = (
return random 0.0 1.0 -- A random floating point number between 0.0 and 1.0
)

ArrayOfFunction (myRandomFunction) 5 -- => #(0.177433, 0.954723, 0.396885, 0.398289, 0.242126)

We condensed this construction into the ArrayOfPseudoRandomFloats compound, which you can use in your own graphs by installing the MCGArrayTests.mcg file linked at the end of this post.


Unit, Array2, Array3, Array4, and Concatenate

To create an array containing a set of hard-coded values (without the ParseIntArray compound we’ve been using so far), you can use Unit, Array2, Array3, and Array4. Use the Concatenate operator on two arrays to join them into a single array.


Range

Given a number “n”, the Range operator will produce an array of integers between 0 and (n-1). In many cases, the Range operator can act as a starting point in your graph to guide the overall behavior of your tool such as the number of iterations, the number of objects to scatter, etc. For example, we used the Range operator to drive the number of stairs in our Staircase building tool.

-- Range
function Range n = (
result = #()
for i = 0 to (n-1) do (
append result i
)
return result
)

Range 5 -- => #(0, 1, 2, 3, 4)


RangeInclusiveFloat / RangeExclusiveFloat

Similar to the Range operator, the RangeInclusiveFloat and RangeExclusiveFloat operators produce an array of “n” decimal values between 0.0 and 1.0. The “Inclusive” and “Exclusive” terms determine whether or not the value of 1.0 is included in the computed range of values.

When you connect them to a Map, these operators can be used to generate an array of proportional samples. For example, in the graph below, we’re using the RangeExclusiveFloat operator to sample four equidistant angles around a circle. Note that in MCG, trigonometric operators such as Sin and Cos require angular values in radians. In the graph below, we’ve chosen to convert these values into degrees to increase the readability of the resulting array.

-- RangeInclusiveFloat

function RangeInclusiveFloat n = (
step = 1.0 / (n-1)
result = #()
for i = 0 to (n-1) do (
append result (i*step)
)
return result
)

-- RangeExclusiveFloat
function RangeExclusiveFloat n = (
step = 1.0 / (n)
result = #()
for i = 0 to (n-1) do (
append result (i*step)
)
return result
)

RangeInclusiveFloat 5 -- => #(0, 0.25, 0.5, 0.75, 1)
RangeExclusiveFloat 5 -- => #(0, 0.2, 0.4, 0.6, 0.8)


Map

Map is one of the most fundamental operators in MCG. We covered it in our very first blog post to explain the function connector. The Map operator closely resembles the “for each” construct common to other programming languages. It iterates over all the items in an array, and it returns a new array of transformed values based on the function you provide.

-- Map
function Map xs fxn = (
result = #()
for item in xs do (
tmp = fxn item
append result tmp
)
return result
)

-- Initialize an array of integers.
A = #(0, 1, 2, 3, 4)

-- Declare the function to apply on each iteration
function myFunc x = (
return x + x
)

Map A myFunc -- => #(0, 2, 4, 6, 8)


Indices

The Indices operator creates an array containing all the valid indices of a given array. In contrast to MaxScript, array indexing in MCG is 0-based instead of 1-based. This means that the first item in an MCG array is indexed at 0 instead of at 1.

Note that we can achieve the same result as the graph above by connecting a Count operator to a Range operator.

-- Indices

function Indices A = (
result = #()
for i=0 to (A.count-1) do (
append result i
)
return result
)

-- Initialize an array of 8 values
A = #(5, 2, 6, 20, 4, 9, 16, 100)

Indices A -- => #(0, 1, 2, 3, 4, 5, 6, 7)


Filter

The Filter operator iterates over an array, and only keeps the items which fulfill the given condition function. For example, you can use the Filter operator to remove values below a certain threshold, or to keep a set of faces whose normals point up in +Z.

-- Filter
-- An underscore is added to "_Filter" because "filter" is a keyword in MaxScript.
function _Filter A fxn = (
result = #()
for item in A do (
cond = fxn item
if cond == true then
append result item
)
return result
)

-- Initialize an array of values.
A = #(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

-- Declare a filtering function which returns true if the input is even.
function filterFn x = (
return mod x 2 == 0
)

_Filter A filterFn -- => #(0, 2, 4, 6, 8)


At

The At operator returns the value at the specified index in the array. It behaves in the same way as the subscript operator “[ ]” in myArray[i]. Recall that array indexing in MCG is 0-based while in MaxScript, it is 1-based.

-- At
-- An underscore is added to "_At" because "at" is a keyword in MaxScript.
function _At A index = (
-- (!) MaxScript array indexing is 1-based so we’ll be adding 1 to the given index.
result = A[index+1]
return result
)

-- Initialize an array of values, and the index of the value we want to retrieve.
A = #(5, 2, 6, 20, 117, 9, 16, 100)
index = 4 -- 0-based index in A

_At A index -- => 117


Combine

The Combine operator acts like the Map operator, but requires two arrays instead of one. Each pair of items between these arrays is applied to the given function to produce a new array of values.

Keep in mind that the function you define for the Combine operator must expose two arguments (i.e. two unconnected connectors). Furthermore, the order of these arguments must match the order in which the arrays are connected into the Combine node. Specifically, the type of the first array “xs” must match the type of the first argument in the function. In the same way, the type of the second array “ys” must match the type of the second argument in the function.

We cover how to distinguish the order of function arguments in The Low Poly Modifier - Part 2, but if you want a quick summary, the idea is to traverse the function backwards in a “Depth-first Top-to-Bottom” manner. Begin your traversal at the end of the function, and continuously visit the incoming nodes prioritizing depth first (i.e. going backwards), then visiting the slots top-to-bottom. The order in which you encounter the unconnected connectors corresponds to the argument order of the function. During your traversal, ignore the arguments you have already discovered.

As a counterexample to the graph above, if we swapped the “xs” and “ys” inputs of the Combine node, the graph would not compile successfully because the type of the first array (IArray) would not match the type of the first argument (Int32). Likewise, the type of the second array (IArray) would not match the type of the second argument (Single).

-- Combine
function Combine A B fxn = (
result = #()
for i=0 to (A.count-1) do (
itemA = A[i+1] -- Adding 1 because MaxScript array indexing is 1-based.
itemB = B[i+1] -- Adding 1 because MaxScript array indexing is 1-based.
tmp = fxn itemA itemB
append result tmp
)
return result
)

-- Declare the function to apply on each iteration of Combine.
function myFunction x y = (
return x * y
)

-- Initialize an array of integer values and an array of random values
A = #(139, 462, 521, 206, 877, 99)
B = ArrayOfFunction myRandomFunction A.count

Combine A B myFunction -- => #(17.9539, 349.924, 510.956, 143.505, 314.525, 43.2008)


Creating an Indexed For Loop - Indices, Map, At

The Combine operator can be a bit tricky to work with, especially if you’re not familiar with functions containing two arguments. As an alternative, you can emulate an index-based for loop to access items in your arrays. To do this, use the Indices, Map and At operators as follows:


ZipToTuple

Another alternative to the Combine operator is to use ZipToTuple with a Map. The idea is to first “Zip” the items contained in the two arrays into an array of pairs (IArray). You can then use a Map to transform each Tuple2. Use the PairItem1 and PairItem2 operators to obtain the contents of the Tuple2, and continue your work from there.

Ultimately, the choice between Combine, ZipToTuple, or indexed for loops depends on your personal preference, with the caveat that in some corner cases, an indexed for loop might cause unexpected subgraph re-evaluations, which can be slower than the other alternatives.


GenerateN

The GenerateN operator creates an array of a fixed size by repeating the same function on the current value to generate the next value in the array. In the Horns and Transforms tutorial, we showed how to use the GenerateN operator to construct an evolving array of transformation matrices to define the shape of the horn.

-- GenerateN
function GenerateN first count nextFn = (
result = #()
current = first

for i=0 to (count-1) do (
-- Append the current value to the result.
append result current

-- Compute the next result based on the current value.
if (i > 0) then (
current = nextFn current
)
)
return result
)

-- Declare the function which GenerateN will use to create the next value in the array.
function myFunction x = (
return x + 10
)
-- The initial value of GenerateN will be 0
init = 0
-- Repeat GenerateN 5 times.
n = 5

GenerateN init n myFunction -- => #(0, 10, 20, 30, 40)


Generate

The Generate operator is very similar to GenerateN, with the exception that it stops building the array when the supplied condition function returns False. In the example below, the Generate operator will continue building the array as long as the current value is less than 50.

-- Generate
function Generate first conditionFn nextFn = (
result = #()
current = first

while true do (
-- Stop looping if the condition function returns false.
cond = conditionFn current
if cond == false then exit

-- Append the current value to the result.
append result current

-- Compute the next result based on the current value.
current = nextFn current
)

return result
)

-- Declare the condition function which Generate will use to continue iterating.
function myConditionFunction x = (
return x
)

-- Declare the function which Generate will use to create the next value in the array.
function myFunction x = (
return x + 10
)

-- The initial value of Generate will be 0
init = 0

Generate init myConditionFunction myFunction -- => #(0, 10, 20, 30, 40)


Aggregate

The Aggregate operator iterates over each item in the array and accumulates (or “aggregates”) a value by applying the given function. The function you connect to the Aggregate must expose two arguments. The first argument is the currently “accumulated” value, and the second argument is the current value in the array.

For example, if you open the Sum compound, you’ll notice it was implemented with an Aggregate.

-- Aggregate
function Aggregate xs init fxn = (
accumulated = init
for current in xs do (
accumulated = fxn accumulated current
)
return accumulated
)

-- Declare the function to apply on each iteration of the Aggregate.
function myFunction acc current = (
return acc + current
)

-- Initialize an array of integers.
A = #(139, 462, 521, 206, 877, 99)

Aggregate A 0 myFunction -- => 2304


Partial Sum with Aggregate

The Aggregate operator can also be used to implement a Partial Sum array, whereby each item in the Partial Sum array corresponds to the sum of the items up to that point in the original array. This can be useful to keep track of the current distance at each point along a path. In this case, the accumulated value is actually a growing array, to which we append the latest computed value.

-- PartialSum using Aggregate
-- Declare the function to apply on each iteration of the Aggregate.
function partialSumFn acc current = (
local lastItem
if acc.count > 0 then
lastItem = acc[acc.count]
else
lastItem = 0

tmp = (lastItem + current)
append acc tmp
return acc
)

-- Initialize an array of integers.
A = #(139, 462, 521, 206, 877, 99)

Aggregate A #() partialSumFn -- => [139, 601, 1122, 1328, 2205, 2304]


Flatten

The Flatten operator joins all the subarrays contained inside an array.

It is particularly useful when you want to “flatten” the result of nested iterations. For example, we required the Flatten operator in the Horns and Transforms tutorial to flatten the nested arrays of points into one continuous array of points for the QuadMeshStrip operator.

-- Flatten
function Flatten xss = (
result = #()
for subarray in xss do (
join result subarray
)
return result
)

-- Initialize an array of subarrays.
A = #( #(139, 139), #(462, 462), #(521, 521) )

Flatten A -- => #(139, 139, 462, 462, 521, 521)


Repeat

At first glance, the Repeat operator may seem like the closest implementation of a for loop. However, once you get familiar with MCG, you’ll realize that its usage is almost always eclipsed by one of the more efficient operators we listed above. To illustrate this point, we translated the following MaxScript code containing a standard for loop using the Repeat operator, and alternatively using the Map operator.

A = #(139, 462, 521, 206, 877, 99)
result = #()
for i=0 to (A.count-1) do (
x = A[i+1] -- add 1 because MaxScript array indexing is 1-based.
tmp = x + x
append result tmp
)
result -- => #(278, 924, 1042, 412, 1754, 198)

Using Repeat:

Using Map:

Here’s a more formal MaxScript translation of the Repeat operator.

-- Repeat

function Repeat init n bodyFn = (
current = init
for i=0 to (n-1) do (
current = bodyFn current i
)
return current
)

-- Define an array of values
A = #(139, 462, 521, 206, 877, 99)

-- Declare the function to repeat on each iteration.
function myFunction currentArray index = (
x = A[index+1] -- add 1 because MaxScript array indexing is 1-based
tmp = x + x
append currentArray tmp
return currentArray
)

Repeat #() A.count myFunction -- => #(278, 924, 1042, 412, 1754, 198)


While

The While operator acts like the Generate operator, however it does not necessarily generate an array, and acts much like a general-purpose while loop. In the graph below, we’re using a while operator to generate a limited Fibonacci sequence.

-- Fibonacci sequence using While
-- An underscore is added to "_While" because "while" is a keyword in MaxScript.
function _While init conditionFn bodyFn = (
current = init
while true do (
cond = conditionFn current
if cond == false then exit

current = bodyFn current
)
return current
)

-- Declare the condition function which will determine if the while operator should continue iterating.
function myConditionFunction x = (
lastItem = x[x.count]
return lastItem
)

-- Declare the body function which will be invoked on each iteration.
function myFunction x = (
-- Add the last and second to last items together to produce the next Fibonacci number.
f_n1 = x[x.count]
f_n2 = x[x.count-1]
tmp = f_n1 + f_n2
append x tmp
return x
)

-- Initialize an array containing the initial values of the Fibonacci sequence 0 and 1.
A = #(0,1)

_While A myConditionFunction myFunction -- => #(0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233)


Download: MCGArrayTests.zip

Instructions: Extract the file anywhere on your filesystem, then go to Scripting > Install Max Creation Graph (.mcg) Package, and select MCGArrayTests.mcg in the extracted location. Once the package is successfully installed, open the MaxScript listener (F11) and type MCGArrayTests() (note the “s” at the end of Tests). This will run the MCG array test cases we built for this tutorial, and will print the results to the Listener. Feel free to open the MCGArrayTests.maxtool file under C:\\Users\\\\Autodesk\\3ds Max 2016\\Max Creation Graph\\Tools\\Downloads to explore each construction in more detail.

Posted By
Published In
Tags
  • 3ds Max
  • Film & VFX
  • Games
2 Comments
To post a comment please login or register
| 2 years ago
I'm so sorry I deleted my orig post...nothing against you..I just didn't wanna be ridiculed, is all.
Edited by BIrMFVbu 2 years ago
| 2 years ago
Good question Vusta! You're right, it actually skips the first iteration. It simply appends the initial value to the result and does not compute the next value unnecessarily. I'll amend the code snippet to reflect that. Good eye!
Edited by UzYnWso1 2 years ago