Shaders
Finished with the lava shader, for now
Today I finished up my work on my lava shader. It turned out pretty nice!

I have a narrated demo video of the shader here: http://www.youtube.com/watch?v=pScLYuWuirw
The shader can be downloaded Here A max 2010 demo scene can be had Here
This project was a fantastic learning experience, and it leaves me wanting to spend more time on shaders. It raised some general questions that I’ve not yet had time to tackle as well:
- How can we tie geometry shaders into materials to make interesting wavy effects? water? bubbly surfaces?
- How can we use more information from the mesh’s world to adjust that object’s visual appearance beyond pre-rendered cube maps?
- It is possible to write a maxscript interface for the hlsl shader. Is this effort even worthwhile?
- Why is 3dsmax mapchannel ordering so wonky?
HLSL wise, I still want to spend more time with screen space effects, and take a look at some fun challenges such as color adjustments using LUTs and writing some sort of mood/environment color adjustment schema.
Meanwhile, it’s time for me to transition into an area that i’ve been neglecting for some time: Python.
Also, some awesome blogs to take a look at :
http://www.jshopf.com/blog/
http://www.rtrowbridge.com/blog/
http://adammechtley.com/
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!
Lava shader update
I’ve been meaning to work on the magma layer of my lavashader for the last few days, but I got caught up in attempting to add distortion and vertex-paint control of the igneous rock.
I’ve accomplished the vertex-paint goals, but UV distortion is still a bit elusive. At least I got around to re-painting the magma layer. I also re-incorporated specular light into the surface, which gave it a more ‘glowy’ feel. Anyway, here’s a current preview of my work. it’s looking pretty decent so far. I love writing shaders.
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.
Working with HLSL.
For the last few days i’ve been getting myself more familiar with HLSL. it’s a pretty fun and exciting process, and being able to see the work converge into a beautiful rendering of a model is very rewarding.
Some things I’ve learned in the last few days:
1) HLSL documentation is difficult to find that’s as thorough as i’d like it to be.
2) My laptop’s video card does not like TEXCOORD0 – possibly a diver problem is rearing it’s head when this channel is used. UVs seem like they only output the U coordinate, and V is always 0. However, UV coordinates are output and display correctly on my desktop PC. Which is nice, considering that for once, a horrendous visual error is not caused by MY code!
3) Handling all normals in world space is very awesome. Moving view vectors and light directions into tangent space was a little convoluted, and not as straightforward as keeping them in world space. Thanks to This archived thread for the advice.
4) “The Danger Zone” is an awesome blog.
Today i’m going to delve into the world of screen space processing. I want to get a handle on post-processing effects such as blur and bloom today. I also want to start planning out a Lava shader, which will be a culmination of all of the things i’ve worked on for the last few days.
