非天夜翔文集:基于GPU屏幕空间的精确光学折射效果

来源:百度文库 编辑:九乡新闻网 时间:2024/07/04 21:59:59

摘要:在实时渲染中,光学物体的折射效果极大的影响场景的真实特性。由于GPU是以光栅化而不是光线跟踪的方式工作的,精确的进行光学特性的模拟需要极大的计算量。Chris Wyman展示了一种简单的基于屏幕空间的折射效果的实现,得到的效果已经极大的接近光线跟踪的结果。而且他的这种方法在最新的由Technische Universität München慕尼黑工业大学在GPGPU.ORG上展示的《Interactive Screen-Space Accurate Photon Tracing on GPUs》这篇文章中都有所提及。这篇我只是简单的实现了Wyman的方法,详情大家可以去Google搜索《An Approximate Image-Space Approach for Interactive Refraction》这篇文章。

  在Cg、HLSL、GLSL中都有Refract与Reflect这两个函数。Reflect大部分用于环境贴图坐标的计算,Refract用于表现一个透明物体的光线折射特性。事实上,当我们在启用了深度测试的屏幕上计算的折射效果时,已经暗示了透明物体只有一层,也就是说我们计算的折射向量是完全错误的,因为折射光线还需要与后面,经过深度缓冲已经剔除的后面发生第二次折射,这样第二次计算过折射向量后才能够作为纹理坐标查询环境贴图。图示如下:

  Wyman使用的方法很简单。运用Multipass多通道技术,分别使用两种深度测试,以物体的向量作为顶点颜色,分别输出4张纹理。渲染时只需要3张。分别如下:

  这是“前面”,使用glClearDepth(1)与glDepthFunc(GL_LESS)这两个参数进行深度测试的结果。相应的深度贴图如下:

  还有“后面”,使用glClearDepth(0)与glDepthFunc(GL_GREATER)进行测试。

  相应的深度纹理。

  我们可以通过FragmentShader,把两张深度纹理相减,获得Perspective方式下的物体前后面的距离d(PS:光线跟踪就没有这样简便了,需要多次遍历测试三角形)。注意,这是在Perspective方式下的距离,不是精确的几何距离,后面我们将看到如何看待这个问题。

  对于需要进行折射计算的物体,我们可以很方便的在VertexShader中得到下面的信息:

每个顶点的位置P1

相应顶点的向量N1

从视点到顶点的向量V

  从文章开头的示意图中可以知道,“后面”的点P2 = P1 + dT1。而且只要我们知道了P2和N2就可以得到了准确的第二次折射的结果。不过我们考虑一个实时,就是当折射体的折射率过大时,折射光线将非常靠近-N1。Wyman对比了其他人的方法,建议对d进行插值计算:

 

  Hoppe(此人是微软DirectX开发组的专家,D3D中的那个优化Mesh功能就是他博士论文的直接成果)使用预计算的Geometry Image进行采样,不过会产生不连续的d导致走样。这个d的计算非常的蹊跷,让我们先看下Wyman的伪代码。

 

for all fragments F (given P1 , V, and N1 ), do

T1
=
Refract( V, N1 )

dV1
=
DistanceFrontFaceToBackFace( F, BackfaceZBuf )

dN
=
DistanceAlongNormal( P1 )

d
=WeightDistance( -
N1 ·T1 , V1 ·T1 , dV1 , dN )

P2
= P1 +
dT1

texFar
=
ProjectToScreenSpace( P2 )

N2 ≈ TextureLookup( texFar , BackfaceNormals )

T2 ≈ Refract( T1 , N2 )

return
IndexEnvironmentMap( T2 )

  有了第一次折射的光线T1后,我们需要找到P2以获得那个面上的N2,以用来再次计算出射向量。而P2又是和d密切相关的,我们如何获得这个d,也就是法线方向上的垂直距离呢?

  对于茶壶,球体、立方体等等这些规则的物体,我们可以简单的使用立体几何的知识进行计算。比如图1的那个d,假如我们已经知道茶壶的半径,那么我们可以简单的利用Object Coordinates计算,d 近似等于sqrt(y^2 + R^2)。即使是不规则的物体,只要我们知道它的主要入射方向上所有的顶点,我们就可以预先计算好插值过的d。

  下面是使用单次折射与多次折射两种效果的对比:

  可以看出多次折射在Teapot体上显示出了更多的效果,多了一个小斑点呵呵。

  第二通道相应的Vertex Shader的代码如下:

 

1
2varying vec4 ProjTexCoord;
3
varying vec3 EyeDir;
4
varying vec3 RefractDir;
5
varying vec3 Normal;
6
varying
float
dN;
7

8void
main()
9
{
10
gl_TexCoord[
0] =
gl_MultiTexCoord0;
11
ProjTexCoord
= gl_ModelViewProjectionMatrix*
gl_Vertex;
12
ProjTexCoord.xy
= 0.5 * (ProjTexCoord.xy +
ProjTexCoord.ww ) ;
13
ProjTexCoord.z
= 0.5 * (ProjTexCoord.z + ProjTexCoord.w + 0.64
);
14
ProjTexCoord.xyz
*=
ProjTexCoord.w;
15

16
EyeDir
= normalize( vec3(gl_ModelViewMatrix*
gl_Vertex) );
17
Normal
= normalize( gl_NormalMatrix*
gl_Normal );
18
RefractDir
= refract(EyeDir,Normal,0.65); //the first time reflect

19 dN = sqrt(pow(gl_Vertex.y,2.0)+1.0);
20

21
gl_Position
=
ftransform();
22
}

23

Fragment Shader

1uniform sampler2D Tex0;
2
uniform sampler2D Tex1;
3
uniform sampler2D Tex2;
4
uniform samplerCube Tex3;
5

6

7
varying vec4 ProjTexCoord;
8
varying vec3 EyeDir;
9
varying vec3 RefractDir;
10
varying vec3 Normal;
11
varying
float
dN;
12

13void
main()
14
{
15

16
float bias = 1.0/512.0
;
17
float dV1 = texture2DProj(Tex2,ProjTexCoord.xyz).x -
texture2DProj(Tex1,ProjTexCoord.xyz).x;
18
float d = mix(dV1,dN,dot(Normal,EyeDir)/
dot(Normal,RefractDir));
19
vec3 P2
= -EyeDir + d*
RefractDir;
20
vec3 N2
=
texture2DProj(Tex0,P2.xyz).rgb;
21
vec3 T2
= refract(RefractDir,N2,0.65
);
22
vec3 color
=
textureCube(Tex3,T2).rgb;
23
gl_FragColor
= vec4(color,1.0
);
24
}

25


  暴露出的问题

  显然,这种方法只考虑的比较规则的模型,甚至连这个茶壶都无法模拟精确,只能对凸立方体有起作用,如果几何体过于复杂,它将只得到第二次折射是最终的结果,对于更加小范围的折射将忽略。而且考虑到光谱,RGB三种颜色的反射率并不同,所以对于更加精确的模拟我更倾向于使用三种颜色分别计算,合成最终的反射向量进行查询。

  我觉得现有的模型表现方法无法满足实际的需求,而如果要基于物理实现更多的效果,更多物体的数值需要被预先计算得到,光栅化的操作却是比不上光线跟踪来的效果。