A fully procedural material made by galinette.
ISL stands for Indigo Shader Language. It's a functional language that allows you to write shaders for every channel in Indigo materials. With shaders you are not tied to the restrictions of textures any more: they are procedurally computed for every point on the surface.
This tutorial covers:
-What's ISL? And why should I use it?
-What's a functional language?
-How to define functions
-Which channel expects what function parameters an return value?
-What data-types are there?
-Functional, huh? Does that mean I have to use functions instead of operators?
-How to use a shader in a material?
-Going further
ISL stands for Indigo Shader Language. Its a functional language that allows you to write shaders for every channel in Indigo materials. With shaders you're not tied to the restrictions of textures any more because they are procedurally computed for every point on the surface (or in the volume, as of Indigo 2.4's ability to have volumetric shaders).
In functional language there are only functions that are used to compute something, no variables no loops (unless you write them as a function). By functions it means functions in the mathematical sense, it calculates something and returns the result. Lets have a look at this shader function: def doSomething(vec3 pos) real: noise(fbm(pos * 3.0, 8)) This is a function called 'doSomething', it has one parameter, a vector called 'pos' and it returns a real value. It uses two other functions, fbm() and noise().
Like in a mathematical function you calculate the innermost parenthesis first. That means the vector 'pos' is multiplied by 3.0, then passed to the fbm() function which calculates a value of the type 'real', which is then passed to the function noise(), which calculates a 'real' value, which is the return value of the function 'def'. Pretty simple, isn't it?
Now, lets see how to define a function. A ISL function definition always starts with the keyword 'def', needs at least a name and a return value type, and it can also have function parameters: def name(parameters) return_value_type: [...actual function...] Although you can give your functions an any name you want the main function in a channel always has to have the name 'eval'. Which takes us directly to the next topic: different channels expect different parameters and return values!
There are three different channel types, wavelength dependent, wavelength independent and displacement. Wavelength dependent expects a vec3 as a return value, wavelength independent expects a real and displacement expects real and cannot use the position in world-space (vec3 pos) as a function parameter. Here's a table that illustrates all that: C
Channel |
Channel type |
Eval function expected |
Diffuse |
Wavelength dependent |
def eval(vec3 pos) vec3: |
Emission |
Wavelength dependent |
def eval(vec3 pos) vec3: |
Base Emission |
Wavelength dependent |
def eval(vec3 pos) vec3: |
Specular Reflectivity |
Wavelength dependent |
def eval(vec3 pos) vec3: |
Absorption Layer |
Wavelength dependent |
def eval(vec3 pos) vec3: |
Exponent |
Wavelength independent |
def eval(vec3 pos) real: |
Sigma |
Wavelength independent |
def eval(vec3 pos) real: |
Bump |
Displacement |
def eval() real: |
Displacement |
Displacement |
def eval() real: |
Blend |
Displacement |
def eval() real: |
First of all, its very important to know that ISL does not convert values implicitly, so if a function expects an integer, you have to make sure you give pass an integer value instead of a real.
A real value always has to be written as a floating point number, for example 214.0.
Only whole numbers, like 20 or -1545.
There are two constructors for a vec3, vec3(real x) and vec3(real x, real y, real z). The first one applies the number passed to any of the 3 components and the second one sets each component separately. You can access the three components separately with the functions doti(), dotj() and dotk().
Like vec3, only just 2 components.
A boolean value, true or false.
2x2 and 3x3 matrix, I'm not going to talk about these right now.
No, you don't have to use functions as operators, but you can, if you like to, are a hardcore mofo or just a little insane :) Operators are available for multiplication, division, addition and subtraction (*, /, + and -, would you believe it?) for every data-type that supports them, but the order of operands is important sometimes, for example: 0.333 * vec3(5.2) will not work since it expects the vec3 first. Vec3(5.2) * 0.333 works. The equivalent functions are called mul(), div(), add() and sub().
Your exporter most likely has an option to use ISL shaders, also, you can use ISL in the Indigo Material Editor it allows you to use ISL shaders. And if you're hardcore, you can edit the .igs file generated by you exporter and insert your shaders manually, here's how you do it: Open the .igs file and look for a material. I'll just assume we found a phong material:
<material>
<name>phong_mat</name>
<phong>
<ior>1.466</ior>
<exponent>
<constant>800</constant>
</exponent>
<diffuse_albedo>
<constant>
<rgb>
<rgb>0.588235 0.588235 0.588235</rgb>
<gamma>2.2</gamma>
</rgb>
</constant>
</diffuse_albedo>
</phong>
</material>
What you have to do is, is to replace the <constant>...</constant> XML elements (and anything in between) in the diffuse_albedo channel with this:
<shader>
<shader>
<![CDATA[
#paste your shader in here, oh by the way: every line starting with # is a comment
)]]>
</shader>
</shader>
Then paste your shader in between '<![CDATA[' and ')]]>'.
For a complete list of all available functions, have a look at the ISL section in the 'Indigo Technical Reference.pdf' and the 'ISL_stdlib.txt' in the Indigo folder. Also, more ISL tutorials are coming!
ISL stands for Indigo Shader Language, the language for creating procedural materials in Indigo. Since it's a functional language, it can be a bit tricky to create some patterns that are easy enough in an imperative language. For example, lets say you want to write a shader to make a polka-dot pattern. In an imperative language you might write some code like
def fillWithColour(background_colour): for(int x=0; x<W; ++x) { for(int y=0; y<H; ++y) { drawDot(x, y, dot_colour); } }
But in ISL, you can't do things this way. You have to write a function that returns the colour, and that depends only on the position and/or UV-coordinates of the current surface point being shaded, e, g:
def getColourForPoint(u, v): if( the point (u, v) is inside a dot ){ return dot_colour } else { return background_colour }
This tutorial shows you how to use such a functional technique to create regularly repeating patterns, like polka-dots, with ISL.
Lets start by discussing the fract function.
As described in the Indigo Renderer Manual, fract takes a single real number, and returns a real number:
fract(x) = x - floor(x)
The useful thing about this function, is that it repeats regularly across the real number line, with period 1. We can use this function to create more complicated behaviour.
So lets say we want to create some stripes, such that they alternate in the U direction of the UV coordinates. Suppose we have a foreground and background colour.
Using the fract function above, we can can assign the foreground colour when fract(x) < C, and the background colour when fract(x) >= C, where C is some constant between 0 and 1. If C is 0.5, the stripes will have the same width as the background stripes.
Let's see how that looks in real ISL:
<material> <name>previewmaterial</name> <diffuse> <albedo> <shader><shader><![CDATA[ def eval(vec3 pos) vec3 : if( fract(doti(getTexCoords(0))) < 0.5, vec3(0.9, 0.0, 0.0), # Red vec3(0.2, 0.2, 0.2) # Dark Grey ) ]]></shader></shader> </albedo> </diffuse> </material>
And the resulting render:
The shader with fract(x), so slowly repeating stripes.
In this example the foreground colour is red, and the background colour is dark grey. This example looks a bit weird because the stripes are large compared to the model. We can solve this problem by multiplying the UV coordinates by a number greater than one before we pass the value to fract, e.g we could use something like fract(10 * x)
The ISL is then:
def eval(vec3 pos) vec3 : if( fract(doti(getTexCoords(0)) * 10.0) < 0.5, vec3(0.9, 0.0, 0.0), # Red vec3(0.2, 0.2, 0.2) # Dark Grey )
And the resulting render is:
The shader with fract(10 * x) so more repeating stripes.
So, at this point in the tutorial, we have more-or-less solved the problem of how to create regularly-repeating patterns, at least with respect to the U coordinate (e.g. in one direction).