基于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的伪代码。
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的代码如下:
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
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三种颜色的反射率并不同,所以对于更加精确的模拟我更倾向于使用三种颜色分别计算,合成最终的反射向量进行查询。
我觉得现有的模型表现方法无法满足实际的需求,而如果要基于物理实现更多的效果,更多物体的数值需要被预先计算得到,光栅化的操作却是比不上光线跟踪来的效果。