博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Unity着色器教程 | 积雪效果
阅读量:4147 次
发布时间:2019-05-25

本文共 5980 字,大约阅读时间需要 19 分钟。

转载自unity官方社区:http://forum.china.unity3d.com/thread-22366-1-1.html

为游戏中的所有纹理都加上雪花可能需要花费大量时间。本文将展示在Unity中如何创建(屏幕空间着色器)来快速改变场景的季节。

使用前后效果图对比如下:



工作原理


上面两张图显示的是同一个场景。它们之间唯一的区别就是第二张图启用了相机上的雪花特效。实现该效果无需对场景的贴图做任何更改。这是什么原理呢?


其实原理非常简单。就是假定所有法线朝上的像素点(如:草,地板等)都需要覆盖雪花。同样,法线朝着其它方向的像素点(如:松树,墙),则需要在雪花纹理和原始纹理之间进行平缓过渡。


获取所需数据


实现上面的雪花效果有以下准备事项:

  • 将渲染路径设置为Deferred(延迟渲染)
  • 将Camera.depthTextureMode设置为DepthNormals


由于第二项可以很方便地由屏幕特效脚本进行设置,所以如果游戏已经使用了
时,第一项很容易出问题。


设置为DepthNormals后可以读取屏幕深度(像素与相机之间的距离)和法线(所朝的方向)。


创建一个屏幕特效(Image Effect)由至少一个脚本和一个着色器构成。通常这个着色器不是用来渲染3D物体的,而是根据给定的输入数据渲染一个全屏的图像。在本文的例子中,输入数据就是一张相机渲染的结果图片以及一些用户设置的属性。


[C#] 
纯文本查看 
复制代码
using
UnityEngine;
using
System.Collections;
 
[ExecuteInEditMode]
public
class
ScreenSpaceSnow : MonoBehaviour
{
 
    
public
Texture2D SnowTexture;
 
    
public
Color SnowColor = Color.white;
 
    
public
float
SnowTextureScale = 0.1f;
 
    
[Range(0, 1)]
    
public
float
BottomThreshold = 0f;
    
[Range(0, 1)]
    
public
float
TopThreshold = 1f;
 
    
private
Material _material;
 
    
void
OnEnable()
    
{
        
// dynamically create a material that will use our shader
        
_material =
new
Material(Shader.Find(
"TKoU/ScreenSpaceSnow"
));
 
        
// tell the camera to render depth and normals
        
GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    
}
 
    
void
OnRenderImage(RenderTexture src, RenderTexture dest)
    
{
        
// set shader properties
        
_material.SetMatrix(
"_CamToWorld"
, GetComponent<Camera>().cameraToWorldMatrix);
        
_material.SetColor(
"_SnowColor"
, SnowColor);
        
_material.SetFloat(
"_BottomThreshold"
, BottomThreshold);
        
_material.SetFloat(
"_TopThreshold"
, TopThreshold);
        
_material.SetTexture(
"_SnowTex"
, SnowTexture);
        
_material.SetFloat(
"_SnowTexScale"
, SnowTextureScale);
 
        
// execute the shader on input texture (src) and write to output (dest)
        
Graphics.Blit(src, dest, _material);
    
}
}


这里只是基础的设置,还不能生成雪花。有趣的事情还在后面。


着色器


雪花着色器是无光照着色器(unlit shader),因为屏幕空间是没有光照的,所以也不会用到任何光照信息。基础模板如下:


[C#] 
纯文本查看 
复制代码
Shader
"TKoU/ScreenSpaceSnow"
{
    
Properties
    
{
        
_MainTex (
"Texture"
, 2D) =
"white"
{}
    
}
    
SubShader
    
{
        
// No culling or depth
        
Cull Off ZWrite Off ZTest Always
 
        
Pass
        
{
            
CGPROGRAM
            
#pragma vertex vert
            
#pragma fragment frag
             
            
#include "UnityCG.cginc"
 
            
struct
appdata
            
{
                
float4 vertex : POSITION;
                
float2 uv : TEXCOORD0;
            
};
 
            
struct
v2f
            
{
                
float2 uv : TEXCOORD0;
                
float4 vertex : SV_POSITION;
            
};
 
            
v2f vert (appdata v)
            
{
                
v2f o;
                
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                
o.uv = v.uv;
                
return
o;
            
}
             
            
fixed4 frag (v2f i) : SV_Target
            
{
                
// the magic happens here
            
}
            
ENDCG
        
}
    
}
}


注意,直接新建无光照着色器(Create->Shader->Unlit Shader) 就会看到几乎同样的代码。


现在看看重要部分:片段着色器。首先,需要通过ScreenSpaceSnow脚本来获取所有数据:

[C#] 
纯文本查看 
复制代码
sampler2D _MainTex;
sampler2D _CameraDepthNormalsTexture;
float4x4 _CamToWorld;
 
sampler2D _SnowTex;
float
_SnowTexScale;
 
half4 _SnowColor;
 
fixed
_BottomThreshold;
fixed
_TopThreshold;
 
 
half4 frag (v2f i) : SV_Target
{
     
}


找出需要下雪的地方


正如之前所说,所有法线朝上的表面都将覆盖雪。相机已经设置了生成深度法线贴图,所以现在直接获取即可。代码如下:

[C#] 
纯文本查看 
复制代码
sampler2D _CameraDepthNormalsTexture;


查看Unity官方文档可以了解该命名的意义:

深度贴图可以作为一个着色器的全局着色器属性进行采样。通过声明名为_CameraDepthTexture的采样器,就能够采样相机的主深度纹理。

_CameraDepthTexture总是引用相机的主深度贴图。


现在开始获取法线:

[C#] 
纯文本查看 
复制代码
half4 frag (v2f i) : SV_Target
{
    
half3 normal;
    
float
depth;
 
    
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
    
normal = mul( (float3x3)_CamToWorld, normal);
 
    
return
half4(normal, 1);
}


Unity文档解释深度和法线的数据都打包为16位。这里需要像代码那样调用DecodeDepthNormal方法进行解包。


这个方法检索的是相机空间的法线。也就是说,如果旋转屏幕相机,那么法线朝向也会改变。脚本中将法线乘以_CamToWorld 矩阵就是为了避免这种情况。它会将法线从相机空间转换为世界空间,这样就不再依赖于相机的透视。


为了让着色器正确编译就必须返回一些东西,所以上面的代码设置了返回语句。这样也便于预览结果以确认计算是否正确。




暂时渲染为RGB图像。在Unity中,Y轴是默认向上的。图中绿色部分表示Y坐标轴的值。目前为止结果不错!


现在将其转换为雪量的因子。

[C#] 
纯文本查看 
复制代码
half4 frag (v2f i) : SV_Target
{
    
half3 normal;
    
float
depth;
 
    
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
    
normal = mul( (float3x3)_CamToWorld, normal);
 
    
half snowAmount = normal.g;
    
half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
    
snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);
 
    
return
half4(snowAmount, snowAmount, snowAmount, 1);
}


这里会用到绿色分量。接下来配置积雪覆盖区域顶部和底部的阀值,以便于调整场景的积雪量。



雪纹理


如果没有纹理,雪看起来会不真实。最难的部分就是将2D纹理(屏幕空间)应用到3D物体上。一种方法是获取像素的世界坐标,然后将世界坐标的X和Z值作为纹理坐标。


[C#] 
纯文本查看 
复制代码
half4 frag (v2f i) : SV_Target
{
    
half3 normal;
    
float
depth;
 
    
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
    
normal = mul( (float3x3)_CamToWorld, normal);
 
    
// find out snow amount
    
half snowAmount = normal.g;
    
half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
    
snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);
 
    
// find out snow color
    
float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
    
float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;
    
float4 wpos = mul(_CamToWorld, float4(vpos, 1));
    
wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;
 
    
half3 snowColor = tex2D(_SnowTex, wpos.xz * _SnowTexScale * _ProjectionParams.z) * _SnowColor;
 
    
return
half4(snowColor, 1);
}


这里涉及到一些数学知识,您只需知道vpos是视口坐标,wpos是由视口坐标与_CamToWorld矩阵相乘而得到的世界坐标,并且它通过除以远平面的位置(_ProjectionParams.z)来转换为有效的世界坐标。最后使用XZ坐标乘以可配置参数_SnowTexScale和远平面,来计算雪的颜色并获取适当的值。




合并


下面将积雪与场景进行合并。


[C#] 
纯文本查看 
复制代码
half4 frag (v2f i) : SV_Target
{
    
half3 normal;
    
float
depth;
 
    
DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), depth, normal);
    
normal = mul( (float3x3)_CamToWorld, normal);
 
    
// find out snow amount
    
half snowAmount = normal.g;
    
half scale = (_BottomThreshold + 1 - _TopThreshold) / 1 + 1;
    
snowAmount = saturate( (snowAmount - _BottomThreshold) * scale);
 
    
// find out snow color
    
float2 p11_22 = float2(unity_CameraProjection._11, unity_CameraProjection._22);
    
float3 vpos = float3( (i.uv * 2 - 1) / p11_22, -1) * depth;
    
float4 wpos = mul(_CamToWorld, float4(vpos, 1));
    
wpos += float4(_WorldSpaceCameraPos, 0) / _ProjectionParams.z;
 
    
wpos *= _SnowTexScale * _ProjectionParams.z;
    
half3 snowColor = tex2D(_SnowTex, wpos.xz) * _SnowColor;
 
    
// get color and lerp to snow texture
    
half4 col = tex2D(_MainTex, i.uv);
    
return
lerp(col, snowColor, snowAmount);
}


上述代码获取原始颜色,并使用snowAmount进行插值渐变为snowColor 。




最后一步:将_TopThreshold设为0.6:




完成!


结论


全屏效果见下图。看起来不错吧?


你可能感兴趣的文章
java8采用stream对集合的常用操作
查看>>
EasySwift/YXJOnePixelLine 极其方便的画出真正的一个像素的线
查看>>
Ubuntu系统上安装Nginx服务器的简单方法
查看>>
Ubuntu Linux系统下apt-get命令详解
查看>>
ubuntu 16.04 下重置 MySQL 5.7 的密码(忘记密码)
查看>>
Ubuntu Navicat for MySQL安装以及破解方案
查看>>
HTTPS那些事 用java实现HTTPS工作原理
查看>>
oracle函数trunc的使用
查看>>
MySQL 存储过程或者函数中传参数实现where id in(1,2,3,...)IN条件拼接
查看>>
java反编译
查看>>
Class.forName( )你搞懂了吗?——转
查看>>
jarFile
查看>>
EJB3.0定时发送jms(发布/定阅)方式
查看>>
EJB与JAVA BEAN_J2EE的异步消息机制
查看>>
数学等于号是=那三个横杠是什么符
查看>>
HTTP协议详解
查看>>
java多线程中的join方法详解
查看>>
ECLIPSE远程调试出现如下问题 ECLIPSE中调试代码提示找不到源
查看>>
java abstract修饰符
查看>>
数组分为两部分,使得其和相差最小
查看>>