Go Back  Xtreme Visual Basic Talk > Legacy Visual Basic (VB 4/5/6) > Knowledge Base > Code Library > Rendering realistic endless water surface with refraction, reflection and caustics


Reply
 
Thread Tools Display Modes
  #1  
Old 07-21-2010, 07:43 AM
Mikle Mikle is offline
Newcomer
 
Join Date: Dec 2007
Location: Russia, Tuapse
Posts: 4
Default Rendering realistic endless water surface with refraction, reflection and caustics

Rendering realistic endless water surface with refraction, reflection and caustics

Content
1. Preface
2. Overview
3. The resulting image
4. Reflections
5. Water
6. Landscape underwater
7. Conclusion
8. Demo program

Preface
This article presents a way to render unlimited the water surface refraction and reflection, using a number of innovative approaches. A demo program accompanies the article with source code in Visual Basic 6. Graphics card with support for vertex and pixel shader 2.0 and DirectX version DirectX 9.0c or better is required to run the demo. The demo does not require installation of SDK runtime, but the demo includes a shader compiler code that will need the Direct X SDK March 2008 (D3DX9_37.DLL).
The article does not attempt to demonstrate the "scientific approach" or algorithms with superior performance, but rather tries to make the effects look good and simple to understand. You do not need Visual Basic in order to experiment with shaders. Shader source code and the included compiler allow you to experiment with the shaders using only a basic text editor, such as Notepad. I hope that you will learn something useful and interesting from the approach taken in this article - pleasant reading!

Overview
The program has three render objects:

Sky (heaven)
When rendering the endless water the skyboxes is always above water level, so using cube map is not rational. Instead I have used a spherical map, “stretched as a cone in the center of the base. Rendering have two passes with disabled Z-buffer. The first pass is a normal render with a 1 in the alpha channel. The second render pass is the reverse image with a 0 in the alpha channel.

Water
For simplicity, assume the water level (on the axis Y) is 0. The water itself does not draw a plane, which is often used, but rather it is an inverted truncated cone, similar to the cone used for the sky. The cone is truncated just below the water level, at -0.3. We will look at this in more detail later in this article.

Landscape
This implies all items that can be over or under water, as well as water surface intersect. The landscape created by using a height map (in a regular grid).

All objects in the demo program are rendered into a texture buffer, which later rendered to the resulting image. There are four textures used in the demo, you can see them by pressing the keys "1", "2", "3", "4". By pressing “space” key, you can see the contents of the alpha channels. Each of the four parts is described below.

The part above Water
First, Z-buffer is cleared and the sky is rendered. This is then applied to the plane with a cut-off level 0 on the Y-axis and onto the landscape. The plane created in this line:
Code:
  PlaneFromPoints PlaneUp, Vec3 (0, 0, 0), Vec3 (0, 0, 1), Vec3 (1, 0, 0)
Before multiply plane using the inverted matrix WorldViewProj:
Code:
  MatrixInverse Mtrx, ByVal 0, Mtrx
  PlaneTransform p, PlaneUp, Mtrx
  Dev.SetClipPlane 0, VarPtr (p)
  Dev.SetRenderState D3DRS_CLIPPLANEENABLE, D3D_TRUE
Reflections
First, Z-buffer is cleared and the sky is rendered. Then we render the reverse image of the landscape as a reflection. It is sufficient to invert the matrix of the world axis Y and the world matrix is initially not transformed (MatrixWorld = MatrixIdentity). This gives us:
Code:
  MatrixScaling MatrixWorld, 1, -1, 1
To work correctly, such a transformation must also reverse the Cull. Also note that we leave the camera unchanged.

Underwater part of the landscape
Clear the Z-buffer and the Color-buffer. The write buffer Color with the color for the water. Next, draw the underwater part of the landscape, just as we did above water.
Code:
  PlaneFromPoints PlaneDown, Vec3 (0, 0.1, 0), Vec3 (1, 0.1, 0), Vec3 (0, 0.1, 1)
The plane is cut-off just above the ground level with axis Y=0. We also impose a pattern glare (caustics).

Water
This is rendered as a normal texture and we do not clear the z-buffer for this rendering. There are areas where the water will get visible artifacts, but these areas are not involved in the formation of the resulting image.

The resulting image
The resulting image is formed from these four parts and is actually in 2D to form a square on the screen. The vertex shader will only send pixel texture coordinates:
Code:
  vs_2_0
  dcl_texcoord0 v1
  mov oT0, v1
The pixel shader will take the pixel coordinates and announce the four samplers:
Code:
  dcl t0
  dcl_2d s0 // Reflections
  dcl_2d s1 // Top
  dcl_2d s2 // Bottom
  dcl_2d s3 // Water
«R» and «G» components of the texture of water are responsible for the shift of the visible image in the refraction and reflection. Their range from 0 to 1, but we change them to be in the range -0.5 to 0.5. For the «B» component the constant c0 we have written the value -0.5 to perform this range shift.
Code:
  // Water Refraction
  texld r3, t0, s3
  add r3.rg, r3, c0.b
To generate reflections we need to make the texture of reflections that originates from the water. In «R» and «G» components of c0 we have written the value of -0.35 and 0.35. This will be a distortion factor on reflection and a refractive index opposite as a positive factor 0.35):
Code:
  // Water Reflections
  mad r4.rg, r3, c0, t0
  texld r0, r4, s0
Water part shows the usual way:
Code:
  // Top
  texld r1, t0, s1
If you study how objects look under real water – there is a distortion of underwater objects depending on the depth. Objects near the surface have nearly no distortion. The deeper down an object is the more distortion is introduced by the refraction. To simulate this effect it is desirable to have a variable ratio of refraction. The shader calculates this ratio during the underwater landscape pass and stores the ratio in the alpha component of the received texture. You can find more details if you study the shader forming the resulting image (see World.psh). We can then take the alpha of texture of the underwater part and multiply the value of refraction (r3.rg) to get the effect of depth distortion. Then apply new coordinates:
Code:
  // Bottom 
  texld r5, t0, s2 
  mul r3.rg, r3, r5.a 
  add r4, r3, t0 
  texld r2, r4, s2
Finally, we need to combine the image refractions and reflections. Often a simple half-sum is used. However we can make it more realistic. Again, study how the refractive index and the reflections depend on the angle of view in real water.
In optic physics, we can get help in the form of the Fresnel formula, according to which light passing through the boundary between two media with different refractive indices, is partially reflected from the boundary, and partly through it. A beam of light (A) is split into two beams (B and C). The Reflected light at the same angle, under which it fell (b = a), but the angle of the beam passing changes (c <> a), it is refracted. The intensity of the reflected and refracted rays also depends on the angle under which it falls.


Furthermore, the magnitude of intensity is depending on the polarization of light. But our eyes do not distinguish between light of different polarization (except when wearing 3D goggles), so we can ignore the effect of different polarization.
In the demo program that accompanies this article you can observe the following. If the angle of view from 0 ° (straight down) to a certain reflection coefficient gradually increases up to unity, and the refractive index decreases, respectively (the sum of the coefficients equals to one). Coming up to 90° are only the reflection without refractions. This factor we have formed in the alpha channel images of water. Then we have refraction and reflection according to this ratio:
Code:
  // Mix refractions and reflections
  lrp r0, r3.a, r0, r2
What now remains is to combine the resulting image with the image of a surface part. Again we have a factor, as a sign (flag), which specifies which image to choose. This flag will be stored in the alpha channel of a surface part image - simply write the alpha unit (see shaders LandUp.vsh and LandUp.psh).
Now we will look in greater details of the elements we have postponed.
Attached Images
File Type: png 1.png (10.9 KB, 30 views)
File Type: png 2.png (12.5 KB, 31 views)
Reply With Quote
  #2  
Old 07-21-2010, 07:50 AM
Mikle Mikle is offline
Newcomer
 
Join Date: Dec 2007
Location: Russia, Tuapse
Posts: 4
Default

Reflections artifacts
If done well, as described above, the resulting image will have some visible artifact. This can be seen on the border of water reflection that "detached" from the beach and is seen a strip of sky.
http://www.xtremevbtalk.com/attachme...1&d=1279719972
When we considered refraction, we got rid of these distortions that have put the refractive index in dependence on the depth (leveraged mask refractions). As a result, the surface of the refractive index does not change the bump texture, and shear texture refractions exactly coincides with the boundary waters. We could try to moved the plane of the cut-off (very little), but this small shift would not solve the problem.
The demo program, for reasons of optimization, textures for rendering selected smaller than the backbufer and the textures are then are scaled when displayed, and as a result, the final image have obtained this thin defect. The shift of the plane cut is intended to eliminate.
We can not use a mask for the reflection. This will not work because on the borders of near and far objects you can see disformation caused by the distortion of on the refractive index, even when the objects themselves are not visible. A big shift (change) of the plane cut-off would eliminate these distortions, but creates other problems. The Image of underwater objects near the surface will begin to fall into the pattern of reflections, as if they are located above the water. One idea is to try what we have done with the skies - to draw texture reflections inverted image of reflections of the landscape. This solves our problem, but this time we get a new problem.There is a reflection of the landscape on the water surface where there should be a reflection of the sky.
http://www.xtremevbtalk.com/attachme...1&d=1279720034
How can we solve this? The “parasitic” reflection comes from the upper part of the landscape, which is visible from the position of the camera, but it is not visible from the position where the virtual camera! So we can try to cut off this part! We can do this by changine the normal of the plane. In a vertex shader we get a vector directed from the vertex at the camera reflections and please note that, we do not move the camera itself. Istead we subtract the normal of which is aimed on the camera from the position coordinates of the vertices since the Dot Product of the vector and the normal vertex is the same as the the negative normal for the vertex.
Subtracting the normal and storing the texture coordinates in a free slot:
Code:
  // C7 - Camera Pos (y =-y)
  sub r0, c7, v0
  dp3 oT1, r0, v1
And in the pixel shader cut off these parts:
Code:
  texkill t1
See full code in LandReflUp.vs and LandReflUp.ps, compare it with the code LandUp.vs and LandUp.ps, who perform regular render a surface part of the landscape.

Generating Water
Recall that we rendered the water using an inverted truncated cone. The cone is always in the position of the camera, ie all its vertices are always visible at the same angle. This means the ratio of refractive index with the reflections can be written directly in the vertex.
When generating the cone we locate vertex number 0 directly below the camera (center) and three sets of vertices with a radius of 3 and 10 and 90, respectively, around the center. The central vertex and the first round we have the coordinate Y = -0.3. The second circle, the outer limit of the truncation, is also a Y = -0.3, while the third circle, which is the boundary of the cone base. The third cicle is not set to Y = 0, but rather a slightly higher value to eliminate any defects between water and sky. In the alpha channel of the lower vertex we write &h10 (hexadecimal 10, i.e. 16 or 16/255 in Shader representation). This is almost complete transparency. In the first round we set a little tranparency (value &hC0), while the second have &hFF i.e. full opacity. The third circle we want to have a low value to make it fairly transparent (&h40 and black color). At this distance, we will not be able to see the submerged part of the landscape, but only the color of water. Remember, when drawing the underwater part, we cleared the buffer in this color and as you can see below there are also color values for the «R», «G», «B» channels.
Code generation of the truncated cone:
Code:
vBuffer(0).Pos = Vec3(0, -0.3, 0)
vBuffer(0).Color = &H10FFFFA0
For n = 0 To SectCount - 1
  vBuffer(n + 1).Pos = Vec3(3 * Sin(n * tmp), -0.3, -3 * Cos(n * tmp))
  vBuffer(n + 1).Color = &HC0AFFF80
  vBuffer(n + 1 + SectCount).Pos = Vec3(10 * Sin(n * tmp), -0.3, -10 * Cos(n * tmp))
  vBuffer(n + 1 + SectCount).Color = &HFF00FF40
  vBuffer(n + 1 + SectCount * 2).Pos = Vec3(90 * Sin(n * tmp), 0.3, -90 * Cos(n * tmp))
  vBuffer(n + 1 + SectCount * 2).Color = &H40000000

  iBuffer(n * 5 * 3 + 0) = 0
  iBuffer(n * 5 * 3 + 1) = n + 1
  iBuffer(n * 5 * 3 + 2) = ((n + 1) Mod SectCount) + 1

  iBuffer(n * 5 * 3 + 3) = n + 1
  iBuffer(n * 5 * 3 + 4) = n + SectCount + 1
  iBuffer(n * 5 * 3 + 5) = ((n + 1) Mod SectCount) + SectCount + 1

  iBuffer(n * 5 * 3 + 6) = n + 1
  iBuffer(n * 5 * 3 + 7) = ((n + 1) Mod SectCount) + SectCount + 1
  iBuffer(n * 5 * 3 + 8) = ((n + 1) Mod SectCount) + 1

  iBuffer(n * 5 * 3 + 9) = n + SectCount + 1
  iBuffer(n * 5 * 3 + 10) = n + SectCount * 2 + 1
  iBuffer(n * 5 * 3 + 11) = ((n + 1) Mod SectCount) + SectCount * 2 + 1

  iBuffer(n * 5 * 3 + 12) = n + SectCount + 1
  iBuffer(n * 5 * 3 + 13) = ((n + 1) Mod SectCount) + SectCount * 2 + 1
  iBuffer(n * 5 * 3 + 14) = ((n + 1) Mod SectCount) + SectCount + 1
Next n
Animating Water
Now we consider the method of forming animated texture normals. We have 64 files with a PNG image in the animation. Each image has the size 64 * 64. Create a volume texture with size 64 * 64 * 64, and into setting these images in as layers. Now, slowly changing the texture coordinates «Z», we can "scroll" this animation. This type animation technic gives a smoother scrolling than the simple change of texture, due to the linear texture filtering between the frames.
However 64 * 64 - a small size, with large water surfaces there will be a clearly visible periodicity of the texture. Alternatively, if we scale the texture, we reduce the detail. To improve this situation we can apply the so-called Detail texture, and we use the same basic structure. We could simply mix the two textures with different scale (the simplest version - half the sum), but a better way is to vary the coefficient. A big coefficient gives larger waves. The coefficient of interpolation is written in a channel «B» color vertices.
The water is always in one position relative to the camera, so when we move the water this can be done by moving the camera texture in the opposite direction and we change the height of the camera to scale the second water bump texture. Below you can see how the vertex shader sets the two float constants:
Code:
   v4.x = QTime * 0.1
   v4.x = v4.x - Int (v4.x)
   v4.y = cPos.y * 1.5 'Scale bump texture
   v4.z = 0
   v4.w = 1
   Dev.SetVertexShaderConstantF 4, VarPtr (v4), a
   v4.x = cPos.x * 1.5 * 0.3
   v4.y = cPos.z * 1.5 * 0.3
   v4.z = 0
   v4.w = 0
   Dev.SetVertexShaderConstantF 5, VarPtr (v4), a
The first line in the «X» we store constant current time multiplied by 0.1, which determines the speed of animation, texture axis «Z», in the next line, will be calculated fractional part of the <<X>> quantity. In «Y» write «Y» coordinate of the camera, multiplied by the scale of the texture. After following a constant share the same «X» and «Z» cameras, multiplied by the same scale and magnitude of 0.3 - remember, this is the height of the camera above the plane of a truncated cone.
Vertex shader:
Code:
  vs_2_0

  // C0 .. c3 - matrix WorldVievProj 
  // C4 - x = time, y = CamPos.y * scale texture, z = 0, w = 1 
  // C5 - x = CamPos.x * scale texture, y = CamPos.z * scale texture, z = 0, w = 0

  dcl_position v0
  dcl_color v1

  def c6, 0.23, 0.23, 1, 1

  m4x4 r0, v0, c0
  mov oPos, r0

  //calculating water bump coordinates, 
  //taking into account both the height of the camera and animation
  mad r0, v0.xzyw, c4.yyzw, c4.zzxz

  // compensate for horizontal displacement of camera
  add r0, r0, c5
  mov oT0, r0
  mul oT1, r0, c6

   mov oD0, v1
Consider this line:
Code:
  mad r0, v0.xzyw, c4.yyzw, c4.zzxz
Componentwise:
r0.x = v0.x * c4.y + c4.z
That is, we multiplied the «X» camera on the scale (c4.y) and have added 0 (c4.z). Thus obtained texture coordinates «X», «Y» is obtained the same way
r0.y = v0.z * c4.y + c4.z

The texture coordinate «Z» is obtained as follows:
r0.z = v0.y * c4.z + c4.x

So it is actually equal to r0.z = c4.x (since c4.z = 0).
Thus in this single instruction, we have calculated all three texture coordinates, taking into account both the height of the camera and animation.
The next few lines will add the horizontal displacement of the camera, the result is recorded in oT0, the result is multiplied by the coefficient for the larger texture (which we have as a coefficient of 0.23) and recorded in the next slot for the texture coordinates - oT1.
Now let us turn to the pixel shader. First we read the texture twice with Detail and conventional texture coordinates. And then we use the <<b>> coefficient to interpolate the textures:
Code:
  texld r0, t0, s0
  texld r1, t1, s0
  lrp r0.rg, v0.b, r0, r1
Attached Images
File Type: jpg 1.jpg (27.4 KB, 16 views)
File Type: jpg 2.jpg (33.4 KB, 14 views)
Reply With Quote
  #3  
Old 07-21-2010, 07:59 AM
Mikle Mikle is offline
Newcomer
 
Join Date: Dec 2007
Location: Russia, Tuapse
Posts: 4
Default

Now we have (in r0.rg) the necessary values for the bump water could finish by sending these values to oC0. However we will do one final effect. In real water, it is not only the coefficient of reflection that depends on the angle of view, but also the magnitude of the deviation of the vertical and horizontal. This is seen in the reflection of point objects approaching the horizon, which does not give a round but rather is stretched as elipses along the vertical reflection.
http://www.xtremevbtalk.com/attachme...1&d=1279720353
The necessary coefficients can be stored in free channels «R» and «G» of color vertices for the water:
Code:
  sub r0.rg, r0, c0.r
  mul r0.rg, r0, v0
  mad r0.a, r0.g, c0.a, v0.a
  add r0.rg, r0, c0.r
Here we have taken on the values of «R» and «G» 0.5 (giving a range of -0.5 to 0.5), multiplied by the rates, and brought back to a range of 0 .. 1. And special attention should be third line:
Code:
  mad r0.a, r0.g, c0.a, v0.a
Remember that we have the refractive index stored in the alpha channel value v0.a (in World.psh). We made a small correction to the slope of the wave, because the angle of surface normal consists of angle of sight to a plane of water and the local angle to the vertical wave. And the part is stored in the «G» channel bump texture (also, in the «R» channel horizontal deflection but that’s not used here).
http://www.xtremevbtalk.com/attachme...1&d=1279720432

Landscape underwater
To complete our understanding we need to elaborate a bit more on the submerged landscape. What we have done so far is to draw the underwater landscape in the same way as the upper part, adding texture caustics and fog water color and dependency on the depth. To imitate that the water is not fully transparent we set the alpha channel of the resulting image with the dependence of refraction on the depth. World matrix can be multiplied by MatrixScaling (1, 0.8, 1), thus simulating the apparent flattening of objects under water.
We set four constants for the
Code:
v4 = Vec4(0.707, 0.707, 0, QTime * 0.1) ' Time stored in W part 
v4.w = v4.w - Int(v4.w) 
Dev.SetVertexShaderConstantF 4, VarPtr(v4), 1 
v4 = Vec4(1, 1, 1, 0) ' Diffuse 
Dev.SetVertexShaderConstantF 5, VarPtr(v4), 1 
v4 = Vec4(0.3, 0.34, 0.35, -0.1) ' Ambient 
Dev.SetVertexShaderConstantF 6, VarPtr(v4), 1 
v4 = Vec4(0.1, 0.15, 0.2, -1 / 4)  ' light 
Dev.SetVertexShaderConstantF 7, VarPtr(v4), 1
We have a vector for Constants, Diffuse, Ambient and Light and color of wather. «A» (or «W») channel of first vector holds the pass the time, refractive index and the coefficient of water clarity. We calculate the alpha value (light) in the «A» channel using the refractive index, adjusted for depth.
Code:
mul oD0.a, c6.a, v0.y
And through oD1 (specular) transmits pixel shader color of the water by a factor of transparency, as well as adjusted for the depth.
Code:
mov oD1, c7 mul oD1.a, c7.a, v0.y
To animate the caustics (light rays reflected or refracted), we use the same method as for the texture animation of water. We will reuse the alpha-channel of the Volume textures that we used for water bump texture to store the caustics (to avoid creating extra textures)
In the pixel shader, after sampling, the texture of stone and multiplying by Diffuse (all standard), the result is multiplied by the caustic:
Code:
mul r0, r0, r1.a
r1.a holds a value in the range 0.5 to 1, and when we multiplying by this value, the effect makes underwater objects a little darker. This makes sense because its always a bit darker underwater, even if it is completely translucent, because of the light reflected from the surface will not enter the water.
Then interpolate the result with the color of water:
Code:
lrp r1, v1.a, v1, r0
In «A» we store the index of refraction:
Code:
mov r1.a, v0.a
and finally sends it to the output shader:
Code:
mov oC0, r1
Attached Images
File Type: jpg 3.jpg (41.0 KB, 13 views)
File Type: png 3.png (9.5 KB, 9 views)
Reply With Quote
  #4  
Old 07-21-2010, 08:20 AM
Mikle Mikle is offline
Newcomer
 
Join Date: Dec 2007
Location: Russia, Tuapse
Posts: 4
Default

Demo to the article:
http://www.xtremevbtalk.com/attachme...1&d=1279721316
Unfortunately forum does not allow faster executables. Therefore, to start the demo, you must first compile dx_vb.dll (written by C++ 2008) and put it in the folder "Demo". To start the shader compiler to compile d3dx_sc.dll and placed in a folder "Demo \ ShaderCompiler".

In the translation of this article, I had great help from Thomas Andersen, DracullSoft.
Attached Files
File Type: zip Demo.zip (1.47 MB, 16 views)

Last edited by Cerian Knight; 07-27-2010 at 10:29 PM. Reason: Additional cleanup. Colin amended grammar per request
Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump

Advertisement:

Powered by liquidweb