Using the OSL shader in Redshift (Part 1)

Netinho Da Costa
Netinho Da Costa
  • Updated

My goal is to make you comfortable using OSL code inside of your Redshift shaders. We will do this by answering 110 questions related to redshift and OSL. 
(I will number these questions and mark them all in green for you)

( 1 ) What is OSL (open shading language) and (2) why would you want to learn and use it inside of your Redshift shaders?

OSL (Open Shading Language) is a small C-like language for writing shaders. A shader is just a function that runs for every shading point and outputs values. These values will help you to manipulate colors and greyscale values and in general to create all kinds of handy procedural texture maps. The fact that it runs over every shading point is the special bit. ( 3) What does a shading point mean? A shading point is a single tiny location on the surface of an object where the renderer calculates color and lighting.

When Redshift renders, it does not shade the whole object at once. It picks one small surface location, asks the shader “what color are you here?”, computes the result, then moves to the next location. So your OSL code runs once for each tiny surface point, not once per object.

If we go by the official definition of OSL as published by Larry Gritz in the open shading language 1.11  language specification. On the first page it's described as a programming language made specifically for writing shaders in advanced renderers.  It is used to describe how materials, light displacements and patterns behave. Based on that description, the focus is to a large degree on the concept of closures. In OSL a closure is simply a built-in material behavior like diffuse, glossy reflection, glass/refraction or emission. However in our case when we use OSL inside of the OSL shader Node with Redshift, it's best for you to see it as a way to manipulate colors, float values, vector values and to build or mix procedural textures.  In other words, to manipulate roughness, colors, masks and so on. Once you get the hang of it it will prove to be an invaluable tool for you to be more creative with your shaders,  it will allow you to bypass technical limitations  and it will prove to be a handy problem-solving tool for all kinds of situations.

You can find many examples of OSL scripts here:
https://github.com/Maxon-Computer/Redshift-OSL-Shaders

When using the OSL Node inside of Redshift you can write your own OSL code or just load an OSL file that you have available. 

 

The basic anatomy of an OSL script

Let's start by analysing a short and simple OSL script.  You might not understand everything that is inside of it right now, but I can assure you you will in a little bit. Before learning details let's first focus on two questions. 
(4) What is the difference between OSL code and code that we can find in other programming languages? And 
(5) What is the anatomy of an OSL script?  in other words what are the building blocks that you need to use to make it work?

 

 

As you can see, OSL code does not look like python or Javascript for example, where you first define methods, and then call them to make them do something. you can use functions in your OSL code. but on top of that, OSL has a specific structure to it.


( 6 ) What is the word "shader" doing in that example and is it mandatory? Every OSL 'shader' that you will create needs a name. That's why this script starts with "shader myShader". The official word "shader" is important, it is a requirement for an OSL shader. A shader in OSL is the main program block that the renderer executes to compute values at each shading point. It defines:

• The inputs and outputs (its interface)
• The logic inside { } that calculates the result

Right after that word you can give it a name of your choice that will describe your shader well.

Right after that you will see some code that you might not recognise right away ( 7 ) What does the section between ( ... ) mean and ( 8 ) what does the section between { ... }  mean? Both these sections are also mandatory for every OSL code that you write. Let's break them down so they feel less alien.

The parameter list ( ... )

Let's first get to know the  parameter list. It is the interface definition of the shader. It is the part between the parentheses right after shader Name. Basically it It defines what inputs and outputs the shader has.

It does not contain executable logic.
It is processed before rendering begins so the renderer can build the UI and node ports. 

( 9 ) What are the main things/characteristics to remember about the parameter list section?

  • Mandatory section in every shader
  • Located between ( ... ) after the shader name
  • Used only for declaring parameters
  • Defines data types (float, color, int, etc.)
  • Sets default values
  • Marks exported values with output
  • Creates UI controls for inputs
  • Creates output ports for output parameters
  • Does not execute per shading point
  • Cannot contain logic or calculations

 

The Code Block (Shader Body) { ... }

Below the parameter list section you will fins a code block section. That's the section between { ... }.  This section is the executable part of the shader. It contains the logic that runs at every shading point.
This is where calculations happen and outputs are assigned.

 

( 10 ) What are the main things/characteristics to remember about the code block  section?

  • Mandatory in every shader
  • Located after the parameter list
  • Contains executable code
  • Runs once per shading point
  • Can declare local variables
  • Can use control flow (if, for, etc.)
  • Must assign values to output parameters
  • Can call helper functions
  • Cannot contain parameter declarations

Before we examine we examine a few extra code block examples, 

let's just have fun with what we learned already.

 

 

Example & experiments wave 1

 

Example 1 - A simple fill color shader

shader myShader
(
   color myColor = color(1,0,0),
   output color outputColor = color (0,0,0)
)
{
   outputColor = myColor;
}

This code creates a simple 'fill' color shader. This shader simply outputs a solid color. Whatever color you set in myColor becomes the final visible color. Since its our first bit of code, let's analyse it in a bit more detail.

`shader myShader ( ... )`

You already know that the parentheses contain the shader’s parameters. This is where you declare what can go in and what comes out.  ( 11 ) Why does the first line(s) inside the parameter list end with a comma? OSL requires parameters to be written as a comma-separated list inside these parentheses. If you forget a comma between parameters, the code will not compile because OSL cannot tell where one parameter ends and the next begins. That is why the first line inside the parameters list ends with a comma.

`color myColor = color(1,0,0),`

  • The first `color` tells OSL what kind of data this is. OSL is strongly typed, ( 12 ) What does strongly types mean? It simply means that every variable must declare its type. In the example above the data 'type' was color.
  • `myColor` is the name of this input.
  • `= color(1,0,0)` gives it a default value. OSL requires parameters to be initialized so they always start with valid data.
  • Like stated above, the comma at the end is mandatory here because another parameter follows.

`output color outputColor = color(0,0,0)`

  • `output` marks this parameter as something the shader sends out. Without the `output`, it would just be an internal variable and nothing would leave the shader. This is an important word. 
  • `color` again declares the type.
  • `outputColor` is the name of the output.
  • `= color(0,0,0)` gives it a required default value, so it is valid even before any assignment happens.
  • This last line inside that block has no comma at the end because it is the last parameter.

Inside the braces:

`outputColor = myColor;`

  • This is the actual instruction that runs.
  • It copies the value of `myColor` into `outputColor`.
  • The semicolon is required in OSL to mark the end of a statement 
    (only required in this { ... }  section as the ( ...) section only declares parameters)

That is all this shader does: declare one input color (so the user can choose one), declare one output color , then pass the input directly to the output. (so your OSL shader tells Redshift what to show)


 

Built-in global variables in Redshift OSL

When using OSL we have access to some handy built-in global variables (values). 

( 13 ) What are some of the main built-in variables that we have available to us?  At every tiny surface location, Redshift provides built-in global variables such as P (position), N (normal), Ng (geometric normal), I (incoming ray direction), u (horizontal UV coordinate), and v (vertical UV coordinate). ( 14 ) What do all these value types/variables allow us to do? Before providing several code examples using them, let's take a look at them one by one.

Built in variable : P

Type: `point`

What it represents:  
The 3D position of the current shading point. A point has three numbers inside it: X, Y, Z.

Think of it like this:  
At every tiny spot on your object, Redshift knows “where in 3D space that spot is.” That location is `P`.

Built in variable : U and V

Type: `float`

What it represents:  
The U coordinate from the object’s UV map at the current shading point. Usually goes from 0 to 1 across the surface. The V coordinate from the UV map. Usually goes from 0 to 1 vertically.

Built in variable : N

Type: `normal`

What it represents:  
The shading normal direction at the shading point. A normal is a direction, not a position.

A normal has 3 components (x,y,z) like a vector, but conceptually it means “which way the surface is facing.”

Built in variable : Ng

Type: `normal`

What it represents:  
The geometric normal. This is the “real” normal from the actual geometry, without shading tricks.
In many cases:

  • `N` can be influenced by smoothing or normal maps.
  • Ng` stays tied to actual geometry.

The visual difference between `N` and `Ng` becomes obvious when you add smoothing or normal maps. On a perfectly flat plane they may look similar.

Built in variable : I

Type: `vector`

What it represents:  
Direction of the incoming ray at the shading point. Think “the direction the camera or light ray came from when it hit the surface.” 

Sometimes you will want to use the value -I. ( 15 )Why would you ever want to use minus I ?  
`I` points in the ray direction as it arrives. Using `-I` flips it, so the direction points outward relative to the surface, which is often what you want when comparing with `N`.

 

One rule that applies to every `[index]` you saw

When you see:

`Something[0]`  
`Something[1]`  
`Something[2]`

That means:

Take a 3-part value and extract one part. You will see this a lot in the examples below. Tip: For a better intuitive feel about this I can recommend watching any general/basic "array" related programming video on YouTube.

This only makes sense for types like:

- point
- vector
- normal
- color

Because they contain 3 components.

 

Example 2a - A dual fill color shader, but now it splits the color based on the position of a specific shading point

shader UseV_Split
(
  color BottomColor = color(0,1,0),
  color TopColor = color(1,1,0),
  output color OutColor = color(0,0,0)
)
{
  if (v < 0.5)
      OutColor = BottomColor;
  else
      OutColor = TopColor;
}

Now that we are familiar with some built-in variables. Let's play with a few of them a little bit more. In this next example, we use v. (The vertical UV position)
v represents the vertical UV coordinate of the current shading point, typically ranging from 0 at the bottom of the UV space to 1 at the top.

(16 ) How does the shader utilizes/understand the v (from UV) coordinate? v ranges from 0 to 1 along the V axis of the UV map, but which way that appears on your object (up, down, left, right) depends on how the UVs are oriented. On my example below the V axis is oriented left-to-right, so the split looks left-to-right.

The example code above compares that value to 0.5 by using if (v < 0.5). If the vertical UV position is below the halfway mark, the color becomes BottomColor. If it is above the halfway mark, the color becomes TopColor.

The color split happens because each shading point makes its own decision based on its own v value. That's the power of using OSL shaders. This example illustrates well what the power is of calculating on a per shading point basis. 

 

Now, let's add a way to manipulate where the split happens live. We will do this by adding a new parameter to the UI (inside the parameter list)

 

Example 2b - A dual fill color shader, but now it splits the color based on the position of a specific shading point, the split point is now editable.

shader UseV_Split
(
   color BottomColor = color(0,1,0),
   color TopColor = color(1,1,0),
   float Split = 0.5 [[ float min = 0.0, float max = 1.0 ]],
   output color OutColor = color(0,0,0)
)
{
   if (v < Split)
       OutColor = BottomColor;
   else
       OutColor = TopColor;
}


Split is now a float parameter that appears as a slider in the UI. It ranges from 0 to 1, which matches the range of the UV coordinate v. SO previously we had  if (v < 0.5) now we have if (v < Split).
Instead of always comparing v to 0.5 (like in our previous example), the shader compares it to whatever value you set with the slider. Again, the split is available as an UI element to the user because it's added inside the ( ... ) parameter list section. 

Moving the slider changes where the color boundary happens across the UV direction.

 

Metadata 

You might have noticed somethign new in that code. (17) How did that [[ ... ]] line work? That was what we call metadata. The [[ float min = 0.0, float max = 1.0 ]] part is metadata. It does not affect the math. It tells Redshift how to build the UI control.

In this case: float Split = 0.5 [[ float min = 0.0, float max = 1.0 ]]

The parameter becomes a float slider.

The slider is limited between 0.0 and 1.0.

The default position of the slider is 0.5.

 


Example 2c - A triple fill color shader, splits based on both U and V

 

shader UseV_Split
(
   color BottomColor = color(0,1,0),
   color TopColor = color(1,1,0),
   color RightColor = color(1,0,0),
   float VSplit = 0.5 [[ float min = 0.0, float max = 1.0 ]],
   float USplit = 0.5 [[ float min = 0.0, float max = 1.0 ]],
   output color OutColor = color(0,0,0)
)

{
   if (u > USplit)
       OutColor = RightColor;
   else if (v < VSplit)
       OutColor = BottomColor;
   else
       OutColor = TopColor;
}

 

As you can see in the code example above float VSplit = 0.5 and float USplit = 0.5 are new input parameters of type float, meaning they store decimal numbers; in our case 0.5 

The if and else statements create decision logic. ( Note: if this sounds alien to you I can recommend any of the thousands YouTube videos explaining the general basics logic of if/else statements as it is something very common to any programming language.)
if (u > USplit) means: if the current shading point’s u value is greater than the split value, use RightColor.
else means: if that condition is not true, run the next block.

If you are fancy, you can also create nested if-statements like in the example below. (18) What is a nested if-statement? Inside that else, there is another if (v < VSplit)  this is called a nested if-statement, meaning one decision is placed inside another. (If this is true do that, but it it WAS true, also check if that other thing is also true and do THAT instead)

Similar to the entire code block there, the { } braces group code together.
Everything inside a pair of braces belongs to that condition. (19) do these indentations matter to the functionality of the code? Indentation (the spacing to the right) does not affect how the shader runs, but it makes the structure readable so you can clearly see which lines belong to which if or else.

So the shader first checks the horizontal split (u), and if that does not apply, it then checks the vertical split (v).

Example: Nested if-statement example (same function as above)

{
   if (u > USplit) {
       OutColor = RightColor;
   } else {
       if (v < VSplit) {
           OutColor = BottomColor;
       } else {
           OutColor = TopColor;
       }
   }
}

 

 

Example 3 - World height mask

The example below simply calculates of the shading point on your object is below or above a certain 'world' position of your scene.

 

shader HeightMask
(
   output color OutColor = color(0,0,0)
)
{
   if (P[1] > 0.0)
       OutColor = color(1,1,1);
   else
       OutColor = color(0,1,0);
}

(20 ) How is this shader calculating a height mask, how does it know that P[1] means height? If the shading point position IS above a certain P[1] it will have a different color.  (this means Position Y , to reiterate, Position X is zero, Y is one, and Z is two)
 

 

Example 4a - Using the Normal ( N ) built in variable to create sun-wear.

In these first examples we are still focusing on using the buil-in variables that we have available to us. The next example addresses a very common production need. Let's make shading points facing upward slightly brighter or more worn. Let's create a sun-wear effect. Only the shading points on the TOP side of geometry are brighter. We can easily calculate this based on the Normals.

 

shader TopWearMask
(
   float Strength = 1.0,
   float Tightness = 1.0,
   output float Mask = 0.0
)
{
   float up = clamp(N[1], 0.0, 1.0);
   Mask = pow(up, Tightness) * Strength;
}

 

This shader creates a black-and-white mask that highlights surfaces that face upward. N[1] reads how much a tiny piece of the surface is pointing upward. The clamp(N[1], 0.0, 1.0) part forces that value to stay between 0 and 1, so anything facing downward becomes 0 (black), and upward-facing parts stay between 0 and 1.

( 21 ) How does the clamp function work? Clamp works as follows: clamp(value, min, max). You will likely use this a lot. This forces a number to stay inside a range. If the number is smaller than min, it becomes min. If it is bigger than max, it becomes max. Otherwise it stays the same.

Examples - again the formula/syntax is clamp(value, min, max).:

clamp(-0.2, 0.0, 1.0) becomes 0.0
(How to 'read' this? If the value (leftmost number) is minus zero point two, it becomes zero, because its smaller than the assigned min value)
clamp(0.7, 0.0, 1.0) stays 0.7
clamp(1.8, 0.0, 1.0) becomes 1.0

In our example code: N[1] can be negative (surface points downward) or positive (points upward).  clamp(N[1], 0.0, 1.0) turns anything that points sideways/down into 0, and keeps upward values between 0 and 1.

( 22 ) How do I use the pow() function?

pow(up, Tightness) makes the effect sharper or softer. higher Tightness values make only the most upward-facing areas stay bright. Strength simply makes the final result stronger or weaker. The final Mask is just a grayscale value you can use to add wear, snow, dirt, or any effect only on top-facing surfaces.

How does this work? The formula is pow(base, exponent)
This means “raise base to a power.” This allows us to 'reshape' the number.

Example with base between 0 and 1:

pow(0.5, 1) = 0.5 (no change)
pow(0.5, 2) = 0.25 (smaller)
pow(0.5, 5) ≈ 0.031 (much smaller)

That is exactly why it’s used here:
pow(up, Tightness) makes the mask more picky. up was the first float value we used (what we just clamped), whereas tightness was the slider we provided.

Tightness = 1 → gradual, soft mask
Tightness = 2 → only more-upward areas stay bright
Tightness = 5 → almost only the most upward-facing areas stay bright

 

( 23 ) What math functions are available to us when coding in OSL? Standard math and vector functions available in OSL include:

  • A list of all available math functions: 
    abs, acos, asin, atan, atan2, ceil, clamp, cos, cosh, cross, degrees, determinant, distance, dot, erf, erfc, exp, exp2, faceforward, filterwidth, floor, fmod, log, log2, log10, max, min, mix, mod, noise, normalize, pow, radians, reflect, refract, round, sign, sin, sinh, smoothstep, sqrt, step, tan, tanh, transform, transpose, luminance, select, comp.

     

Example 4b - Using the Normal ( N ) built in variable to create a slope mask.

In the next example we will create a handy slope mask. We will utilize the N (normal) built in variable once more in this example. This example will detect steep slopes.

Flat surfaces return 0.
Steep slopes return 1.

Let's say you want to add a rocky shader on the cliffs (steep sections) and grass on the flat sections.

 

shader SlopeMask
(
   float Threshold = 0.5,
   output float Mask = 0.0
)
{
   if (abs(N[1]) < Threshold)
       Mask = 1.0;
   else
       Mask = 0.0;
}
 

 

Example 5 - UV tiling without a texture (to check your UVs with)

Quick checker pattern without bitmap.

shader SimpleChecker
(
   float Scale = 10.0,
   output color OutColor = color(0,0,0)
)
{
   float u2 = floor(u * Scale);
   float v2 = floor(v * Scale);
   if (fmod(u2 + v2, 2.0) == 0.0)
       OutColor = color(1,1,1);
   else
       OutColor = color(0,0,0);
}

This one might be handy to debug your UVs with. It can help you to detect texture stretching, or for quick pattern testing.

TEST: Now based on everything you learned so far and without looking at the code snippet below.
Can you rewrite the code to allowed you to pick the two colors that are used inside the checker pattern as UI elements? I recommend you to try (and if needed fail a few times) doing that. That's where the actual learning often happens.

 


The solution to the test question above.

shader SimpleChecker
(
   float Scale = 10.0,
   color ColorA = color(1,1,1),
   color ColorB = color(0,0,0),
   output color OutColor = color(0,0,0)
)
{
   float u2 = floor(u * Scale);
   float v2 = floor(v * Scale);
   if (fmod(u2 + v2, 2.0) == 0.0)
       OutColor = ColorA;
   else
       OutColor = ColorB;
}


Example 6a - A simple circle/dot pattern in the middle of your shader

In the next example we will calculate a simple dot/circular shape in the middle of your shader.

shader UV_Circle
(
   color BackgroundColor = color(0,0,0),
   color CircleColor = color(1,0,0),
   float Radius = 0.25,
   output color OutColor = color(0,0,0)
)
{
   float dx = u - 0.5;
   float dy = v - 0.5;
   float dist = sqrt(dx*dx + dy*dy);
   if (dist < Radius)
       OutColor = CircleColor;
   else
       OutColor = BackgroundColor;
}

Let's analyse this in a bit more detail

  • u and v are the UV coordinates (0 to 1).
  • 0.5, 0.5 is the center of UV space.
  • dx and dy measure how far the current shading point is from the center.
  • sqrt(dx*dx + dy*dy) calculates the distance from the center. If that distance is smaller than Radius, the point is inside the circle.

Now that we know what parts we uses, let's analyse it in a bit more detail. As this example is a good example showing how using a bit more math helps is to create all kinds of shaders. 

We used float dx = u - 0.5;. his part measures how far the current UV location is from the center of the UV space.

u (yes the built-in U and V variable) 

goes from 0 to 1 across the surface.
Subtracting 0.5 shifts that range so the center becomes 0.
So:

Left edge or our circle → about -0.5

Center → 0

Right edge of our circle → about +0.5

dx now represents horizontal distance from the center.

float dy = v - 0.5;
Same idea, but vertically.
dy represents vertical distance from the center.

Then we used float dist = sqrt(dx*dx + dy*dy);
This calculates the actual distance from the center point.
Perhaps you recognise that formula from high school? It’s using the Pythagorean formula:
distance = √(x² + y²). (Now you can finally brag that you used it in a real-live scenario)

So dist becomes:

Small near the center, and larger as you move away based on our radius input

 

 

Example 6b - A simple grid of dot patterns 

What if we are not satisfied with a simple dot? What if we want an army of dots distributed as a grid all over our We can do that as well. Do you remember that we have the build in values U and V available to us? As these values are just that "values". Did it occur to you that you can multiply these values for nice effects?

 

shader UV_CircleGrid
(
   float Tiles = 10.0,
   float Radius = 0.25,
   color BackgroundColor = color(0,0,0),
   color CircleColor = color(1,1,1),
   output color OutColor = color(0,0,0)
)
{
   float fu = u * Tiles;
   float fv = v * Tiles;
   float cellU = fu - floor(fu);
   float cellV = fv - floor(fv);
   float dx = cellU - 0.5;
   float dy = cellV - 0.5;
   float dist2 = dx*dx + dy*dy;
   float r2 = Radius * Radius;
   if (dist2 < r2)
       OutColor = CircleColor;
   else
       OutColor = BackgroundColor;
}

In this example float fu = u * Tiles; and float fv = v * Tiles; scale the UVs so instead of going 0 to 1 once across the object, they repeat Tiles times, which creates a grid of cells.

For this effect, we also need to isolate the “position inside the current cell.”
floor(fu) gives the whole-number cell index (0, 1, 2, 3…). Instead of the regular float value that we don't need
cellU = fu - floor(fu) removes that whole-number part and keeps only the fractional part, so cellU is always between 0 and 1 inside each cell. Same for cellV. This is what makes the pattern repeat cleanly.

How can we then draw the same circle inside every cell?
dx = cellU - 0.5 and dy = cellV - 0.5 shift the cell coordinates so the center of each cell becomes (0,0). Then dist2 = dx*dx + dy*dy measures how far the current point is from the center of its cell (without using sqrt). If that distance is smaller than Radius, we output CircleColor; otherwise we output BackgroundColor.

Now each shading point gets its own u and v. We first map that point into a repeated grid (many cells), then for the cell the point lands in, we compute its local position inside that cell and apply the same “circle test” there, producing a grid of identical circles.


 

Was this article helpful?

/

Comments

0 comments

Please sign in to leave a comment.