Triplanar mapping shader with normal map support

Post those lines of code you feel like sharing or find what you require for your project here; or simply use them as tutorials.
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Triplanar mapping shader with normal map support

Post by Mel »

This is a triplanar mapping shader, What it does is to give any object the ability to pick its mapping coordinates from its object space, that are resized accordingly to a tileSize parameter (in world units), which can be freely chosen, The support for normal mapping is achieved using the properties of the normal maps that they flow in the direction of the texture coordinates, which in this case, are axis aligned, hence, their correct calculation is very fast. So, there is no need for neither texture coordinates, nor tangent space vectors in the original mesh. making this shader specially useful for meshes that don't have mapping coordinates, or that are generated procedurally, and is very useful when it is used as a terrain shader.

The textures then are blended together using the object space normal, and the rest is the lighting equation, which is performed over a directional light, and uses the object's material ambient, diffuse and specular colors to render.

Finally, it comes in 2 flavours: Open GL GLSL and DirectX 9C HLSL. Their functionality is identical (at least, rendermonkey shows identical results)

In alphabetical order:
GLSL

vertex program
--------------

Code: Select all

//Triplanar coordinate maps with normal maps support
//by Santiago A. Navascues
 
//Use at your own risk. I make no warranties. 
//If you want to use it, give the proper credits
 
uniform mat4 matWorld;
uniform vec4 eyePosition;
uniform float tileSize;
 
varying vec2 TexCoordX;
varying vec2 TexCoordY;
varying vec2 TexCoordZ;
varying vec3 ViewDir;
varying mat3 tangentSpace;
     
void main( void )
{
   gl_Position = ftransform(); 
   vec4 worldPos = matWorld * gl_Vertex;//So we obtain the world position
   
   TexCoordX = worldPos.zy/tileSize;//here are our texture coordinates...
   TexCoordY = worldPos.xz/tileSize;
   TexCoordZ = worldPos.xy/tileSize;
   
   //binormal and tangent become normal dependant
   
   vec3 xtan,ytan,ztan;
   vec3 xbin,ybin,zbin;
   
   xtan = vec3(0,0,1);//tangent space for the X aligned plane
   xbin = vec3(0,1,0);
   
   ytan = vec3(1,0,0);//tangent space for the Y aligned plane
   ybin = vec3(0,0,1);
   
   ztan = vec3(1,0,0);//tangent space for the Z aligned plane
   zbin = vec3(0,1,0);
   
   vec3 n = gl_Normal;
   n*=n;
   
   vec3 worldNormal = gl_Normal;
   vec3 worldBinormal = xbin*n.x+ybin*n.y+zbin*n.z;//Average Binormal
   vec3 worldTangent = xtan*n.x+ytan*n.y+ztan*n.z;//Average Tangent
   
   ViewDir = (worldPos-eyePosition).xyz;
   
   //This is done so it can be rotated freely
   
   worldTangent = (matWorld*vec4(worldTangent,1)).xyz;
   worldBinormal = (matWorld*vec4(worldBinormal,1)).xyz;
   worldNormal = (matWorld*vec4(worldNormal,1)).xyz;
   
   tangentSpace[0] = worldTangent;
   tangentSpace[1] = worldBinormal;
   tangentSpace[2] = worldNormal; 
}
fragment program
----------------

Code: Select all

//Note this is the 6 textures version, hence, you will need to recompile Irrlicht if you want
//to use it.
 
//Can be reduced to a 4 textures version if you sample the baseX instead of the baseZ texture
//and the normalX instead of the normalZ texture.
 
//And can be reduced further more if you use a single normal map, and sample it from every side
//everytime, leaving space for a shadowmap, the shader does the rest.
 
//By the way, lighting is performed in worldspace, because otherwise, the normal calculation
//and the rotation support could be too complex.
 
uniform vec4 ambientColor;
uniform vec4 diffuseColor;
uniform vec4 specularColor;
uniform float specularPower;
uniform vec3 lightDirection;
 
uniform sampler2D baseX;
uniform sampler2D baseY;
uniform sampler2D baseZ;
uniform sampler2D normalX;
uniform sampler2D normalY;
uniform sampler2D normalZ;
 
varying vec2 TexCoordX;
varying vec2 TexCoordY;
varying vec2 TexCoordZ;
varying vec3 ViewDir;
varying mat3 tangentSpace;
 
void main( void )
{
   vec3 normal = tangentSpace[2];
   
   vec3 n = normal;
   n*=n;
   
   vec4 col = texture2D(baseX,TexCoordX)*n.x+
              texture2D(baseY,TexCoordY)*n.y+
              texture2D(baseZ,TexCoordZ)*n.z;
   
   vec4 nrm = (2.0*texture2D(normalX,TexCoordX)-1.0)*n.x+
              (2.0*texture2D(normalY,TexCoordY)-1.0)*n.y+
              (2.0*texture2D(normalZ,TexCoordZ)-1.0)*n.z;
   
   vec3 realNormal = normalize(tangentSpace*nrm.xyz);
   vec3 lightDir = normalize(lightDirection);
   vec3 viewDir = normalize(ViewDir);
   
   float NdL = max(0.0,dot(realNormal,lightDir));
   
   vec3 reflVect = normalize(reflect(lightDir,realNormal));
   float RdL = max(0.0,dot(reflVect,viewDir));
      
   vec4 ambient,diffuse,specular,fresnel;
   
   ambient = col*ambientColor;
   diffuse = diffuseColor*col*NdL;
   specular = specularColor*pow(RdL,specularPower)*col;
   fresnel = max(0.0,dot(realNormal,viewDir))*specularColor;
   
   gl_FragColor = ambient+diffuse+specular+fresnel;
       
}
HLSL

vertex shader
-------------

Code: Select all

//Triplanar coordinate maps with normal maps support DirectX 9c version
//by Santiago A. Navascues
 
//Use at your own risk. I make no warranties. 
//If you want to use it, give the proper credits
 
float4x4 matWorldViewProjection;
float4x4 matWorld;
float tileSize;
float4 viewPos;
 
struct VS_INPUT 
{
   float4 Position : POSITION0;
   float2 Texcoord : TEXCOORD0;
   float3 Normal :   NORMAL0; 
};
 
struct VS_OUTPUT 
{
   float4 Position :        POSITION0;
   float2 TexcoordX :       TEXCOORD0;
   float2 TexcoordY :       TEXCOORD1;
   float2 TexcoordZ :       TEXCOORD2;
   float3 viewDirection   : TEXCOORD3;
   float3x3 tangentSpace :  TEXCOORD4;
};
 
VS_OUTPUT vs_main( VS_INPUT Input )
{
   VS_OUTPUT Output;
 
   Output.Position         = mul( Input.Position, matWorldViewProjection );
   float4 realPos          = mul( Input.Position,matWorld);//So we obtain the world position
   
   Output.viewDirection = (realPos-viewPos).xyz;
   
   Output.TexcoordX         = realPos.zy/tileSize;//here are our texture coordinates...
   Output.TexcoordY         = realPos.xz/tileSize;
   Output.TexcoordZ         = realPos.xy/tileSize;
   
   float3 worldNormal = Input.Normal;
   float3 n = worldNormal;
   n*=n;
   
   //binormal and tangent become normal dependant
   
   float3 xtan,ytan,ztan;
   float3 xbin,ybin,zbin;
   
   xtan = float3(0,0,1);//tangent space for the X aligned plane
   xbin = float3(0,1,0);
   
   ytan = float3(1,0,0);//tangent space for the Y aligned plane
   ybin = float3(0,0,1);
   
   ztan = float3(1,0,0);//tangent space for the Z aligned plane
   zbin = float3(0,1,0);
   
   float3 worldBinormal = xbin*n.x+ybin*n.y+zbin*n.z;//Average Binormal
   float3 worldTangent = xtan*n.x+ytan*n.y+ztan*n.z;//Average Tangent
   
   //This is done so the object can be later rotated
   
   worldNormal = mul(matWorld,worldNormal);
   worldBinormal = mul(matWorld,worldBinormal);
   worldTangent = mul(matWorld,worldTangent);
   
   Output.tangentSpace[0]   = worldTangent;
   Output.tangentSpace[1]   = worldBinormal; 
   Output.tangentSpace[2]   = worldNormal;
      
   return( Output );
   
}
Pixel shader
------------

Code: Select all

//Note this is the 6 textures version, hence, you will need to recompile Irrlicht if you want
//to use it.
 
//Can be reduced to a 4 textures version if you sample the baseX instead of the baseZ texture
//and the normalX instead of the normalZ texture.
 
//And can be reduced further more if you use a single normal map, and sample it from every side
//everytime, leaving space for a shadowmap, the shader does the rest.
 
//More good news: Only PS2 required!
 
float4 fvAmbient;
float4 fvSpecular;
float4 fvDiffuse;
float fSpecularPower;
float3 lightDirection;
 
sampler2D baseX;
sampler2D baseY;
sampler2D baseZ;
sampler2D normalX;
sampler2D normalY;
sampler2D normalZ;
 
struct PS_INPUT 
{
   float4 Position :        POSITION0;
   float2 TexcoordX :       TEXCOORD0;
   float2 TexcoordY :       TEXCOORD1;
   float2 TexcoordZ :       TEXCOORD2;
   float3 viewDirection   : TEXCOORD3;
   float3x3 tangentSpace :  TEXCOORD4;
};
 
float4 ps_main( PS_INPUT Input ) : COLOR0
{      
   float3 fvLightDirection = normalize( lightDirection );
   float3 fvNormal         = normalize( Input.tangentSpace[2]);
   float3 fvViewDirection  = normalize( Input.viewDirection );
    
   float3 n                = fvNormal;
   n*=n;
   
   float4 fvBaseNormal     = tex2D( normalX, Input.TexcoordX )*n.x+
                             tex2D( normalY, Input.TexcoordY )*n.y+
                             tex2D( normalZ, Input.TexcoordZ )*n.z;
                             
   float3 normal           = normalize(mul(2.0*fvBaseNormal.xyz-1.0,Input.tangentSpace));    
    
   float  fNDotL           = max(0.0,dot( normal, fvLightDirection )); 
   float3 fvReflection     = reflect(fvLightDirection,normal);
   float  fRDotV           = max(0.0, dot( fvReflection, fvViewDirection ) );
   
   float4 fvBaseColor      = tex2D( baseX, Input.TexcoordX )*n.x+
                             tex2D( baseY, Input.TexcoordY )*n.y+
                             tex2D( baseZ, Input.TexcoordZ )*n.z;
                             
   float4 fvTotalAmbient   = fvAmbient * fvBaseColor; 
   float4 fvTotalDiffuse   = fvDiffuse * fNDotL * fvBaseColor; 
   float4 fvTotalSpecular  = fvSpecular * pow( fRDotV, fSpecularPower )*fvBaseColor;
   float4 fresnel          = max(0.0,dot(fvViewDirection,normal))*fvSpecular;
   
   return( fvTotalAmbient + fvTotalDiffuse + fvTotalSpecular+fresnel );
      
}
And a result screen. Before anyone asks, this screen is OpenGL, but in DX9C looks the same

Image
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
Nadro
Posts: 1648
Joined: Sun Feb 19, 2006 9:08 am
Location: Warsaw, Poland

Re: Triplanar mapping shader with normal map support

Post by Nadro »

It looks really nice :)
Library helping with network requests, tasks management, logger etc in desktop and mobile apps: https://github.com/GrupaPracuj/hermes
Granyte
Posts: 850
Joined: Tue Jan 25, 2011 11:07 pm
Contact:

Re: Triplanar mapping shader with normal map support

Post by Granyte »

look preatty sweet
i just implemented the same thing last week for asteroids
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: Triplanar mapping shader with normal map support

Post by hendu »

The pic needs some aniso :)

The downside is that aniso makes texture lookups up to 2xLEVEL more expensive, and triplanar makes a lot of reads.
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Re: Triplanar mapping shader with normal map support

Post by Mel »

I tried to enable the anisotropic in Rendermonkey, but in GL it only let me to do bilinear filtering. In RM the GL support is lame, it doesn't even allow for floating point rendertargets, too sad it is no longer under development
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
silence
Posts: 6
Joined: Fri Dec 28, 2012 4:14 pm
Location: United Kingdom

Re: Triplanar mapping shader with normal map support

Post by silence »

Very useful. Will give this a try.
Jinto
Posts: 24
Joined: Mon Oct 13, 2003 2:59 am
Location: Ukraine

Re: Triplanar mapping shader with normal map support

Post by Jinto »

D't work...

Code: Select all

class cShaderCallback : public IShaderConstantSetCallBack
{
    public:
        cShaderCallback(){}
        void OnSetConstants(IMaterialRendererServices* services, s32 userData)
        {
            video::IVideoDriver* drv = services->getVideoDriver();
            //
          core::matrix4 worldViewProj;
                worldViewProj = drv->getTransform(video::ETS_PROJECTION);
                worldViewProj *= drv->getTransform(video::ETS_VIEW);
                worldViewProj *= drv->getTransform(video::ETS_WORLD);
         services->setVertexShaderConstant("matWorld", worldViewProj.pointer(), 16);
 
            //
            core::vector3df pos = drv->getDynamicLight(0).Position;
            services->setVertexShaderConstant("eyePosition", reinterpret_cast<f32*>(&pos), 3);
 
            //
            f32 ts(1.f);
            services->setVertexShaderConstant("tileSize", &ts, 1);
 
 
            //
            video::SColorf aCol(Material.AmbientColor);
            services->setPixelShaderConstant("ambientColor",reinterpret_cast<f32*>(&aCol),4);
            video::SColorf dCol(Material.DiffuseColor);
            services->setPixelShaderConstant("diffuseColor",reinterpret_cast<f32*>(&dCol),4);
            video::SColorf sCol(Material.SpecularColor);
            services->setPixelShaderConstant("specularColor",reinterpret_cast<f32*>(&sCol),4);
 
            core::vector3df dir = drv->getDynamicLight(0).Direction;
            services->setPixelShaderConstant("lightDirection",reinterpret_cast<f32*>(&dir),3);
 
            f32 power(.003f); // Тут неясность...
            services->setPixelShaderConstant("specularPower",&power,1);
 
            //
            s32 baseX = 0;
            services->setPixelShaderConstant("baseX",&baseX,1);
            s32 baseY = 1;
            services->setPixelShaderConstant("baseY",&baseY,1);
            s32 baseZ = 2;
            services->setPixelShaderConstant("baseZ",&baseZ,1);
            s32 normalX = 3;
            services->setPixelShaderConstant("normalX",&normalX,1);
            s32 normalY = 4;
            services->setPixelShaderConstant("normalY",&normalY,1);
            s32 normalZ = 5;
            services->setPixelShaderConstant("normalZ",&normalZ,1);
 
        }
       void OnSetMaterial (const SMaterial &material)
      {
           Material = material;
        }
    private:
        SMaterial Material;
};

Code: Select all

io::path psFileName = "frag.glsl"; //Фрагментный шейдер
io::path vsFileName = "vert.glsl"; //Вершинный шейдер
 
 
IGPUProgrammingServices* gpu = driver->getGPUProgrammingServices();
cShaderCallback* sC = new cShaderCallback();
s32 shaderMaterialType = gpu->addHighLevelShaderMaterialFromFiles(
                vsFileName, "vertexMain", EVST_VS_1_1 ,
      psFileName, "pixelMain", EPST_PS_1_1,
       sC, video::EMT_SOLID);
 
...
 
cube->setMaterialType((E_MATERIAL_TYPE)shaderMaterialType);
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Re: Triplanar mapping shader with normal map support

Post by Mel »

BaseX,Y,Z and NormalX,Y and Z are texture objects, so you don't have to set them, (and if you haven't recompiled irrlicht to have more than 4 textures, you won't have access to the last ones...) and matWorld refers to the ETS_WORLD transformation of the video driver, you are passing the WorldViewProjection matrix. I am finishing an example wich uses 4 textures... the only thing is that in OpenGL, in the shaders, only the first texture is set?... (1.8 release version i guess it is already solved) It works properly, but it won't set any texture beyond the first one... or maybe i am missing something in the OpenGL shaders.
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: Triplanar mapping shader with normal map support

Post by hendu »

@Mel

Sounds like the float to int change for GLSL texture unit setting. Make sure you use ints.
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Re: Triplanar mapping shader with normal map support

Post by Mel »

sorry to sound like a noob but how the frick do i access any texture beyond the first one in irrlicht?! i have a running example, it blends the textures well across the whole terrain doing the triplannar mapping well, but with only one texture. I think the shader is well, but not having more textures is frustrating. In DX looks well, i want the same in GLSL
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: Triplanar mapping shader with normal map support

Post by hendu »

Code: Select all

uniform sampler2D tex1, tex2, tex3;

Code: Select all

int tex = 0;
srv->setPixelShaderConstant("tex1", &tex, 1); // Bind sampler tex1 to texture slot 0 - we send one integer
tex = 1;
srv->setPixelShaderConstant("tex2", &tex, 1);
tex = 2;
srv->setPixelShaderConstant("tex3", &tex, 1);
smso
Posts: 246
Joined: Fri Jun 04, 2010 3:28 pm
Location: Hong Kong

Re: Triplanar mapping shader with normal map support

Post by smso »

As the camera moves, some texture pop-in occurs nearer to camera. Is there any way to reduce this?

Regards,
smso
Mel
Competition winner
Posts: 2292
Joined: Wed May 07, 2008 11:40 am
Location: Granada, Spain

Re: Triplanar mapping shader with normal map support

Post by Mel »

No, that is a terrain updating issue, not something related to the shader.
"There is nothing truly useless, it always serves as a bad example". Arthur A. Schmitt
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: Triplanar mapping shader with normal map support

Post by hendu »

Is that with the irrlicht changing terrain? Better use a static one instead. (ie terrainmesh instead of terrainscenenode IIRC)
Granyte
Posts: 850
Joined: Tue Jan 25, 2011 11:07 pm
Contact:

Re: Triplanar mapping shader with normal map support

Post by Granyte »

or use a normal map that's how i do in my planetary renderer

it require one more texture but makes it compeltly independant of the terrain vertex
Post Reply