【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记

教程链接:Overwatch Shield
项目链接:GearsHammerOfDawnTemplate
Pipeline & Shader:Built-in,Unlit

本文是对 Overwatch Shield 学习过程的记录和总结,不是完全的翻译,更多的细节和图文建议跳转原博

效果分析

使用守望先锋中Reinhardt的护盾作为参考

整体观察
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
可以看出护盾是半透明的,带有一层基色,可以微微照亮周边区域
绕着护盾走可以发现,护盾的背面也可以显示(关闭Cull),并且有明显的厚度

蜂窝纹理
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
蜂窝会从中心沿x轴方向搏动,仔细观察还可以发现蜂窝并不是同时搏动的

蜂窝状电流
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
电流沿着蜂窝状边缘向外扩散,不能难发现扩散的形状一个菱形
所以这个效果其实是一个从护盾中心,从点变大的菱形框点亮蜂窝边缘的过程

固定边缘
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
仔细看就会发现边缘效果由两部分组成:固定边缘、相交边缘

相交边缘
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
相交边缘的效果基本和固定边缘一样

效果总结

  1. 基础效果:整体透明,带有基色,Cull Off
  2. 蜂窝纹理:蜂窝纹理从护盾中心沿x轴搏动,带有一定先后顺序
  3. 电流脉冲:蜂窝电流从护盾中心按菱形扩散
  4. 固定边缘:越边缘颜色越实
  5. 相交边缘:与其他物体交叉处有高亮
  6. 自发光:护盾可以微微照亮周边

项目设置

场景方面,模拟了视频(图片)中的物体:
护盾与地面相交,有物体穿过护盾,方便测试效果
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记

另外项目的色彩空间使用了线性空间,因为线性空间的精度更高
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
更多关于颜色空间的内容,可以查看GAMMA AND LINEAR SPACE - WHAT THEY ARE AND HOW THEY DIFFERUnity User Manual Color space

Shader编写

基础效果

目标效果:整体透明,带有基色,Cull Off
没啥东西,改 Tag,关 Cull,设置 Blend,完事

Pass
{
	Tags {"RenderType" = "Transparent" "Queue" = "Transparent"}
	Cull off
	Blend SrcAlpha one
	
	HLSLPROGRAM
	// ...
		
	fixed4 frag (v2f i) : SV_Target
	{
		return _Color;
	}
	ENDHLSL
}

基础效果图
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
 

蜂窝搏动

目标效果:蜂窝纹理从护盾中心沿x轴搏动,带有一定先后顺序

效果拆分:

  1. 蜂窝纹理 + 整体呼吸效果
  2. 打乱呼吸顺序
  3. 呼吸效果沿x轴扩散

1.蜂窝纹理 + 整体呼吸效果

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝图案
	fixed4 pulseTex = tex2D(_PulseTex, i.uv);
	fixed4 pulseTerm = pulseTex * _Color * _PulseIntensity;
	
	// 呼吸效果
	pulseTerm *= abs(sin(_Time.y * _PulseTimeScale));
	
	return fixed4(_Color.rgb + pulseTerm.rgb, _Color.a);
}

【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
 

2.打乱呼吸顺序

打乱顺序的方法有很多,因为这里使用的蜂窝纹理刚好有深浅变化,可以直接用来作为决定呼吸顺序的因子

这里的代码会让 r 通道越大的地方亮的越早

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝图案
	fixed4 pulseTex = tex2D(_PulseTex, i.uv);
	fixed4 pulseTerm = pulseTex * _Color * _PulseIntensity;

	float breath = _Time.y * _PulseTimeScale;
	float pulseOffset = pulseTex.r * _PulseTexOffsetScale;

	// 随机呼吸效果
	pulseTerm *= abs(sin(breath + pulseOffset));

	return fixed4(_Color.rgb + pulseTerm.rgb, _Color.a);
}

为了防止 sin() 的负值导致奇怪的结果出现,需要滤去负值
同时也不希望负值直接被处理为常数导致图案无变化,使用了 abs()
 

3.呼吸效果沿x轴扩散

由于制作模型时,将模型的中心点设置在了正中心(默认是底部中心),可以直接取模型空间的x坐标来确定顶点与中心轴的距离

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝图案
	fixed4 pulseTex = tex2D(_PulseTex, i.uv);
	fixed4 pulseTerm = pulseTex * _Color * _PulseIntensity;

	float breath = _Time.y * _PulseTimeScale;
	float pulseOffset = pulseTex.r * _PulseTexOffsetScale;
	float horizontalDist = abs(i.posOS.x);
	float xOffset = horizontalDist  * _PulsePosScale;

	// 沿x轴扩散的随机呼吸效果
	pulseTerm *= abs(sin(breath + pulseOffset - xOffset));

	return fixed4(_Color.rgb + pulseTerm.rgb, _Color.a);
}

 

电流脉冲

目标效果:蜂窝电流从护盾中心按菱形扩散
大体上思路和蜂窝搏动效果有很多相似的地方

效果拆分:

  1. 蜂窝电流 + 整体呼吸效果
  2. 蜂窝电流以菱形从护盾中心扩散

1.蜂窝电流 + 整体呼吸效果

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动
	// ...
	
	// 蜂窝电流
	fixed4 hexEdgeTex = tex2D(_HexEdgeTex, i.uv);
	fixed4 hexEdgeTerm = hexEdgeTex * _HexEdgeColor * _HexEdgeIntensity;

	// 呼吸
	float edgeBreath = _Time.y * _HexEdgeTimeScale;
	
	hexEdgeTerm *= saturate(sin(edgeBreath));
	
	// 只看电流
	// return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb, _Color.a);
	return fixed4(hexEdgeTerm.rgb, _Color.a);
}

1.5呼吸周期调整

现在的呼吸周期使用的是 saturate(sin(x)) 函数,呼吸效果如下图:
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
图中当曲线位于 x 轴上方时,电流才会出现,到达 1 时达到最亮,随后变暗直到消失,开始下一轮循环

由于现在的曲线是一个单纯的 sin 曲线,电流亮和不亮的时间是相同的

将曲线向下平移,就会让亮的时间变短,暗的时间变长
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
可以将这个下移量设置为变量 _HexEdgeWidthModifier,方便调试

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动
	// ...
	
	// 蜂窝电流
	fixed4 hexEdgeTex = tex2D(_HexEdgeTex, i.uv);
	fixed4 hexEdgeTerm = hexEdgeTex * _HexEdgeColor * _HexEdgeIntensity;

	// 呼吸
	float edgeBreath = _Time.y * _HexEdgeTimeScale;

	hexEdgeTerm *= saturate(sin(edgeBreath) - _HexEdgeWidthModifier);
	
	// 只看电流
	// return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb, _Color.a);
	return fixed4(hexEdgeTerm.rgb, _Color.a);
}

但是根据实现效果看,电流只是淡淡的闪一下就灭了,效果非常不明显

这是因为在 x 轴上方的部分因为被下移,而无法到达 1 值,导致了电流颜色一直无法到达饱和的效果

解决方法是把它拉长到 1:
(1 - _HexEdgeWidthModifier)* edgeNormalizer = 1,
edgeNormalizer = 1 - _HexEdgeWidthModifier
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动
	// ...
	
	// 蜂窝电流
	fixed4 hexEdgeTex = tex2D(_HexEdgeTex, i.uv);
	fixed4 hexEdgeTerm = hexEdgeTex * _HexEdgeColor * _HexEdgeIntensity;

	// 呼吸
	float edgeBreath = _Time.y * _HexEdgeTimeScale;
	// 周期调整
	float edgeNormalizer = 1 - _HexEdgeWidthModifier;

	hexEdgeTerm *= saturate(sin(edgeBreath) - _HexEdgeWidthModifier) / edgeNormalizer;
	
	// 只看电流
	// return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb, _Color.a);
	return fixed4(hexEdgeTerm.rgb, _Color.a);
}

 

2.菱形光从护盾中心扩散

在蜂窝搏动效果中,使用的是 sin 沿 x 轴扩散
这里要使用一个菱形扩散(“ 菱形 ” 虽然不准确,方便起见先这么叫)
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
由于 x 对应的 y 不唯一,这个菱形无法使用函数表示

但是,它可以表示为这个式子:abs(y) + abs(x) = 1,等式右边的数字越大,菱形就越大

这意味着如果计算 sin(abs(x)+abs(y)),就可以构造一个边长不断在一定区间变化的菱形的图案,只要加入时间参数就可以让它动起来

补充
上面的加粗字是这篇笔记全文唯一一个完完全全的翻译,没有一点自己的理解
因为坦白说,这句话压根就没理解…
想了好久也想不明白这到底是什么从 0 突变到 2048 的因果联系啊…???
 
Desmos 没办法表示二维图像,好在还有Gooogle计算器:
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
有了图果然就好理解多了,这波啊,绝对是先有图,后有粗体字

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动
	// ...
	
	// 蜂窝电流
	fixed4 hexEdgeTex = tex2D(_HexEdgeTex, i.uv);
	fixed4 hexEdgeTerm = hexEdgeTex * _HexEdgeColor * _HexEdgeIntensity;

	// 呼吸
	float edgeBreath = _Time.y * _HexEdgeTimeScale;
	float verticalDist = abs(i.posOS.z);
	// 周期调整
	float edgeNormalizer = 1 - _HexEdgeWidthModifier;
	// 菱形
	float diamondPattern = (horizontalDist + verticalDist) * _HexEdgePosScale;

	hexEdgeTerm *= saturate(sin(diamondPattern - edgeBreath) - _HexEdgeWidthModifier) / edgeNormalizer;
	
	// 只看电流
	// return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb, _Color.a);
	return fixed4(hexEdgeTerm.rgb, _Color.a);
}

这里的verticalDist取的是z方向的值,因为在这个模型的坐标系中,上方向是z
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
把最后的return修改一下,电流这里也算是完成了
 

轮廓线

目标效果:越边缘颜色越实
这个效果相对于前面的两个很好实现,只要使用渐变边缘的遮罩纹理,再进行一些调整就可以了

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动、蜂窝电流
	// ...

	fixed4 edgeTex = tex2D(_EdgeTex, i.uv);
	fixed4 edgeTerm = edgeTex.a * _Color * _EdgeIntensity;

	// return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb + edgeTerm, _Color.a);
	return fixed4(edgeTerm.rgb, _Color.a);
}

【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
大粗边缘太傻太闷了,要想办法把边缘变薄
如果不想改贴图的话,就需要进行一些数学计算,比如pow()

fixed4 edgeTerm = pow(edgeTex.a, _EdgeExponent) * _Color * _EdgeIntensity;

【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
需要注意的是,因为颜色的值域是[0, 1],_EdgeExponent越大意味着边缘遮罩将更快的达到1并保持恒定,导致边缘看起来缺乏过渡
 

相交边缘

目标效果:与其他物体相交处有高亮

虽然把固定边缘和相交边缘分成了不同的效果制作,为了让最终的效果不突兀,还是需要保证固定边缘和相交边缘看起来就像是护盾的连续边界一样

把它们分成两部制作,只是因为两个边缘的形成原因不一样,需要使用不同的方法来判定边缘:
一个是护盾原有的边缘,固定存在(边缘纹理);一个是护盾与其他物体相交形成的边缘,需要根据相交情况进行判断(深度纹理
 

原理分析

为什么判断相交情况需要深度纹理呢?

首先,当两个物体相交时,它们之间的距离会变成0
而深度值指的是点到相机(屏幕)的距离
那么只要护盾上某点的深度值和环境中其他物体上的点的深度值差值足够小,就会表明这两个点足够接近,足以产生相交线

护盾的深度值可以在相机空间计算获得,环境的深度值可以通过采样相机渲染的深度纹理获得

补充
根据之前学过的渲染队列和渲染顺序可以知道,在渲染护盾(渲染队列Transparent)时,摄像机的深度纹理中存储的是场景中相机能看到的所有不透明点(即不包括护盾上的点)的深度值
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记

 

摄像机深度纹理获取以及采样

想要获得相机的深度纹理,需要在相机上挂一个脚本,确保它渲染深度纹理:

void OnEnable()
{
    GetComponent<Camera>().depthTextureMode = DepthTextureMode.DepthNormals;
}

如果设置正确,在相机组件的Inspector面板上,会出现下面的提示:
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
然后就可以在Shader中使用这个纹理了

sampler2D _CameraDepthNormalsTexture; // 必须使用这个名字

这里需要注意的是,此时相机渲染出来的深度纹理是在屏幕空间中得到的,因此也需要在屏幕空间进行采样

o.posSS = ComputeScreenPos(o.pos);

// UnityCG.cginc中的ComputeScreenPos大意
inline float4 ComputeScreenPos (float4 pos) 
{
    float4 o = pos * 0.5f;
    o.xy = float2(o.x, o.y*_ProjectionParams.x) + o.w;
    o.zw = pos.zw;
    return o;
}

其中 _ProjectionParams.x 取值为 1.0 或 -1.0,取决于当前平台的屏幕坐标系的 y 轴是向上还是向下,其他的具体原理可以见之前的计算机图形学笔记,齐次裁剪空间到屏幕空间的变换
 

护盾深度值计算

想要计算深度值,必须先计算相机空间的顶点位置,它的 z 值就是该点的深度

因为在片元着色器中进行比较时,两个深度值都为正更方便,所以把这个深度值乘以 -1(相机空间中 z 的反方向指向相机的朝向,在相机面前的点的 z 都为负数)

o.depth = -mul(UNITY_MATRIX_MV, v.vertex).z;

现在,这个深度值是顶点到相机的实际距离
但是深度纹理存储的值是颜色,值域是 [0 ,1](在*面上为 0,在远平面上为 1)
所以为了能正确比较两个深度值,将顶点深度值重映射到 [0,1],需要除以相机到远平面的距离(_ProjectionParams.w = 1 / FarPlane)

o.depth = -mul(UNITY_MATRIX_MV, v.vertex).z * _ProjectionParams.w;

补充
其实这里数学上并不严谨,顶点到相机的实际距离值域应该是 [near,far],重映射的结果应该是 (z - near) / (far - near),而不是简单的 z / far
但是由于本例中相机的*面为 0.1,所以在这里没有太大影响,不如简化计算直接取 z / far

 

相交效果制作

将上面两步得到的屏幕空间坐标、护盾深度值传递到 Fragment Shader,然后就开始制作效果

首先是求护盾深度值和深度纹理中环境深度值的差值

float diff = tex2D(_CameraDepthNormalsTexture, i.posSS.xy).r - i.depth;

然而事情并没有这么简单,还有两个问题要处理:

  1. 因为使用了透视投影,需要做一个透视除法来减小透视带来的歪斜效果
  2. 深度值并不是简单的存储在_CameraDepthNormalsTexture纹理的一个通道中,而是存储在 zw 两个通道中(可以提升精度)

解决方法倒是很简单:

// DecodeFloatRG为UnityCG.cginc中自带的辅助函数
float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) - i.depth;

关于透视除法,可以复习之前的计算机图形学笔记,或者画个侧面的视锥体切一切

理论上讲,现在已经得到了护盾深度值和环境深度值的差值,只要取 1 - saturate(diff),就可以得到一个相交边缘的遮罩

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动、电流脉冲、固定边缘
	// ...
	
	float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) - i.depth;
	float intersectGradient = 1 - min(diff, 1.0f);
	
	return intersectGradient;
}

但是事情好像没有这么简单:
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
这是因为 min(diff, 1.0f) 的值太小了,没能将非相交边缘的部分减到 0,可以通过乘上一个常量来增大这个值,比如 20
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
但是这个方法并不通用,因为当改变相机的远平面时,这个遮罩也会跟着改变
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
这其实是因为在一开始计算护盾深度值的时候,做了一个 “ 除以相机到远平面的距离 ” 的操作

当时这么做的理由是,z 值是距离,纹理采样是颜色,值域不同
既然现在发现,统一成颜色值域的话会出问题,那干脆就都整成距离吧…

所以解决方法是给差值 “ 乘以相机到远平面的距离 ”

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动、电流脉冲、固定边缘
	// ...
	
	float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) - i.depth;
	diff *= _ProjectionParams.z;
	float intersectGradient = 1 - min(diff, 1.0f);
	
	return intersectGradient;
}

这下就完全不受相机设置的影响了,因为它就是一个绝对的距离,不是一个基于远平面参数的 Range(0, 1)

推荐直接从前面就改成使用距离计算,可以节省一点计算量…
前面的笔记就不改了,反正栽坑爬坑也是思考的一部分…

v2f vert (appdata v)
{
	// ...
	
	// 距离
	o.depth = - mul(UNITY_MATRIX_MV, v.vertex).z;

	return o;
}

fixed4 frag (v2f i) : SV_Target
{
	// 蜂窝搏动、蜂窝电流、固定边缘
	// ...

	// 距离差
	float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) * _ProjectionParams.z - i.depth;
	float intersectGradient = 1 - min(diff, 1.0f);
	
	fixed4 intersectTerm = _Color * pow(intersectGradient, _IntersectExponent) * _IntersectIntensity;

	return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb + edgeTerm + intersectTerm, _Color.a);
}

虽然想要两种边缘无缝连接,计算相交线的一些参数(指数、强度)还是设置成了与固定边缘不一样的变量参数
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
图中护盾材质的固定边缘和相交边缘的强度、指数使用的是同样的值
 

自发光

目标效果:护盾可以微微照亮周边

因为护盾的自发光是整体效果,可以把它交给后处理

给相机添加 Post Process Layer 和 Post Process Volume
在 Post Process Layer 中,选择 Default Layer,反走样选择 FXAA

补充
正常情况下应该对需要进行不同后处理的物体在 Inspector 设置 Layer 进行分层,但是这里只是为了实现一个效果,而且还是整体的效果,所以可以忽略

在 Post Process Volume 中,勾选 is Global,创建一个新的 Profile,添加Bloom效果,调节 Bloom 强度
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记
 

优化

将使用同一套UV的多张灰度纹理存储进一张纹理,通过.r.gb.a使用对应通道的纹理
一张纹理有RGBA四个通道,所以一张纹理最多可以存4张灰度图
R:蜂窝边缘,G:蜂窝纹理,B:护盾边缘
【ShaderLab实例笔记】Overwatch Shield - 守望先锋护盾特效制作笔记

这是一个很常见的优化手段,有很多优点:

  1. 节省项目空间:多张图存成了1张
  2. 节省运行内存,提升加载速度:只需要读取并存储1张纹理在内存中
  3. 对于多张图只需要进行一次纹理采样、一次 TRANSFORM_TEX 操作,减小时间空间消耗,简化代码

简单来说就是节省空间,提升速度,简化操作
 

总结

完整 Shader

Shader "Lexdev/CaseStudies/OverwatchShield"
{
	Properties
	{
		_HexTex("R:HexEdge  G:HexPulse  B:Edge", 2D) = "white" {}
		_Color ("Color", Color) = (1,1,1,1)

		[Header(Hex Pulse)]	
		_PulseIntensity ("Hex Pulse Intensity", float) = 3.0
		_PulseTimeScale("Hex Pulse Time Scale", float) = 2.0
		_PulsePosScale("Hex Pulse Position Scale", float) = 50.0
		_PulseTexOffsetScale("Hex Pulse Texture Offset Scale", float) = 1.5

		[Header(Electronic Pulse)]
		_HexEdgeColor("Hex Edge Color", COLOR) = (0,0,0,0)
		_HexEdgeIntensity("Hex Edge Intensity", float) = 2.0
		_HexEdgeTimeScale("Hex Edge Time Scale", float) = 2.0
		_HexEdgeWidthModifier("Hex Edge Width Modifier", Range(0,1)) = 0.8
		_HexEdgePosScale("Hex Edge Position Scale", float) = 80.0

		[Header(Edge)]
		_EdgeIntensity("Edge Intensity", float) = 10.0
		_EdgeExponent("Edge Falloff Exponent", float) = 6.0
		_IntersectIntensity("Intersection Intensity", float) = 10.0
		_IntersectExponent("Intersection Falloff Exponent", float) = 6.0
	}
	SubShader
	{
		Pass
		{
			Tags {"RenderType" = "Transparent" "Queue" = "Transparent"}
			Cull off
			Blend SrcAlpha one
			

			HLSLPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			sampler2D _HexTex;  float4 _HexTex_ST;
			float4 _Color;

			float _PulseIntensity;
			float _PulseTimeScale;
			float _PulsePosScale;
			float _PulseTexOffsetScale;

			float4 _HexEdgeColor;
			float _HexEdgeIntensity;
			float _HexEdgeTimeScale;
			float _HexEdgeWidthModifier;
			float _HexEdgePosScale;
			float _EdgeIntensity;
			float _EdgeExponent;

			// Camera rendered depth texture, must use the name
			sampler2D _CameraDepthNormalsTexture;
			float _IntersectIntensity;
			float _IntersectExponent;

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
				float4 posOS : TEXCOORD1;
				float4 posSS : TEXCOORD2;
				float depth : TEXCOORD3;
			};

			v2f vert (appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _HexTex);
				o.posOS = v.vertex;
				o.posSS = ComputeScreenPos(o.pos);

				// Distance to Percentage: z / far (Simplified remap)
				// o.depth = - mul(UNITY_MATRIX_MV, v.vertex).z * _ProjectionParams.w;

				// Z-Distance
				o.depth = - mul(UNITY_MATRIX_MV, v.vertex).z;

				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 hexTex = tex2D(_HexTex, i.uv);
				fixed pulseTex = hexTex.g;
				fixed hexEdgeTex = hexTex.r;
				fixed4 edgeTex = hexTex.b;

				// Percentage to Distance
				// float diff = _ProjectionParams.z * (DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) - i.depth);
				// Z-Distance
				float diff = DecodeFloatRG(tex2D(_CameraDepthNormalsTexture, i.posSS.xy / i.posSS.w).zw) * _ProjectionParams.z - i.depth;

				float intersectGradient = 1 - min(diff, 1.0f);

				// Hex pulse
				float breath = _Time.y * _PulseTimeScale;
				float pulseOffset = pulseTex * _PulseTexOffsetScale;
				float horizontalDist = abs(i.posOS.x);
				float xOffset = horizontalDist * _PulsePosScale;

				// Electronic pulse
				float edgeBreath = _Time.y * _HexEdgeTimeScale;
				float edgeNormalizer = 1 - _HexEdgeWidthModifier;
				float verticalDist = abs(i.posOS.z); // Z-Up in Object-Space
				float diamondPattern = (horizontalDist + verticalDist) * _HexEdgePosScale;

				// Terms
				fixed4 pulseTerm = pulseTex * _Color * _PulseIntensity;
				fixed4 hexEdgeTerm = hexEdgeTex * _HexEdgeColor * _HexEdgeIntensity;
				fixed4 edgeTerm = pow(edgeTex, _EdgeExponent) * _Color * _EdgeIntensity;
				fixed4 intersectTerm = _Color * pow(intersectGradient, _IntersectExponent) * _IntersectIntensity;

				// Terms & Masks
				pulseTerm *= abs(sin(breath + pulseOffset - xOffset));
				hexEdgeTerm *= saturate(sin(diamondPattern - edgeBreath) - _HexEdgeWidthModifier) / edgeNormalizer;
				
				return fixed4(_Color.rgb + pulseTerm.rgb + hexEdgeTerm.rgb + edgeTerm + intersectTerm, _Color.a);
			}
			ENDHLSL
		}
	}
}

基本思路总结
总的来说效果可以分为两大部分:图案和边缘

图案部分可以拆解为沿 x 轴扩散的蜂窝图案,和以菱形向外扩大的电流
边缘部分可以拆分为护盾的固有边缘,和护盾与其他物体相交产生的边缘

图案部分的计算是先用颜色纹理强度因子得到基础图形,再通过对时间进行数学运算获得图形遮罩,二者相乘得到动画
边缘部分的计算是采样纹理获得遮罩,再通过对颜色进行数学计算,二者相乘得到边缘

 
杂七杂八总结
平移 sin 函数可以打算 > 0 和 < 0 的长度平衡;可能需要对平移后的值做矫正
abs(x) + abs(y) = n(常)是中心在原点的斜放正方形
sin(abs(x) + abs(y)) 是s&fja@inc*ud^ishd(#svd%abaabaaba
有奇奇怪怪的非二维函数可以扔到Google里看图像
采样相机渲染的深度图时,需要先做一个透视除法矫正透视导致的歪斜

 

一点题外话
感觉捡到宝了,这个博主真的好良心啊
博客里一共有5个教程,目前算是跟着学完了其中两个最感兴趣的效果(Gears Hammer of DawnOverwatch Shield),受益良多,慢慢的开始明白拆解思路、参数设置思路了

虽然依然是数学渣一个,但是还是很开心终于开始慢慢体会到数学的神奇了

哎…失去了 gif 感觉放图的乐趣都没了QWQ

上一篇:Unity Shader入门总结(一)


下一篇:Unity Shader 纹理动画 滚动的背景