Gamasutra - Book Excerpt - "Using Vertex Texture Displacement for Realistic Water Rendering"
It's free to join Gamasutra!|Have a question? Want to know who runs this site? Here you go.|Targeting the game development market with your product or service? Get info on advertising here.||For altering your contact information or changing email subscription preferences.
Registered members can log in here.Back to the home page.

Search articles, jobs, buyers guide, and more.

By Yuri Kryachko
[Author's Bio]

Gamasutra
January 3, 2006

Introduction

18.2.4 Quality Improvements and Optimizations

18.2.5 Rendering Local Perturbations

Printer Friendly Version
   


Excerpted from:



[More information...]
 


Change Login/Pwd
Post A Job
Post A Project
Post Resume
Post An Event
Post A Contractor
Post A Product
Write An Article
Get In Art Gallery
Submit News

 


 


Latest Letters to the Editor:
Perpetual Layoffs by Alexander Brandon [09.21.2007]

Casual friendliness in MMO's by Colby Poulson [09.20.2007]

Scrum deals and 'What is Scrum?' by Tom Plunket [08.29.2007]


[Submit Letter]

[View All...]
  



Upcoming Events:
Women In Games Conference
Coventry, United Kingdom
09.10.08

3rd ACM International Conference on Digital Interactive Media in Entertainment and Arts - DIMEA 2008
Athens, Greece
09.10.08

GDC Austin
Austin, United States
09.15.08

Game Career Seminar
Austin, United States
09.17.08

Games Convention Asia 2008
, Singapore
09.18.08

[Submit Event]
[View All...]

 


[Enter Forums...]

Note: Discussion forums for Gamasutra are hosted by the IGDA, which is free to join.
 


Features

Book Excerpt:
GPU Gems 2
:
Using Vertex Texture Displacement for Realistic Water Rendering

18.2.4 Quality Improvements and Optimizations

Packing Heights for Bilinear Filtering Vertex texture fetch can be quite expensive. On GeForce 6 Series hardware, a single vertex texture fetch can introduce noticeable latency in the vertex program. So we want to minimize the number of texture fetches inside the vertex program. On the other hand, it is very desirable to perform some kind of filtering on the texture values; otherwise, visual quality will be significantly degraded. Traditional well-known filtering methods are bilinear and trilinear filtering. Bilinear filtering computes the weighted average of four texels closest to the coordinates of the texture fetch. Trilinear filtering averages the results of bilinear lookups in adjacent mip levels, weighting each by the corresponding LOD fraction. Because the current generation of graphics hardware doesn't support any form of filtering of vertex texture values, we have to emulate filtering in the shader with explicit math instructions. When implemented naively, even the simplest bilinear filter would require four texture lookups to calculate a single filtered value. A trilinear filter would require twice as many texture lookups. To reduce the number of texture fetches necessary for filtering, we build our texture in a special way, so that each texel contains all the data necessary for a single bilinear texture lookup. This is possible because our height maps are essentially one-component textures, and we can pack four height values into a single texel of a four-component texture:

where i = 0..N - 1, j = 0..M - 1. H is our height map value, F is a filtering function, and A is the packed output texture.

Listing 18-2 implements bilinear texture lookup into vertex texture, packed as shown previously.


float tex2D_bilinear4x( uniform sampler2D tex,
  float4 t,
  float2 Scales)

{
  float size = Scales.x;
  float scale = Scales.y;

  float4 tAB0 = tex2Dbias (tex, t);

  float2 f = frac (t.xy * size);
  float2 tAB = lerp (tAB0.xz, tAB0.yw, f.x);
  return lerp (tAB.x, tAB.y, f.y);
}

Listing 18-2. Efficient Bilinear Texture Interpolation in Vertex Shaders
Based on fetching the appropriate four height components with a single texture fetch.

We can extend this approach for trilinear filtering. Because trilinear filtering requires a fractional LOD value, we can use the distance from the camera as a good approximation of LOD. The code in Listing 18-3 implements trilinear texture lookup into the packed vertex texture.


float tex2D_trilinear( uniform sampler2D tex,
  float4 t,
  float2 Scales)
{
  float fr = frac (t.z);
  t.z -= fr; // floor(t.zw);
  float
Res;
  if (fr < 0.30)
    Res = tex2D_bilinear4x(tex, t.xyzz, Scales);
  else if (fr > 0.70)
    Res = tex2D_bilinear4x(tex, t.xyzz + float4 (0, 0, 1, 1),
                           Scales * float2 (0.5, 2));
  else {
    Res = tex2D_bilinear4x(tex, t.xyzz, Scales);
    float Res1 = tex2D_bilinear4x(tex, t.xyzz + float4 (0, 0, 1, 1),
                                  Scales * float2 (0.5, 2));
    fr = saturate ((fr - 0.30) * (1 / (0.70 - 0.30)));
    Res = Res1 * fr + Res * (1 - fr);
  }
  return Res;
}


Listing 18-3. Extending the Bilinear Filtering Technique to Trilinear Filtering

Note that we've further optimized trilinear texture fetch by performing two texture lookups only in the region where the influence of both mip levels is significant. In other regions, we “snap” the LOD value to the closest mip level, thus saving on texture bandwidth.

Avoiding Unnecessary Work with Branching

Even with optimized texture filtering, the number of texture fetches during water rendering can still be high, which significantly affects performance. We could reduce the total number of rendered vertices, but that would lower overall visual detail and increase aliasing.

Because we render our water with large batches of geometry, some of the triangles end up being completely off-screen. Note that even for such triangles, the vertex program is still executed, wasting precious computational resources. We can save a significant amount of per-vertex work if we skip computation for triangles falling outside the camera frustum.

The vertex program operates at one vertex at a time and has no access to topological information, so we can make decisions only on the per-vertex level, but not on the per-triangle level. This can create artifacts if some of the vertices within a triangle skip vertex texturing and others don't. We have found that in practice our triangles and our vertex texture displacements are small enough for this artifact not to be detectable.

The following pseudocode illustrates this idea:

float4 ClipPos = mul (ModelViewProj, INP);
float3 b0 = abs (ClipPos.xyz) < (ClipPos.www * C0 + C1);

if ( all (b0)) {
  
// Vertex belongs to visible triangle,
  // Perform texture sampling and displace vertex accordingly
}

In the preceding code, we use the clip-space vertex position to determine if the current vertex lies within the frustum, and then we perform the expensive computations only when necessary.

The values C0 and C1 are special “fudge” constants that control how much triangles need to extend beyond the camera frustum to trigger clipping. That way, we avoid artifacts caused by skipping texturing for out-of-frustum vertices whose triangles are still visible. Effectively, we are making our “clipping” frustum slightly wider, allowing for a certain amount of “guard-band” space along screen edges. Because our water plane is tessellated finely enough and the vertex texture displacements are reasonable, this simple method works well in practice.

Using Render-to-Texture

We can also improve the speed of our approach by first combining our height map textures into a single floating-point texture in a separate pass. It then becomes unnecessary to perform multiple expensive filtering operations in the vertex shader. Additionally, we can now use a more compact texture format, 16-bit floating point, for storage of the original height maps. We could also store a sequence of animated height maps as slices of a 3D texture, which would make animation smoother.

With this optimization, our rendering loop becomes two passes:

  1. Combine the height maps using a special pixel shader by rendering a single quadrilateral into an fp32-texture. Texels in this texture map to the vertices of the radial mesh.
  2. Use the generated height map as a vertex texture to displace the radial mesh vertices, as described previously.

Back Sides of Waves

Because our lighting computations are performed in the pixel shader under the assumption that the water surface is flat, this approximation can cause visual artifacts in certain cases.

In the case depicted in Figure 18-4, we see the back side of the wave, even though it is directed outward from the viewer due to geometrical displacement and shouldn't be visible in reality. This results in disturbing bright areas at the tops of the waves.

To minimize these artifacts, we adjust our normal vectors used for the lighting calculation by “tilting” them toward the viewer a bit, so that they correspond to the front faces of the wave more closely. You can find source code for this technique on the accompanying CD. Figure 18-5 shows a scene produced using the methods described in this chapter.


Figure 18-4. A Source of Rendering Artifacts
The back side of a wave (green) may be shaded even though it shouldn't be visible. Adjusting the normal vector used in the lighting calculation can significantly reduce this error.


Figure 18-5. Extremely Realistic Water Rendered with Techniques Presented Here

 

 


join | contact us | advertise | write | my profile
news | features | companies | jobs | resumes | education | product guide | projects | store



Copyright © 2005 CMP Media LLC

privacy policy
| terms of service