Writing a VEX shader for Houdini
This tutorial is designed to show how easy it is to use VEX to create a simple shader. It’ll also illustrate how to use inline code in your VEX shader, as personally I believe it’s a lot easier to create, maintain, and debug a few lines of code rather than a giant spaghetti network of nodes and connections.
The goal is to create a light wrap shader. This is a common shader used in CG production that can help to suggest a more ambient lighting environment than the default Lambert shading achieves. It basically "pulls" light further round the surface and means that even normals facing away from the light can receive some illumination.
Starting with VEX
The first step is to create a VEX network; this will act as a container of your shader code. There are many different types of VEX network and they each have their own particular context. Each context makes certain information available appropriate to that context, so for example, the VEX Surface Shader context gives you access to the position and normal (in camera space) of the point being shaded.
- Open Houdini(!)
- Choose VEX Builder as current context (i.e. /vex/->)
- Press Tab. Select "VEX Surface Shader"
- Select new node and rename it to "lightWrap"
- In parameters, change the "SHOP Type Name" parameter to "Light Wrap"
A VEX Surface Shader network can be used to define a single instance of a shader or it can be used to create a digital asset. If you create a shader, the "SHOP Type Name" is the name that will be given to the shader that we create. If you create a digital asset, the "SHOP Type Name" is the name that will appear in the Tab menu when you create a new shader using that method.
The new VEX node
So let’s crack on with creating the basic structure of our shading network. The shader is simple, we first need to gather the illumination of the surface from each light while taking into account our light wrapping effect, and we then use that to illuminate the base color of the surface. To do the first stage, we need to use an "Illuminance Loop" node. This is what interrogates each light to find out how much light is hitting the surface.
- Press "i" or <Enter> to navigate into the VEX surface shader node.
- Press Tab. Select "Illuminance Loop".
We now need to give the illuminance node information about the point on the surface that it is shading. To do this we create a Global Variables node and this is where we get access to the context’s variables. We then just need to hook up the position and normal.
- Press Tab. Select "Global Variables".
- Connect "P" from "global1" node to "P" on the "illuminance1" node.
- Connect "N" from "global1" node to "N" on the "illuminance1" node.
The illuminance node has an optimisation feature that allows it to ignore any parts of the surface that face more than a certain angle away from the light. For our purposes however, we want to be able to shade all parts of the surface, even the ones facing away from the light (as that’s the whole point of the light wrap shader). So this is what the next step achieves by setting the angle threshold to 180.
- Middle click on the "angle" input on the "illuminance1" node and select "Create Constant".
- Select the new "angle" node and change the "1 Float Default" parameter to 180.
To get a value out of the illuminance loop, we need to give it something to modify. To do this, we feed a constant value in. This is initialised to black so that we don’t pass in an initial luminance in. If you wanted, you could expose this as a parameter instead of a constant and use it as a default flat ambience control.
- Press Tab. Select "Constant". Rename this node to "illum_value".
- In parameters for "illum", set "Constant Type" to "Color". Also, change "Constant Name" to "illum", and "Constant Label" to "Illumination". Verify that the "Color Default" on the "Color" tab is black.
- Connect "illum" of the "illum_value" node to the white "next" input of the "illuminance1" node.
The start of the VEX lightwrap network
We don’t yet have an output of the illuminance node to connect to. This is because we need to go inside the illuminance node to create a network to define the illumination. Until we do that, no output connection is available.
- Select the "illuminance1" node and press "i" or <Enter> to navigate into the sub network.
- Press Tab. Select "Inline Code".
- Connect "_illum" from "subinput1" to the white "next" input of the "inline1" node.
The "inline1" node is what will contain the code to calculate the light wrapping effect. But first, we need to give it a bit more information. Remember what I was saying about VEX networks each having their own context? Well the Illuminance Loop subnetwork also has it’s own context. Here, the global variables also give you access to the light color and the direction of the incident light from the current light being evaluated. We’ll also obtain the surface normal, although we could equivalently use the N output from the "subinput1" node.
- Press Tab. Select "Global Variables".
- Add "N", "Cl", and "L" from the "global1" node to the "inline1" node in a similar way.
Since we want to give ourselves control over the amount of light wrap, we’ll add a "wrap" parameter to this shader. Parameters are exposed in the user interface of the shader and allow the artist to modify the shader
- Press Tab. Select "Parameter". Rename this node to "lightwrap_param".
- In parameters for "lightwrap_param", change the "Parameter Name" to "wrap" and the "Parameter Label" to "Light Wrap". Set the "Float Range" to be from -0.99 to 10.
- Connect the "wrap" output of "lightwrap_param" to the white "next" input of the "inline1" node.
Typically, the useful range for this light wrap shader will be from 0 (the default Lambertian response) to 1, where the full range of lighting is still available. However, it can be useful to expose a greater range to the artist for "special effects". It just depends how much you trust the users of the shader not to go nuts with putting in daft values!
The next step is to create an appropriate output for the "inline1" node. Note that there is no "color" type for the output, but a vector works fine as it also is made up of 3 floating point components.
- Select the "inline1" node. In parameters, change the "Output 1 Type" to "Vector". Set the "Output 1 Name" to "out" and the second field (the label) to "out" as well.
- Connect "out" output of the "inline1" node to the "_illum" input of the "suboutput1" node.
The start of the illuminance network
The next page shows how to write the code to create the functionality of the light wrap shader.