Skip to content

Instantly share code, notes, and snippets.

Last active August 28, 2022 10:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yratof/0b6f704b87f11f1af48f2163c8c9dde9 to your computer and use it in GitHub Desktop.
Save yratof/0b6f704b87f11f1af48f2163c8c9dde9 to your computer and use it in GitHub Desktop.
OTOY Octane Height_To_Normal.OSL
// Backup of this post
// Just incase it goes down or the forum moves etc/
I’ve been working on this one for a while as I was learning more and more about normal maps in Octane. It went through a lot of changes and adjustments. I use it on a daily basis and whenever I would be annoyed by something or wish that some parameter had a different min/max value, I would change that. It solves a few limitations of the builtin bump map input and it gives you total control.
This node converts a bump/height map into a tangent space normal map based on average, luminance or the intensity of each RGB channel.
Connect an image texture node with your bump map to the Input pin. Give it OSL delayed UV projection. Adjust Radius and Power to control intensity of the map, and that's it! If you find that you need to use too high or too small values, adjust Scale. Default values for quality and source should work just fine most of the time.
- Input
Connect your ImageTexture node with a bump/height map here.
IMPORTANT: For your ImageTexture node to work, you need to connect a Texture Projection node with ‘OSL delayed UV’ projection. Don’t worry, you can still add your own projection in the Projection input of the OSL shader. :)
- Projection
You can add your Texture Projection node here. By default this input should return Mesh UV but currently, in C4D, without anything connected, projection will be box for some reason so make sure you add a Texture Projection node here.
- Source
Here you can choose how the height information is extracted from your texture. Default value is Green because that channel is the least affected by compression artifacts. If you want to mach how your displacement map looks you can choose the Red channel here.
Average – average of all three channels.
Luminance – luminance of a color according to ITU-R.
Red, Green, Blue – Individual channel intensity.
htm vs disp.png
- Quality
Here you can choose the method that does the sampling and conversion. These methods are based on standard convolution filters.
Low quality method is using 3 samples. Cheap + fast. Works great on procedural maps.
Medium - 4 samples – This the default. Almost as good as high with only 4 samples.
High (slow) - 8 samples. This is using a sobol algorithm. Great results but 2x more samples than medium.
What this means is that any node graph connected on Input will be evaluated 3,4 or 8 times in order to create a color of one sample that this shader is returning as a result. If you have a very complex network of procedural nodes or too many layered textures connected on input you will quickly realize what I meant by “(slow)” (and why SD is such a popular tool). In most cases, if you are just converting a bump map into normal map, you will not notice any performance issues but in some extreme cases you might want to use a baking texture after this shader
- Radius
Radius of the area that is sampled. You can think of this as blur. But for bump/normal maps, blur also means “more intense”. Also this is one parameter that we don’t have in the Octane default bump input. If you take a big texture and use transform node to scale it down too much you might run into the issue that you see below. In this OSL shader you can easily avoid that by adjusting Radius to a smaller value.
- Power
This is blending between the resulting normal map and a neutral (0.5,0.5,1) value. Basically this changes the total intensity of your normal map but it also deforms the shape of the features in your map a bit as it gets lower. That ‘deformation’ also tends to reduce artifacts so sometimes it’s a desired effect.
- PowerMap
You can add a texture here to modulate the Power parameter. For example, you can use this to paint-out your normal map from certain areas of your model. This input does not need a OSL delayed UV.
- Z_Power
This changes the influence of the blue/Z channel of the normal map. Reducing this can reduce some artifacts that are typical for normal maps. Also this allows you to preserve the shapes of the features in your normal map as you reduce Power. Since normal maps depend a lot on the viewing angle, sometimes you might want to increase this parameter, the option is there for you.
- Scale
This value is scaling the sensitivity of Radius and Power. Since those parameters will change a lot depending on the resolution or tiling scale of your map, I added this option to avoid dealing with too small or too big values with a lot of zeroes and having to type them in.
- Radius_x_Source and Power_x_Source
These parameters allow you to use height information from your map to scale Radius or Power. Depending on the level of sharpness of the features in your map, sometimes you might want to increase one or the other.
- Falloff
This parameter will reduce the Power of the map on the edges of your object. It is based on view direction and it’s a cheap way of faking parallax but it also helps avoid some annoying specular highlights and other artifacts.
Limitations/To do
Currently, this node doesn’t support Z from XYZtoUVW projection. It is evaluating textures only in UV space. I have an idea how to solve this.
Have fun!
#include <octane-oslintrin.h>
// IMPORTANT: Make sure the input
// is using OSL delayed UV projection.
// This shader will convert a height map to a tangent normal map
// based on the average value, luminance or intensity of one of the RGB channels.
// If you need many of these nodes with Quality set to High,
// please consider using the baking texture node.
// Version: 1.0
// Milan - milanm @ Otoy forums
// 0 - Average, 1 - Luminance, 2 - Red, 3 - Green, 4 - Blue.
float toFloat( int method, color input )
if (method<=0||method>=5)
{return (input[0]+input[1]+input[2])/3;}
else if (method==1)
{return luminance(input);}
return input[method-2];
float liftz(float z, float lift) { return 1-lift*(1-z); }
shader mm_HeightToNormal(
color Input = 1,
point Projection = point(u,v,0),
int Source = 3
[[ string widget = "mapper",
string options = "Average:0|Luminance:1|Red:2|Green:3|Blue:4" ]],
int Quality = 1
[[ string widget = "mapper",
string options = "Low (fast):0|Medium:1|High (slow):2" ]],
float Radius = 1 [[ float sensitivity = 0.01, float min=0, float slidermax=2 ]],
float Power = 1 [[ float sensitivity = 0.1, float min=0, float slidermax=5 ]],
color PowerMap = 1,
float Z_Power = 0.5 [[float min=0,float slidermax=5]],
float Scale = 1 [[ float sensitivity = 0.1, float min=0, float slidermax=1 ]],
float Radius_x_Source = 0.5 [[float min=0,float max=1]],
float Power_x_Source = 0.5 [[float min=0,float max=1]],
float Falloff = 0 [[ float min=0, float max=1 ]],
output color c = 0)
color norm = 1;
float x, y, z, bScale, sc;
float uu = Projection[0];
float vv = Projection[1];
float Power2 = Power*PowerMap[1];
if (Radius_x_Source||Power_x_Source)
bScale = toFloat(Source,_evaluateDelayed(Input,uu,vv));
sc = mix(Power2,Power2*bScale,Power_x_Source)*Scale;
else { bScale = 1; sc = Power2*Scale; }
float o = (( mix(Radius,Radius*bScale,Radius_x_Source) )/1000)*Scale;
float VD = mix(1,dot(-I,N),Falloff);
if (Quality==0)
float ho = o*0.5;
// 3 samples
float m = toFloat(Source,_evaluateDelayed(Input,uu-ho,vv-ho));
float up = toFloat(Source,_evaluateDelayed(Input,uu-ho,vv+ho));
float rg = toFloat(Source,_evaluateDelayed(Input,uu+ho,vv-ho));
x = (m-rg);
y = (m-up);
z = liftz( sqrt(1-((x*x)+(y*y))), Z_Power );
norm = mix(color(0.5,0.5,1),color(x,y,z)/2+0.5,sc*VD);
norm[2] = clamp(norm[2],0.5,1.0);
c = norm;
c = normalize(c*2-1)*0.5+0.5;
else if (Quality==1)
// 4 samples
float dn = toFloat(Source,_evaluateDelayed(Input,uu,vv-o));
float lf = toFloat(Source,_evaluateDelayed(Input,uu-o,vv));
float up = toFloat(Source,_evaluateDelayed(Input,uu,vv+o));
float rg = toFloat(Source,_evaluateDelayed(Input,uu+o,vv));
x = (lf-rg);
y = (dn-up);
z = liftz( sqrt(1-((x*x)+(y*y))), Z_Power );
norm = mix(color(0.5,0.5,1),color(x,y,z)*0.5+0.5,sc*VD);
norm[2] = clamp(norm[2],0.500001,1.0);
c = normalize(norm*2-1)*0.5+0.5;
else if (Quality>=2) // Some plugins don't have a mapper ui
// 8 samples
float up = toFloat(Source,_evaluateDelayed(Input,uu,vv+o));
float dn = toFloat(Source,_evaluateDelayed(Input,uu,vv-o));
float lf = toFloat(Source,_evaluateDelayed(Input,uu-o,vv));
float rg = toFloat(Source,_evaluateDelayed(Input,uu+o,vv));
float ur = toFloat(Source,_evaluateDelayed(Input,uu+o,vv+o));
float dr = toFloat(Source,_evaluateDelayed(Input,uu+o,vv-o));
float ul = toFloat(Source,_evaluateDelayed(Input,uu-o,vv+o));
float dl = toFloat(Source,_evaluateDelayed(Input,uu-o,vv-o));
x = (sc*VD) * -(dr-dl+2*(rg-lf)+ur-ul);
y = (sc*VD) * -(ul-dl+2*(up-dn)+ur-dr);
z = 1;
norm = normalize(vector(x,y,z));
norm[2] = liftz( sqrt(1-((norm[0]*norm[0])+(norm[1]*norm[1]))), Z_Power*0.25 );
c = norm*0.5+0.5;
c[2] = clamp(c[2],0.5,1);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment