Parallax Occlusion Mapping

Everything related to 3D programming
User avatar
Samuel
Enthusiast
Enthusiast
Posts: 756
Joined: Sun Jul 29, 2012 10:33 pm
Location: United States

Parallax Occlusion Mapping

Post by Samuel »

Here's a little example showing Parallax Occlusion Mapping. I found it on the Ogre forums several months ago and unfortunately I don't remember the link, but I do remember that it is free to use.
It's a HLSL shader meaning it will only work under DirectX, but unlike my past shaders the cg.dll is not required.
If people want OpenGL support it should be easy to setup, but I'm busy at the moment. So, you'll have to be patient until I find the time to set it up.

Supported graphic cards are from : ATI Radeon HD 2000+, nVidia GeForce FX 6 series or better.

I'd post a picture of the effect, but the site I used for image hosting is now charging a fee. So, I'm going to find a new one.
I did include the picture in the download. Kinda pointless I know, but it's there.

I'm in a bit of a hurry. So, for the time being you will have to use my download to get the source and example.
Later today or tomorrow I'll post the code with a better explanation about the shader.
Let me know if you have any problems.

ParallaxOcclusionMapping.zip

EDIT:
With POM shading you need to use a normal map with the height set as alpha. Otherwise it won't look right.
You can look at BeachStoneNH.png in my download for an example. Just open it in gimp or something that supports alpha and you'll
see what I'm talking about.

Here's the source. If you want the textures you'll have to get them from my download or create your own for the time being.

Purebasic code. You'll need to set the 3D Archive to the directory containing the textures and shader files.

Code: Select all

UsePNGImageDecoder()

If InitEngine3D(#PB_Engine3D_DebugLog)

InitSprite()
InitKeyboard()

Enumeration
  #Window
  #Font
  #Help
  #Camera
  #Light
  #Node0
  #Node1
  #Texture
  #Material
  #ScriptMaterial
  #PlaneMesh
  #CubeMesh
  #PlaneEntity
  #CubeEntity
EndEnumeration

ExamineDesktops()
DeskTopW = DesktopWidth(0)
DeskTopH = DesktopHeight(0)

Wireframe=0
Material=0
ShowHelp=0

If LoadFont(#Font, "Courier New", 10,#PB_Font_Bold)
  SetGadgetFont(#PB_Default, FontID(#Font))
EndIf

CPU$ = CPUName()

OpenWindow(#Window, 0, 0, DeskTopW, DeskTopH, "Parallax")
  OpenWindowedScreen(WindowID(0), 0, 0, DeskTopW, DeskTopH)

    Add3DArchive("/", #PB_3DArchive_FileSystem)
    Parse3DScripts()
    
    CreateSprite(#Help, 280, 180)
    StartDrawing(SpriteOutput(#Help))
      Box(0,0,280,180,RGB(0,0,0))
    StopDrawing()
    
    CreateCube(#CubeMesh,12)
    BuildMeshTangents(#CubeMesh)
    CreatePlane(#PlaneMesh,500,500,1,1,20,20)
    BuildMeshTangents(#PlaneMesh)
   
    LoadTexture(#Texture,"BeachStones.png")
    CreateMaterial(#Material,TextureID(#Texture))
    SetMaterialColor(#Material, #PB_Material_SelfIlluminationColor, RGB(20,20,20))
    SetMaterialColor(#Material, #PB_Material_SpecularColor, RGB(255,255,255))
    MaterialShininess(#Material,30)
    ScaleMaterial(#Material, 0.5, 0.5)
    
    GetScriptMaterial(#ScriptMaterial,"MaterialPOM")
    
    CreateEntity(#CubeEntity,MeshID(#CubeMesh),MaterialID(#ScriptMaterial),0,0,0)
    CreateEntity(#PlaneEntity,MeshID(#PlaneMesh),MaterialID(#ScriptMaterial),0,-10,0)

    CreateCamera(#Camera, 0, 0,100,100)
    MoveCamera(#Camera, 0, 0, 30)
    CameraLookAt(#Camera, 0, 0, 0)
    CameraBackColor(#Camera, RGB(100, 100, 100))
    
    CreateLight(#Light, RGB(50,0,0), 0,4,8)
    SetLightColor(#Light, #PB_Light_DiffuseColor, RGB(250,250,250))
    SetLightColor(#Light, #PB_Light_SpecularColor, RGB(255,255,255))

    AmbientColor(RGB(0,0,0))
    
    CreateNode(#Node0,0,0,0)
    AttachNodeObject(#Node0, LightID(#Light))
    CreateNode(#Node1,0,0,0)
    AttachNodeObject(#Node1,CameraID(#Camera))
    
    Repeat
      If ExamineKeyboard()
        If KeyboardPushed(#PB_Key_Up)
          MoveCamera(#Camera, 0, 0, -0.2)
        ElseIf KeyboardPushed(#PB_Key_Down)
          MoveCamera(#Camera, 0, 0, 0.2)
        EndIf
        If KeyboardPushed(#PB_Key_Left)
          RotateNode(#Node1,0,-1,0,#PB_Relative)
          CameraLookAt(#Camera, 0, 0, 0)
        ElseIf KeyboardPushed(#PB_Key_Right)
          RotateNode(#Node1,0,1,0,#PB_Relative)
          CameraLookAt(#Camera, 0, 0, 0)
        EndIf
        If KeyboardReleased(#PB_Key_W)
          If Wireframe=0
            CameraRenderMode(#Camera, #PB_Camera_Wireframe)
            Wireframe=1
          Else
            CameraRenderMode(#Camera, #PB_Camera_Textured)
            Wireframe=0
          EndIf
        EndIf
        If KeyboardReleased(#PB_Key_E)
          If Material=0
            SetEntityMaterial(#CubeEntity, MaterialID(#Material))
            SetEntityMaterial(#PlaneEntity, MaterialID(#Material))
            Material=1
          Else
            SetEntityMaterial(#CubeEntity, MaterialID(#ScriptMaterial))
            SetEntityMaterial(#PlaneEntity, MaterialID(#ScriptMaterial))
            Material=0
          EndIf
        EndIf
        If KeyboardReleased(#PB_Key_H)
          If ShowHelp=0
            ShowHelp=1
          Else
            ShowHelp=0
          EndIf
        EndIf
      EndIf

      RotateNode(#Node0,0,0,1,#PB_Relative)
      
      RenderWorld()
      If ShowHelp=0
        CurrentFPS = Engine3DFrameRate(#PB_Engine3D_Current)
        AverageFPS = Engine3DFrameRate(#PB_Engine3D_Average)
        MaximumFPS = Engine3DFrameRate(#PB_Engine3D_Maximum)
        MinimumFPS = Engine3DFrameRate(#PB_Engine3D_Minimum)
        CountTris=CountRenderedTriangles()
        StartDrawing(SpriteOutput(#Help))
          Box(0,0,280,180,RGB(40,40,40))
          DrawingFont(FontID(#Font)) 
          DrawText(2,2,CPU$,RGB(255,0,0),RGB(40,40,40))
          DrawText(2,22,"Current FPS : "+Str(CurrentFPS),RGB(0,255,255),RGB(40,40,40))
          DrawText(2,42,"Average FPS : "+Str(AverageFPS),RGB(0,255,255),RGB(40,40,40))
          DrawText(2,62,"Maximum FPS : "+Str(MaximumFPS),RGB(0,255,255),RGB(40,40,40))
          DrawText(2,82,"Minimum FPS : "+Str(MinimumFPS),RGB(0,255,255),RGB(40,40,40))
          DrawText(2,102,"Rendered Triangles : "+Str(CountTris),RGB(0,255,0),RGB(40,40,40))
          DrawText(2,122,"Press W for wireframe",RGB(200,200,200),RGB(40,40,40))
          DrawText(2,142,"Press E to view plain material",RGB(255,255,0),RGB(40,40,40))
          DrawText(2,162,"Press H to hide help",RGB(255,200,255),RGB(40,40,40))
        StopDrawing()
        DisplayTransparentSprite(#Help,20,20)
        If FirstFrame=0
          Engine3DFrameRate(#PB_Engine3D_Reset)
          FirstFrame=1
        EndIf
      EndIf
      FlipBuffers()

    Until WindowEvent() = #PB_Event_CloseWindow Or KeyboardPushed(#PB_Key_Escape)
EndIf
End
POM.material

Code: Select all

abstract material POM
{
	technique
	{   
		pass
		{
			vertex_program_ref POM_Vert_hlsl
			{
				param_named scale float $scale
				param_named fHeightMapScale float $depth   
			}

			fragment_program_ref POM_Frag_hlsl
			{   
				param_named spec_exponent float $specular_exponent
				param_named spec_factor float $specular_factor   
			}

			texture_unit
			{
				texture        $bump_map
				tex_coord_set  0
			}
			texture_unit 
			{
				texture        $diffuse_map
				tex_coord_set  0
			}         
		}      
	}
	technique
	{   
		pass
		{
			texture_unit
			{
				texture        $diffuse_map
			}      
		}      
	}
}

material MaterialPOM : POM
{
	set $scale 2
	set $depth 0.1
	set $specular_exponent 128
	set $specular_factor 0.6
	set $bump_map BeachStonesNH.png
	set $diffuse_map BeachStones.png
}
POM.program

Code: Select all

vertex_program POM_Vert_hlsl hlsl
{
	source POM.hlsl
	entry_point POM_Vert
	target vs_3_0    
 
	default_params
	{
		param_named scale float 1
		param_named_auto lightPosition light_position_object_space 0
		param_named_auto eyePosition camera_position_object_space
		param_named_auto worldViewProj worldviewproj_matrix
		param_named_auto lightAttenuation light_attenuation 0 
	}  
}

fragment_program POM_Frag_hlsl hlsl
{
	source POM.hlsl
	entry_point POM_Frag
	target ps_3_0    

	default_params
	{
		param_named_auto lightDiffuse light_diffuse_colour 0
		param_named_auto lightSpecular light_specular_colour 0
		param_named_auto lightAmbient ambient_light_colour 0
		param_named spec_exponent float 127
		param_named spec_factor float 0.5   
	}  
} 
POM.hlsl

Code: Select all

    float3 expand(float3 v)
    {
       return (v - 0.5) * 2;
    }

    void POM_Vert(float4 position   : POSITION, 
                  float3 normal      : NORMAL, 
                  float2 uv         : TEXCOORD0, 
                  float3 tangent     : TANGENT0, 
                  // outputs 
                  out float4 oPosition    : POSITION, 
                  out float2 oUv          : TEXCOORD0, 
                  out float3 oLightDir    : TEXCOORD1,
                 out float3 oEyeDir       : TEXCOORD2, 
                 out float3 oNormal        : TEXCOORD3,
               out float oAttenuation: TEXCOORD4,   
               out float2 oParallaxOffsetTS : TEXCOORD5,    
                  // parameters 
                  uniform float fHeightMapScale,
                  uniform float scale,
                  uniform float4 lightPosition,
                  uniform float3 eyePosition, 
                  uniform float4x4 worldViewProj,
                  uniform float4 lightAttenuation) 
    {  
       // calculate output position 
       oPosition = mul(worldViewProj, position); 

       // pass the main uvs straight through unchanged 
       oUv = uv * scale; 

       float Dist = distance(mul(worldViewProj, lightPosition), mul(worldViewProj, position)); 
       oAttenuation = 1/(lightAttenuation.y + lightAttenuation.z * Dist + lightAttenuation.w * Dist * Dist);
       
       // calculate tangent space light vector 
       // Get object space light direction 
       float3 lightDir = normalize(lightPosition.xyz -  (position * lightPosition.w));
       
       float3 eyeDir = eyePosition - position.xyz; 
       
       // Calculate the binormal (NB we assume both normal and tangent are 
       // already normalised) 
       // NB looks like nvidia cross params are BACKWARDS to what you'd expect 
       // this equates to NxT, not TxN 
       float3 binormal = cross(tangent, normal); 
        
       // Form a rotation matrix out of the vectors 
       float3x3 rotation = float3x3(tangent, binormal, normal); 
        
       // Transform the light vector according to this matrix 
       lightDir = (mul(rotation, lightDir)); 
       eyeDir = (mul(rotation, eyeDir)); 
       oNormal = (mul(rotation, normal)); 

       oLightDir = lightDir; 
       oEyeDir = eyeDir; 
     
        // Compute the ray direction for intersecting the height field profile with 
        // current view ray. See the above paper for derivation of this computation.
             
        // Compute initial parallax displacement direction:
        float2 vParallaxDirection = normalize(  oEyeDir.xy );
           
        // The length of this vector determines the furthest amount of displacement:
        float fLength         = length( oEyeDir );
        float fParallaxLength = sqrt( fLength * fLength - oEyeDir.z * oEyeDir.z ) / oEyeDir.z; 
           
        // Compute the actual reverse parallax displacement vector:
        oParallaxOffsetTS = vParallaxDirection * fParallaxLength;
           
        // Need to scale the amount of displacement to account for different height ranges
        // in height maps. This is controlled by an artist-editable parameter:
        oParallaxOffsetTS *= fHeightMapScale;  
    }

    void POM_Frag(
            float2 uv   : TEXCOORD0,
            float3 lightVec : TEXCOORD1,
            float3 eyeDir : TEXCOORD2,
            float3 iNormal: TEXCOORD3,
            float Attenuation: TEXCOORD4,
            float2 vParallaxOffsetTS : TEXCOORD5,

            out float4 oColor   : COLOR, 

            uniform float4 lightDiffuse,
            uniform float4 lightAmbient,
            uniform float4 lightSpecular,
            uniform float spec_exponent,
            uniform float spec_factor,
            uniform float fHeightMapScale,

          uniform sampler2D normalHeightMap : register(s0),
          uniform sampler2D diffuseMap : register(s1)
          )
    {
       eyeDir = normalize(eyeDir);
       lightVec = normalize(lightVec);
       float3 halfAngle = normalize(eyeDir + lightVec); 
       //nMinSamples = 12
       //nMaxSamples = 60
       float nMinSamples = 30;
       float nMaxSamples = 60;
       float3 N = normalize( iNormal );   
       int nNumSamples = (int)lerp( nMinSamples, nMaxSamples, dot( eyeDir, N ) );
       float fStepSize = 1.0 / (float)nNumSamples;
       float2 dx, dy;
       dx = ddx( uv );
       dy = ddy( uv );
       
          float fCurrHeight = 0.0;
          float fPrevHeight = 1.0;
          float fNextHeight = 0.0;

          int    nStepIndex = 0;

          float2 vTexOffsetPerStep = fStepSize * vParallaxOffsetTS;
          float2 vTexCurrentOffset = uv;
          float  fCurrentBound     = 1.0;
          float  fParallaxAmount   = 0.0;

          float2 pt1 = 0;
          float2 pt2 = 0;
           
          float2 texOffset2 = 0;   
          while ( nStepIndex < nNumSamples ) 
          {
             vTexCurrentOffset -= vTexOffsetPerStep;

             // Sample height map which in this case is stored in the alpha channel of the normal map:
             fCurrHeight = tex2Dgrad( normalHeightMap, vTexCurrentOffset, dx, dy ).a;

             fCurrentBound -= fStepSize;

             if ( fCurrHeight > fCurrentBound ) 
             {   
                pt1 = float2( fCurrentBound, fCurrHeight );
                pt2 = float2( fCurrentBound + fStepSize, fPrevHeight );

                texOffset2 = vTexCurrentOffset - vTexOffsetPerStep;

                nStepIndex = nNumSamples + 1;
                fPrevHeight = fCurrHeight;
             }
             else
             {
                nStepIndex++;
                fPrevHeight = fCurrHeight;
             }
          } 
          float fDelta2 = pt2.x - pt2.y;
          float fDelta1 = pt1.x - pt1.y;
          
          float fDenominator = fDelta2 - fDelta1;
          
          // SM 3.0 requires a check for divide by zero, since that operation will generate
          // an 'Inf' number instead of 0, as previous models (conveniently) did:
          if ( fDenominator == 0.0f )
          {
             fParallaxAmount = 0.0f;
          }
          else
          {
             fParallaxAmount = (pt1.x * fDelta2 - pt2.x * fDelta1 ) / fDenominator;
          }
          
          float2 vParallaxOffset = vParallaxOffsetTS * (1 - fParallaxAmount );

          // The computed texture offset for the displaced point on the pseudo-extruded surface:
          float2 newTexCoord = uv - vParallaxOffset;           
              
       float3 PixelNormal = expand(tex2D(normalHeightMap, newTexCoord).xyz);
       PixelNormal = normalize(PixelNormal);
       float3 diffuse = tex2D(diffuseMap, newTexCoord).xyz;

       float NdotL = dot(normalize(lightVec), PixelNormal);
       float NdotH = dot(normalize(halfAngle), PixelNormal); 
       float4 Lit = lit(NdotL,NdotH,spec_exponent);   
       //oColor = float4(diffuse, 1);
       float3 col = lightAmbient * diffuse + ((diffuse * Lit.y * lightDiffuse + lightSpecular * Lit.z * spec_factor) * Attenuation);
          
       oColor = float4(col, 1);
    }

Last edited by Samuel on Wed Feb 19, 2014 4:04 am, edited 2 times in total.
User avatar
Bananenfreak
Enthusiast
Enthusiast
Posts: 519
Joined: Mon Apr 15, 2013 12:22 pm

Re: Parallax Occlusion Mapping

Post by Bananenfreak »

Wow, Samuel, good work :)
I will try it with my trees and post it here.

Here you can see his demo:
Image

Here´s a second Picture from me (Tree is still not working -.-):
Image

Hmm, is there a bug? If I use a number > 1 for scale, texture will be smaller; If I use a number smaller then 1, the texture gets bigger.
Image
User avatar
Samuel
Enthusiast
Enthusiast
Posts: 756
Joined: Sun Jul 29, 2012 10:33 pm
Location: United States

Re: Parallax Occlusion Mapping

Post by Samuel »

Bananenfreak wrote: Here´s a second Picture from me (Tree is still not working -.-):

Hmm, is there a bug? If I use a number > 1 for scale, texture will be smaller; If I use a number smaller then 1, the texture gets bigger.
Did you give your tree tangents?

As for the scale it's not a bug. It just works the opposite way Purebasic's ScaleMaterial does.
It's calculated in the hlsl shader with this line.

Code: Select all

oUv = uv * scale
oUv is the newly calculated UV coordinate that the shader gives to each vertex.
Let us say scale equals 2 and the vertex uv coordinate is 1.
oUv now equals 2 instead of 1.

I'm guessing ScaleMaterial() works with division. Which is why it has the opposite result.

Code: Select all

oUv = uv / scale
If scale and uv are the same as above, but with division we get oUv = 0.5.

The reason we don't use division, unless necessary, is because multiplication is usually calculated faster. When dealing with graphics you want the fastest possible solution.
User avatar
AndyLy
Enthusiast
Enthusiast
Posts: 228
Joined: Tue Jan 04, 2011 11:50 am
Location: GRI

Re: Parallax Occlusion Mapping

Post by AndyLy »

with Samuel code:
Image

In my game (I can not find the reason, but the material does not react to dynamic light) :
Image
'Happiness for everybody, free, and no one will go away unsatisfied!'
SMsF town: http://www.youtube.com/watch?v=g6RRKYf_Pd0
SMf locations module (Ogre). Game video: http://www.youtube.com/watch?v=ZlhBgPJhAxI
User avatar
Samuel
Enthusiast
Enthusiast
Posts: 756
Joined: Sun Jul 29, 2012 10:33 pm
Location: United States

Re: Parallax Occlusion Mapping

Post by Samuel »

Do your meshes have vertex normals and tangents?

You might also check your Ogre log (in your programs root directory) for errors with InitEngine3D(#PB_Engine3D_DebugLog).
If you want. You can post it or PM the log to me and I can take a look at it.
Post Reply