Unity3D教程:实现水面渲染(三)
水面渲染在很多游戏项目中都会碰到,下面就分四篇文章去给大家介绍在Unity3D中水面渲染的实现方法,现在介绍的水面渲染(三)。
一、前言
本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。
二、概述
在前面的章节中,我们已经完成了波的模拟和水面的反射效果。这一节中,我们来看看折射效果的实现。
其实如果读者真的对反射效果的实现理解了的话,那么我相信实现折射也是非常简单的一件事。同样的,我们也会生成一个相机用于渲染折射贴图,然后在Shader中对其进行采样。当然,可能读者会看到Unity3D自带的水效中,折射贴图是通过GrabPass来获取的(具体细节请大家自行百度),这个方法不是说不可以,但是它有一丢丢问题。第一,某些Android设备不支持这个操作;第二,获取到的折射贴图是当前相机渲染的,如果我想让折射贴图的视角更大,这个方法就行不通了(为什么会有这个需求各位看下去就知道了)。
三、实现折射
代码的实现分为两个部分,一个是脚本,用于生成折射相机,渲染折射贴图并传递给shader;一个就是在shader中处理折射贴图了。
先来看脚本部分,很简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | Void Start() { if ( null == RefCamera) { GameObjectgo = new GameObject(); go.name= "refrCamera" ; RefCamera= go.AddComponent(); RefCamera.CopyFrom(Camera.main); //RefCamera.fieldOfView *= 1.1f; RefCamera.enabled= false ; RefCamera.cullingMask= ~(1 << LayerMask.NameToLayer( "Water" )); } if ( null == RefMat) { RefMat= this .GetComponent().sharedMaterial; } refTexture= new RenderTexture(Mathf.FloorToInt(RefCamera.pixelWidth), Mathf.FloorToInt(RefCamera.pixelHeight), 24); refTexture.hideFlags= HideFlags.DontSave; RefCamera.targetTexture= refTexture; } public voidOnWillRenderObject() { RefCamera.transform.position= Camera.main.transform.position; RefCamera.transform.rotation= Camera.main.transform.rotation; RefCamera.targetTexture= refTexture; RefCamera.Render(); RefMat.SetTexture( "_RefrTexture" ,RefCamera.targetTexture); } |
这个脚本应该挂在水面网格模型上,原因跟反射的相同。只不过在这里我们没有进行反射变换。然后在Shader中,获取_RefrTexture贴图,进行处理就Ok了。
1 | float4 refractionColor=tex2D(_RefrTexture,i.screenPos.xy/i.screenPos.z+offsets*_RefrOffset); |
代码跟反射贴图的采样几乎一模一样,然后我们直接返回refractionColor来看看效果:
第一张是RefrOffset=0.15的效果,第二张是RefrOffset=0的效果。可以看到,RefrOffset=0就是没有进行扰动,获取到的就是我们透过水看到的场景。
仔细观察第一张渲染图,可以发现两个问题:
1、边缘处有撕裂的情况,扰动越大越明显,这是因为渲染折射贴图的折射相机跟观察相机(主相机)的视角是一样大小的,当进行UV扰动时,有可能UV会超出[0,1]的界限,所以会造成这个情况。解决办法是将渲染折射贴图的相机的视角设置的大一些。
即:
1 | RefCamera.fieldOfView *= 1.1f; |
当然,在这么做了之后,在计算ScreenPos时,就要使用变换到折射相机空间的顶点坐标了。即原来是这么算的:
1 | o.screenPos =ComputeScreenPos(o.vertex).xyw; |
现在要这么算:
1 | o.screenPos =ComputeScreenPos(mul(_RefractCameraVP, float4(worldPos, 1))).xyw; |
其中_RefractCameraVP是将世界坐标系的顶点转换到折射相机的投影空间的矩阵。
我们可以在脚本中通过函数GetGPUProjectionMatrix来获取投影矩阵:
1 | P =GL.GetGPUProjectionMatrix(RefCamera.projectionMatrix, false ) |
然后是世界坐标系到视角坐标系(或者叫相机坐标系)的矩阵:
1 | V =RefCamera.worldToCameraMatrix |
将P和V相乘得到我们需要的VP = P*V。(注意矩阵相乘的顺序)
2、由于我们是进行整张图的扰动,所以某些本应该被遮挡住的部分也漏出来了。仔细观察立方体左上角和右上角的部分。
这个小问题在视角更垂直于水面的时候会更明显。解决方法是,我们只渲染水面以下的物体,对于水面往上的景物我们直接cull掉。这里又使用到了我们在反射中提及的视锥体的自定义裁剪问题。相关内容请看这里:http://gad.qq.com/article/detail/7157769。
因为处理的代码比较长,这里就不贴出来了。原理确实比较晦涩难懂,我还不能很好的讲清楚,大家就直接看论文吧。
修复了上述两个问题后:
四、菲涅耳效应!
到此为止,我们终于得到了比较正确的折射图。下面考虑将折射和反射进行混合。
这就要提到菲涅耳效应了,什么是菲涅耳效应?听着很高大上,请看下图:
(图来自网上)
看到视线与水面的夹角越小,反射越强烈,折射越弱;反之视角与水面的夹角越接近垂直,则折射越强,反射越弱。这就是菲涅耳效应要说的一件事。
一个菲涅耳反射系数的近似算法是:(3D游戏与计算机图形学中的数学方法)
其中,f0指的是入射角度接近0时的fresnel反射系数,V是指向视点的方向,H是半角向量。但是我看到有些版本里将H半角向量替换为N法向量,而且我试过,貌似使用N法向量效果更好,使用H半角向量的话基本看不到菲涅耳效应。
一个简单的实现如下所示:
1 2 3 4 5 6 7 8 9 10 11 | inline half FastFresnel(float3 I,float3 N, float R0) { float icosIN =saturate(1-dot(I, N)); float i2 =icosIN*icosIN, i4 = i2*i2; return R0 +(1-R0)*(i4*icosIN); } |
然后根据fresnel系数,将反射和折射进行混合:
1 2 3 | float fresnel = FastFresnel(-viewVector,worldNormal, 0.02f); result.xyz = lerp(refractionColor.xyz,reflectionColor.xyz, fresnel); |
本章最终的效果我们来看一看:
已经有了水的一些效果,但是还少了点什么,我们下章接着学。
最后给出1~3章的源码: