Menu Close

Category: Mini Tutorial

OSL mini tutorial #3 – utilizing scene data and some Math

Welcome to my 3rd OSL tutorial! In this tutorial, we well learn how to query scene/object data and utilize it with MATH! Yes, you heard it right. MATH! I know you always have been regretted that you didn’t pay attention to the math class when you were in middle school. But, never late than never. Re-learning some simple math will make your life easier. WE CAN DO IT TOGETHER!

Like always, we will not write a single line of code in this tutorial. We will use SlateME as our OSL editor. Let me say it again, YOU DON’T NEED TO KNOW HOW TO CODE TO USE OSL IN 3dsMax.

I’ll use MeetMat2 for this tutorial. You can download from Substance website.

First, let’s see how we can query the position of pixel in the scene and utilize. For example, we can make a transition of 2 maps between certain heights from the ground.

You can use “Named Coord Space” map to get a position of a coordinate space.Let’s make one and s some basic setup for the tutorial.

  • Make “Named Coord Space” map and a Standard material.
  • Make Self-Illumination 100.
  • Connect UVW of “Named Coord Space” to the Diffuse Color of Standard material.
  • Make sure to turn on Show Realistic Material in Viewport.
  • Select “High Quality” mode in the viewport.
  • Apply the Standard material to Mat.

3dsMax OSL has an amazing OSL > HLSL auto conversion features as I posted before. You can see OSL map exactly same as render in viewport for most cases. All OSL map has a indicator at the bottom to show if this map could be displayed in viewport. To utilize this feature, your material must set to Show Realistic Material in Viewport, and your viewport must set to Advanced Rendering mode which High Quality preset has.

Your viewport should like this if you follow me correctly. What you are seeing is the World coordinate position as color. If you know what is World coordinate and Object coordinate, you can jump to the next section.

World position is the position from the world origin. Since we plug X, Y, Z, into R, G, B. You can see more Red color along X. Green along Y, Blue along Z. Color can only display from 0-1, that’s why you can only see a little gradient around an axis. Mat’s size is 8.2×1.0x9.4. If value is less than 0, it will be all black. If you rotate the model, you can see the color is not moving with object. Because the coordinate is fixed in world.

Another coordinate you might use is “Object” which is based on each object’s local coordinate. The origin will be at object’s pivot point. The axis will use object’s local axis. This means when the object is moving or rotating, the value will move with objects. If you need to make a map that is stick to the object, this is coordinate you need to use.

Now you know what World/Object position is and how to get the value with “Named Coord Space” map. Let’s utilize the value we got. We will try to blend 2 check map along the height(Z-axis)

  • Make a Maps > OSL > Math Vector > Component (Vector ).
  • Connect UVW of Named Coord Space to Input of Component (Vector)
  • Make a Maps > OSL > Math Float> Range/Remapper.
  • Connect Z of Named Coord Space to Input Value of Range/Remapper
  • Connect Out of Range/Remapper to Diffuse Color of the Standard material.

This should be what it looks like. BTW, I turned off AO. What’s happening here. We took only Z axis value with Component (Vector) map. This map is you can separate each channel from a vector or assemble a vector from 3 floats. Then, we fed the Z value to Range/Remapper which doesn’t do anything with default values. You can see the gradient goes from 0 to height 1. Again, as a color we can only visualize 0-1. I put 1 unit height box as reference.

Now we need to manipulate this value so the value can go from 0.0 – 1.0 between height 1.5 – 3.5. That’s what Range/Remapper  does. Click the map and set Input Range Start to 1.5, Input Range End to 3.5. Now this map takes World Z position as Input Value. You can see “M” button shows that the value is coming from the connection, Then, map the input value 1.5 – 3.5 as 0.0 – 1.0 as output.  You can visually see the gradient is moved up and 2x wider.

We can utilize this value as the Mix value for Mix map.Mix map is a map that Mix 2 color. Surprise! I could use Composite Map, too. But, this map is simpler. Also this is a tutorial. You gotta something new.

  • Make a Maps > OSL > Math Color > Mix(Color) map.
  • Make 2 OSL checker map with different colors and Size 0.05.
  • Connect each Checker map as A and B of Mix(Color) map.
  • Connect Out of Range/Remapper to Mix of Mix(Color) map.

Now let’s make it a little bit more complicated. What if I want to have blue check only top of Mat’s head like snow on his head. We can utilize normal for that.

You can get the normal data with Normal map. Duh. It is under Scene Attribute. It has one option, Coordspace. It should be “World”. Normal is “normal is an object such as a line, ray, or vector that is perpendicular to a given object.” according to Wiki. You can thin think as an arrow that coming out of a face. OK, that’s cool. But, so what? How can it help me?

Usually you need two sidekicks to utilize the Normal data, “Dot product” and another vector. Wha… WTH is “Dot poduct”? My head is already hurting!!! If you really want to know what it is. You can suffer from reading this. Butm I have a good news for ya. You don’t actually need to know what it is. We just need to know how to use this.

  • Make a Maps > OSL > Math Vector > Dot product (vector).
  • Make a Maps > OSL > Values > Vector Value.
    Put 1.0 as Z value. Make sure X, Y are 0.0
  • Make a Maps > OSL > Scene Attribute > Normal.
  • Connect Out of Normal to A of Dot product (vector).
  • Connect Out of Vector Value to B of Dot product (vector).
  • Connect Out of  Dot product (vector) to the Diffuse Color of Standard material.

What did we just do? It looks like face becomes whiter if it face more to the top. When you dot product 3 vectors, Normal and [0, 0, 1] for us. The more 2 vectors look the same direction, The result becomes closer to 1.0. If two vectors are aligned exactly and toward same( direction. the dot product becomes 1.0. If two vectors are at right angle(90 degree), the dot product becomes 0.0. If two vectors are looking at the exact opposite direction. the dot product becomes -1.0. That’s all you need to know. This is how Falloff map works under the hood.

just for the test’s sake, change the vector Z value to -1.0. As you expected, it gets whiter as the point more face down.

How about X = 1.0 and Y, X =0.0?

Got it? Then, Let’s set back to [0, 0, 1].

Now we need some house cleaning. When you dealing with normals and dot product. It is always a good idea to normalize the incoming vectors like this. this makes the incoming vector as a unit vector. If you don’t want to know what/why. just memorize and do it. It is good for you. Normal map is at Maps > OSL > Math Vector > Normalize (vector).

Another item for house cleaning is Clamp. As I mentioned above, dot product generates value from -1.0 to 1.0. You can not see the negative value in render or viewport since both only shows between 0.0 – 1.0. But, if you use negative value for other operation, it could cause issues, therefore. it is always a good idea to cut negative values with Clamp map. Clamp map limits any value outside of Min and Max value as Min and Max value. The default is 0.0 and 1.0. So, any value less than 0.0 will become 0.0. Any value bigger than 1.0 will become 1.0. The map is in Maps > OSL > Math Float > Clamp.

OK. Now we have 2 map trees. One for blending by height. Another one for the direction. We want to combine both so we can have blue check only at the top of Mat’s head. For this kinds of case, we can simply multiply two masks.

  • Select Range/Remapper. Set Input Range Start to 3.0, Input Range End to 4.0.
    This should move mas above Mat’s head.
  • Make a Maps > OSL > Math Float > Multiply map
  • Connect Out of Range/Remapper to A of Multiply.
  • Connect Out of Clamp to B of Multiply.

I know… after all those node, what you got is not that cool. But, this is how you learn.

In this tutorial…

  • we learned how to get position and normal information from the scene
  • how to utilize normal with dot product
  • many of frequently used important math maps such as Range/Remapper, Clamp, Normalize, Multiply. Component.

BUT! Yes, there is always BUT!

The portion that we used to make a mask by face normal exist as one map, Falloff map. This map is basically same as the map tree we set up with a bunch of maps. It take cares Normalize and Clamp, it also have option to map ti the different range. It also allow to define each end as color which is same as Remapping the result with Gradient.

In Falloff map, you have coordinate to choose just like Normal map. You have Face and Away color for each end. Face means the color when dot product is 1.o. Away is the color when dot product is 0 because Type is Perpendicular/Parallel. If you switch to Toward/Away. The Color will map between dot product 1.0 to -1.0.

Thanks!

Do you like my OSL mini tutorial series? Then click here and check out renderStacks, too!

OSL mini tutorial #2 Random map per tile for SimpleTiles map

Using only 1 map.

Welcome to my second OSL tutorial. Again, we will not write a single line of code in this tutorial. We will use SlateME as our OSL editor. Let me say it again, YOU DON’T NEED TO KNOW HOW TO CODE TO USE OSL IN 3dsMax.

I don’t want to spoil the ending. But, you should read til the end. 🙂

One of the advantage(or could be disadvantage for some users) of using OSL in 3dsMax is that it brings more granular and lower level of control which provide a greater flexibility. But, it also means that user need to learn and understand a new way of thinking(or workflow). Again it doesn’t mean you need to learn to code. But, you need to understand how and what kinds of data is flowing between lower level maps and how to control them. So, please try pay more attention to the explanation of “why” I’m connect port A to port B instead of memorizing map tree. 🙂

Today’s goal is randomizing textures in the tiles of Simple Tiles OSL shader so we can get infinite random tile texture from a few texture files.

    1. Open SlateME.
    2. Make a Simple Tiles map. Maps > OSL > Textures > Simple Tiles.
      This is an equivalent of Tiles legacy map. You can make a various tile or brick patterns.
    3. Double click the thumbnail so we can have a bugger thumbnail.
    4. Change Tiling Mode to Twist Box.

As you can see, OSL can output not only color information but also various data information. For this tutorial, we will mainly utilize Index data which is a integer index number for individual tiles. Let’s see that it means visually.

    1. Make 2 OSL > Math Vector > Random by Index.
    2. Connect Index port of Simple Tiles to Idx of both Random by Index map.

So, what’s happening here?

Random by Index map generates a random float number between Min and Max and drives the randomization with the Idx and Seed parameters.

Since Idx of Random by Index is provided by the Index value of Simple Tiles, all pixels in the same tile will get the same random value.You can see that well from the thumbnail, each tile has a different shade of gray.

But, you can see both map has exactly same pattern and color. That’s because both map has same Seed number by default. What is the Seed number? This is from the Wikipedia. “A random seed (or seed state, or just seed) is a number (or vector) used to initialize a pseudorandom number generator.”, which brings another important concept, pseudorandom.

In CG, we can not use true random number, if a number is truly random. That means every time when you render or even open file again. You will have a different number and different pattern! Therefore, all random number in CG is pseudorandom driven by Seed number. If Seed number is same you get the same ransom number just like the above image.

  1. Select the bottom Random by Index map
  2. Set Seed to “77”, You should get this.

OK. let’s make a bunch more maps and actually do something with these 2 random maps.

  1. Make the following maps
    OSL > Math Vector > Component (Vector)
    OSL > UVWCoordinates > UVW Transform
    OSL > BitmapLookUp
  2. Choose “C:\Program Files\Autodesk\3ds Max 2021\maps\uvwunwrap\uv_checker.png” for BitmapLookUp
    This is a new 4k UV template which is added in 2021.
  3. Connect Out of the top Random by Index > X of Component (Vector)
  4. Connect Out of the bottom Random by Index > Y of Component (Vector)
  5. Connect Out of the Component (Vector) > Offset of UVW Transform
    Do not connect anything to BitmapLookUp yet.

The 2 Random by Index we made were for the random Offset value of UVW, and it is a vector value. How do I know? If you see in UI, you can ses that it is made out of 3 values. The, it is a vector value.

So, we can not directly plug 2 float values into the Offset port. We need to assemble a vector data and plug into Offset. Component (Vector) map allow to compose or decompose a vector from/to 3 floats. Since we need to use only X and Y value. you don’t have to plug anything into Z. If no map is connected to the property, OSL map will use the value in the UI which was 0.

What we got so far? As you see in the thumbnail of UVW Transform, we randomly offset UV per tile. If you see more red, that means the pixel is offset more along U. If you see more green, that means the pixel is offset more along V. Remember Slate can only show any value range from 0-1 because it is made for color. Fortunately this case out data range is also 0-1. So, we could see what’s going on as image. But, it would be be the case all the time.

  1. Now Connect UVW of the UVW Transform > UV Coordinates of BitmapLookUp
    Tada! you can see your texture randomly offset by Tile ID.

Cool. Now how can I control the scale of texture? Yes, you change the Scale value of UVW Transform.

How can I control the size of tiles? Scale in Simple Tiles.

Let’s see what it looks like with a real texture. Remember this technique only works with seamless tileable texture. This is with the TexturesCom_RockSmooth0172_1_seamless_S.jpg from here. https://www.textures.com/. I use scale to 5.0 for UV.

How about some mid tone variation? We can use Tweak/Levels OSL map for this. We will also need another Random by Index map driven by Index.

  1. Select one of Random by Index map and SHIFT+drag to make a copy.
  2. Set Min as 0.75, Max as 1.25. Seed as 131.
  3. Add a OSL > Tweak/Levels map.
  4. Connect Out of the new Random by Index map to MidTones of Tweak/Levels.
  5. Connect Out(RGB) of BitmapLookUp map to the Input of Tweak/Levels.

OK, I hope you get the hang of how to wrangle data to drive values at this point. Next, let’s put the gaps in. There are unlimited ways to how to handle gaps. But, I’ll just go easiest way since the main purpose of this post is tutorial. The Bump output of Simple Tiles map already give you a black gaps and white tiles. I’ll use that output to composite with Multiply mode.

  1. Make OSL > Compositing > Composite map.
  2. Connect Out of the Tweak/Levels > Bottom layer RGB of Composite
  3. Connect Bump of the Simple Tiles  > Top layer RGB of Composite
  4. Set Top layer Alpha as 0.7, BlendMode as Multiply.

OK, it is getting there. Let’s add one more randomization, the rotation. By now, you should already know what to do.Yes, you need to have another Random by Index (Float) with range 0.1 – 360 and feed into Rotate of UVW Transform. BUT, since it is a tutorial, let’s make things more complicate to learn. What if we want to rotate only at right angle like 90, 180, 270 degree?

Our goal is get only one of the 0, 90, 180. 270 per tile. How we do that? Right, we can get a random value between 0 – 3 and multiply 90.0. But, Random by Index (Float) generates float number, and we don’t have Random by Index (Integer) map. Well, don’t worry. Here comes Float-to-Int map to the rescue!

  1. Copy one of Random by Index (Float).
  2. Set Min as 0.0, Max as 3.99. Seed as 666.
  3. Make OSL > Math Float > Float-to-Int map
  4. Set Mode as floor.
  5. Make OSL > Math Float > Multiply map
  6. Set B as 90.0
  7. Connect Out of the new Random by Index map to Input of Float-to-Int.
  8. Connect Out of the Float-to-Int map to A of Multiply
  9. Connect Out of the Multiply map to Rotate of UVW Transform.
    You may wonder why 3.99 instead of 3.00, and what the heck is “floor”? Floor is a way to convert a float value to integer value by returning the largest whole number (integer) that is less than or equal to the number. if you had 1.24, you would get 1.o. So, it is a floor of the range 1.0-2.0. There is also “ceil” which is kinda opposite. The ceil of 1.24 would be 2.0. By setting range as 0.0-3.99 and mode as floor, we are trying to make sure all 4 numbers are getting even chance. 0.0-1;0 > 0.0. 1.0-1.99 > 1, 2.0-2.99 > 2.0. 3.0-3.99 > 3.0.
    Left is without the rotation randomization. Right is with the rotation randomization.

As you can see, you can randomize any parameters you want. You just need to know when to stop. Should we stop now then? No, not yet. So far, we have used only one map file and looks like getting a good result. But, what if we can use multiple map files and randomly use per file?

Here is a great news. One of the new feature of 3dsMax 2021.2 is 1-of-N(Filename) map. If we don’t have this map, we have to setup a small tree with multiple BitmapLookup , 1-of-N Switcher and Random by Index map. Now we just need 2 maps.

  1. Add a OSL > Switchers > 1-of-N(Filename) map.
  2. Choose all 5 maps.
  3. Add a OSL > Switchers > Random Index by Number/Color map. BTW, such a great map, I wonder who made this? 🙂
  4. Connect Index of the Simple Tiles map to Input Number of Random Index by Number/Color.
  5. Connect Out of the Random Index by Number/Color to Filename of BitmapLookUp.

OK… This is the full tree. I guess we end up with not-so-mini-tutorial.

BUT! Yes, there is always. “BUT”.

We went through all these to learn how to work with OSL maps to build own randomization. Now I have to tell you this. Sorry, we didn’t need to go through all these if you wanted to just use it. Why? Because Zap made the awesome Bitmap Random Tiling for 3dsMax 2021.2.

This is what it looks like if you use Bitmap Random Tiling. Youjust need to plug Index into Seed. Make sure to turn off Randomize by UV position.

You can even randomize color, too.

If you want to use multiple map file? Then, re-use the 1-of-N(Filename). Another 2021.2 feature.

I guess it is worth to upgrade??? 🙂

If you are really lazy, here is the max file. It has the full setup and simple 2021.2 setup. I can not include texture file. So, you probably need to download some seamless tileable maps. Here is another good news. Because ,max file actually embed the source OSL code in the scene file, you can even open this file in 2019. You will see the new 2021.2 OSL shader there. Save the OSL map in a material library. Then, you can even use the new map in 2019 or 2020.  Another nice thing about 3dsMax OSL implementation!

Click to download .max file of this tutorial (2019 version)

Since you follow me all the way down here, you need to check renderStacks. Click here!

A teaser for the future article! What is the difference between the following 4 images?

The answer is… they all rendered in a different renderer. From the left, Corona, VRay, VRayGPU, Arnold. Yes, all different renderer. You can have exactly same map tree across different renderer even CPU/GPU. I can say this is the first time I ever see this is possible in CG history. I’ll have a blog post with more examples in the future.

Mini OSL tutorial #1 – Random map rotation per tile

Let me just borrow text from 3dsMax help. You guys should read manual all the time’ Three are many good in formation! I highlighted important aspect of 3dsMax OSL map for you!

Open shading language (OSL) is an open source shading language that is fairly simple to understand. It can be used in several different ways. You can use the OSL Map, which is an execution environment for OSL shaders inside of 3ds Max, and it works like any regular built-in 3ds Max map. There is also a category of pre-loaded OSL maps that you can easily use. In addition, you can use any OSL maps you download from the internet. Finally, you can creating a shader or map in OSL using our development tools. This is a much simpler method to create custom maps than developing the equivalent functionality as a 3ds Max C++ map.

OSL works in any renderer supporting the regular 3ds Max shading API (Scanline, vRay, Corona, etc.). It also works outside of renderers, anywhere in 3ds Max where a regular map is requested, such as in the Displacement modifier. It also works with renderers that support OSL natively, such as Arnold. In those cases, the execution environment inside the OSL map is not sued, instead, the OSL source code, the parameter values and shader bindings are sent to the renderer, which executes the OSL code. More renderers supporting OSL natively are appearing daily.

OSL uses “just-in-time” compilation and optimization of entire shade trees at once, as long as all the shaders in the shade tree are OSL shaders. You can mix OSL shaders and regular shaders, but the optimizations will suffer.

First of all, I really really want to make sure about this.

As I posted before, You don’t have to code to use OSL maps in 3dsMax.

3dsMax OSL is seamlessly integrated just like all other C++ maps. There is ZERO difference in terms of how to use and where you can use. Also if you chain OSL map together, 3dsMax combine them the entire OSL chain and make a single shader under the hood. Essentially Slate ME is acting as an OSL node editor for you. Even better 3dsMax 2021 ships with 123 build-in shaders to start with. At these point, almost all 3dsMax legacy map could be replace with OSL. This mini tutorial is a very good example of using Slate ME as an OSL node editor.

At a Stack post, we god a question.

In this tutorial, the blender guru is using a custom tool in order to randomize the uv’s rotation so we can’t see anymore the repetitive pattern on a large scale tiling texture. He says that, as far as he knows, this kind of tool doesn’t exist in any other 3d software because it involves maths tricks and vectors and nobody wants to deal with this.

Good news! you don’t need custom node for this. Master Zap let me know how to do this with built-in OSL node. I’m posting the master;s answer with my explanation so you can go further.

First, this is the graph. 4 nodes!

 

Let’s see one by one.

UVTransform : Tiling

This OSL map is like Coordinate rollout in other maps. It allows you to move, rotate and scale uv coordinate. I tiled uv coordinate here. So, I tilted here with Tiling parameters.

Tip! You can connect one UVTransform to many OSL maps whicn means you can control the coordinate of all those map at once.

Noise : Give random value per tile

This map generates a random 0-1 value per tile which will be used as rotation value later.

As the name says, it is an OSL version of Noise map. It has 6 types of noise in a map. We will use Cell type which makes random pixel bitmap patter.

Then, set Scale to 1.0 and Octave to 1. This makes the noise function generates one value per tile. If you increase Scale or Octave, it will essentially divide each tile.

Then, turn off Step Function to prevent blending.

Multiply : convert to degree

Multiply 360 so we can get rotation value between 0-360. As you can see, you don’t have to make a map for value B. You can just type in B parameters of the map.

UVTransform : randomize uv rot

This map rotates UV per tile. You don;t need to set any value here. Just connect UVTransform : Tiling to Input(UVW) which inherit tiling from UVTransform : Tiling map. Then, connect Multiply : convert to degree to Rotate.

Now you can connect this map to any maps UVW port.

Download random rotation per tile OSL setup max file

Do you like to work smart? Just like this map? Then, you will like renderStacks. Click here and check it out!

UIAccessor mini tutorial – How to control Make Preview dialog

I have been using jpg sequence for my Make Preview output. It is allways easier and more flexible to deal with image sequence than avi, mov or mp4.

The problem is Make Preview windows is the one of the old window which doesn’t have full exposure to Mmaxscript.  3dsMax dev added more argument for createPreview method in 3dsMax 2020. But, unfortunately some of option in the Make Preview dialog is still not available for Maxscript.

But, that doesn’t mean you can not set Make Preview automatically. 3dsMax has the ultimate hack(?) for controlling any UI component. UIAccessor and DialogMonitorOPS.

This allow you to emulate user interaction with UI like clicking button, choosing dropdown items and pressing Enter with Maxscript.

If you don’t want all these, Download the final template.

Skeleton code of DialogMonitorOPS  

Let’s start with very simple script.

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WindowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		print "Hello"
	)
	True
)

DialogMonitorOPS.enabled = true
DialogMonitorOPS.RegisterNotification setMakePreview id:#setMakePreview  

max preview
	
DialogMonitorOPS.unRegisterNotification id:#setMakePreview 
DialogMonitorOPS.enabled = false 

DialogMonitorOPS.enabled = true

First, you need to turn on DialogMonitorOPS.so 3dsMax can monitor any UI. Of course, you don’t want to turn on this all the time. So, after our job is done, we will turn off.

DialogMonitorOPS.RegisterNotification setMakePreview id:#setMakePreview

Then, resister your function to run(setMakePreview) and give id. Again, after our job is done, make sure to unresister.

max preview

This runs Make Preview

DialogMonitorOPS.enabled = true
DialogMonitorOPS.RegisterNotification setMakePreview id:#setMakePreview

Unresister setMakePreview and turn off DialogMonitorOPS

Now let’s see the setMakePreview fucntion. This function will run all the time while DialogMonitorOPS is running.

The most important thing to know is that this function need to return true at the end. I forgot why. But, you MUST do it. So, just do it.

local WindowHandle = DialogMonitorOPS.GetWindowHandle()

How would you let Maxscript know which UI you want to control? We will use window handle or hwnd which is a unique id of each UI element. The above line will give is the handle of window which DialogMonitorOPS detected.

local WindowTitle = (UIAccessor.GetWindowText WindowHandle)

Then, this above line will give us the title of dialog.

if WindowTitle == “Make Preview” then (
print “Hello”
)
True

DialogMonitorOPS will check if the dialog is “Make Preview” dialog. If so, it will print Hello.

Let’s set custom output path

From now on I’ll only show setMakePreview function.

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WIndowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		for i in (windows.getChildrenHWND WindowHandle) do (format "%\n" i)
		UIAccessor.PressButtonByName WindowHandle "File..."
	)
	True
)

I removed print “Hello” and added UIAccessor.PressButtonByName WindowHandle “File…”. As you can read, this will find a button named “File…” and press it for you.

for i in (windows.getChildrenHWND WindowHandle) do (format “%\n” i)

What does this do? It just printed a bunch of things in Maxscript listener. This is how we sees what kinds of UI element is in the current dialog and fid a way to access each UI element. As I said in the begining, we use windows handle to specify UI element. This line will print out the information of all children of the dialog with given handle, Make Preview dialog. it gives us an array for each UI element. The important ones are first(hwnd of child), forth(UI type) and fifth( displayed text).

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WindowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		UIAccessor.PressButtonByName WindowHandle "File..."
	)
	if WindowTitle == "Create Animated Sequence File..." then (
		-- Set cusom output path
		local edits = for i in (windows.getChildrenHWND WindowHandle) where i[4] == "Edit" collect i[1]
		uiaccessor.setwindowtext edits[1] @"c:\temp\test_.jpg"	
		UIAccessor.PressButtonByName WindowHandle "&Save"
	)		
	True
)

Because we pressed “File…” button. A new dialog pops up, “Create Animated Sequence File…”. In this dialog, we need to these.

  1. Set custom output path
  2. Set Save as Type to jpg
  3. Press Save button

To set custom output path, we need to know hwnd of path input UI. But, if you check fifth item of array. Text input doesn’t have name! What should I do? Other information we have is type of control on fourth item. The type UI you can input text is “Edit”. So, I collected hwnd of “Edit”s. Fortunately 3dsMax seems collecting UI info in the same order from top to bottom. So, let’s try on the first one. You can use uiaccessor.setwindowtext to set value on Spinner of Edit. If you want to use own naming convention. Replace @”c:\temp\test_.jpg” with own function or variable.

uiaccessor.setwindowtext edits[1] @”c:\temp\test_.jpg”

The, press “Save” button.

UIAccessor.PressButtonByName WindowHandle “&Save”

Wait? why the name iis “&Save”. How do I know I need &? I also don’t know where & come from. But, I know “Save” did not work. So, I printed out all child UI elem data and checked the names.

Did it work? Maybe or Maybe not. Because 3dsmax remembers the format you used last time, if it was not jpg, Make Preview window will automatically switch to the format. So, we need to choose jpg from format dropdown. Now this is real fun!

fn setMakePreview = (
	local WindowHandle = DialogMonitorOPS.GetWindowHandle()
	local WindowTitle =  (UIAccessor.GetWindowText WindowHandle)

	if WindowTitle == "Make Preview" then (
		UIAccessor.PressButtonByName WindowHandle "File..."
	)
	if WindowTitle == "Create Animated Sequence File..." then (

		local edits = for i in (windows.getChildrenHWND WindowHandle) where i[4] == "Edit" collect i[1]
		uiaccessor.setwindowtext edits[1] @"c:\temp\reallyanothertest_.jpg"	

		local comboboxes = for i in (windows.getChildrenHWND WindowHandle) where i[4] == "ComboBox" collect i[1]
		local filetypeHwnd = comboboxes[3] 
		
		local CB_SHOWDROPDOWN = 0x014F
		local CB_SETCURSEL = 0x014E 
		local WM_LBUTTONDOWN = 0x0201
		local WM_LBUTTONUP = 0x0202
		windows.sendMessage filetypeHwnd CB_SHOWDROPDOWN 1 0 -- Open combobox dropdown
		windows.sendMessage filetypeHwnd CB_SETCURSEL 7 0 -- Select 7th item
		windows.sendMessage filetypeHwnd WM_LBUTTONDOWN 0 -1  -- Press left mouse button	
		windows.sendMessage filetypeHwnd WM_LBUTTONUP 0 -1  -- Raise left mouse button
		windows.sendMessage filetypeHwnd CB_SHOWDROPDOWN 0 0    -- Close dropdown
		
		UIAccessor.PressButtonByName WindowHandle "&Save"
	)		
	True
)

I guess you already have figured out what this does. Yes, it collect hwnd of all comboboxes.Them 3rd one was the Save As Type dropdown.

local comboboxes = for i in (windows.getChildrenHWND WindowHandle) where i[4] == “ComboBox” collect i[1]
local filetypeHwnd = comboboxes[3]

All cool. Butn thet the heck is the next lines?

windows.sendMessage Sends a Win32 message to the HWND specified in the first argument. This is how you emulate UI interaction programmatically.

I commented on the code what each lines does. But, you may think how am I suppose to know all the secret code?

CGTalk maxscript forum has a lot of answers for common operations. You can also google windows message reference like this.

Now since you set jpg as a new format, JPEG Image Control windows pops up. This one is easy. We can just press OK button like this.

if WIndowTitle == “JPEG Image Control” then ( UIAccessor.PressButtonByName WindowHandle “OK” )

How about other controls like checkbox?

Since checkbox text usually doesn’t change, we can search the string pattern of fifth item to find hwnd. This is function to get hwnd using UI name. Then you can BM_SETCHECK window message to check the checkbox. if the first argument is 1, the chebox will be checked. If it is 0, the checkbox will be unchecked.

 

fn getChildHwndByName parent_hwnd childUIname = (
    local child_hwnd = 0
    for i in (windows.getChildrenHWND parent_hwnd) where matchPattern i[5] pattern:childUIname do (child_hwnd = i[1])
    child_hwnd
)
local frameNumHwnd = (getChildHwndByName WindowHandle  "Frame Numbers" )
windows.sendMessage frameNumHwnd BM_SETCHECK 1 0

Runscript after Make Preview is done

If you want to automatically run image sequence player like PDPlayer or RAMPlayer or resister to Shotgun, simple add the code after max preview.

Final template code

Here is the cleaned final template code. If you don’t want to read all this, start from this.

Download the final template.

This is made in 3dsMax 2019. Other version might not work with this if there is UI difference.

3dsMax 2020 Preview Enhancement

3dsMax 2020 has some nice improvement for Make Preview.

  • Much faster. 1.5 – 3x faster creation on local drives
  • Capture size greater than viewport dimensions supported
  • “Quality” setting accessible from Preview UI (Nitrous only)
  • Default preview filename based on current scene filename
  • 100% output resolution on by default
  • MXS snippet can be executed per frame for custom strings
  • Filename and MXS snippet values can be specified from MXS command line of CreatePreview()
  • After executing the preview, the time slider is returned to the original starting frame
  • “Play when done” accessible from Preview UI
  • If running from MXS command line, avoid dialog boxes, output to listener instead

3dsMax 2020 also has the bug fix for “User defined” Per-view preset missing issue. This issue is related to the permission. If you are still on older version. Make sure to open the permission for folders under 3dsMax root to be able to choose “User Defined” Per-view preset in Make Preview. Or, upgrade to 2020.

renderStacks – Render Pass Manager for 3dsMax.

 

How to share your awesome MCGs – MCG Installation and network deployment

MCG had a big changed in 3dsMax 2018. This post is based on 3dsMax 2018+.

One of the biggest changes was Improved MCG Package Installation Experience. Let me just norrow the words from dev.

“In previous versions of MCG, the package installation process of an .mcg file involved the automatic extraction of its contained .maxtool and .maxcompound files into the user’s 3ds Max /Max Creation Graph/Tools/Downloads directory. A consequence of this installation method was that common compounds would often conflict with each other, resulting in duplication messages.

In MCG 2018, we’ve simplified the package installation process to make it much more robust. You can now install a .mcg file by dragging it into the viewport. All installed packages now reside in the user’s 3ds Max 2018/Max Creation Graph/Packages directory, and are evaluated as standalone .mcg files. No more file extraction, no more conflicts, no more problems.”

Basically, 3dsMax will consume .mcg package file directly and use the compound in that package first to avoid compound version conflict. Now .mcg file act much like a plugin dll file.

Packaging MCG

MCG Editor > File > Package Tool Graph… will allow you to package the current tool graph and all compound in a .mcg file.

Installing MCG

It means just copying .mcg file into MCG package folder, and drag and dropping .mcg file into 3dsMax viewport will do that f or you. The MCG packages folder is in your user folder/Autodesk. 2018/2019 shares the same structure. But, 2020 MCG package folder is a slightly different.
C:\Users\[username]\Autodesk\3ds Max 2018\Max Creation Graph\Packages
C:\Users\[username]\Autodesk\3ds Max 2019\Max Creation Graph\Packages
C:\Users\[username]\Autodesk\3ds Max 2020\User Tools\Max Creation Graph\Packages

ProceduralContent.ms

Before I tal about network deployment. I need to mention about this file first. The mCG is implemented with dotnet and Maxscript. The engine is dotnet and UI and communicaion with 3dsMax portion id Maxscript. This means we can actually see the source of many MCG functions which are in C:\Program Files\Autodesk\3ds Max 2020\scripts\Startup\ProceduralContent.ms file. If you dissect this file, you can learn a lot about how MCG is working.

Custom MCG Path

By default, 3dsMax uses the above MCG folders. But, you can also have own custom path for MCGs. RegisterCustomGraphPaths function in ProceduralContentOps struct in  ProceduralContent.ms manages how to set the path.

By default, it is set to use 3dsMax.ini file, C:\Users\[username]\AppData\Local\Autodesk\3dsMax\2020 – 64bit\ENU\3dsMax.ini. You can type getMaxiniFIle() in Maxscript Listener to get your 3dsMax.ini file path

You can add MCG Compound Directories, MCG Tools Directories, MCG Package Directories sections and add path like this.

[MCG Tools Directories]
tools_dir=C:\path\to\my\tools
other_tools=C:\path\to\other\tools
[MCG Compound Directories]
common_compounds=C:\path\to\my\compounds
experimental_compounds=C:\path\to\experimental\compounds
[MCG Package Directories]
networkPackages=\\path\to\network\packages

Custom MCG path without using 3dsMax.ini #1

But, what if you do not want to use 3dsMax.ini. The one way of using own .ini file for MCG path would be modifying ProceduralContent.ms. Open the file,C:\Program Files\Autodesk\3ds Max 2020\scripts\Startup\ProceduralContent.ms, search “getMAXIniFile()”. Then, replace with whatever path you want.

fn RegisterCustomGraphPaths =
(
local iniFile = getMAXIniFile()
local settings = dotNetClass "Viper3dsMaxBridge.Settings"

Custom MCG path without using 3dsMax.ini #2

But, then you have to modify on all workstations and render node. That might be too much. The next methods is taking the function from ProceduralContent.ms and make own script.

If you check the code, you can see all the functionality is coming from Viper3dsMaxBridge.Main dotnet class. So, I check what kinds of methods it has with showMethods command. CompileGraphsByFolder is what we need. There are a lot more methods. But, I revmoed not to scare you.

bridge = dotNetClass "Viper3dsMaxBridge.Main" 
dotNetClass:Viper3dsMaxBridge.Main
showMethods bridge
.[static]CompileGraphsByFolder <System.String>folder
....

Now, this is the final maxscript you can use

local viperbridge = dotNetClass "Viper3dsMaxBridge.Main"
-- Just in case if Viper3dsMaxBridge.dll has not been loaded yet
if viperbridge == undefined then (
    local bridgePath = (symbolicPaths.getPathValue "$max") + @"\Viper3dsMaxBridge.dll"
    dotNet.loadAssembly bridgePath returnPassFail:true
    viperbridge = dotNetClass "Viper3dsMaxBridge.Main"
)
viperbridge.ReloadOperators()
viperbridge.CompileGraphsByFolder @"D:\myfolder1\"
viperbridge.CompileGraphsByFolder @"E:\myfolder2\

Put this in a .ms file like myMCGload.ms. Then, throw in one of your network shared plugin folder. I’m sure you probably already have a plugin folder for a free plugins. Any .ms fil in plugin folder will be automatically runs when 3dsMax start.

 

A few more things to know

  • mcg files are registered in 3dsMax file as an asset. It will show up in Asset Tracker and asset metadata stream.
  • If you use BackBurner and use Include Maps, .mcg filw will be submiited with the job like maps.
  • When 3dsMax starts in slave mode, it will automatically evaulate all .mcg file in the folder where the max files are. Therefore, if you use 3rd party render farm, all you need to do is put .mcgs in the same folder as max file. You don’t need to set any path.

I hope this post is helpful for MCGers.