3dsmax
Generic read/write methods for generic data in maxscript
I’ve spent a lot of time experimenting with multiple ways of saving data from 3dsmax: INI Files, raw text files written and read via maxscript (ew), and some pretty long-winded C# save/load classes. Each had its own pluses and minuses, but it was a chore to make any adjustment to the read/write code when anything about the structure of the data changed because it was all so very static in its functionality. The ideal solution is to allow our data to define our exports in a dynamic and flexible way that’s reusable.
The key to understanding how this works within max is to take a look at the maxscript command “getPropNames”, which allows us to query the names of all properties within a given structure or object. With Maxscript we can call this command using the self-referencing keyword “this” inside of a struct to gather all properties within the current context. Combine this with “getProperty” and “classof” and we have a simple way to query every property name, property value, and property type for most every structure within Maxscript. It is also a possibility to implement this within C# using similar methods, but we’ll focus on maxscript for this post:
struct animalType ( prop1 = "bear", prop2 = "cat", /* lots and lots of properties */ fn exportData = ( local properties = getPropNames this local propValues = for property in properties collect (getProperty this property) local propTypes = for value in propValues collect (classof value) ) )
These three commands give us three parallel arrays of all properties and methods within our structure, and most importantly, enough information to reconstruct the data when we want to re-load it again. This does not however, handle arrays or node references, which complicate things and will be discussed in a further post.
Moving on, we want to be able to write all of this accumulated data to some sort of file to save it. My favorite format this month is XML, so we’ll go ahead and utilize C#’s wonderful XMLWriter class to handle our output. Using our gathered data, We simply initialize our XMLwriter class and then iterate over our arrays, producing a single element for each property within our structure. At less than 40 lines with white space, it’s a pretty small piece of code that handles a lot of scenarios.
/*inside of a struct...*/
fn exportData xmlFile =
(
/* get all of our properties and methods*/
local properties = getPropNames this
local propValues = for property in properties collect (getProperty this property)
local propTypes = for value in propValues collect (classof value)
/*build a format class so we indent out output. makes it readable */
local xmlFormatter = dotnetobject "System.Xml.XmlWriterSettings"
xmlFormatter.Indent = true
/*Instantiate our xml writer class */
local XMLWriter = dotnetclass "System.Xml.XmlWriter"
XMLWriter = XMLWriter.Create xmlFile xmlFormatter
/*write the start of our document*/
XmlWriter.WriteStartDocument()
XmlWriter.WriteStartElement("structDef")
/* Iterate over all of our gathered properties, except methods*/
for i = 1 to properties.count where propTypes[i] != MAXScriptFunction do
(
/* create an element for each property. save it's type and name as an attribute */
XmlWriter.WriteStartElement("property")
XmlWriter.WriteAttributeString "name" (properties[i] as string)
XmlWriter.WriteAttributeString "type" (propTypes[i] as string)
XmlWriter.WriteString (propValues[i] as string)
XmlWriter.WriteEndElement()
)
/* write the tail of the xml document */
XmlWriter.WriteEndElement()
XmlWriter.WriteEndDocument()
/* close the file. finalizes the write and puts the data to the target */
XmlWriter.Close()
),
Running the above code yields xml similar to the example below.
<structDef> <property name="numEyes" type="Integer">6</property> <property name="id" type="String">hypnocat</property> <property name="type" type="String">cat</property> <property name="numLegs" type="Integer">15</property> <property name="averageLegLength" type="Float">70.31</property> </structDef>
When we load this data, are presented with an interesting challenge. Any data that we pull from the XML file is going to be of type string. If we assign all of these raw values straight back to our structure, we’re going to end up with a struct full of strings and lose any previous and possibly important type data regarding our parameters. Luckily, we can use our self-referencing to cast/convert all of our strings to the proper type.
fn loadData xmlFile =
(
/*open our xml file */
local xmlReader = dotNetObject "System.Xml.XmlDocument"
xmlReader.Load(xmlFile)
/* select all xml nodes of type 'property' */
local properties = dotNetClass "System.Xml.XPathNodeList"
properties = xmlReader.SelectNodes("structDef/property")
/*get the enumerator and iterate over all nodes*/
enum = properties.GetEnumerator()
while enum.MoveNext() do
(
/*build correct data type*/
local thisPropertyName = enum.current.Attributes.Item[0].Value
local thisPropertyType = enum.current.Attributes.Item[1].Value
local thisPropertyValue = enum.current.InnerText as (classof (getProperty this thisPropertyName))
/*set the property generically*/
setProperty this thisPropertyName thisPropertyValue
)
)
The key to preserving our property types is (classof (getProperty this thisPropertyName)). We can’t cast our values to their appropriate type using just the name of the Type as a string, so we work around that limitation by relying on the structure our load function exists within. Using the property name we’ve loaded, we call getProperty to fetch the already-existing value within our struct . This returns a “Type” rather than a string, and can be used to cast using “as”. Clever (or at least, I think so.)
This setup should handle most common struct properties except for arrays and node references, which we’ll handle later. This post is already incredibly long!
Magma distortion
Today I sat down to convert my messy and convoluted lava shader into a cleaner two-pass variety. Previously, the shader required me to do lots of samples, ifs, lerps and the such. For example, this little snippet of code lerps between the magma and igneous rock layers depending on the value of the alpha. The alpha used vertex paint, and a user-defined float “lavaAmount” to control the amount of magma being shown.
// sample both the rock and the magma diffuse tex
float4 rockDiffuse = tex2D(s_IgneousRockTex, vert_in.UV0);
// build a lerp value between the igneous tex and the magma tex.
//int because we only want 1s and 0s. no floaty values.
int lerpValue = clamp((rockDiffuse.a + lavaAmount + vert_in.vColor.r), 0, 1);
// finally, build the diffuse texture.
float4 diffuseTex = lerp(rockDiffuse, tex2D(s_magmaTex, ( vert_in.UV0 + -(wTime/80)) ), lerpValue);
The normal mapping was equally icky-gross.
// sample and average the normal map and panning map.
float3 magmaNormal = (tex2D(s_MagmaNormalMap_surface, (vert_in.UV0 * 0.2 + -(wTime / 80)) ) * 0.5f ) +
(tex2D(s_MagmaNormalMap_panning, vert_in.UV0 * 1.2 + (wTime/ 100)) * 0.5f );
float3 rockNormal = tex2D(s_IgneousRockNormal, vert_in.UV0);
// If we're on the igneous level, use igneous. else, use magma.
float3 nrml = lerpValue < 1 ? rockNormal : magmaNormal;
// finally, move that vector to world space.
curNormal = mul(nrml * 2 - 1, tanToWorldSpace);
Now with two passes, Igneous rock layer is simply alpha’d out by our previously-defined lerp value. This layer is the second pass, which is drawn over the magma layer.
// sample our igneous rock texture.
float4 rockDiffuse = tex2D(s_IgneousRockTex, vert_in.UV0);
// build our lerp values. The 1 minus is simply to make semantics of the UI match the value.
int lerpValue = (1 - lavaAmount + rockDiffuse.a);
// make our 1 -> 0 and our 0 -> 1
lerpValue = !lerpValue;
// and then, simply set our values.
output_color.rgb = rockDiffuse;
output_color.a = lerpValue;
Here’s a video of my magma layer. It incorperates the averaging of the two normals, which are then used to distort the UV channel. This is a first-pass of this effect.
Here’s the code for the distortion and the normal map. After sampling our normal map the code distorts our UV input and then re-samples the normal map to ensure that the normal map warps with the diffuse map.
// build the texture of our normal maps;
curNormal = tex2D(s_MagmaNormalMap_surface, vert_in.UV0);
curNormal += tex2D(s_MagmaNormalMap_panning, vert_in.UV0 * 1.2 + (wTime/ 100));
//Distort our UVs. the scalar 0.1 is scale value for the size of the distortion.
vert_in.UV0 += curNormal.xz * 0.1;
//Now re-sample the normal map with distorted UVs to get a normal from the new location;
float3 magmaNormal = tex2D(s_MagmaNormalMap_surface, vert_in.UV0);
magmaNormal += tex2D(s_MagmaNormalMap_panning, vert_in.UV0 * 1.2 + (wTime/ 100));
//Because we added two normal maps together, divide by two to get the mean of the normals. (average)
magmaNormal /= 2.0f;
curNormal = mul(magmaNormal * 2 - 1, tanToWorldSpace);
This shader is coming along quite nicely, and the split to two passes makes it quite readable, unlike before.
Ramp shading.
Recently, I’ve gotten my base Blinn/Phong shader to a state where i’m relatively happy with it. It’s posted to my portfolio on the Tech Art Page. I plan on using this shader in the future as the base for any material-related shaders. For example, today I wrote a very fast ramp shader mod for the blinnphong. This took me little under 30 minutes to implement, and the results are pretty nice!

Also, using distance attenuation on your shader causes your light brightness to increase as it gets closer to the surface of your object. As this lambertian term of brightness is also used as lookup on the ramp texture, you get a neat effect if you don’t clamp it between 1 and 0. Look!:
The ramp texture loops around as the “U” value increases beyond 1, and wraps to the front of the texture. This causes the banding pattern shown in the above video. This issue is fixed by setting your sampler_state AddressU and AddressV to ‘clamp’.
I love this stuff!
Fetching max lights
I set out last night searching to understand how to query lights within 3dsmax scenes. Surprisingly, accomplishing this took less time than expected as I was able to get a very simple shader working in less than an hour. Lets take a look at how we query lights within the 3dsmax world, and where we can use this data.
3dsMax offers three specific semantic types for directx shaders to use with scene light data.
POSITION – For the light position,
DIRECTION – The light direction,
and LightColor – The light color.
Firstly, because we will be using both annotations and semantics, our light data is limited to ‘global’ declarations at the top of our fx files. Structs cannot have annotations, and local variables within functions cannot use semantics. Here’s a typical example of a vector linked to a light object within 3dsmax using semantics and annotations:
float3 lightPosition : POSITION <
string UIName = "light position source";
string Object = "omnilight";
int RefID = 0;
> = {0,40,0};
This simple declaration pulls the position data from any light object within a max scene with a position (which means, any light). Lets break it down line by line and see which each value does.
float3 lightPosition : POSITION <
This first line is simply a declaration of a three-component vector. The semantic POSITION hints that this value will be, well, a position. The open carrot is the open-bracket equivalent for the value’s annotations.
string UIName = "light position source";
This declares the title of the UI element of this vector value. Because this value is tied to an object, instead of having a colorpicker like vectors usually have, the UI object will be a drop-down list of lights.
string Object = "omnilight";
This annotation is a key element of this value. The string ‘Object’ hints to 3dsmax that we’re interested in querying an object within the scene. The string is the name of the preferred default object within the scene to use for the value. If a light with the name “omnilight” exists within the scene, this vector’s value will be predisposed to be drawn from the position of the light.
If the preferred object is not present, or another light object wishes to be sourced, the UI allows the selection of which specific light should be used to draw a value from. If there are no lights within the scene to draw a position from, the default value is used.
int RefID = 0;
Adds a simple reference number to your light data. Aids in ensuring light data is updated properly within 3dsmax when handling multiple lights. Mostly associated with the “lightcolor’ semantic. Although I’ve not seen any clear-cut cases of this value being used, most examples have a unique refID for each light being queried.
> = {0,40,0};
The final line of the declaration specifies a default value for the variable. Keep in mind that all default values are in viewspace, while light directions and positions when queried from an object are in world space. I might be wrong here, but this seemed to be the case with my implementation.
How do we use these values? We can simply query these values to pass them through to our lighting functions. Pretty basic.
// lightDirection and lightColor,
//queried from an object perferably named 'fspot'
float3 lightDirection : DIRECTION <
string UIName = "light direction source";
string Object = "fspot";
int RefID = 0;
> = {0,1,0};
float3 lightColor : LightColor <
string UIName = "light color source";
string Object = "fspot";
int RefID = 0;
> = float3(1,1,1);
// light function
float4 simpleDirectionalLight(float3 normal, float3 lightDir)
{
float4 outputColor;
float a = clamp(dot(lightDir, normal),0,1);
outputColor.rgb = lightColor * a;
outputColor.a = 1.0;
return outputColor;
}
// my pixel shader
float4 ps_lightDir(vs_output in_vertData) : COLOR0
{
float4 output_color = in_vertData.color;
float3 curNormal = normalize(in_vertData.normal);
output_color += simpleDirectionalLight(curNormal, lightDirection);
return output_color;
}
This sample is omitting our vertex shaders and passes/techniques. Overall, it’s pretty simple to query lights within a 3dsmax scene. These features can be expanded upon by utilizing a custom interface written in maxscript, but this post is already quite long. I’ll see about covering that in a future post.
For reference of the 3dsmax specific semantics and annotations, check out this doc (in a zip file) from autodesk, which is a list of all unique and common 3dsmax semantics/annotations. (source)
Lava material, map coords, and progress.
I’ve gotten my initial pass on my lava material finished. Currently the shader lerps between a ‘igneous rock’ layer and the underlying ‘magma’ layer, using an alpha value pulled from the igneous rock diffuse as the lerp amount. The normal map of the magma layer is averaged between a static ‘lava’ normal map and a panning distortion normal map that hopefully I can tweak to look like ripples.
One issue I’ve run into is the correct utilization of different mapping channels within 3dsMax (which is the environment i’m viewing these materials in). 3dsMax allows users to indicate which map channel to pull uvs from using some custom semantics:
texture IgneousRockSpecular < string UIName = "Igneous specular"; int Texcoord = 0; int MapChannel = 2; >;
It seems that once one texture object within code indicates which map channel and texcoord it utilizes as a parameter, all others will ignore defaults and not display until these values are also specified for them. Despite this, the alternative Mapping channels do not seem to work when passed through the vertex shader. Another problem to figure out.
In the future, I want to incorporate actual 3dsMax scene lights into the material, instead of using statically-positioned omnilights that only exist within the code. Meanwhile, i’ve written a very basic ‘blur’ shader as a starting point for understanding post-processing. 3dsMax offers a .fxh file that contains methods to generate screen-aligned polygons. These functions work well, however, it’s difficult to understand the actual processes of doing screen-space effects when the grit is obfuscated by a simple function call.
