Showing posts with label Theory. Show all posts
Showing posts with label Theory. Show all posts

2015/05/26

A note about performance

While the new shader generates quite nice results, we also want the program to run smooth. Using 20 number of steps for both linear and binary search gives us steady 60 FPS on my old desktop with nVidia GTX 460 and Intel Q9550 @ 3.9 ghz (2560x1440 resolution). Increasing to 30 binary search steps immediately results in FPS drops down to 40 FPS. It's really hard to spot any difference between using 30 and 20 steps for binary search, there is a bigger visual difference between 20 and 10. 20 steps seems like a good number.

2015/05/20

The specific RM shader.

I spent the whole afternoon yesterday writing the shader for this specific project, trying to make it as minimalistic as possible. The result became pretty good, the shader does everything I want it to do, and nothing more. 

Implementation in CG:

Defining the variables that should be used in this shader:

  sampler2D _MainTex;
  sampler2D _NormalMap;
  sampler2D _HeightMap;
  float _Height;
  float _Gloss;
  float _ColorIntensity;
  float _Specular;
  
  struct Input { // vertex input
   
   float2 uv_MainTex;
   float2 uv_NormalMap;
   float2 uv_HeightMap;
   float3 viewDir;

  };
I think each of them is pretty self-explained.  The struct represent the incoming parameters; uv coordinates of the textures applied and view direction.

Luckily, writing shaders in CG for Unity doesn't involve that much math. The ray is set up:

  IN.viewDir = normalize(IN.viewDir); 
   
  // set up the ray
  float3 p = float3(IN.uv_MainTex,0);
  float3 v = normalize(IN.viewDir*-1);
  v.z = abs(v.z);
  v.xy *= _Height;

Implementing the ray tracer:

  // ray tracer with linear and binary search
  const int linearSearchSteps = 20;
  const int binarySearchSteps = 20;
   
  v /= v.z * linearSearchSteps;
   
  int i;
  for( i=0;i(lessthen)linearSearchSteps;i++ )
  {
      float tex = tex2D(_HeightMap, p.xy).a;
      if (p.z(lessthen)tex) p+=v;
  }
   
  for( i=0;i(lessthen)binarySearchSteps;i++ )
  {
      v *= 0.5;
      float tex = tex2D(_HeightMap, p.xy).a;
      if (p.z(lessthen)tex) p += v; else p -= v;
  }
This the exact same procedure that was explained in the post about Relief Mapping. As you can see in the binary search, we move up if the height map shows a greater value. If it shows a smaller value, we move down.
Note: The html editor didn't want me to use > and < signs.

Next thing is to generate the output:

  // generate the output  
  half4 tex = tex2D(_MainTex, p.xy);
   
  half3 normal = UnpackNormal(tex2D(_NormalMap,p.xy)); // normal map

  normal.z = sqrt(1.0 - dot(normal.xy,  normal.xy));
  OUT.Normal = normal; 
     
  OUT.Gloss = tex.a*_Gloss;
  OUT.Specular = _Specular;
  OUT.Albedo = tex.rgb *_ColorIntensity;

 The _Gloss, _Specular and _ColorIntensity variables manipulates the output of the texture. Which is basically created out of the mods I did to the test shader.

The last thing is to implement what parameters that should be modified from the Unity interface. In addition to the most obvious (textures, specular color, height), I added those variables showed above.
  
  _ColorIntensity ("Color Intensity", Range(0.5, 1.5)) = 1
  _SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
  _Gloss ("Gloss", Range(0.5, 1.5)) = 1
  _Height ("Height", Range(-0.05, 0.05)) = -0.01
  _Specular ("Shininess", Range (0.01, 0.1)) = 0.014
  _MainTex ("Base (RGB), Spec (A)", 2D) = "white" {}
  _NormalMap ("Normalmap", 2D) = "bump" {}
  _HeightMap ("Height (A)", 2D) = "bump" {} 

This is about it for the implementation. A very minimal relief mapping shader, that works flawless for its purpose. When I've created a proper height map, I'm gonna post some result using this shader and the new height map. 

2015/05/16

Theory: Relief Mapping

It's time to check out the actual relief mapping technique, as we now know how parallax occlusion mapping works, this will be pretty straight forward.

In parallax occlusion mapping the illusion of depth was created by stepping fixed values, which gives a pretty nice result, but not as nice as we want it to be. What we want to do with relief mapping is to find the exact displacement for every fragment. In other words: the surfaces depth will be an exact match with the values that the height map gives us. If we can achieve that, then we could generate a very accurate illusion of depth in our surfaces.
How can we be so exact without making the ray tracer run forever? We use binary search. Say, we needed a step size of 1048576 to get a good result with our ray tracer, that isn't really practical. With binary search that could be achieved with only 20 steps (2^20 = 2048). Binary search in the ray tracer works like this: The currentHeight is initially set to 0.5, then we step either up or down. That depends on what our height shows: if the height map value is greater then our currentHeight we step up with currentHeight/2. If it is smaller, we step down with currentHeight/2 and stores this as a possible match. This procedure continues for the specified number of binary search steps.

Now, we got a match, but it isn't really exact yet. If an object occludes another object, using binary search, we will not get the correct silhouettes. It's pretty easy to understand because of what we stated above: we start height / 2, we step up, finds an intersection. Then we never will be stepping down, but the correct match could still be down there, because of this occlusion.
This can easily be solved though. If we use a simple linear search with a big step size before our binary search, we can find an intersection which is fairly good. This intersection is then refined with the binary search, just searching in the appropriate section, and there we have our exact match.

As you easily could understand, this shader is quite more hardware consuming then the parallax occlusion mapping shader. But when finding a sweet spot with the binary search step size and the linear search step size, we could use this in real time applications. On fairly powerful machines, this shader would run with pretty good frames per second. These days, it would be quite possible to have it as an enthusiast option in video games. As Intel Skylake, AMD Zen and the Radeon 300 series is upcoming, I'm sure we will see some popular game engines implementing this in the future.

Thats about it for the relief mapping shader. Gonna post some details and updates about the project next.  


  A pretty cool result of using relief mapping with shadowing that I found.

2015/05/13

Theory: Parallax Occlusion Mapping

The easiest way to understand the relief mapping completely is to first understand the parallax occlusion mapping shader. The parallax occlusion mapping is quite similar to the technique we will use, but a bit easier to start with. This is probably be the toughest part theoretically, relief mapping should be pretty easy to understand when this part is clear.

Intro

Parallax occlusion mapping is a popular technique used in many real time applications and modern video games such as Grand Theft Auto V, The Elder Scrolls: Skyrim and Crysis, where the goal is to make realistic looking textures. Parallax occlusion encodes the surface information in textures to reduce its geometric model's complexity while giving a fairly realistic look. To manipulate the surface details a height map is used, which represents displacement in the texture or surface. A height map is often based on the original texture but in gray scale, where the difference between black and white represent the material's displacement. More about height maps later. The texture's details are reconstructed in the pixel shader, using the height map, when the model is rendered, so that it creates an illusion of displacement (in most cases depth).

 A good example of a parallax occlusion shader applied in GTA V. As you can see, the stone bricks really seems to have depth, but it's really just a 2D texture.

Have in mind though, that you need a bunch of other techniques to generate something like that, like anti-aliasing for example. That said, this would look totally boring without the parallax occlusion applied. 

  

The algorithm

We know that the idea is, with the help of a height map, to generate volumetric shapes for each pixel of the rendered surface. This sounds pretty advanced, but with some basic linear algebra math it's not that hard. Before we get into the algorithm itself, I'll show a illustration about what we want to with it and how our height map works. 

As I stated before, the height map is based on the original texture, but covered in gray scale. If we say that the scale is ranging from 0.0 to 1.0, where 0.0 is black and 1.0 is white, we could describe the height in the texture. The more precise the height map are, the more precise will the illusion of depth in the surface be. 

 Example of a well done height map for some brick wall texture.

Back to the algorithm - I hope my poorly made illustration that follows, is enough to get the basic idea of how parallax occlusion works.



What you see here, is the actual ray trace in the pixel shader that we want to implement. The thin lines under the flat surface represents the height map values, and the red line represents the ray that's being traced. The point 1 is where the ray first intersects with the surface, this point represents the texture coordinates we would normally render. Instead, we wait until the ray intersects with the height map. When it does, we can calculate our illusionary coordinates, which gets rendered instead. Lets move on to some more technical (linear algebra) stuff! 

The main loop of this shader is going to find the intersection of the camera vector with the height map. To skip some extra processing, we are going to quit the loop as soon as any intersection is found. 

The vertex shader
Here is what's going to happen in the vertex shader:

1. Calculate the vector from the camera to the vertex. 

1.1. Transform the vertex position into world space and subtract its position from the camera position. 

1.2. Subtract the world space vertex position from the light position to find the light direction vector.

2. Transform camera vector, light direction vector and vertex normal to tangent space.

2.1. Create the transformation matrix with the binormal, tangent vectors and vertex normal together with the world matrix. Be sure to take the transpose of the matrix as we want to transform from world to tangent and not from tangent to world.

2.2. Use the transformation matrix just created to transform the vectors to tangent space.

2.3. Multiply the incoming vertex position with the world view projection matrix to get the correct output position.

The pixel shader
This is where the actual parallax occlusion mapping takes place.

1. Calculate the parallax constants.

1.1  Calculate the maximum parallax offset and it's direction vector with the help of the texture coordinate, the value our height map is showing and the tangent vector from the pixel to the camera. The camera vector must be normalized to give the offset direction vector.

1.2. Determine the number of samples that should be used. 

1.3. Calculate the step size. This is simply done by divide the maximum height (1.0 as stated earlier) with the number of samples. 

2. Set up the core.

2.1. Initialize the variables used in the main loop: currentRayHeight, currentOffset, lastOffset, lastSampledHeight. currentSampledHeight, currentSample. 

2.2. Create the main loop. The main loop should run while the current sample is less then the total number of samples.

2.2.1. Calculate currentSampledHeight.

2.2.2. Check if currectSampledHeight is bigger then currentHeightRayHeight. 

If it is, set values:
2.2.2.1. Calculate delta of currentSampledHeight and currentRayHeight, also the delta of currentRayHeight plus the step size and lastSampledHeight. 

2.2.2.2. Calculate the ratio which should be applied to lastOffset and currentOffset by dividing the first delta with the first delta plus the second delta.

2.2.2.3. Calculate currentOffset with the ratio applied to lastOffset and currentOffset. (ratio * lastOffset + (maxHeight - ratio) * current offset). 

2.2.2.4 Set currentSample to maximum number of samples to quit the loop.

If not, step:
2.2.3.1. Decrement currentRayHeight by step size, increment currentSample by one.

2.2.3.2. Set lastOffset = currentOffset and add (stepSize * maxOffset) to currentOffset.

2.2.3.3 Set lastSampledHeight = currentSampledHeight.

3. Set the finals.

3.1. Set the final coordinates to texture coordinates + currentOffset.

3.2. Calculate the final normal and the final color with the help of the different maps used. 

3.3 Optional: Manipulate the outgoing color and other parameters. 

--

Thats pretty much it. The tricky part is in the pixel shader, but these steps together with a linear algebra book should make it doable.