当前位置: 首页 > news >正文

Unity-Shader详解-其五

关于Unity的Shader部分的基础知识其实已经讲解得差不多了,今天我们来一些实例分享:

溶解

效果如下:

代码如下:

Shader "Chapter8/chapter8_1"
{Properties{// 定义属性[NoScaleOffset]_Albedo("Albedo", 2D) = "white" {} // 基础颜色纹理,默认白色_Noise("Dissolve Noise", 2D) = "white" {} // 溶解噪声纹理,默认白色_Dissolve("Dissolve", Range(0, 1)) = 0 // 溶解程度,范围0到1,默认0[NoScaleOffset]_Gradient("Edge Gradient", 2D) = "black" {} // 边缘渐变纹理,默认黑色_Range("Edge Range", Range(2, 100)) = 6 // 边缘范围,范围2到100,默认6_Brightness("Brightness", Range(0, 10)) = 1 // 亮度,范围0到10,默认1}SubShader{Tags{ "RenderType"="TransparentCutout" "Queue" = "AlphaTest" // 渲染类型为透明剪切,队列为Alpha测试}CGPROGRAM#pragma surface surf StandardSpecular addshadow fullforwardshadows // 使用StandardSpecular表面着色器,并添加阴影和全向阴影struct Input{float2 uv_Albedo; // Albedo纹理的UV坐标float2 uv_Noise; // 噪声纹理的UV坐标};sampler2D _Albedo; // Albedo纹理sampler2D _Noise; // 噪声纹理fixed _Dissolve; // 溶解程度sampler2D _Gradient; // 边缘渐变纹理float _Range; // 边缘范围float _Brightness; // 亮度void surf (Input IN, inout SurfaceOutputStandardSpecular o){// 溶解遮罩fixed noise = tex2D(_Noise, IN.uv_Noise).r; // 从噪声纹理中获取红色通道值fixed dissolve = _Dissolve * 2 - 1; // 将溶解程度从0-1映射到-1到1fixed mask = saturate(noise - dissolve); // 计算遮罩值,限制在0到1之间clip(mask - 0.5); // 根据遮罩值进行剪切,小于0.5的部分将被剔除// 燃烧效果fixed texcoord = saturate(mask * _Range - 0.5 * _Range); // 计算纹理坐标,用于边缘渐变o.Emission = tex2D(_Gradient, fixed2(texcoord, 0.5)) * _Brightness; // 根据渐变纹理和亮度计算自发光fixed4 c = tex2D (_Albedo, IN.uv_Albedo); // 从Albedo纹理中获取颜色o.Albedo = c.rgb; // 设置表面颜色}ENDCG}
}

属性中有一个[NoScaleOffset]:

一言以蔽之, [NoScaleOffset]修饰的纹理的尺寸不可修改(至少在Inspector界面不可修改)。

        Tags{ "RenderType"="TransparentCutout" "Queue" = "AlphaTest" // 渲染类型为透明剪切,队列为Alpha测试}

Tags中设置渲染类型为TransparentCutout而渲染队列为AlphaTest。

        #pragma surface surf StandardSpecular addshadow fullforwardshadows // 使用StandardSpecular表面着色器,并添加阴影和全向阴影

正如注释所写,使用表面着色器,相关函数为surf,采用StandardSpecular光照模型,添加阴影以及全向阴影。

如果我们需要去查询Unity内置的光照模型和阴影模型可以:

        struct Input{float2 uv_Albedo; // Albedo纹理的UV坐标float2 uv_Noise; // 噪声纹理的UV坐标};

 这是作为输入的结构体,内部包含的是基础的纹理的UV坐标和噪声纹理的UV纹理。

        void surf (Input IN, inout SurfaceOutputStandardSpecular o){// 溶解遮罩fixed noise = tex2D(_Noise, IN.uv_Noise).r; // 从噪声纹理中获取红色通道值fixed dissolve = _Dissolve * 2 - 1; // 将溶解程度从0-1映射到-1到1fixed mask = saturate(noise - dissolve); // 计算遮罩值,限制在0到1之间clip(mask - 0.5); // 根据遮罩值进行剪切,小于0.5的部分将被剔除// 燃烧效果fixed texcoord = saturate(mask * _Range - 0.5 * _Range); // 计算纹理坐标,用于边缘渐变o.Emission = tex2D(_Gradient, fixed2(texcoord, 0.5)) * _Brightness; // 根据渐变纹理和亮度计算自发光fixed4 c = tex2D (_Albedo, IN.uv_Albedo); // 从Albedo纹理中获取颜色o.Albedo = c.rgb; // 设置表面颜色}

我们实现了两个效果:溶解的遮罩,我们获取噪声纹理的红色通道值之后减去溶解的程度值来作为遮罩值,遮罩值小于0.5的部分将会被剔除。

然后是边缘的燃烧效果,我们利用之前生成的遮罩值来计算纹理坐标之后再根据纹理坐标来乘以自发光强度实现渐变纹理。最后我们从基础纹理中获取颜色后再添加到输出的模型中。

透视

效果如图:

能够看到我们可以透过岩石看到人物的轮廓。

代码如下:

Shader "Chapter8/chapter8_2"
{Properties{// 定义属性[Header(The Blocked Part)] // 标题,表示以下属性是用于被遮挡部分的设置[Space(10)] // 在Inspector中留出10像素的空白_Color ("X-Ray Color", Color) = (0,1,1,1) // X射线颜色,默认青色_Width ("X-Ray Width", Range(1, 2)) = 1 // X射线宽度,范围1到2,默认1_Brightness ("X-Ray Brightness",Range(0, 2)) = 1 // X射线亮度,范围0到2,默认1}SubShader{Tags{"RenderType" = "Opaque" "Queue" = "Geometry"} // 渲染类型为不透明,队列为几何体//---------- 被遮挡部分的效果 ----------Pass{ZTest Greater // 深度测试设置为大于当前深度值时才渲染(即渲染被遮挡的部分)ZWrite Off // 关闭深度写入,避免影响后续渲染Blend SrcAlpha OneMinusSrcAlpha // 设置混合模式,实现透明效果CGPROGRAM#pragma vertex vert // 顶点着色器#pragma fragment frag // 片段着色器#include "UnityCG.cginc" // 引入Unity的CG库// 定义顶点着色器的输出结构struct v2f{float4 vertexPos : SV_POSITION; // 顶点在裁剪空间中的位置float3 viewDir : TEXCOORD0; // 视线方向float3 worldNor : TEXCOORD1; // 世界空间中的法线方向};// 顶点着色器v2f vert(appdata_base v){v2f o;o.vertexPos = UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换到裁剪空间o.viewDir = normalize(WorldSpaceViewDir(v.vertex)); // 计算视线方向o.worldNor = UnityObjectToWorldNormal(v.normal); // 将法线从对象空间转换到世界空间return o;}// 声明属性变量fixed4 _Color; // X射线颜色fixed _Width; // X射线宽度half _Brightness; // X射线亮度// 片段着色器float4 frag(v2f i) : SV_Target{//计算边缘光强度half NDotV = saturate(dot(i.worldNor, i.viewDir)); // 计算法线与视线方向的点积NDotV = pow(1 - NDotV, _Width) * _Brightness; // 根据宽度和亮度调整边缘光强度fixed4 color;color.rgb = _Color.rgb; // 设置颜色color.a = NDotV; // 设置透明度(基于Fresnel值)return color; // 返回最终颜色}ENDCG}}
}
        // 定义属性[Header(The Blocked Part)] // 标题,表示以下属性是用于被遮挡部分的设置[Space(10)] // 在Inspector中留出10像素的空白_Color ("X-Ray Color", Color) = (0,1,1,1) // X射线颜色,默认青色_Width ("X-Ray Width", Range(1, 2)) = 1 // X射线宽度,范围1到2,默认1_Brightness ("X-Ray Brightness",Range(0, 2)) = 1 // X射线亮度,范围0到2,默认1

首先是属性中,[Space(10)]在Inspector中留出10像素的空白,效果如图。

Tags{"RenderType" = "Opaque" "Queue" = "Geometry"}

Tags中渲染类型为不透明,队列为几何体。

            ZTest Greater // 深度测试设置为大于当前深度值时才渲染(即渲染被遮挡的部分)ZWrite Off // 关闭深度写入,避免影响后续渲染

开启深度测试的同时关闭深度写入。

            // 定义顶点着色器的输出结构struct v2f{float4 vertexPos : SV_POSITION; // 顶点在裁剪空间中的位置float3 viewDir : TEXCOORD0; // 视线方向float3 worldNor : TEXCOORD1; // 世界空间中的法线方向};

顶点着色器的输出中多了一个视线方向。

            // 顶点着色器v2f vert(appdata_base v){v2f o;o.vertexPos = UnityObjectToClipPos(v.vertex); // 将顶点从对象空间转换到裁剪空间o.viewDir = normalize(WorldSpaceViewDir(v.vertex)); // 计算视线方向o.worldNor = UnityObjectToWorldNormal(v.normal); // 将法线从对象空间转换到世界空间return o;}

这里的视线方向计算方法:

最后是片元着色器的内容:

            // 片段着色器float4 frag(v2f i) : SV_Target{//计算边缘光强度half NDotV = saturate(dot(i.worldNor, i.viewDir)); // 计算法线与视线方向的点积NDotV = pow(1 - NDotV, _Width) * _Brightness; // 根据宽度和亮度调整边缘光强度fixed4 color;color.rgb = _Color.rgb; // 设置颜色color.a = NDotV; // 设置透明度(基于Fresnel值)return color; // 返回最终颜色}

 这里的边缘光强度计算的内容可能比较难以理解,我们先用法线和视线方向进行一个点积之后调整该值到[0,1]之间,然后根据宽度和亮度来调整光强,这里我们采用了幂次计算,宽度作为幂,那么宽度值越小则光强越小,同时注意我们的base是一减去点积,意思就是法线和视线的夹角越大则光强越强(夹角越大则越边缘),我们通过这些函数实现了边缘光越边缘强度越大的效果。

切割

效果如图:

非常直接的切割效果,代码如下:

Shader "Chapter8/chapter8_3"
{Properties{// 纹理部分[Header(Textures)] [Space(10)] // 标题和空白[NoScaleOffset] _Albedo ("Albedo", 2D) = "white" {} // 基础颜色纹理,默认白色,无缩放偏移// 切割部分[Header(Cutting)] [Space(10)] // 标题和空白[KeywordEnum(X, Y, Z)] _Direction ("Cutting Direction", Float) = 1 // 切割方向枚举(X、Y、Z),默认Y轴[Toggle] _Invert ("Invert Direction", Float) = 0 // 是否反转切割方向,默认关闭}SubShader{Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" } // 渲染类型为透明剪切,队列为Alpha测试Cull Off // 关闭背面剔除,渲染双面CGPROGRAM#pragma surface surf StandardSpecular addshadow fullforwardshadows // 使用StandardSpecular表面着色器,添加阴影和全向阴影#pragma target 3.0 // 目标着色器模型3.0#pragma multi_compile _DIRECTION_X _DIRECTION_Y _DIRECTION_Z // 多编译选项,支持X、Y、Z三个方向的切割sampler2D _Albedo; // 基础颜色纹理float3 _Position; // 切割位置fixed _Invert; // 是否反转切割方向struct Input{float2 uv_Albedo; // 基础颜色纹理的UV坐标float3 worldPos; // 世界空间中的顶点位置fixed face : VFACE; // 判断当前渲染的是正面还是背面};void surf (Input i, inout SurfaceOutputStandardSpecular o){// 获取基础颜色fixed4 col = tex2D(_Albedo, i.uv_Albedo);// 如果是正面,使用纹理颜色;如果是背面,使用黑色o.Albedo =  i.face > 0 ? col.rgb : fixed3(0,0,0);// 判断切割方向#if _DIRECTION_X// 如果选择X轴方向,根据世界坐标的X值与切割位置比较col.a = step(_Position.x, i.worldPos.x);#elif _DIRECTION_Y// 如果选择Y轴方向,根据世界坐标的Y值与切割位置比较col.a = step(_Position.y, i.worldPos.y);#else // 如果选择Z轴方向,根据世界坐标的Z值与切割位置比较col.a = step(_Position.z, i.worldPos.z);#endif// 判断是否反转切割方向col.a = _Invert? 1 - col.a : col.a;// 根据透明度进行剪切,小于0.001的部分将被剔除clip(col.a - 0.001);}ENDCG}
}
    {// 纹理部分[Header(Textures)] [Space(10)] // 标题和空白[NoScaleOffset] _Albedo ("Albedo", 2D) = "white" {} // 基础颜色纹理,默认白色,无缩放偏移// 切割部分[Header(Cutting)] [Space(10)] // 标题和空白[KeywordEnum(X, Y, Z)] _Direction ("Cutting Direction", Float) = 1 // 切割方向枚举(X、Y、Z),默认Y轴[Toggle] _Invert ("Invert Direction", Float) = 0 // 是否反转切割方向,默认关闭}

 这里有两个新东西:KeywordEnum和一个Toggle。

效果如下:

        struct Input{float2 uv_Albedo; // 基础颜色纹理的UV坐标float3 worldPos; // 世界空间中的顶点位置fixed face : VFACE; // 判断当前渲染的是正面还是背面};

作为输入的结构体里除了基本的纹理UV坐标和顶点坐标以外还有一个声明为VFACE的face变量,用来表明具体渲染的是正面还是背面。

        void surf (Input i, inout SurfaceOutputStandardSpecular o){// 获取基础颜色fixed4 col = tex2D(_Albedo, i.uv_Albedo);// 如果是正面,使用纹理颜色;如果是背面,使用黑色o.Albedo =  i.face > 0 ? col.rgb : fixed3(0,0,0);// 判断切割方向#if _DIRECTION_X// 如果选择X轴方向,根据世界坐标的X值与切割位置比较col.a = step(_Position.x, i.worldPos.x);#elif _DIRECTION_Y// 如果选择Y轴方向,根据世界坐标的Y值与切割位置比较col.a = step(_Position.y, i.worldPos.y);#else // 如果选择Z轴方向,根据世界坐标的Z值与切割位置比较col.a = step(_Position.z, i.worldPos.z);#endif// 判断是否反转切割方向col.a = _Invert? 1 - col.a : col.a;// 根据透明度进行剪切,小于0.001的部分将被剔除clip(col.a - 0.001);}

表面着色器里,我们首先获取纹理的颜色,然后先判断是正面还是背面,接着从X,Y,Z轴选择切割的方向,根据选择的轴来确定世界坐标和切割位置的比较。这里我们使用了一个函数step来进行比较,step的具体用法如下:

最后把小于阈值的部分直接剔除掉即可。

切割轴为Y轴时效果如图:

广告

效果如下:

就是实现无论哪个位置看到的图片效果都一样。

代码如下:

Shader "Chapter8/chapter8_4"
{Properties{[NoScaleOffset] _Tex ("Texture", 2D) = "white" {}[KeywordEnum(Spherical, Cylindrical)] _Type ("Type", float) = 0}SubShader{Tags{"RenderType" = "Transparent""Queue" = "Transparent""DisableBatching" = "True"}//Blend OneMinusDstColor OneZWrite OffPass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 声明枚举的关键词#pragma shader_feature _TYPE_SPHERICAL _TYPE_CYLINDRICALstruct appdata{float4 vertex : POSITION;float2 texcoord : TEXCOORD0;};struct v2f{float4 vertex : SV_POSITION;float2 texcoord : TEXCOORD0;};sampler2D _Tex;v2f vert (appdata v){v2f o;// 计算面片朝向摄像机的前方向量float3 forward = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)).xyz;// 判断Billboard的类型#if _TYPE_CYLINDRICALforward.y = 0;#endifforward = normalize(forward);// 当摄像机完全在面片正上方或者正下方的时候,旋转临时的上方向量float3 up = abs(forward.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(forward, up));up = normalize(cross(right, forward));// 将顶点在新的坐标系上移动位置float3 vertex = v.vertex.x * right + v.vertex.y * up;o.vertex = UnityObjectToClipPos(vertex);o.texcoord = v.texcoord;return o;}float4 frag (v2f i) : SV_Target{return tex2D(_Tex, i.texcoord);}ENDCG}}
}
        Tags{"RenderType" = "Transparent""Queue" = "Transparent""DisableBatching" = "True"}

这次的Tags里有新东西DisableBatching:

总结来说就是,批处理可以减少draw call但是会导致顶点坐标从模型空间转换为世界空间,所以如果在后续的代码中我们要使用模型空间的坐标的话就无法使用,所以我们需要显式地禁止使用批处理。

            // 声明枚举的关键词#pragma shader_feature _TYPE_SPHERICAL _TYPE_CYLINDRICAL

比起往常的shader多了一个shader_feature。

shader_feature本质上更像一个shader代码里的宏定义,我们可以根据不同的宏定义替换不同的shader变体。 

            v2f vert (appdata v){v2f o;// 计算面片朝向摄像机的前方向量float3 forward = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1)).xyz;// 判断Billboard的类型#if _TYPE_CYLINDRICALforward.y = 0;#endifforward = normalize(forward);// 当摄像机完全在面片正上方或者正下方的时候,旋转临时的上方向量float3 up = abs(forward.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 right = normalize(cross(forward, up));up = normalize(cross(right, forward));// 将顶点在新的坐标系上移动位置float3 vertex = v.vertex.x * right + v.vertex.y * up;o.vertex = UnityObjectToClipPos(vertex);o.texcoord = v.texcoord;return o;}

我们将摄像机的世界空间坐标从世界坐标系转换到模型空间坐标系,获得该面片朝向摄像机的方向向量。如果是圆柱形的广告牌,我们将这个方向向量的y轴修改为0。否则如果该方向向量的y轴分量大于0.999(基本就是在纯上方)我们把向上的向量直接设置为z轴方向向量(此时forward向量和向上向量高度重合,叉乘大概率为0,后续计算无法展开),否则就设置为y轴方向向量,然后用前向向量和上方向量叉乘得到右侧向量,之后再用右向向量和前方向量叉乘得到上方向量。

最后我们把v的顶点坐标和纹理坐标都根据这个右向向量和上方向量更新之后即可,这样我们就实现了一个面片永远朝向摄像机的效果。

扭曲

效果如下:

可以看到这些形形色色的颜色球在的位置视线被扭曲了。

代码如下:

Shader "Chapter8/chapter8_5"
{Properties{_StrengthColor("Color strength", Float) = 1 // 颜色强度,默认1_DistortionStrength ("Distortion strength", Range(-2,2)) = 0.1 // 扭曲强度,范围-2到2,默认0.1_DistortionCircle ("Distortion circle", Range(0,1)) = 0 // 扭曲圆形范围,范围0到1,默认0_NormalTexture("Normal", 2D) = "blue" { } // 法线纹理,默认蓝色_NormalTexStrength("Normal strength", Range(0,1)) = 0.5 // 法线强度,范围0到1,默认0.5_NormalTexFrameless("Normal circle", Range(0,1)) = 0.5 // 法线圆形范围,范围0到1,默认0.5_UVOffset("UVOffset XY, ignore ZW", Vector) = (0,0.01,0,0) // UV偏移,默认(0, 0.01, 0, 0)}Category{Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "IgnoreProjector" = "True" } // 渲染队列为透明,渲染类型为透明,忽略投影器Blend SrcAlpha OneMinusSrcAlpha // 混合模式:SrcAlpha, OneMinusSrcAlphaZWrite Off // 关闭深度写入SubShader{GrabPass // 抓取屏幕内容{Name "BASE"Tags { "LightMode" = "Always" }}Pass{Name "BASE"Tags { "LightMode" = "Always" }CGPROGRAM#pragma vertex vert // 顶点着色器#pragma fragment frag // 片段着色器#include "UnityCG.cginc" // 引入Unity的CG库sampler2D _GrabTexture; // 抓取的屏幕纹理float _DistortionStrength; // 扭曲强度float _DistortionCircle; // 扭曲圆形范围float _StrengthColor; // 颜色强度sampler2D _NormalTexture; // 法线纹理float4 _NormalTexture_ST; // 法线纹理的缩放和偏移float _NormalTexStrength; // 法线强度float _NormalTexFrameless; // 法线圆形范围float4 _UVOffset; // UV偏移// 顶点着色器输入结构struct VertexInput{float4 vertex : POSITION; // 顶点位置float2 texcoord0 : TEXCOORD0; // 纹理坐标float4 color : COLOR; // 顶点颜色};// 顶点着色器输出结构struct Vert2Frag{float4 position : SV_POSITION; // 裁剪空间中的顶点位置float4 uv_grab : TEXCOORD0; // 抓取纹理的UV坐标float2 uv : TEXCOORD1; // 纹理坐标float2 uv_normal : TEXCOORD2; // 法线纹理的UV坐标float2 movement: TEXCOORD3; // UV偏移运动float4 color : TEXCOORD4; // 顶点颜色};// 顶点着色器Vert2Frag vert (VertexInput vertIn){Vert2Frag output;output.position = UnityObjectToClipPos(vertIn.vertex); // 将顶点从对象空间转换到裁剪空间output.uv_grab = ComputeGrabScreenPos(output.position); // 计算抓取纹理的UV坐标output.uv = vertIn.texcoord0; // 传递纹理坐标output.uv_normal = vertIn.texcoord0.xy * _NormalTexture_ST.xy + _NormalTexture_ST.zw; // 计算法线纹理的UV坐标output.movement = _UVOffset.xy*_Time.y; // 计算UV偏移运动output.color = vertIn.color; // 传递顶点颜色return output;}// 获取从中心到当前UV的向量float2 getVectorFromCenter(float2 uv){float factor = _ScreenParams.y / _ScreenParams.x; // 计算屏幕宽高比float2 direction = float2((uv.x-0.5), (uv.y-0.5)) * factor; // 计算从中心到当前UV的向量return (direction);}// 获取扭曲强度float getDistortionStrength(float2 uv){float2 diff = float2(distance(0.5, uv.x), distance(0.5, uv.y)) * 2.0; // 计算UV到中心的距离float dist = saturate(length(diff)); // 计算距离并限制在0到1之间return 1.0-dist; // 返回扭曲强度}// 获取法线float2 getNormal(sampler2D _NormalTexture, float2 normalUv, float2 uv, float2 uvOffset, float frameless, float strength){float2 normal = tex2D( _NormalTexture, normalUv+uvOffset ).zy; // 从法线纹理中获取法线值float length = getDistortionStrength(uv); // 获取扭曲强度float normalTexStrength = ((1-frameless) + frameless*length) * strength; // 计算法线强度normal.x = ((normal.x-.5)*2) * normalTexStrength; // 调整法线X分量normal.y = ((normal.y-.5)*2) * normalTexStrength; // 调整法线Y分量return normal; // 返回法线}// 片段着色器half4 frag (Vert2Frag fragIn) : SV_Target{float4 uvScreen = UNITY_PROJ_COORD(fragIn.uv_grab); // 获取抓取纹理的UV坐标float2 direction = getVectorFromCenter(fragIn.uv); // 获取从中心到当前UV的向量float strength = getDistortionStrength(fragIn.uv); // 获取扭曲强度strength = (_DistortionCircle*strength + (1-_DistortionCircle)) * _DistortionStrength; // 计算最终扭曲强度direction *= strength; // 调整方向向量uvScreen += float4(direction.x, direction.y, 0, 0); // 调整抓取纹理的UV坐标float2 influence = normalize(direction) * strength; // 计算影响向量float2 offset = fragIn.movement; // 获取UV偏移float2 normal = getNormal(_NormalTexture, fragIn.uv_normal, fragIn.uv, offset, _NormalTexFrameless, _NormalTexStrength); // 获取法线uvScreen += float4(normal.x, normal.y, 0, 0); // 调整抓取纹理的UV坐标influence += normal.xy; // 调整影响向量float4 final = tex2Dproj(_GrabTexture, uvScreen); // 从抓取纹理中获取颜色float alpha = 1; // 设置透明度final = float4(final.xyz, alpha); // 设置最终颜色strength = saturate(sqrt(pow(abs(influence.x), 2.0) + pow(abs(influence.y), 2.0)) * _StrengthColor); // 计算最终强度final = final + (fragIn.color*strength); // 调整最终颜色final.w = saturate(final.w*fragIn.color.w); // 调整最终透明度return final; // 返回最终颜色}ENDCG}}}
}

我们应该首先能发现这一次的shader中没有Pass而是Category:

			GrabPass // 抓取屏幕内容{Name "BASE"Tags { "LightMode" = "Always" }}

如果还记得我们透明章节的话,我们在那里介绍过GrabPass:从屏幕中抓取缓冲来使用。

				// 顶点着色器输入结构struct VertexInput{float4 vertex : POSITION; // 顶点位置float2 texcoord0 : TEXCOORD0; // 纹理坐标float4 color : COLOR; // 顶点颜色};// 顶点着色器输出结构struct Vert2Frag{float4 position : SV_POSITION; // 裁剪空间中的顶点位置float4 uv_grab : TEXCOORD0; // 抓取纹理的UV坐标float2 uv : TEXCOORD1; // 纹理坐标float2 uv_normal : TEXCOORD2; // 法线纹理的UV坐标float2 movement: TEXCOORD3; // UV偏移运动float4 color : TEXCOORD4; // 顶点颜色};

 顶点着色器的输入和输出都有很多东西,把顶点位置、纹理坐标和顶点颜色作为输入而输出裁剪空间的顶点位置、抓取的纹理坐标、纹理坐标、法线纹理坐标、UV的偏移和颜色。

				// 顶点着色器Vert2Frag vert (VertexInput vertIn){Vert2Frag output;output.position = UnityObjectToClipPos(vertIn.vertex); // 将顶点从对象空间转换到裁剪空间output.uv_grab = ComputeGrabScreenPos(output.position); // 计算抓取纹理的UV坐标output.uv = vertIn.texcoord0; // 传递纹理坐标output.uv_normal = vertIn.texcoord0.xy * _NormalTexture_ST.xy + _NormalTexture_ST.zw; // 计算法线纹理的UV坐标output.movement = _UVOffset.xy*_Time.y; // 计算UV偏移运动output.color = vertIn.color; // 传递顶点颜色return output;}

这是顶点着色器的函数代码内容。

				// 获取从中心到当前UV的向量float2 getVectorFromCenter(float2 uv){float factor = _ScreenParams.y / _ScreenParams.x; // 计算屏幕宽高比float2 direction = float2((uv.x-0.5), (uv.y-0.5)) * factor; // 计算从中心到当前UV的向量return (direction);}// 获取扭曲强度float getDistortionStrength(float2 uv){float2 diff = float2(distance(0.5, uv.x), distance(0.5, uv.y)) * 2.0; // 计算UV到中心的距离float dist = saturate(length(diff)); // 计算距离并限制在0到1之间return 1.0-dist; // 返回扭曲强度}// 获取法线float2 getNormal(sampler2D _NormalTexture, float2 normalUv, float2 uv, float2 uvOffset, float frameless, float strength){float2 normal = tex2D( _NormalTexture, normalUv+uvOffset ).zy; // 从法线纹理中获取法线值float length = getDistortionStrength(uv); // 获取扭曲强度float normalTexStrength = ((1-frameless) + frameless*length) * strength; // 计算法线强度normal.x = ((normal.x-.5)*2) * normalTexStrength; // 调整法线X分量normal.y = ((normal.y-.5)*2) * normalTexStrength; // 调整法线Y分量return normal; // 返回法线}

这里有三个函数,分别用于计算从中心到当前UV的向量,计算扭曲强度以及获取法线。

第一个函数,我们首先计算一个屏幕的高宽比,然后把uv的x轴和y轴各减去0.5之后乘以这个比值,因为我们知道uv坐标是一个从0到1的坐标系,所以这样能得到正确的向量。

第二个函数中,我们先计算uv坐标的xy坐标到屏幕的中心的距离,然后把这个距离限制在[0,1]之间,最后返回一减去这个距离即可。

第三个函数中,我们首先从法线纹理中获取到法线,然后使用第二个函数获取扭曲强度,然后我们根据扭曲强度和参数中的边缘衰减(frameless)来动态调整法线强度。

				// 片段着色器half4 frag (Vert2Frag fragIn) : SV_Target{float4 uvScreen = UNITY_PROJ_COORD(fragIn.uv_grab); // 获取抓取纹理的UV坐标float2 direction = getVectorFromCenter(fragIn.uv); // 获取从中心到当前UV的向量float strength = getDistortionStrength(fragIn.uv); // 获取扭曲强度strength = (_DistortionCircle*strength + (1-_DistortionCircle)) * _DistortionStrength; // 计算最终扭曲强度direction *= strength; // 调整方向向量uvScreen += float4(direction.x, direction.y, 0, 0); // 调整抓取纹理的UV坐标float2 influence = normalize(direction) * strength; // 计算影响向量float2 offset = fragIn.movement; // 获取UV偏移float2 normal = getNormal(_NormalTexture, fragIn.uv_normal, fragIn.uv, offset, _NormalTexFrameless, _NormalTexStrength); // 获取法线uvScreen += float4(normal.x, normal.y, 0, 0); // 调整抓取纹理的UV坐标influence += normal.xy; // 调整影响向量float4 final = tex2Dproj(_GrabTexture, uvScreen); // 从抓取纹理中获取颜色float alpha = 1; // 设置透明度final = float4(final.xyz, alpha); // 设置最终颜色strength = saturate(sqrt(pow(abs(influence.x), 2.0) + pow(abs(influence.y), 2.0)) * _StrengthColor); // 计算最终强度final = final + (fragIn.color*strength); // 调整最终颜色final.w = saturate(final.w*fragIn.color.w); // 调整最终透明度return final; // 返回最终颜色}

我们的片元着色器就是重点了,可以看到很多内容啊,我们来看看AI怎么说吧:

扫描

这是一个更为复杂的项目,效果如下:

我们实现的效果是鼠标点击某个地点之后会发射这样一个扫描的波形。

这里就不只是一个shader可以实现的效果了,我们还需要一个C#脚本。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;// 该脚本实现了一个扫描效果,当用户点击物体时,扫描效果会从该位置开始,并随时间扩展
// 这个脚本会在编辑模式下也生效([ExecuteInEditMode]属性)
[ExecuteInEditMode]
public class PosScanEffect : MonoBehaviour
{// 用于存储扫描效果的材质(shader),此材质会控制实际的视觉效果public Material ScanMat;// 控制扫描速度,扫描的范围会根据这个值逐渐增大public float ScanSpeed = 20;// 扫描计时器,决定扫描的进度,随着时间的推移,扫描的范围会增大public float scanTimer = 0;// 存储相机组件的引用private Camera scanCam;// 记录扫描的中心点,即鼠标点击时的物体位置private Vector3 ScanPoint = Vector3.zero;// 初始化方法,这里没有初始化操作void Awake(){}// 每一帧调用,主要用于计算扫描参数和更新扫描效果private void Update(){// 获取当前物体上的相机组件scanCam = GetComponent<Camera>();// 启用深度纹理(Depth)和法线深度纹理(DepthNormals),这些纹理对后续的渲染处理至关重要scanCam.depthTextureMode |= DepthTextureMode.Depth;scanCam.depthTextureMode |= DepthTextureMode.DepthNormals;// 获取相机的长宽比float aspect = scanCam.aspect;// 获取相机的远裁剪平面(远离相机的最大距离)float farPlaneDistance = scanCam.farClipPlane;// 根据相机的视野(field of view)计算出上方向量,用于定位视锥体的边界Vector3 midup = Mathf.Tan(scanCam.fieldOfView / 2 * Mathf.Deg2Rad) * farPlaneDistance * scanCam.transform.up;// 计算右方向量,同样用于确定视锥体的边界Vector3 midright = Mathf.Tan(scanCam.fieldOfView / 2 * Mathf.Deg2Rad) * farPlaneDistance * scanCam.transform.right * aspect;// 计算远裁剪平面的中心点位置Vector3 farPlaneMid = scanCam.transform.forward * farPlaneDistance;// 根据计算出的参数确定视锥体的四个角的世界坐标Vector3 bottomLeft = farPlaneMid - midup - midright;Vector3 bottomRight = farPlaneMid - midup + midright;Vector3 upLeft = farPlaneMid + midup - midright;Vector3 upRight = farPlaneMid + midup + midright;// 创建一个矩阵来表示视锥体的四个角Matrix4x4 frustumCorner = new Matrix4x4();frustumCorner.SetRow(0, bottomLeft); // 设置底左角frustumCorner.SetRow(1, bottomRight); // 设置底右角frustumCorner.SetRow(2, upRight); // 设置上右角frustumCorner.SetRow(3, upLeft); // 设置上左角// 将视锥体的矩阵传递给ShaderScanMat.SetMatrix("_FrustumCorner", frustumCorner);// 进行射线检测,获取鼠标点击的世界坐标RaycastHit hit;Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 将鼠标屏幕坐标转换为射线if (Input.GetMouseButton(0) && Physics.Raycast(ray, out hit)) // 如果按下左键并且射线与物体碰撞{scanTimer = 0; // 重置扫描计时器ScanPoint = hit.point; // 获取碰撞点的位置作为扫描的起始点}// 增加扫描计时器,控制扫描的进度scanTimer += Time.deltaTime;// 将扫描的起始位置和扫描进度传递给材质(Shader)ScanMat.SetVector("_ScanCenter", ScanPoint);ScanMat.SetFloat("_ScanRange", scanTimer * ScanSpeed);// 将相机的世界坐标变换矩阵传递给ShaderScanMat.SetMatrix("_CamToWorld", scanCam.cameraToWorldMatrix);}// 在渲染图像时调用,进行后处理操作,应用扫描效果private void OnRenderImage(RenderTexture source, RenderTexture destination){// 将相机的远裁剪平面距离传递给材质(Shader),用于控制扫描的范围ScanMat.SetFloat("_CamFar", GetComponent<Camera>().farClipPlane);// 使用Graphics.Blit方法将源纹理渲染到目标纹理,并应用扫描效果的材质Graphics.Blit(source, destination, ScanMat);}
}

具体的代码作用注释里已经写明了。

我们重点还是来看看着色器的写法:

Shader "Chapter2/PointScanShader"
{Properties{// 主要纹理,用于物体表面的纹理_MainTex ("Texture", 2D) = "white" {}// 扫描纹理,用于显示扫描的图像或效果_ScanTex("ScanTexure", 2D) = "white" {}// 扫描的范围,控制扫描从中心开始的距离_ScanRange("ScanRange", float) = 0// 扫描宽度,控制扫描的宽度,影响扫描的可见区域_ScanWidth("ScanWidth", float) = 0// 扫描的背景颜色_ScanBgColor("ScanBgColor", color) = (1, 1, 1, 1)// 扫描时网格的颜色_ScanMeshColor("ScanMeshColor", color) = (1, 1, 1, 1)// 网格线的宽度_MeshLineWidth("MeshLineWidth", float) = 0.3// 网格的宽度,用于控制网格分割的尺寸_MeshWidth("MeshWidth", float) = 1// 缝隙的平滑度,控制缝隙的过渡效果_Smoothness("SeamBlending", Range(0, 0.5)) = 0.25}SubShader{// 不使用背面剔除(Cull Off),不写入深度缓存(ZWrite Off),且始终通过深度测试(ZTest Always)//Cull Off ZWrite Off ZTest AlwaysPass{CGPROGRAM#pragma vertex vert#pragma fragment frag// 引用Unity的内置CG代码库#include "UnityCG.cginc"// 定义顶点着色器的数据结构struct appdata{float4 vertex : POSITION; // 顶点位置float2 uv : TEXCOORD0; // 顶点纹理坐标};// 定义片段着色器的数据结构struct v2f{float2 uv : TEXCOORD0; // 传递给片段着色器的纹理坐标float2 uv_depth : TEXCOORD1; // 用于传递深度信息的纹理坐标float4 interpolatedRay : TEXCOORD2; // 传递与扫描效果相关的视锥体数据float4 vertex : SV_POSITION; // 顶点最终位置};// 定义一个矩阵,用于描述视锥体四个角的坐标float4x4 _FrustumCorner;// 顶点着色器:计算顶点的最终位置,并计算视锥体四个角的插值数据v2f vert(appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex); // 计算顶点的最终位置o.uv = v.uv; // 将纹理坐标传递给片段着色器o.uv_depth = v.uv; // 将纹理坐标传递给深度计算// 根据UV坐标的不同区域选择不同的视锥体角int rayIndex;if (v.uv.x < 0.5 && v.uv.y < 0.5){rayIndex = 0;}else if (v.uv.x > 0.5 && v.uv.y < 0.5){rayIndex = 1;}else if (v.uv.x > 0.5 && v.uv.y > 0.5){rayIndex = 2;}else{rayIndex = 3;}// 从视锥体四个角中选择一个,传递给片段着色器o.interpolatedRay = _FrustumCorner[rayIndex];return o;}// 声明材质参数,允许外部设置sampler2D _MainTex;sampler2D _ScanTex;float _ScanRange;float _ScanWidth;float3 _ScanCenter;fixed4 _ScanBgColor;fixed4 _ScanMeshColor;float _MeshLineWidth;float _MeshWidth;float4x4 _CamToWorld;fixed _Smoothness;// 声明用于获取深度信息的纹理sampler2D_float _CameraDepthTexture;sampler2D _CameraDepthNormalsTexture;// 片段着色器:计算像素的最终颜色fixed4 frag(v2f i) : SV_Target{float tempDepth;half3 normal;  // 获取该像素的法线和深度值DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), tempDepth, normal);// 将法线从相机空间转换到世界空间normal = mul((float3x3)_CamToWorld, normal);normal = normalize(max(0, (abs(normal) - _Smoothness)));  // 对法线进行平滑处理// 获取该像素的颜色fixed4 col = tex2D(_MainTex, i.uv);// 通过深度纹理获取该像素的深度值,并转换为线性深度float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);float linearDepth = Linear01Depth(depth);// 计算该像素在世界空间中的位置float3 pixelWorldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay;// 计算像素到扫描中心的距离float pixelDistance = distance(pixelWorldPos, _ScanCenter);// 计算该像素的方向向量float3 pixelDir = pixelWorldPos - _ScanCenter;// 使用网格宽度将像素位置进行取模操作,用于创建网格效果float3 modulo = pixelWorldPos - _MeshWidth * floor(pixelWorldPos / _MeshWidth);modulo = modulo / _MeshWidth;// 使用平滑插值创建网格线效果float3 meshCol = smoothstep(_MeshLineWidth, 0, modulo) + smoothstep(1 - _MeshLineWidth, 1, modulo);// 将扫描背景颜色和网格颜色进行插值fixed4 scanMeshCol = lerp(_ScanBgColor, _ScanMeshColor, saturate(dot(meshCol, 1 - normal)));// 如果像素距离扫描中心在指定范围内,执行扫描效果if (_ScanRange - pixelDistance > 0 && _ScanRange - pixelDistance < _ScanWidth && linearDepth < 1){// 根据像素距离计算扫描百分比fixed scanPercent = 1 - (_ScanRange - pixelDistance) / _ScanWidth;// 通过插值将当前颜色与网格颜色混合,生成扫描效果col = lerp(col, scanMeshCol, scanPercent);}// 返回最终颜色return col;}ENDCG}}
}
    Properties{// 主要纹理,用于物体表面的纹理_MainTex ("Texture", 2D) = "white" {}// 扫描纹理,用于显示扫描的图像或效果_ScanTex("ScanTexure", 2D) = "white" {}// 扫描的范围,控制扫描从中心开始的距离_ScanRange("ScanRange", float) = 0// 扫描宽度,控制扫描的宽度,影响扫描的可见区域_ScanWidth("ScanWidth", float) = 0// 扫描的背景颜色_ScanBgColor("ScanBgColor", color) = (1, 1, 1, 1)// 扫描时网格的颜色_ScanMeshColor("ScanMeshColor", color) = (1, 1, 1, 1)// 网格线的宽度_MeshLineWidth("MeshLineWidth", float) = 0.3// 网格的宽度,用于控制网格分割的尺寸_MeshWidth("MeshWidth", float) = 1// 缝隙的平滑度,控制缝隙的过渡效果_Smoothness("SeamBlending", Range(0, 0.5)) = 0.25

一些主要的属性,含义已在注释中写明。

            // 定义片段着色器的数据结构struct v2f{float2 uv : TEXCOORD0; // 传递给片段着色器的纹理坐标float2 uv_depth : TEXCOORD1; // 用于传递深度信息的纹理坐标float4 interpolatedRay : TEXCOORD2; // 传递与扫描效果相关的视锥体数据float4 vertex : SV_POSITION; // 顶点最终位置};

顶点着色器的输出以及片元着色器的输入中除了uv坐标和顶点坐标以外还有一个传递深度信息的uv坐标以及一个与扫描效果相关的变量。

            // 定义一个矩阵,用于描述视锥体四个角的坐标float4x4 _FrustumCorner;

注意这里定义矩阵的方式:float4*4,这个矩阵用来描述视锥体的四个角。

            // 顶点着色器:计算顶点的最终位置,并计算视锥体四个角的插值数据v2f vert(appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex); // 计算顶点的最终位置o.uv = v.uv; // 将纹理坐标传递给片段着色器o.uv_depth = v.uv; // 将纹理坐标传递给深度计算// 根据UV坐标的不同区域选择不同的视锥体角int rayIndex;if (v.uv.x < 0.5 && v.uv.y < 0.5){rayIndex = 0;}else if (v.uv.x > 0.5 && v.uv.y < 0.5){rayIndex = 1;}else if (v.uv.x > 0.5 && v.uv.y > 0.5){rayIndex = 2;}else{rayIndex = 3;}// 从视锥体四个角中选择一个,传递给片段着色器o.interpolatedRay = _FrustumCorner[rayIndex];return o;}

我们根据不同的视锥体的位置来给定一个序号,从这四个序号中选择一个传给片元着色器。

// 片段着色器:计算像素的最终颜色
fixed4 frag(v2f i) : SV_Target
{float tempDepth;half3 normal;  // 获取该像素的法线和深度值DecodeDepthNormal(tex2D(_CameraDepthNormalsTexture, i.uv), tempDepth, normal);// 将法线从相机空间转换到世界空间normal = mul((float3x3)_CamToWorld, normal);normal = normalize(max(0, (abs(normal) - _Smoothness)));  // 对法线进行平滑处理// 获取该像素的颜色fixed4 col = tex2D(_MainTex, i.uv);// 通过深度纹理获取该像素的深度值,并转换为线性深度float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);float linearDepth = Linear01Depth(depth);// 计算该像素在世界空间中的位置float3 pixelWorldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay;// 计算像素到扫描中心的距离float pixelDistance = distance(pixelWorldPos, _ScanCenter);// 计算该像素的方向向量float3 pixelDir = pixelWorldPos - _ScanCenter;// 使用网格宽度将像素位置进行取模操作,用于创建网格效果float3 modulo = pixelWorldPos - _MeshWidth * floor(pixelWorldPos / _MeshWidth);modulo = modulo / _MeshWidth;// 使用平滑插值创建网格线效果float3 meshCol = smoothstep(_MeshLineWidth, 0, modulo) + smoothstep(1 - _MeshLineWidth, 1, modulo);// 将扫描背景颜色和网格颜色进行插值fixed4 scanMeshCol = lerp(_ScanBgColor, _ScanMeshColor, saturate(dot(meshCol, 1 - normal)));// 如果像素距离扫描中心在指定范围内,执行扫描效果if (_ScanRange - pixelDistance > 0 && _ScanRange - pixelDistance < _ScanWidth && linearDepth < 1){// 根据像素距离计算扫描百分比fixed scanPercent = 1 - (_ScanRange - pixelDistance) / _ScanWidth;// 通过插值将当前颜色与网格颜色混合,生成扫描效果col = lerp(col, scanMeshCol, scanPercent);}// 返回最终颜色return col;
}

首先我们使用DecodeDepthNormal函数从_CameraDepthNormalsTexture中获取到法线和深度值,然后将法线经过一系列处理转换到世界空间。我们将深度值转换成线性深度之后用来计算该像素在世界空间的位置与扫描中心的距离,同时计算这个像素到扫描中心的向量。因为我们要实现网格效果,网格效果是一格一格实现的,所以我们需要将像素的位置取模来确定具体在第几个网格,然后将背景的颜色和网格颜色进行插值实现混合的效果,最终返回颜色。

关于线性深度:

相关文章:

  • Coco AI 开源应用程序 - 搜索、连接、协作、您的个人 AI 搜索和助手,都在一个空间中。
  • sherpa-ncnn:Endpointing(断句规则)
  • 【最新版】likeshop连锁点餐系统-PHP版+uniapp前端全开源
  • 计数排序-详解
  • 机器学习第一讲:机器学习本质:让机器通过数据自动寻找规律
  • jquery+ajax+SpringBoot实现前后端分离技术
  • Java游戏服务器开发流水账(2)开发中Maven的管理
  • React 实现 JWT 登录验证的最小可运行示例
  • Oracle EBS FORM快捷键与触发器的关系与使用
  • 永久免费的小工具,内嵌微软接口
  • 二叉搜索树的插入操作(递归遍历)
  • C语言 指针(9)
  • 动态SQL与静态SQL
  • 10.王道_HTTP
  • 【AI论文】FlexiAct:在异构场景中实现灵活的动作控制
  • Spring Boot 实现验证码生成与校验:从零开始构建安全登录系统
  • 并发与并行的关系
  • 如何防止域名DNS被劫持?
  • 【Go】优化文件下载处理:从多级复制到零拷贝流式处理
  • 极狐GitLab 容器镜像仓库功能介绍
  • 见微知沪|优化营商环境,上海为何要当“细节控”自我加压?
  • 云南临沧一行贿案金额认定比受贿案多41万,重审时检方变更金额起诉
  • 叙利亚政权领导人首访西方国家,与法国总统讨论叙局势
  • 美联储宣布维持联邦基金利率目标区间不变
  • 4月外汇储备增加410亿美元,黄金储备连续6个月增加
  • 外交部介绍中国赞比亚共同举办人工智能能力建设主题活动情况