新建shader文件,先声明一些变量,之后都会用到,等用到时再说明

Properties设置为

在subshader中关闭深度写入,还需要设置渲染状态(blend oneone),渲染标签设置为

加入这几个头文件
把抓屏纹理也声明上

attributes和varyings的声明

采样深度纹理并和观察空间下的z值相减

在开始之前检查下方图片中的设置有没有勾选上

新建这俩玩意做测试用

我们的目标是在frag中写出 >> highLight = linearDepth + i.positionVS.z

其中linearDepth 代表观察空间下的线性深度值,i.positionVS.z代表观察空间下的模型的z值,也就是xyzw的那个z值,​​而highlight是交界处的值,这个交界处的值一会再说

采样深度纹理之前必须声明深度纹理,或者加入对应的头文件(前面有),这里用头文件的方式,采样深度纹理必须使用屏幕空间中的 0到1 的线性值,所以在顶点着色器中写上 :

o.screenUV = ComputeScreenPos(o.positionCS);//把齐次裁剪空间下的坐标转换到0 到 w 的范围

函数内传入的裁剪空间下的坐标,接着在片元中写上
 

half2 screenUV =  i.screenUV.xy / i.screenUV.w;//将坐标的xy值除以w值映射到0到1 的屏幕坐标值

用上一步中的screenUV采样深度纹理

half depthTex = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV).r;//由于SAMPLE_TEXTURE2D返回的是四个通道的值,我们只需要一个,所以取结果的r分量

该深度值并不是线性深度值,为什么不是?答案可在裁剪空间到ndc空间的推导中寻找答案.
用float linearDepth = LinearEyeDepth(depthTex, _ZBufferParams);得到观察空间下的线性深度值.
_ZBufferParams它的核心作用就是将非线性的屏幕空间深度逆向还原为线性的观察空间深度,如果你感兴趣可问ai它的推导过程,就是个数学公式而已


求世界下观察方向 :

o.positionVS = TransformWorldToView(TransformObjectToWorld(v.positionOS)) ;

现在得到了深度值和观察方向,可以使用公式highLight = linearDepth + i.positionVS.z了

但是先看看深度图是否正确,如果是一片空白,就拉近视角观察

写上half4 highLight = linearDepth + i.positionVS.z;即可得到下面的效果

如图所示,物体交界处的颜色相比其他地方更深,这是为什么?

首先要明确highLight = linearDepth + i.positionVS.z并不是加法而是减法,因为观察方向的z值是相反的,所以如果 - 去i.positionVS.z实际上是在做加法,负负得正嘛.

我们原来做深度值是通过一个面片来看面片后面的物体,观察后方物体的深度值
从空间位置上看,深度图里记录的背景物体永远在当前透明面片的后方,因此 场景深度必定大于表面深度,因为深度值是在模型内部的,一个内部的值跟表面的值相比,谁更大?那必然是内部的值更大,
linearDepth + i.positionVS.z 再区分来看,交界处的深度值和表面的观察值是不是很接近?那么相减就是0,内部的深度值 大于 表面的值,他俩做相减,是不是等于越靠内的深度值越白一些?

接下来做相反效果,因为后面要给交界处混合颜色,黑色 * 啥都是黑色,肯定不行.

写下这个计算公式,_factor在前面声明的有,它其实就是个运算值的集合,xyzw对应不同地方需要乘进去的值

假设挑一个模型内部的深度值是 0.4,当_factor.x = 1 时,1 - 0.4 = 0.6,该点效果反转;

当_factor.x = 2时,从0.4 *2 = 0.8, 1 - 0.8 = 0.2 ,此时该点从0.4的亮度变为0.2的亮度;

当_factor.x = 3时,从0.4 *3 = 1.2,由于有限制,取1, 1 - 1 = 0 ,此时该点从0.4的亮度变为0的亮度,也就是黑色

假设挑一个模型内部的深度值是 0.1,当_factor.x = 1 时,1 - 0.1 = 0.9,该点效果反转;

当_factor.x = 2时,从0.1 *2 = 0.2, 1 - 0.2 = 0.8 ,此时该点从0.1的亮度变为0.8的亮度;

当_factor.x = 3时,从0.1 *3 = 0.3, 1 - 0.3 = 0.7 ,此时该点从0.1的亮度变为0.7的亮度;

发现了吗,乘上 _ factor.x之后,白色的区域经过计算反转后会迅速减少(如果不理解就自己代入别的数计算一下),但是黑色区域0不会变,0 * 任何数都是 0 ,经过反转之后就是白色,也就是说会有下方的效果

这样就可以美滋滋的乘上颜色了,这里选择一个交界处的颜色_EdgeColor

可添加混合模式blend oneone 测试效果(注意大写,我爱偷懒,如果效果不对就试试大写),或者是blend srcalpha oneminussrcalpha,如果出现奇怪的效果,检查一下渲染tags

添加外发光

外发光的公式是 fresnel : 外发光公式 pow(max(0,dot(N,V)),intensity);

跟slick菲涅耳近似值有点像,但这里不说他俩的区别,实际上还是直视和掠视的那一套,法线和视角的点乘,如果是1,代表这俩向量重合了,摄像机垂直看向模型,如果是0,就是掠视.如果不懂,可以看看菲涅耳的原理解析

我们的目的是写出

注意这里使用了_factor.y,也就是properties里声明的_factor的第二个值

它的效果是

首先计算视角向量和法线向量,因为要在frag中使用,所以在varyings中声明一下

你们应该都会计算,我就不展示了,将点乘结果减一之后得到中间黑,四周白的效果,再混合一个颜色

得到下面的效果

如果你的中间是黑色的,那么说明没有开启混合模式blend oneone或者 blend srcalpha oneminussrcalpha

将交界处的颜色和菲涅耳相加,返回出来试试看

这样就得到了外发光和交界变色的效果,但是稍显单调

添加纹理

我们可以在上面添加纹理,这张纹理使用久违的_MaincolorTex(我自己习惯这么写),添加纹理采样纹理之后,我们这样写

你就得到了这个,为什么要 * 0.05 ? 因为是降低颜色值,如果全加上去的话,颜色值是不是就过曝了呀?而且全部都是_Maincolor的图案了,我们的菲涅耳和交界值就很可怜了

同样的,也可以给这个纹理混合颜色,自己定义颜色并乘上去就好了,注意是在采样的时候乘上去

添加偏移和流动效果

先返回一下模型空间的uv的y轴看看效果,可以发现该模型的y值范围 是 0 到1

那么 将y乘 2之后就是 0到2的范围,需要加上frac取y上面的小数值,得到下面的结果,看起来有点像咕噜球,为什么是这种效果?因为0到1的时候,由于取小数,1.0刷的一下就变为了0,所以模型中间会有一条黑色的线

我们再加上时间变量 _time.y 让它流动起来,这里使用了_factor的z值用来控制模型上有几条黑线

这样的话就得到了一个流动的uv遮罩效果,放不了gif,所以不放图了

现在先不结合,等添加完偏移效果再结合输出

添加偏移效果

为了让蜂窝护盾更有体积感和玻璃质感,我打算加一层折射扭曲。具体做法是:先把护盾后面的场景画面‘抓拍’下来,然后利用蜂窝纹理,把这张抓拍图扭曲一下,最后再贴回护盾上。这样透过护盾看背后的物体,就会产生类似看透镜或水波的折射错觉。

首先要声明抓屏纹理和采样器,抓屏纹理也可以通过include来声明

本来采样抓屏纹理应该使用屏幕空间的uv,但这里使用了扭曲颜色,为什么?

扭曲颜色实际上来自于屏幕uv和流动纹理的单通道的两个值,流动纹理又是什么?下面就来说说

我们如果只用前面的流动的y轴遮罩混合上主纹理,得到的效果就是主纹理一会黑一会白,但是纹理并不会流动,所以我们应该让主纹理也流动起来! 因此将i.uv.xy 加上时间变量来循环流动纹理,注意这里只是想流动y轴,并非x轴;返回出去看看效果

得到这样的流动的效果

再来看为嘛要用 float2 disortColor = lerp(screenUV,colorFlow.rr,_Factor.w);来当作抓屏纹理的采样值呢?

如果只是单纯的用screenuv采样,得到的就是普普通通的屏幕图,并无变化,而颜色值rgb实际上也是xyz值,它就是个值而已,用不同的颜色值去偏移 等于 用 不同的xyz值偏移,这里我讲不好,总之就是用颜色值来偏移采样,具体可以问问聪明的ai,哈哈.

我们将fianlcolor 和 遮罩值相乘混合得到 边缘光,交接光,还有流动的y轴遮罩;

再加上偏移后的抓屏纹理return disort + finalColor得到

这样一个简单的效果就做好了,后续还会再优化一下

源码 :

Shader "Custom/energy"
{
    Properties
    {     
         _MainTex("主纹理", 2D) = "white" {}
         _MainColor("主颜色", Color) = (1, 1, 1, 1)
        _RimLightColor("交界色",Color) = (1,1,1     ,1)
        _EdgeColor("边缘颜色",Color) = (1,1,1,1)
        _EdgeLight("交界范围",Range(0,20)) = 2
       [PowerSlider(3)]  _Fresnel("菲涅耳值",Range(0,7)) = 2
        _Repeat("流动次数",Range(0,5)) = 2
        _Distort("偏移值",Range(0,1)) = 0.2
    }

    SubShader
    {
        // 既然在片元中手动采样了 _CameraOpaqueTexture(抓屏背景)并将其 return,
        // 那么就不应该使用 Blend One One。否则管线会在后期把你的背景再和帧缓存叠加一次,导致画面严重过曝。
        // 这里我们关闭硬件混合(相当于 Blend One Zero),完全由 Shader 接管背景与特效的合成。
//        Blend One One 
//        Blend SrcAlpha OneMinusSrcAlpha
        Blend One Zero 
        ZWrite Off
        Tags 
        { 
            "RenderType"="Transparent" 
            "RenderPipeline"="UniversalPipeline" 
            "Queue"="Transparent"
        }

        Pass
        {
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
            
            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);

            CBUFFER_START(UnityPerMaterial)
            half _EdgeLight;
            half _Fresnel;
            half _Repeat;
            half _Distort;
                float4 _MainTex_ST;
                half4 _MainColor;
                half4 _EdgeColor;
                float4 _RimLightColor;
            CBUFFER_END
              TEXTURE2D(_CameraOpaqueTexture);//声明抓屏纹理
                SAMPLER(sampler_CameraOpaqueTexture);//声明采样器

            struct Attributes //输入结构体
            {
                float3 normalOS : NORMAL;
                float4 positionOS   : POSITION;  // 模型空间顶点坐标 (Object Space)
                float2 uv           : TEXCOORD0; // UV 坐标
            };

            struct Varyings  //输出结构体
            {
                float4 uv           : TEXCOORD0;// xy >> _MainTex  zw >> 模型空间的uv
                float4 positionCS  : SV_POSITION; 
                //因为需要把观察空间的顶点传入到片元中进行对比,所以声明一个寄存器
                float4 screenUV  : TEXCOORD1;
                float3 positionVS : TEXCOORD2;
                float3 normalWS : TEXCOORD3;
                float3 viewWS : TEXCOORD04;
            };

            Varyings vert(Attributes v)
            {
                Varyings o;
                 o.normalWS = TransformObjectToWorldNormal(v.normalOS);
                half3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
                //如何得到观察空间下的顶点坐标? >> 先从模型到世界,再从世界到观察
                o.positionVS = TransformWorldToView(TransformObjectToWorld(v.positionOS.xyz));
                o.positionCS = TransformWViewToHClip( o.positionVS);
                o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
                o.uv.zw = v.uv;
                o.screenUV = ComputeScreenPos(o.positionCS);
                //世界空间下的相机的坐标 >>  _WorldSpaceCameraPos.xyz
               // 调用 URP 内置函数,直接获取【已经归一化 (Normalize)】的世界空间视角方向
                o.viewWS = GetWorldSpaceNormalizeViewDir(positionWS);//推荐使用这个,因为返回的是归一化的值,省一些计算
                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                half2 screenUV =  i.screenUV.xy / i.screenUV.w;
                // float2 uv = sin(_Time.y) + i.uv;
                    half4 color = 
                    SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv.xy  + float2(0.0, _Time.y * 0.3)) * _MainColor;
                // [TA修正 1]:API 数据类型错误。纹理采样返回的是 float4,但 LinearEyeDepth 函数只需要一个标量深度值(即 r 通道)。
               half depthTex = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_CameraDepthTexture, screenUV).r;
                // [TA修正 2]:使用解析出来的线性深度 linearDepth 参与计算
                float linearDepth = LinearEyeDepth(depthTex, _ZBufferParams);
                //_ZBufferParams >> 计算因子,它可以将depth变为观察空间下的线性深度值
                //因为观察空间下的z值是负的,减去z值实际上是加上,所以 直接 写 + 就是 减
                half4 highLight = linearDepth + i.positionVS.z;//用深度值减去观察空间下的z值,深度值的来源是插入到该材质依附的物体中的其他物体
                //它的深度值 > 该物体在观察空间下的z值 //因为该物体在观察空间下的z值是从模型表面到摄像机的距离,而深度值在模型内部,相比
                //表面的值肯定是更远的 // 在模型表面的交界处,深度值和观察值的差值接近0,返回的是黑色,反之则相反
                highLight =saturate(1 - ( highLight * _EdgeLight));
                highLight *= _EdgeColor;
                //fresnel : 外发光公式 pow(max(0,dot(N,V)),intensity);
                //值接近 1 (余弦 0°):两个向量指向几乎相同的方向。这发生在胶囊体正对着你的地方(中心区域)。
                // 我们想要的效果是边缘(点积小)亮,中心(点积大)暗。所以我们用 1 减去点积。中心区域:1 - 1  = 0
                // i.normalWS = normalize(i.normalWS);
                // i.viewWS = normalize(i.viewWS);
                half4 fresnel = pow(saturate(1 - dot(i.viewWS,i.normalWS)),_Fresnel);//当直视模型时,法线和视角重合,fresnel = 1,白色
                fresnel *= _RimLightColor;//边缘光
                
                half4 finalColor = saturate(fresnel + highLight + color * 0.05);
                
                // frac(i.uv.y * 5)原本的y是只有1个 0 >> 1 的渐变,现在是 0 >> 5,也就是说 每次 0到1 ,1到2 的时候其实都是 0.0 到 0.99,循环5次
                //再 * _time.y 让 uv 循环流动
                half flowMask = frac(i.uv.y * _Repeat + _Time.y);//循环流动效果的遮罩
                float2 offsetUV = lerp(screenUV,color.rr,_Distort);
                half4 grabPass = SAMPLE_TEXTURE2D(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, offsetUV);
                half4 disort = half4(grabPass.xyz,1);
                
                // 
               
                // 视觉逻辑:流动遮罩 (flowMask) 应该用来裁剪能量罩自身的光效 (finalColor),
                // 然后把发光的能量罩叠加 (+) 到扭曲后的背景 (disort) 上。
                finalColor *= flowMask; 
                return disort + finalColor; // 返回扭曲的背景 + 发光的能量条纹
                
                //i.uv.y * 5 的作用是将 UV 的 V 轴缩放 5 倍,产生 5 条条纹。如果你写成 frac(i.uv.y * 5 * _Time.y)
                // 随着时间不断增加(1, 2, 3...),你的缩放倍数就会变成 5, 10, 15...,条纹的密度就会无限翻倍。
                //加法 f(x + t)决定了相位(条纹的起始偏移)。把时间放在加法里,你就是在随着时间改变偏移(流动)。
                //如果还是不懂直接代入数进行计算即可
            }
            ENDHLSL
        }
    }
}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐