使用GLSL实现水面反射效果 1(翻译)

请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com

原文地址: http://khayyam.kaplinski.com/2011/09/reflective-water-with-glsl-part-i.html

出于物理准确性或为游戏提高可玩性,考虑上对水面及其反射效果的实现,会增加渲染引擎的功能。虽然只有通过光线跟踪才能完成真正的反射,但是通过使用非常简单的场景设置和一些GPU编程,就可以实现令人惊讶的逼真的近似效果。

良好的水模拟至少应具有以下特征:

  1. 真实反射(具有正确的视差)
  2. 能正确地裁剪绘制在反射图像上的水底下的物体
  3. 视角取决于水的透明度/反射率
  4. 实现涟漪和(或者)波浪的效果
  5. 水散射(即随着深度增加,水逐渐变得不透明)

其他不是很明显但是会让渲染效果更好的细节:

  1. 折射
  2. 焦散 - 即浅水底部的光点
  3. 反射光 - 即反射到水附近物体的光点

目前我只把第一个列表中的功能实现到到Khayyam/Sehle/Shinya的代码中。您可以查看我之前发布的一些引擎内图片。在这里,我将描述背后的数学机制,并提供编写自己的水面效果的系统/对象/渲染通道的指南。

1.Rendering reflection texture

没有反射的水看起来完全无趣 - 就像任何其他半透明表面一样。因此,我们从实现反射开始,然后继续其他效果。

1.1. Parallax

尽管到目前为止你仍然设法在一个render pass中渲染你的场景,但从这现在开始你需要至少使用两个render pass去进行场景绘制(实际上至少有N + 1个render pass,其中N是可见反射表面的数量)。

原因是我们无法重复使用第一次渲染出来的主场景图像用作反射效果的实现。首先是因为它可以使视锥体变得非常大(例如 - 如果从y一个高角度俯视观察水面,在当前的主视图中只看到地面和水,但要反射的内容却是天空)。其次是因为视差。“反射”不是真实的发生反射的场景的完美副本,而是从不同视点复制同一场景而已。下图说明了这一点。

$$ M{camera}^{‘}=M{reflection}\times M_{camera} $$

这意味着你需要进行“渲染内容到纹理”这一功能进行设置工作。我们将“水面反射的内容”渲染到一个目标纹理,然后在主场景中渲染水面时使用此目标纹理。

因此,为了获得存储了“水面反射的内容”的纹理,我们首先必须将场景从“反射用虚拟相机”视点P’渲染到纹理。首先,我们必须找到“反射用虚拟相机”的位置——或者更确切地说,是实际摄像机的观察矩阵的“反射用观察矩阵”。(因为除了位置我们还需要摄像机方向)。“反射用观察矩阵”可以通过以下公式来获得:

$$ M_{camera}^{'}=M_{reflection}\times M_{camera} $$

其中\(M_{camera}^{'}\)反射是镜面的反射矩阵。它可以通过反射平面的位置来计算:

其中\((N_x,N_y,N_z,D)\)是平面方程\(xN_x+yN_y+zN_z+D=0\)的系数。注意\((N_x,N_y,N_z)\)也是给定平面的法向量。\(M_{camera}\)就是实际相机所对应的观察矩阵。

1.2. Mirrored geometry

实际上我们针对上一张图片中作了点弊。我们对“镜像图像”旋转了180度,使之更加类似于原始图像,从而可以明显地看出视差效果。实际的镜像图像如下图所示:

镜像图像上的不同绕组顺序

请注意,图像中多边形的顶点环绕顺序,在镜像图像上是发生了翻转的,即三角形在原始时为CCW,但在反射时为CW。

如果你的场景都是双面材质,那么拣选方向是可以随意的。如果开启了拣选的话,比如总是定义拣选方式为CCW的话,必须对反射的图像进行某些操作,否则几何图形将无法正确渲染。

我们将利用相机始终(至少在大多数应用中)矩形并以视图方向为中心的功能。因此我们可以在沿着Y方向,翻转相机方向,使得缠绕顺序再次被正确设置。(它翻转反射的图像,因此它看起来像第一张图片上的(3))。这可以用一个以上的反射矩阵来完成:

其中(M_{flip})是简单的另一个反射矩阵,它在(XZ)平面上进行反射。现在,如果我们使用M’’ 相机作为相机矩阵渲染镜像,管道可以保持不变。当然,我们必须保存此矩阵以供以后参考,因为需要在主渲染阶段将纹理正确映射到水对象。

1.3. Underwater clipping

看看下面的图片:

我们在场景中添加了一个水下物体(Q). 现在它不应该出现在反射上,因为它不会阻挡实际的反射光线(PB’B)和(PA’A)。但我们没有进行光线追踪。相反地,我们将相机移动到镜像视点(P’)并像正常图像一样渲染反射。但正如你所看到的,物体(Q)阻挡了射线(PA’A),因此会出现在我们的反射中。这显然是不对的。

因此,我们必须确保在反射平面(水面)下的任何东西都不会出现在镜像渲染中。这可以通过三种不同的方式实现

  1. 在GPU上使用额外的剪裁平面。它可以非常快或非常慢 - 取决于使用的显卡和驱动程序。
  2. 在反射渲染过程中使用倾斜投影矩阵。你可以在这里阅读更多相关信息。这是很酷的技术,但我个人从来没有让它运作得很好,因为它弄乱了相机远的飞机。
  3. 在像素着色器中手动剪辑。它浪费了一些GPU周期,但其他方便且万无一失。

我选择了(3)因为倾斜投影矩阵似乎不能很好地适应广角相机(远处的平面移动通过无限远创造各种奇怪的效果)。剪辑本身就像在所有像素着色器的开头添加以下代码一样简单(或者更准确地说是用于可反射对象的代码):

 1uniform vec4 clip_plane;
 2varying vec3 interpolatedVertexEye;
 3
 4void main()
 5{ 
 6    float clipPos = dot (interpolatedVertexEye, clip_plane.xyz) + clip_plane.w;
 7    if (clipPos < 0.0) {
 8        discard;
 9    }
10    ...
11}

当然,您必须为着色器提供\(clip_plane\)并在顶点着色器中计算interpolatedVertexEye(它只是视图/眼睛空间中的顶点坐标:\(VertexEye=M_{modelview}\times Vertex\)。如果您不需要剪切,只需将\(clip_{plane}\)的xyz分量设置为零,即可渲染所有像素。

1.4. Putting it all together

在开始主渲染过程之前(正向或延迟)执行以下操作:

创建需要反射的所有对象的列表(以及所有反射平面的参数)。然后为每个反射平面:

  1. 计算反射的相机矩阵
  2. \( M_{camera}^{''}=M_{reflection}\times M_{camera}\times M_{flip} \)
  3. 设置相机矩阵(您可以使用剪切投影矩阵优化渲染,但这里不再讨论)。
  4. 设置剪裁平面到反射平面
  5. 渲染完整的场景
  6. 将渲染图像保存为与反射对象一起使用的纹理

如果您使用HDR,则不应使用色调映射反射纹理 - 除非您想要实现某些非常特定的效果。

2 Rendering reflective object

这实际上非常简单 - 前提是你手头有所有必要的参数。您还需要决定在哪个渲染阶段执行此操作。我使用透明舞台,因为水基本上只是场景中的一个半透明表面,但您也可以在透明度之前或之后添加另一个通道。

你需要手头:

  1. 反射相机矩阵\( M^{''}\)
  2. 您用于渲染投影到水面上产生反射效果的矩阵\( M_{ProjectionReflection}\)(通常这与您用于主相机的投影相同)
  3. 反射纹理

2.1.Vertex shader

1attribute vec3 vertex;
2uniform mat4 o2v_projection;
3varying vec3 interpolatedVertexObject;
4
5void main()
6{
7    gl_Position = o2v_projection * vec4(vertex.xy, 0.0, 1.0);
8    interpolatedVertexObject = vertex;
9}

我们在这里添加另一个约束 - 水面将位于对象局部坐标系的XY平面。如果你有适当的反射平面,这是完全没有必要的,但我发现它更容易。只需使用XY平面作为反射平面,并适当放置物体(水体)。

实际上,这允许我们做另一个很酷的技巧。我们可以使用水体底部(即河流,湖泊……)作为水体。它将在着色器中展平,但我们可以使用Z数据来确定给定点的水深。但下一部分还有更多相关内容。o2v_projection只是复合矩阵\( M_{Projection}\times M_{ModelView}\)的名称

我更喜欢用助记符名称来命名矩阵,描述它们所做的坐标系转换 - 在给定的情况下,它是Object To View,乘以Projection。interpolatedVertexObject只是对象局部坐标系中的顶点坐标 - 我们需要它来查找反射纹理。

2.2. Fragment shader

 1uniform mat4 o2v_projection_reflection;
 2uniform sampler2D reflection_sampler;
 3
 4varying vec3 interpolatedVertexObject;
 5
 6void main()
 7{
 8    vec4 vClipReflection = o2v_projection_reflection * vec4(interpolatedVertexObject.xy, 0.0 , 1.0);
 9    vec2 vDeviceReflection = vClipReflection.st / vClipReflection.q;
10    vec2 vTextureReflection = vec2(0.5, 0.5) + 0.5 * vDeviceReflection;
11
12    vec4 reflectionTextureColor = texture2D (reflection_sampler, vTextureReflection);
13
14    // Framebuffer reflection can have alpha > 1
15    reflectionTextureColor.a = 1.0;
16
17    gl_FragColor = reflectionTextureColor;
18}

o2v_projection_reflection是在反射渲染过程中使用的复合矩阵\( M_{Projection}\times M_{ModelView}\)。即:

$$ M_{camera}^{''}=M_{reflection}\times M_{camera}\times M_{flip} $$

顾名思义,它从物体坐标系转换到反射相机的剪辑坐标系。

在片元着色器中,我们只需在反射渲染过程中重复完整的变换管道,并使用最终的2D坐标进行纹理查找。为此,我们需要初始的,未转换的对象顶点 - 因此它们是从顶点着色器( interpolatedVertexObject)插值的。

我将反射 alpha设置为 1.0因为我使用HDR缓冲区,并且由于加法混合,最终的alpha可以在那里有一些非常奇怪的值。并渲染图像

最后的渲染结果如图:

Not very realistic? Up to now we have implemented water as perfect mirror. This is very far from reality (look at the feature list in the first section).

In the next parts I will show how to add viewing angle based transparency, water color and depth-dependent ripples to your water.

Have fun!