元旦快乐,送大家一条水晶龙
0x00 前言
hi,大家好。不知不觉时间就推进到了2018年,所以首先祝大家元旦快乐。由于前一段时间工作的内容发生了一些变化,因此有一段时间没有更新博客了。不过时间来到了元旦,还是得写点东西的。那么本文就来聊一聊如何利用环境映射来实现一条水晶龙吧,内容主要包括如何实现反射、折射以及菲涅耳效果。
0x01 反射
首先,我仍然使用来自斯坦福的上镜率超高的模型,这次是龙的模型。
将它导入到Unity的工程中。
同时,我们还需要一个CubeMap,用来提供环境信息。可以看到,使用了模型导入后Unity的默认material的龙,在蓝天白云的映衬下… 就像一条石膏龙。
好了,基本准备工作已经完成了。那么作为水晶龙打造计划的第一步,我们首先来实现水晶龙对环境的反射,让龙模型根据反射的结果去这个CubeMap上采样。
可能有些朋友会好奇,水晶龙的反射怎么才能和CubeMap建立起联系呢?其实很简单,我画一张示意图各位就明白了。
其实这个过程很简单,就是求的反射方向然后去cubemap上采样作为水晶龙身上该点的颜色即可。
当然,由于折射和反射是一个常用的需求,因此hlsl和glsl都有内置的a方法用来计算反射方向和折射方向。但是下文我还是会给出相关的推导过程。
接下来,我们需要实现对反射方向的计算。同样,我也来画一张示意图来推导反射的过程,大家就应该能明白了。
其中向量I代表入射光线,从眼睛到物体表面。向量N代表表面法向量,向量R则代表反射光线。
通过上面的推导,我们就可以在shader内实现自己的反射函数了。
//计算反射方向 float3 CaculateReflectDir(float3 I, float3 N) { float3 R = I - 2.f * N * dot(I, N); return R; }
一旦获取了反射方向之后,就可以在fragment shader内对CubeMap采样了。
fixed4 frag(v2f input) : SV_Target { float3 reflectedDir = CaculateReflectDir(input.viewDir, input.normalDir); fixed4 reflectCol = texCUBE(_Cube, reflectedDir); return reflectCol; }
结果我们的龙就从石膏龙变成了能够反射周围环境颜色的龙了。
0x02 折射
实现了反射的效果之后,我们还可以利用相似的思路来实现一些类似的效果,例如光的折射效果。
所谓的光的折射指的是当光通过不同密度的两种材质之间的介面时——比如空气和水——光的方向会发生改变。
当光波从一种介质传播到另一种具有不同折射率的介质时,会发生折射现象,其入射角与折射角之间的关系,可以用斯涅尔定律(Snell’s Law)来描述。
下面我就利用斯涅尔定律来计算折射方向,进而正确的在CubeMap上为龙的折射效果进行采样。同样,我也会画一张示意图,并将推导过程写在上面。
按照上文的公式推导,我们可以很简单的翻译成对应的shader方法。
//计算折射方向 float3 CaculateRefractDir(float3 I, float3 N, float ratio) { float cosTheta = dot(-I, N); float cosTheta2 = sqrt(1.f - pow(ratio,2) * (1 - pow(cosTheta,2))); float3 T = ratio * (I + N * cosTheta) - N * cosTheta2; return T; }
设n1/n2的值为0.9,对龙进行一次折射(因为光从空气进入模型会发生一次折射,从模型进入空气仍然会有一次折射,为了简单我们只考虑一次折射的情况)的结果如下图:
fixed4 frag(v2f input) : SV_Target { float3 reflectedDir = CaculateReflectDir(input.viewDir, input.normalDir); fixed4 reflectCol = texCUBE(_Cube, reflectedDir); float3 refractedDir = CaculateRefractDir(normalize(input.viewDir), input.normalDir, .2f); fixed4 refractCol = texCUBE(_Cube, refractedDir); }
0x03 菲涅尔效果
ok,在上文中我们分别讨论了如何在shader内实现反射和折射效果。但是在现实生活中,反射和折射可能是同时发生的。
一个生活中简单的小例子就是我们只有在几乎垂直向下看的时候,才能看到池塘内水下的情况。而在一个较小的角度观察池塘时,又会因为大部分会被反射而几乎没有什么折射,因而很难透过水的表面看到池塘内的情况。
这就是因为当光从一种具有折射率为n1的介质向另一种具有折射率为n2的介质传播时,在两者的交界处(通常称作界面)可能会同时发生光的反射和折射——这便是菲涅耳效果。
而菲涅尔方程则描述了不同光波分量被折射和反射的情况。也描述了波反射时的相变。但是,精确描述底层物理现象的菲涅耳公式是非常复杂的,因此在图形学中往往会采用菲涅耳公式的近似,而非菲涅耳公式本身。
常见的菲涅耳公式的近似包括所谓的Schlick’s approximation,它的公式如下:
R(θ) = R0 + (1 - R0)(1 - cosθ)5
详细的信息各位可以查看https://en.wikipedia.org/wiki/Schlick%27s_approximation。
还有一个近似则是我们要使用的经验近似,相对来说经验近似的效果更可控。
R = max(0, min(1, bias + scale * (1.0 + I • N)power))
其中R代表了反射系数。向量I从眼睛指向物体表面,向量N是该点的法线,bias、scale、power则是用来调整菲涅耳效果表现的经验参数。
而这个近似公式的基本概念就是当I和N几乎重合的时候,反射系数应该接近0,大部分的光会被折射。当I和N分开的时候,反射系数逐渐增大,被反射的光变多。而这个系数也应该保持在0~1之间。因此,在shader中可以像下面这样来实现:
//菲涅耳效果 float CaculateFresnelApproximation(float3 I, float3 N) { float fresnel = max(0, min(1, _FresnelBias + _FresnelScale * pow(min(0.0, 1.0 + dot(I, N)), _FresnelPower))); return fresnel; }
最终,在fragment shader内利用菲涅耳近似公式将反射效果和折射效果结合在一起,就能够实现一个更加真实的水晶龙效果了。
Demo地址:https://github.com/chenjd/Unity-Miscellaneous-Shaders
-EOF-
最后打个广告,欢迎支持我的书《Unity 3D脚本编程》
欢迎大家关注我的公众号慕容的游戏编程:chenjd01