这两周用Unity Shader做了点简单的水体渲染,有真实感的也有非真实感的,打算这几天总结整理一下贴出来。
毛星云大牛有一篇详细的真实感水体渲染介绍:https://zhuanlan.zhihu.com/p/95917609 介绍了常用的波形模拟和着色技术
我目前做了着色,主要实现了:反射(Planar Reflection & 菲涅尔)、折射、流动、基于深度的水体颜色、浮沫、岸边过渡、Bloom等效果。
话不多说. Here we go
1. 流动效果
使用一张法线纹理来模拟水面的起伏,这样我们就可以对法线纹理的采样坐标进行偏移来模拟水体的流动。
float2 speed = _Time.y * float2(_SpeedX, _SpeedY); fixed3 normal1 = UnpackNormal(tex2D(_BumpMap, i.uv.zw + speed)).rgb; fixed3 normal2 = UnpackNormal(tex2D(_BumpMap, i.uv.zw - speed)).rgb; fixed3 normal = normalize(normal1 + normal2); //采样后是切线空间的
其实这样效果已经还可以了,如果想再增加点控制的自由度,可以考虑增加一张Distortion Texture作为速度图
float2 surfaceDistortion = tex2D(_SurfaceDistortion, i.uv_surfaceDistort).rg * 2 - 1; //把[0,1]映射到[-1,1],这样也可以朝负方向移动 fixed3 normal = UnpackNormal(tex2D(_BumpMap, i.uv.zw + speed + surfaceDistortion)).rgb; //加上采样得到的速度
normal = normalize(normal);
加了Distortion Texture后很难说有没有变得更好看,只能说,多了种控制水流的方法。
2. 水体基础颜色
我们需要模拟出水体对颜色的吸收,水体越深的地方颜色应该越深。因此,水体基础颜色与水体深度、浅水颜色、深水颜色、过渡深度、过渡系数这个几个参数相关。
相对深度 = 水底深度(不透明物体深度) - 水面深度(当前渲染的片元深度)
- 不透明物体的深度可以通过摄像机渲染一张深度纹理获得(由于我们在渲染水面时把RenderType设置为了Transparent,因此不会出现在这张深度纹理中)
- 水面深度,也就是当前片元的深度,可以通过i.scrPos.w分量获得。这等价于先计算出片元的NDC坐标的z分量,再用LinearEyeDepth转换到view space中
为获取当前片元的深度,我们需要计算它的屏幕空间坐标i.scrPos,再把它转换为观察空间中的线性深度。
那么首先需要在顶点着色器中计算屏幕坐标
o.scrPos = ComputeGrabScreenPos(o.pos);
ComputeScreenPos的输出。我们可以直接通过i.scrPos.w分量(也就是clip.w,也就是-view.z)获取片元在观察空间中的线性深度,也可以计算NDC后,把其z分量转换到观察空间
float3 fragNDC = i.scrPos / i.scrPos.w; //片元的NDC坐标 float fragDepth = LinearEyeDepth(fragNDC.z); //片元在观察空间的深度值,或者直接用i.scrPos.w
float d = tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(i.scrPos)); //深度纹理采样得到的是NDC的z分量
float opaqueDepth = LinearEyeDepth(d); //观察空间中的线性深度
float deltaDepth = opaqueDepth - fragDepth;
有了相对深度之后,就可以模拟水体对颜色的吸收。按照 一波江水动京城:游戏水面渲染与互动 - 知乎 (zhihu.com) 的做法,是用深度系数对浅水颜色和深水颜色插值。
float depthCoef = pow(saturate(deltaDepth / _AbsorbCutOff), _TransCoef);
fixed4 baseColor = lerp(_ShallowColor, _DeepColor, depthCoef);
然而我这里采用了一张渐变纹理来模拟水体对颜色的吸收,因为感觉渐变纹理的自由度更高一些。注意渐变纹理的Ramp Mode一定要设置为Clamp!!
fixed4 baseColor = tex2D(_AbsorbTex, saturate(deltaDepth / _AbsorbCutOff).xx);
可以看到这样采样坐标是随着深度增加线性增加的,效果也还行。可以加点正弦函数啥的,或者制作渐变纹理的时候让颜色非线性变化,不过我这里就线性了。
3. 折射
为什么先做折射后做反射呢?因为感觉折射简单点。反射是个挺复杂的东西。
折射可以采用GrabPass + 屏幕坐标偏移的方式实现
首先用一个GrabPass抓屏,得到一张铺屏纹理
GrabPass { "_RefractTex" }
这样,我们在渲染水体的Pass中,就可以声明这个纹理进行访问。此外我们还声明了_RefractTex_TexelSize,等会儿在偏移中使用
sampler2D _RefractTex; half4 _RefractTex_TexelSize;
在片元着色器中对屏幕坐标进行偏移,起伏越大(切线空间的normal.xy)偏移越大,并用一个_Distortion属性进行控制
float2 offset = normal.xy * _Distortion * _RefractTex_TexelSize.xy; //水面起伏越大偏移越大 i.scrPos.xy += offset * i.scrPos.z; //越深偏移越大 fixed3 refractColor = tex2D(_RefractTex, i.scrPos.xy/i.scrPos.w).rgb; //透视除法得到真正的屏幕坐标
看一下第2节中的ComputeScreenPos函数的输出,可以看到还需要经过透视除法才能将i.scrPos转换为真正的屏幕坐标
_Distortion得调到非常大才能有比较明显的折射效果,毕竟GrabPass产生的纹理分辨率比较高的话_RefractTex_TexelSize太小了(也可以不乘这一项,这样_Distortion就可以小很多)
最后,我们还需要把2中计算出来的水体颜色和折射的水下物体颜色进行混合,来模拟水的深度越深,观察到的折射部分越少。
refractColor = lerp(refractColor, baseColor, _AbsorbCoef);
3.1 折射的修正
这样的折射还存在一点问题:靠近水面上方物体的屏幕像素,扰动之后可能会采样到水面上方的物体,看起来就像水面上方的物体也发生了折射。
因此,渲染当前片元时,只有扰动后的屏幕像素深度仍然大于水面深度,才保留扰动;如果扰动后的屏幕像素深度小于水面的深度了,那就取消扰动,用原始的屏幕像素作为refractColor
half2 offset = normal.xy * _Distortion * 5 * _RefractTex_TexelSize.xy;
half4 tempScrPos = i.scrPos;
tempScrPos.xy = i.scrPos.xy + offset * i.scrPos.z;
float distortDepth = LinearEyeDepth(tex2Dproj(_CameraDepthTexture, tempScrPos).x);
if (distortDepth < fragDepth)
tempScrPos = i.scrPos;
fixed3 refractColor = tex2D(_RefractTex, tempScrPos.xy / tempScrPos.w).rgb;
4. 平面反射
反射的实现方式有:Cubemap环境映射、反射探针、平面反射、屏幕空间反射SSR等。可以参考这篇文章
一开始我使用的Cubemap模拟环境映射的方法,但是水面毕竟很大,就没法通过一个点的环境映射来模拟整个水面的反射。我尝试在相机关于水面镜像的位置生成Cubemap,这样按理说可以通过reflectDir的方向采样Cubemap获得反射颜色,但是在生成Cubemap时,会把水面以下的部分也包含进去,这样效果就有些离谱。因此最终采用上面那篇参考文章中的平面反射的方法,水面是个平面,因此用平面反射的方法很合适。
描述一个平面的平面方程可以用它的法向量和平面上任意一点来描述: n·p + d = 0 其中n为法向量,p为平面上任意一点的坐标
得到平面方程后,对于空间中任意一点A,我们可以求得它关于平面堆成的点A'。从A到A'的变换(在其次坐标下)可以由一个4*4的矩阵R来描述
为了得到反射贴图,我们同步反射相机和主相机,并且把反射相机的worldToCameraMatrix(View矩阵)设置为主相机的View矩阵 * 反射矩阵R。由于矩阵乘法是从右到左的,也就是反射相机先做了关于平面对称的变换,再做了View变换。
reflectionCamera.worldToCameraMatrix = Camera.current.worldToCameraMatrix * reflectM;
设置反射相机的render target
reflectionCamera.targetTexture = reflectionRT;
并且设置好渲染纹理的名称,后续在shader中就可以访问反射贴图了
reflectionMaterial.SetTexture("_ReflectionTex", reflectionRT);
和GrabPass获得的当前屏幕相似,我们也是使用真正的屏幕坐标(i.scrPos.xy / i.scrPos.w)对反射贴图进行采样。
并且可以添加扰动,增强水体流动的感觉。
offset = normal.xy * _ReflectOffset * 5 * _ReflectionTex_TexelSize.xy; tempScrPos = i.scrPos; tempScrPos.xy += offset; fixed3 reflectColor = tex2D(_ReflectionTex, tempScrPos.xy / tempScrPos.w).rgb;
之后,我们再加点高光。毕竟高光反射也属于反射。
不过,高光应该加在反射颜色和折射颜色经过菲涅尔系数插值之后,因为我们想让高光的地方呈现白色(或我们想要的高光颜色),而不是用带高光的反射去和折射插值。
5. 浮沫
然后我们在finalColor上添加浮沫的颜色
浮沫在相对深度比较小的地方产生,我们用一张噪声纹理来模拟浮沫。并且用_FoamDistance来控制浮沫的宽度,用_NoiseCutOff使得浮沫纹理采样大于这个值的时候才产生浮沫。
fixed noise = tex2D(_FoamTex, i.uv_foam + normal.xy).r; half foam = smoothstep(_FoamDistance, 0, deltaDepth); //浮沫区域,用smoothstep让这个区域边缘处不会突变
foam *= smoothstep(_NoiseCutOff, 1, noise);
finalColor += foam * _FoamColor;
6. 岸边过渡
当反射很强时,水体和岸边的颜色有很明显的分界线,这是不太好的。我们希望在岸边时能透过水体看到水底下的颜色,起到岸边颜色平滑过渡的效果。
之前我们已经计算出了水体的深度deltaDepth,用这个值和一个阈值_ShoreDistance相比较,如果小于此阈值,我们就减小菲涅尔系数。
if(deltaDepth < _ShoreDistance) fresnel *= deltaDepth / _ShoreDistance;
7. Bloom效果
最后我们把高光反射的部分做个Bloom的后处理
在Camera上挂一个脚本,用RenderImage函数做后处理
在后处理的Shader中,共需要4个Pass。第一个Pass用于提取高亮区域,第二个和第三个Pass用于高斯模糊,第四个Pass用于混合Bloom纹理和原图像
最终效果如下:
8. References
(27条消息) Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection)_puppet_master的博客-CSDN博客_unity反射效果
Unity Shader 水体渲染 - 知乎 (zhihu.com)
Unity Shader ScreenPos详解 - 知乎 (zhihu.com)
一波江水动京城:游戏水面渲染与互动 - 知乎 (zhihu.com)
3D渲染技术分享:实时水面渲染方案(反射、折射、水深与水岸柔边) - 知乎 (zhihu.com)
标签:反射,Shader,水体,Unity,xy,真实感,深度,纹理,scrPos From: https://www.cnblogs.com/KimiRaikkonen/p/17081259.html