Programming the Skin with VSL

The reptile skin material is now attached to the body. It defines only bumpiness, no color patterns or other shading information. Let's make the skin material more interesting:

  • More variations to bumps
  • Color patterns
  • Metallic shine
  • Efficient antialiasing

Original and improved skin

The example project to start this chapter is 'tutorprojects/harry/body_and_materials.r3d'. You can also load the improved materials from the example project 'body_and_skin.r3d' if you wish to skip this VSL oriented chapter.

Surface Geometry

Switch to the material tab of the select window, make sure the reptile material is selected and open the property window. Check the Advanced option to see the structure of the material. It is very simple:

    Surface geometry
        Bump height += Cell (Map coords)

A VSL material consist of top level blocks called shaders: Surface geometry, Surface properties etc. Shaders define what happens in each step of the rendering pipeline. As the name suggests, a Surface geometry shader defines geometrical modifications for target surfaces. Although you can add shaders into a material in an arbitrary order, shaders have a fixed, natural order of evaluation. For example, a Surface geometry shader is always evaluated before a Surface illumination shader. Within shader levels, the evaluation order is the order of the objects in the VSL tree.

The first step is to make bumps more interesting. A surface geometry shader that does this looks as follows:

Below is a description how to build the shader. Creating a finished shader from scratch may look like a complicated task especially for users with no programming experience. However, in a real modeling situation, such shaders are created one small step at a time. For example, you could first solve how to control the bump shape only. While adjusting the shader and rendering previews of it, that part of the shader becomes very familiar to you and it is easier to proceed into the next part.

Let's start: drop a Variable object to the root level of the material. You can add VSL objects either by selecting the target level in the VSL hierarchy and choosing a VSL object from the popup menu, or by drag and dropping VSL object icons from the VSL Objects frame. The latter alternative has the advantage that you can place the object exactly at the desired location. Popup menu adds new objects to the end of the selected level, and you often have to drag the new item to the right place.

The new VSL object - Variable - becomes automatically selected. The bottom part of the VSL window shows a property gadget for it. Set the Type of variable to Float and rename it as 'bh'. VSL supports 3 kinds of variables: one dimensional float and 3 dimensional color and vector variables. The rendering system handles type conversions automatically, but it is wise to use the most appropriate type. For example, when you assign a value to color variable, VSL editor shows you a RGB color selector instead of a numeric vector gadget. Bump height is clearly a one dimensional value, so we use a float variable here.

This root level variable 'bh' will store bump height information and pass it to the Surface properties shader - we will use it there to define color from the bumpiness. Root variables are visible in all shaders, and therefore they can be used to pass information between shaders. Adding a new channel is another alternative, but this is a lighter, only locally visible alternative. One must be careful when passing information in root variables. Because of ray trace recursion, the material can be evaluated several times between shading steps. For example, do not expect that a root variable initialized in Surface properties still contains the same value in Surface illumination. Shadow computation may evaluate the same material on another position to find out object's transparency. The method can be used here because there are no shading events between Surface geometry and Surface properties shaders in our scene.

The original bumps were quite regular. A standard solution to make a pattern irregular is to add some noise to the parameter channel. The channel that contains the 3D space related parameter (suitably warped by a geometry of a material map object such as a parallel or a spherical map) is Map coords. Its original value may be needed later, and therefore we compute the modified parameter and store the result to a local variable. So, drop a new variable object to the beginning of the Surface geometry shader. Set the type to Vector and rename it as 'mc'. Add a Linear object after the variable, set its Input0=Surface:Map coords and Output=mc (you can change Input and Output from the popup menu of the shader tree gadget). Set the Multiply value of the Linear object to (0.5 0.5 0.5). This value controls the density of bump shape irregularities. Density should be in a suitable proportion to the bump size. If the noise is clearly denser as the skin cell pattern, individual cells get distorted. If the noise density is lower, cells keep their original shape but their positions change and dimensions twist along the noise curvature.

Original skin, distortion by a low density noise and distortion by a high density noise

Add a Noise object after the Linear object, set Input=mc, Output=mc, Octave ratio=2, Smooth option enabled and Amplitude=0.1. Amplitude is the most important value here - it defines how large the bump shape irregularities become. You can find suitable values by experimenting, but 0.1 is a good starting point. The Octaves value of the noise controls how much detail the noise has. If you plan to make an animation, which shows the skin from a very close distance, you should use a high number of octaves so that the magnified skin shows enough details. For this example, 3 or 4 octaves is enough.

Now mc contains noise values for the bump shape distortion. To get the original parameterization included, add a Copy object after the Noise object, set Destination to mc from the popup menu, Source to Surface:Map coords and Operation from the General tab of the property window to +.

Noise properties

Now mc is a sum of irregular noise and regular Map coords. Using pure noise to define bumps would create a totally distorted bump pattern. Therefore we will use a sum of noise and Map coords. To summarize the computation with some clarifying comments:

    mc=Linear(Map coords) // Define noise density as mc = 0.5 * Map coords 
    mc=Noise(mc) // Compute noise value.
    mc+=Copy(Map coords) // Add regular parameter

Next activate the Cell object, which is already in the shading tree. It should be the last object in the Surface geometry shader. If not, drag it to the end of the object list. Change its Input0 to mc and Output to bh. Change Minimum value to 0 and amplitude to 1. We will modify the bump shape with a curve object, and the 0...1 range is a suitable parameter interval for it. In the General tab, change the operation from '+' to '='. The Cell object now initializes the new bh variable instead of modifying the actual Bump height channel. Other Cell settings can be as in the figure below.

We can adjust the shape of bumps with a curve object. The purpose is to define grooves between relatively flat bumps. Add a Curve object after the Cell object and set Input0=bh and Output=bh. Open the popup menu over the Curve gadget and select Set Minimum and Maximum Values. The default Curve scale, 0-1 meters, is definitely not suitable for skin details. Set Max Y = 0.01 meters in the opened dialog, set Rescale and hit O.K. Then edit the curve as shown in the picture below.

If you want to preview the bump pattern now, just change the Output of the Curve object to Bump height and render. When the result is OK, change Output back to bh to continue the example. The curve controls the profile of bumps, and you can change it here easily and intuitively. Just note that the curve defines only one half of the bump, from the edge to the middle point.

The bumps have now somewhat irregular shape, but the surface does not have very fine detail. The bumps are actually quite smooth. The standard solution is to add a suitably dense noise field. Because the fine details must be clearly denser as 'large scale' bumps, we need again a modified parameter. So, drop a new Linear object to the end of the Surface geometry shader and set its Output to mc. We use the already defined mc variable here again, because its old value is no longer needed. Set Multiply of the Linear object to 5 5 5. This will give roughly 5 times denser bump pattern as the base pattern consisting of cells.

Then add a new Noise object to the end of the shader. This is very fine detail noise, so set Amplitude to 0.002 (2 millimeters) by typing 0.002 in the Amplitude box. Activate the Smooth option (there are no sharp edges on the skin) and set the 1D Noise option, because Bump height is a one-dimensional value. 1D noise is faster to compute as 3D or 4D noise. Set Input0 to mc, Output to bh and Operation from the General tab to '+'.

The final step is to move the computed bh value to the actual Bump height channel. We do this in an intelligent way, which improves antialiasing quality efficiently.

First, drop a new variable to the Surface geometry shader. You can place it to the beginning of the shader to make the VLS code nicely structured. Change the type of the variable to Float and rename it, for example, as 'aa'. Then add a new Linear object to the end of the shader, set Input0=Surface:Antialiasing and Output=aa. The Multiply value of the Linear object should be relatively high, 500 or so. The value of the Add field should be 1.

The last VSL object is an Operation object - add it to the end. Set its Type to Divide. Input0 should be bh, Input1 = aa and Output = Surface:Bump height. In the General tab, change the assignment Operation from '=' to '+'. It is a good practice to use + operation when modifying the bump height channel. All materials coded like this can be multi mapped to a same target object and bump maps become automatically summed up.

Let's consider the last VSL line a bit more: the computed bump height value is divided by an antialiasing factor, which describes 'information density' around the examined point. The density is very high if you view the object from very far away or in a low angle. When we divide the bump height with a high-density factor, bumps will gradually fade away. This is exactly what we need: small specular highlights from bump peaks, dark grooves and other details which would cause distracting flickering in animations unless a very high ray trace sampling rate were used, now automatically blend towards a nicely behaving smooth surface.

The Surface geometry shader is now ready; make some test renders and play with the Multiply value of the Linear object which scales Antialiasing, to find a suitable value, which does not reduce orthogonal bumps close to the camera too much.

No antialiasing on the left, shader based antialiasing on the right
[Note] Note

If you intend to use displacement mapping with these antialiased bumps, remember to turn antialiasing off by setting Multiply=0 for the Linear VSL object which scales antialiasing channel. The shader computes a different bump height value for a camera view and for light sources. In other words: light sources see the object shape in another way as the camera does. Inconsistent geometry definition by surface displacement will create strange shadows, which almost certainly spoil the image.

Note for advanced users: the antialiasing channel has a 3 dimensional value. The third Z component contains a base value which depends on camera projection and focal length, distance from the examined point and the surface angle against the viewing ray. The first two sub components X and Y incorporate the contribution of the material mapping geometry. Shrinking a parallel map dimensions to one half doubles the antialiasing X and Y factors. This option is useful for example when mapping bitmap textures to surfaces. In our example, parameter is defined using a Default map object, which simply moves the value of UV coords (stored in vertices) to the Map coords channel. The Default map has no 3D space geometry of its own, and therefore it sets all three sub values of the antialiasing channel to equal. In the example above, we could use any of the sub channels; now the first X component is used because of the type conversion rules of VSL.

Surface Properties

Next we will define some color patterns for the skin. The goal is: bumps have a certain base color and the grooves between them have a different color. In addition to this, bumps have some large dark irregular rings on them. Rings do not color grooves.

Add three color variables to the root level of the material and rename them as groove, bump and ring. For each variable, check the Initialize option and set the color value as you find suitable.

In the example scene, grooves are dark bluish red (0.6 0.1 0.2). Bumps are turquoise green (0.1 0.5 0.4) and rings are almost black (0.1 0.1 0.1). Initializing the values into root variables is an efficient solution: it is done only once at the beginning of rendering, not in every examined point. You can also initialize the values efficiently using Constant objects in a Material Initialization shader (bump = Constant(0.1 0.5 0.4) etc.).

Add a new shader object to the material. Its default type, Surface properties, is just what we need. First we compute a value, which adds the rings. Add two variables to the beginning of the Surface properties shader. The first one is a vector variable 'mc' and the second is a float variable 'k'. Set the types and names as before. The variable mc is used the same way as in the Surface geometry shader: we compute a noise distorted parameter into it, so that we can generate irregular patterns. Computation goes now as:

    mc = Linear(Map coords)
    mc += Noise(mc)

So drop a Linear and a Noise object to the Surface properties shader, and set Input and Output channels as shown above. The Multiply factor of the Linear object should be now much smaller, for example (0.2 0.2 0.2). The variable mc will be used for ring patterns, which are much larger as the skin bumps. When we multiply the Map coords with a number smaller than one, the product changes slower in 3D space and hence the patterns become larger. Noise Amplitude can be for example 0.07. The bigger the value, the more irregular rings you will get. Change also the assignment operation of the Noise from = to + in the General tab.

Next add a Cell object to the end of the Surface properties shader. It defines where rings appear. Set Output=k and Input0=mc. Cell options are shown in the image below. It is important that Minimum value is 0 and amplitude is 1, and amplitude variation is 0 - we will use k variable to 'key' the black rings to the skin. Also, cell Degree must be 0 so that the output value jumps straight up to 1 from the base value 0. A higher degree would produce gradient-edged rings. Set also 1D option for faster computation. Size variation can be 0.

The Cell object now produces spots, not rings. Subtracting a slightly modified smaller Cell signal turns spots into rings. To modify the inner spot shape, we add more noise to mc. So, drop a new Noise object to the end of Surface properties. Set its Output to mc and Operation to +:

    mc += Noise(Map coords)

Noise Amplitude must be quite small, for example 0.002 - otherwise rings will easily break. Press Ctrl key down and drag the already added Cell object to the end of the Surface properties shader. This creates a duplicate of the Cell object. Decrease the Size from 0.3 to 0.2. Change the Operation from = to -.

To ensure that the key value k does not get negative, drop a Constant object to the end of the Surface properties shader. Set its Output to k. Change the Operation to max:

    k = max(k, Constant(0))

Now the key value is set. We need one more color variable. Drop a Variable object to the beginning of the Surface properties shader and rename it as 'c' (you can also use longer names if you like). Add an Operation object to the end of the Surface properties shader. Set the Type of the operation to the formula labeled as (1-p1)*p2 + p1*p3. This cryptic looking formula actually does simple linear interpolation. With such an operation, you can blend a color over another color using 'alpha key'. Set Output to the color variable c, Input0 to k, Input1 to bump and Input2 to ring. Remember that the two last input parameters were the colors defined in the root level.

Finally we will key color c - combination of bump and ring colors - with the groove color using the bump height as a parameter. In the Surface geometry shader, we stored non-antialiased bump height to a root level variable bh. Drop a new Linear object to the end, set its Output to k and Input0 to bh:

    k = Linear(bh)

Because the bump height was maximally about 0.005 meters, the Multiply value of the Linear object should be 200. That scaling gives us the suitable keying range 0...1.

We can antialiase also the color pattern efficiently within the shader, just like we did with the bump height. The idea is to blend the color pattern towards the average color. Especially fine details, such as the thin grooves between bumps, need careful antialiasing. We can incorporate the antialiasing step easily into the final color interpolation as follows: drop a new Linear object to the end of the Surface properties shader. Set Output=k, Input0=Surface:Antialiasing and Multiply=100. The Multiply value may need some adjusting later - this is just a guess to get started. Set Operation in the General tab to +. Now the interpolation key value grows by the bump height and by the antialias channel value. Therefore, the groove color will gradually fade away at object edges or when the object gets further away from the camera.

To ensure that the key value - a sum of two factors - does not exceed 1, add a Constant object to the end. Set its output to k and the Constant value to 1.0, and change the General tab's Operation to 'min':

   k = min(k, constant(1))

The last shading action in Surface properties is color interpolation. Ctrl -drag a copy of the already existing Operation object, which computes c, to the end of the shader. Change the Output to Surface:Color, Input1 to groove and Input2 to c. The definition of surface's color is now ready.

Customized Specular Illumination

The third shader will control specular illumination. A metallic shine is very easy to add, so let's make the shine more interesting by controlling its color with a viewing angle dependency. Shine color will change from green to blue, depending on how we look at the surface.

Add two new root level variables to the beginning of the material, where you already placed some other root variables. Both new variables are color variables. Rename the first as speccol1 and the second speccol2. Check Initialize option for both variables and choose the color you like; in our example scene, speccol1 is light green (0.1 1 0.5) and speccol2 light blue (0.1 0.5 1).

Select Specular Color from the wizard selector at the top of the VSL window and hit the Add button. This initializes the illumination shader, which we will customize. Add two variables to the beginning of the new Surface illumination shader. Rename the first color variable as speccol. Change the second variable to type Float and rename it as k. The latter variable is again an interpolation key.

Drop a new Operation object to the Surface illumination shader, just above the last Specular object. Change the operation Type to dot product. Set Output=k, Input0=Light:Ray and Input1=Surface:Ray. Dot product is a quick way to measure angle between two directions: k=0 when rays are orthogonal, -1 or 1 when rays have opposite or parallel direction.

To ensure that the key is positive, drop another Operation object after the previous one. Set the Type to Abs.value, Input0=k and Output=k. Now k is suitable for interpolation. Add a third Operation object above the Specular object and select the interpolation formula (1-p1)*p2+p1*p3 to the Type field. Set Output to speccol, Input0 to k, Input1 to speccol1 and Input2 to speccol2. This operation will interpolate a specular highlight color using the surface viewing angle from two base colors. The result, varying metallic shine, will look interesting especially in animations. As the final step, select the last Specular object and set its Input0 to speccol.

At this point, you should save the project again.