Skin Shading - Part 1
Maya ProjectDownload the Maya project for this tutorial that includes the scene files and textures.
In this tutorial we will explore some of the features of alSurface by creating a skin shader. We'll be using the excellent scan data provided by Lee Perry-Smith at Infinite Realities as our starting point. We'll cover the basics of setting up a linear workflow in Maya 2016 and MtoA, then use the provided maps to create a good starting point for a skin shader. Along the way we'll look at setting up displacement, how to linearize color maps, remapping specular maps to get our desired range, and we'll look at the way subsurface scattering works in alSurface.
First, open the file 00_start.ma from the scenes directory in the Maya project. This scene was originally created in Maya 2015, before Maya finally came out of the dark ages and implemented a managed color pipeline. To set this up in Maya 2016, open the preferences and turn on the Enable Color Management checkbox.
Then go to the Arnold globals and set all the Gamma settings to 1. This tells Arnold that all your inputs and outputs are linear.
If you do a render now you'll get the image below.
Currently there's a default standard shader applied, so go ahead and attach a new alSurface. One of the main features of alSurface is support for light groups, so let's set those up now. Light groups allow us to put all the information for each light (or group of lights) into their own AOVs, up to a maximum of 8. This is equivalent to doing separate renders for each light, except we get them all in one render, and once we have the AOVs we can rebalance them in comp to tweak the lighting to our hearts' desire.
First, select the shape node for each of the three lights in the scene using the outliner.
Next select Modify>Add attribute... from the Maya menu, and instruct it to create an integer attribute called mtoa_constant_lightGroup. The mtoa_constant_ prefix tells MtoA to export this attribute with the geometry as user data so that the shader can pick it up.
Go to the AOVs tab in the globals and add the first three light groups AOVs to the active AOV list.
Now if you render again you will have access to the light groups in the render view AOV dropdown and by selecting them you will be able to see the contribution of each light individually. We will use this later to help tune our subsurface sscattering by examining each light in turn.
By far the most important part of any skin shader is the displacement map, and thankfully the model we're using comes with an excellent texture extracted from the scan. First we need to convert our head mesh to a subdivision surface by editing the Arnold>Subdivision attributes on the shape node and setting Type to catclark and Iterations to 4.
Next add a File texture to the Displacement slot in the shading group for our shader and load disp.tx from the sourceimages directory. By default the displacement will be a little too strong...
...so set it to a more reasonable value like 0.1. When you're setting up displacement for skin shading, you want the result without subsurface scattering to look like a plaster cast of a head, and the subsurface scattering we'll add later will soften some of the hard detail we have currently.
Now the fine detail seems to be at about the right level, but our guy is still looking a bit puffy. This is because the displacement map has values from zero to one, which means that the surface is always going to be pushed outward, whereas what we want is to make the low values displace inward so that the surface keeps the same overall volume. To do this, open the Arnold rollout on the displacement shader and set the Scalar zero value attribute to 0.5. This tells Arnold to consider any value less than 0.5 an inward displacement and any value greater than 0.5 an outward displacement.
Next we'll add the color map. Just assign a regular Maya File texture to the Diffuse > Color parameter in the alSurface shader and point it to sourceimages/col.tx. If we do a render now, the result will look very washed out, like this:
The color is wrong because the texture is in sRGB. In the first part of the tutorial we set all Arnold's gamma settings to 1, so Arnold will not automatically try and convert the texture for us. There are several ways to handle this. We'll do a quick fix first and then do the correct way. The quick fix is just to convert the color space of the texture after reading it in the shading network. You can do this by dropping down an alColorSpace node after the texture and setting its Source space parameter to sRGB (the default). This will convert the texture result from sRGB to linear for us and the render will now look correct.
This works fine for our example, but is in fact not correct. The reasons why are outside of the scope of this tutorial, but in short it's because you have to do color space conversions before mip-map generation and filtering, not after. To fix this, we'll need to preconvert our texture to linear. We could do this in Nuke, but an easier way is to use the conversion option built into OIIO's maketx utility. I prefer to use the shell to do this:
» maketx col.jpg --colorconvert sRGB linear -o col_lin.tx
but you can also do it by passing those same options to MtoA's Tx Manager window. With the texture converted, we can now point our File texture to col_lin.tx and remove the alColorSpace node:
If you compare this image to the previous method, you can notice a very tiny difference between the two. While it's small here, it can make a big difference in the general case, particularly when you're rendering very high-resolution textures at a distance, so it's a good habit to get into in general.
The specular qualities of a material are perhaps the most important thing to get right in order for the material you're creating to match its real-world counterpart. As you can see, the default settings in alSurface don't do a very good job for human skin, so let's fix that. A good starting point is to set the Specular 1 > Roughness value to 0.5 and set the Specular 1 > Advanced > Distribution parameter to ggx. The GGX distribution is new to Arnold 4.2.12 and generally matches real-world materials better than the default Beckmann distribution.
We'll also set the Specular 1 > Fresnel > IOR parameter to 1.38 instead of the default of 1.4. Index of Refraction is probably the single most important parameter for accurately matching specular properties of a material, and we'll cover them in more detail in another tutorial, but for now, 1.38 is a good baseline for skin.
This gives us a good base, for example the area around the temples is working well, but the nose is feeling very dry. Next we'll add a map to drive the roughness differently in different areas.
Create a new File texture on our Specular 1 > Roughness attribute and point it to sourceimages/spec.tx. By default Maya will connect the alpha of the texture to our attribute, which is not what we want (because the texture has no alpha). We can solve this by enabling the Alpha is luminance checkbox on the File node, but I prefer to plug the channel we want from the texture directly, so delete the connection from outAlpha and connect outColor.r to our roughness parameter instead to just take the red channel as our scalar value.
If we do a render now it will look totally wrong.
We can use the Debug mode on alSurface to figure out why. The debug feature of alSurface can be very handy for figuring out why your shader isn't responding as expected by showing you exactly what values are being fed into it. Change the Advanced > Debug parameter to specular-1-roughness and the render will show you the value that is driving the roughness. As you can see, the nose, forehead, lips and cheekbones are brighter than the rest of the head because the map specifies the shininess of the surface rather than the roughness.
We want to invert the values in the map, and also to remap them to a different range: at the moment the values are between zero and one and we know that for skin we want our roughness to be somewhere around 0.5 on average. To do this we'll use an alRemapFloat node, so create one and plug it in between the File texture and the surface shader like so:
We want to change the range of the map so that the black values correspond to our base roughness of 0.5 and the white values correspond to some shinier value, say around 0.3. To do that we use the Output min and Output max parameters of alRemapFloat. Output min says "A value of zero in the input becomes this value" and likewise Output max says "A value of one in the input becomes this value". So we want our min to be 0.5 and our max to be 0.3, which will invert the map and remap to range so that all values lie between 0.5 and 0.3.
Now our values look like they should so switch the Debug mode back to off to see the difference it's made to the surface. Much better!
The final touch to apply for our character is to add subsurface scattering. SSS is handled a little differently in alSurface to the standard shader so it's worth going over the logic briefly.
In alSurface, SSS is considered a part of the diffuse layer of the shader. That's because this is how it works in the real world: light enters a material the bounces around a little bit, changing colour as it goes, then is bounced back out of the surface again towards the viewer. If the material is very dense, then the distance the light travels inside the material will be very small and can be modelled accurately with a regular diffuse layer. If the material is less dense, then the light can travel far enough that the material starts to look "squidy" and we need to use SSS to model the effect.
The Diffuse > SSS > Mix parameter allows you to control how much the SSS is "mixed in" to the diffuse layer. Generally you want this to be either zero (for "hard diffuse only") or one (for "sss only"). It can sometimes be useful to use values somewhere in between, but most of the time you jsut want one or the other. Go ahead and set it to one now.
Oops. Now our guy is made out of jelly. Since SSS depends on the world-space distance light travels through an object, it is heavily dependent on the size of that object. We could use the Diffuse > SSS > Distance parameters to fix this, but it's easier to change the Diffuse > SSS > Density scale instead. The density scale acts as a multiplier (well, divisor) on the distance parameters, so it's useful for globally adapting the shader to the size of your object. If you change density scale to 10 instead of the default of 1, you'll see we have a more appropriate SSS effect.
Arnold's default SSS algorithm is known as cubic and is simple to control and is pretty fast. Unfortunately it also softens out surface details a lot, which would be fine if we were trying to make a blob of jelly or something like that, but it's not so great for skin. The new empirical mode does a better job, but we want the most detail possible so we'll switch Diffuse > SSS > Mode to directional. The directional algorithm is slower than cubic, but gives a much more detailed result as it takes the light direction into account.
If you compare the previous two images, you can see that the directional mode preserves surface detail much better, particularly in the forehead and the chin. If you look closely, you'll also notice that there's a tiny bit of red light between the lips in the cubic image that's not present in the directional image - this is light that has erroneously crossed the empty space between the lips, and directional gets rid of it automatically for us. You'll also notice that the directional version is slightly brighter and less saturated than the cubic version, which is the single scattering component kicking in.
A good rule of thumb when tuning sss for skin is "if you can see it, it's too strong." Ours is currently in a pretty good place, but it could be a little stronger. We can use the light groups feature of alSurface to help tune the scattering. Switch the render view to the light group for the key light (light_group_2 in my scene), and play with the Diffuse > SSS > Distance 1 parameter until you're happy with the amount of red light you see blurring the edges of the shadows. In my case it's around 1.7.
Now let's do a high-resolution render and see what we've made. My final sample settings are 6/1/1/1/2 and I've also cranked up the subdivision iterations to 6 to get some more detail out of the displacement map.
That's it for part one. This would do fine for a standard mid-ground digital double. In part two we'll take our shader to the next level of realism.