Unity3D教程:实现水面渲染(三)

发表于2016-08-22
评论3 6.6k浏览

  水面渲染在很多游戏项目中都会碰到,下面就分四篇文章去给大家介绍在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

PV相乘得到我们需要的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章的源码:

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引