Marschner Shader Part III
This is the last part of the three post regarding the Marschner shader. I will explain how to efficiently make the shader for this model, how to add ambient and diffuse lighting and at the end of the post I will also give source code for generating Marschner lookup textures and a video showing the results I had in CS.
Lookup Textures
Because there are too many computations done in M and N functions to be put in the pixel shader, the best optimization is to use lookup textures, that need to be updated as rarely as possible.
We can easily observe that apart from the constants defined in Table 1 ( page 8 ) from Marschner’s paper, the M function only depends on q i and q r , and N on q d and f d. Although this might seem a good optimization at first, taking into account that all these angles must be computed from inverse trigonometric functions, such as acos and asin, which aren’t fast at all, indexing the lookup textures directly by cos and sin sounds a better idea.
The way in which sinus and cosinus values can be computed for all these angles can be found in GPU Gems 2, Chapter 23:
- sin q i = (light · Tangent),
- sin q o = (eye · Tangent).
- lightPerp = light – (light · tangent) x tangent,
- eyePerp = eye – (eye · tangent) x tangent.
- cos f d = (eyePerp · lightPerp) x ((eyePerp · eyePerp) x (lightPerp · lightPerp))-0.5
As for the cos q d if we observe that q d depends on q i and q r then we figure out that we can use a channel from the lookup texture indexed by the sins of these two angles.
The easiest way to build these two textures is to make a lookup texture for M, having MR, MTT, MTRT and cos q d, and a lookup texture for N. However, in the original paper NTT and NTRT each have three channels, but they can be reduced to only one channel if we consider the absorption to have one channel as well.
These are the lookup textures obtained with my first implementation of the Marschner project:
Ambient and diffuse lighting
The Marschner model only specifies the specular component for lighting, so in order to obtain nice visual effects, both ambient and diffuse lighting were added to this model.
I used the lighting from the Nalu Demo, presented in detail in one of my previous posts:
/* Compute diffuse lighting with phi-dependent component */ float diffuse = sqrt(max(0.0001, 1 - uv1.x * uv1.x)); /* Pass colors */ float4 diffuseColor; diffuseColor.rgb = diffuse * objColor.rgb * DiffuseCol; diffuseColor.a = objColor.a; float3 ambientColor; ambientColor = objColor.rgb * AmbientCol; float3 lighting = (( M.r * N.r + M.g * N.g + M.b * N.b ) / (cos_qd * cos_qd)); lighting += diffuseColor.rgb; OUT.xyz = lighting + diffuseColor.rgb * 0.2 + IN.AmbientColor;
Source code
Here you can find the first version of my Marschner C# Project, which generates the lookup textures needed for a shader similar to the one presented in the Nalu Demo post.
There are still some things that can be improved, but I plan to release another version for that, as soon as I get a chance. Until then feel free to improve the project yourself.
These are the two adjustments done to the original model, as described in Marschner:
- The absorption is specify by only one channel.
- Instead of the standard NTRT component, the simplify version was used.
You can find more information in the README, INSTALL and LICENSE files from the archive.
Demo
Next you can see the effects this shader has on Krystal’s hair. If you want to play with the application yourself checkout the hair branch from CS main repository.


Hi, it’s me again
There is something that’s been killing me since I had a look at the N texture. Do you know why it looks so different from the one in http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter23.html ?
Hello Javier,
It should look closer to the one from here, Figure 5.8.1, page 40. I think I used the same model presented here, even the N_TRT function. However, the two N textures don’t look very much alike, but if I managed to find out what the problem is I will include it in the next Marschner Project.
As far as the one from GPU Gems 2, they have in that N texture, only N_TT (three channels) and N_R (one channel). And the shape of the colors don’t seem to match those from the report, from the above link, either.
hey, thanks for the very impressive and helpful information. I’ve managed to follow along your implementation but saw that you in your above comment mention knowing what the reason behind the discrepancy between your and their Nfunction LUT.
I’d be interested in hearing more about that.
Hi!
I can only guess why my texture is different from the Nalu demo (http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter23.html).
I encode in the N_texture N_R, N_TT and N_TRT (using one channel for each), while in the GPU Gems 2 implementation, they have encoded in the N_texture (N_R – 1 channel and N_TT – 3 channels).
However, if you look closely in the Nalu demo (you can download it from http://www.nvidia.co.uk/object/cool_stuff_uk.html#/demos/152) when you disable the self-shadowing (the opacity shadow maps), you get kind of a poor rendering (not that much different from a Kajiya and Kay shader). So the realistic part of the rendering is given by the self-shadowing effect.
I am currently working on a project for self-shadowing translucent objects, and will make a blog for it this week sometime.
Cheers,
Alex.