首页 > 其他分享 >Unity Shader 真实感水体渲染

Unity Shader 真实感水体渲染

时间:2023-02-10 02:33:22浏览次数:71  
标签:反射 Shader 水体 Unity xy 真实感 深度 纹理 scrPos

这两周用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反射效果

水体渲染+GPUI(一) - 知乎 (zhihu.com)

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

相关文章

  • threejs 第二十用 shaderMaterial
    自己写shader就得用这个材质需要vertexfragment先<scriptid="vertex-Shader"type="x-shader/x-vertex">varyingvec2vUv;voidmain(){gl_P......
  • unity乃至的特殊文件夹
    Assets文件夹:游戏资源的顶层文件夹。AssetDatabase方法可以访问里面的任意资源。Editor文件夹:编辑模式下的代码需要放在这里。打包以后,会自动剥离它。EditorDefaultRes......
  • unity2d碰撞和移动的反弹问题
    使用rigidbody2d和collider2d,可以实现控制物体移动和碰撞。但是有一个问题让博主抓狂:代表玩家的小方块撞上障碍物时,会有一瞬间嵌入障碍物,然后被弹回去。简单的控制代码如......
  • Unity操作-脚本使物体移动+旋转
    1.构建物体和对应的脚本 2.完成脚本编辑usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;publicclassObjectMoving:MonoBehav......
  • Unity音乐播放
    1.Unity场景搭建  2.脚本编码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;publicclassAudioPlay:MonoBehaviour{......
  • Unity之UnityWebRequest和使用
    一、前言1.UnityWebRequest官方描述:UnityWebRequest提供了一个模块化系统,用于构成HTTP请求和处理HTTP响应。UnityWebRequest系统的主要目标是让Unity游戏与Web......
  • VS2019和unity绑定设置
    第一步:打开本机VisualStudioInstaller 第二步:点击修改  第三步:下载对应组件  第四步:修改Unity脚本编辑工具绑定 点击edit -> preferences -> ......
  • Unity学习路线
    【第一阶段】01Unity3D基础操作02C#语言基础03PS图像简单处理04Unity3D界面UI(UGUI或NGUI)05Unity3D动画系统(Mecanim和DOtween)06Unity3D图形数学(点乘、欧......
  • Unity3D入门基础知识
    一、基础概念1、物体与空物体物体(GameObject),其实是一个节点或容器。一般所谓的“物体”,即有形状的东西,对应的Mesh,网格信息代表了物体(形状)。空物体(EmptyObject),即空对象......
  • Unity_FlowMap制作水面材质
    UnityFlowMap制作水面材质今天学习完了TA百人计划的FlowMap相关课程,自己尝试一下制作一个水的材质,实现一个简单的水面效果。现在记录一下过程。资源获取水的贴图下载......