基于GPU屏幕空间的精确光学折射效果

 

摘要:在实时渲染中,光学物体的折射效果极大的影响场景的真实特性。由于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 ) 

=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;
 3varying vec3 EyeDir;
 4varying vec3 RefractDir;
 5varying vec3 Normal;
 6varying 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;
 2uniform sampler2D Tex1;
 3uniform sampler2D Tex2;
 4uniform samplerCube Tex3;
 5
 6
 7varying vec4 ProjTexCoord;
 8varying vec3 EyeDir;
 9varying vec3 RefractDir;
10varying vec3 Normal;
11varying 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三种颜色的反射率并不同,所以对于更加精确的模拟我更倾向于使用三种颜色分别计算,合成最终的反射向量进行查询。

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

posted on 2007-05-29 18:26  Bo Schwarzstein  阅读(5643)  评论(16编辑  收藏  举报