终于实现出美术同事想要的这个效果了:

  下面就来讲述我写这个shader的思路。

  首先需要一张魔法阵的底图:

  用一个平面(Plane)做模型,将底图贴在模型上:

  代码如下:

Shader "Inception-Fx/FlowInsideOut" {Properties {_MainTex ("Base (RGB)", 2D) = "white" {}}SubShader {Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}Cull OffLighting OffZWrite OffFog { Mode Off }Blend One OnePass {CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"sampler2D _MainTex;struct appdata_t {float4 vertex : POSITION;half2 texcoord : TEXCOORD0;};struct v2f {float4 pos : POSITION;half2 mainTex : TEXCOORD0;};v2f vert (appdata_t v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);o.mainTex = v.texcoord;return o;}fixed4 frag (v2f i) : COLOR{fixed4 baseColor = tex2D(_MainTex, i.mainTex);return baseColor;}ENDCG}} FallBack "Diffuse"
}
代码1

  这是一个非常普通的半透shader。如果对此理解有困难的话,需要先读一读Unity shader的相关文档,补充一下shader的基础知识。不过这里有几点需要提及的是:

Cull Off 关闭裁剪,因为我们需要Plane的背面面向摄像机时,我们也能看到这个流光效果。

Lighting Off 关闭光照。我们不希望这个魔法阵受到光照的影响。

ZWrite Off 像素深度不写入Z缓冲区。一般半透shader都要加上这句,不然离摄像机更远的被Plane挡住的模型可能会直接显示不出来,半透效果也就失去了本身的意义。

Fog { Mode Off } 关闭雾效,不然如果场景开启了雾效,这张图就会变成下面的样子,整个Plane的轮廓都被显示出来了:

Blend One One  为了节省内存,贴图采用了ETC压缩格式,没有alpha通道,所以为了呈现半透的效果,Blend就要用叠加(additive blending)而不是alpha混合(alpha blending)的方式。

  接下来就要考虑如何做出流光的效果了。这也是难点。整个shader的复杂度就在这里。

  我们可以从Vetex & Fragment Shader的逻辑开始考虑。Vertex & Fragment Shader的流程大体分为两步:第一步是Vertex Shader,也就是上文代码中的vert函数,作用是计算模型上每个顶点的信息——这里包括顶点在屏幕上的投影坐标和uv;第二步是Fragment Shader,也可以简单地理解为Pixel Shader,也就是上文代码中的frag函数,作用是最终决定模型的每个像素的着色。上文代码的frag函数只是根据uv对_MainTex贴图进行采样,显示出贴图上对应的像素。要实现出精细的流光效果,就要在frag函数中根据像素离圆心的距离,判断当前像素是否在光带中。

  假设当前像素离圆心的距离是r,又假设某一时间点上光带的半径是flowRadiusMin到flowRadiusMax。那么若r < flowRadiusMin或r > flowRadiusMax,当前像素就不在光带中,像素颜色就如代码1一样,只要对贴图进行采样即可。如果flowRadiusMin <= r <= flowRadiusMax,那当前像素就在光带中,像素颜色就应该是光带和底图“合成”的颜色。随着时间的推移,flowRadiusMin和flowRadiusMax会周而复始地从小增大,但flowRadiusMax - flowRadiusMin,即光带的宽度始终保持不变。假设光带的宽度是_FlowWidth(可以作为shader参数开放给美术调节),那么在一个时间周期中,一开始flowRadiusMax为0,flowRadiusMin为-_FlowWidth,整个魔法阵上还没有光带;然后flowRadiusMin和flowRadiusMax匀速地变大,光带开始从魔法阵圆心向外流动;到周期最后,flowRadiusMin增大到魔法阵的半径——设为radiusMax,flowRadiusMax则为radiusMax + _FlowWidth,光带从魔法阵上消失。

  接下来要确定圆心的坐标和半径的长度单位。其实这两者可以归结为一个问题,就是用什么做坐标系。我曾经考虑过用顶点坐标,也就是模型自身的局部坐标系,但后来发现顶点坐标的值会收到Drall Call合批的影响(见http://blog.csdn.net/zzxiang1985/article/details/50502624),而且顶点坐标的单位也很难摸索出来。相对而言,uv坐标的分布规律可控。在Plane中,圆心就位于uv坐标(0.5, 0.5)处。魔法阵半径radiusMax可以定为0.5,即圆心到Plane的四条边的距离:

float radiusMax = 0.5;

  _FlowWidth的单位也就是以uv坐标系的长度单位。

  在代码1中,我们已经在vert函数中将底图的uv赋值给返回值的mainTex成员,并将其传给frag函数。因此在frag函数中,我们就可以求出当前像素离到圆心的距离r:

float2 center = float2(0.5, 0.5);

float r = distance(i.mainTex, center);

  知道了当前像素到圆心的距离,还要知道当前时刻的光带半径——flowRadiusMin和flowRadiusMax,我们才能得出当前像素的颜色。假设光带从圆心开始出现,一直流动到魔法阵边缘,从魔法阵上消失的时间周期是_Period(可以作为shader参数开放给美术调节)。那么当前时刻的光带半径则是

float flowRadiusMax = fmod(_Time.y, _Period) / _Period * (radiusMax + _FlowWidth);

float flowRadiusMin = flowRadiusMax - _FlowWidth;

其中fmod是对浮点数取余的Unity shader自带函数,_Time是Unity shader自带的游戏时间变量。

现在可以求当前像素的颜色了。如果flowRadiusMin <= r && r < flowRadiusMax,那么像素颜色就是光带与底图合成的样色。假设光带本身的颜色(即未与底图合成的颜色)为_FlowColor(作为shader参数开放给美术调节),那么当前像素的颜色就是

float finalColor = baseColor + _FlowColor * baseColor;

======================================================================================

  PS: 这个地方让我研究了一阵子。我曾考虑过finalColor = baseColor + _FlowColor,但出来的结果是这样(_FlowColor为白色):

  这个效果显然是错误的,光带的颜色完全没有根据魔法阵底图的颜色、镂空和半透情况而变化。

  于是又在想会不会是finalColor = baseColor * _FlowColor。这显然就更不对了。这种情况下当_FlowColor为白色时,finalColor还是和baseColor一样。就算_FlowColor是别的颜色比如红色,看上去也没有光的感觉,反而像是魔法阵被啃了一圈:

======================================================================================

  如果r < flowRadiusMin或r > flowRadiusMax,那么finalColor就仍是baseColor。综上所述,finalColor的计算代码就是:

float finalColor;
if (flowRadiusMin <= r && r < flowRadiusMax)
{finalColor = baseColor + _FlowColor * baseColor;
}
else
{finalColor = baseColor;
}

  但是在shader编程中我们应该尽量避免写if语句。上面的代码可以再优化成这样:

float isInFlow = step(flowRadiusMin, r) - step(flowRadiusMax, r);
fixed4 finalColor = baseColor + isInFlow * _FlowColor * baseColor;

  step是Unity shader的自带函数。step(a, b)的作用是:如果b >= a就返回1,否则返回0。

  finalColor作为frag函数的返回值,这个shader就算大体完成了。代码如下:

Shader "Inception-Fx/FlowInsideOut" {Properties {_MainTex ("Base (RGB)", 2D) = "white" {}_FlowColor ("Flow Color", Color) = (1, 1, 1, 1)_Period ("Period (Seconds)", float) = 1_FlowWidth ("Flow Width", Range(0, 1)) = 0.1}SubShader {Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}Cull OffLighting OffZWrite OffFog { Mode Off }Blend One OnePass {CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"sampler2D _MainTex;fixed4 _FlowColor;float _Period;float _FlowWidth; struct appdata_t {float4 vertex : POSITION;half2 texcoord : TEXCOORD0;};struct v2f {float4 pos : POSITION;half2 mainTex : TEXCOORD0;};v2f vert (appdata_t v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);o.mainTex = v.texcoord;return o;}fixed4 frag (v2f i) : COLOR{fixed4 baseColor = tex2D(_MainTex, i.mainTex);float2 center = float2(0.5, 0.5);float r = distance(i.mainTex, center);float radiusMax = 0.5;float flowRadiusMax = fmod(_Time.y, _Period) / _Period * (radiusMax + _FlowWidth);float flowRadiusMin = flowRadiusMax - _FlowWidth;float isInFlow = step(flowRadiusMin, r) - step(flowRadiusMax, r);fixed4 finalColor = baseColor + isInFlow * _FlowColor * baseColor;return finalColor;}ENDCG}} FallBack "Diffuse"
}

  以下是_FlowColor为白色,_Period为3秒的效果:

  有没有发现这和文章最开头展示的效果还是有些不同?文章开头的动画中的流光显得更自然一些,因为光带的边缘很柔和,魔法阵上的每个像素从不发光到发光的过程很平滑;而上面这个动画中,光带的边缘非常清楚,魔法阵上的每个像素从不发光到发光是突然发生的,没有一个渐变的过程。

  要解决这个问题,可以让美术多准备一张N x 1大小的灰度贴图(下图是128 x 4,也是等效的)。这张贴图可以理解为光带从内环到外环的alpha分布,两端是黑色,往中间渐渐过渡到白色:

  我们需要为该贴图多添加一个shader参数:

_FlowTex ("Flow Texture (RGB)", 2D) = "black" {}

  在frag函数中用该贴图的颜色乘以_FlowColor就行了。不过怎么知道当前像素应该对应_FlowTex上的哪个点呢?还是要根据r,flowRadiusMin和flowRadiusMax算出来:

float2 flowTexUV = float2((r - flowRadiusMin) / (flowRadiusMax - flowRadiusMin), 0);

  于是最终像素颜色finalColor的计算变为:

fixed4 finalColor = baseColor + isInFlow * tex2D(_FlowTex, flowTexUV) * _FlowColor * baseColor;

  这下效果就自然多了:

  到这里,魔法阵的效果基本上就做好了。

  接下来还可以对shader做些改进。

  比如我们可以为shader添加一个_InvertDirection参数。这个参数本质是一个布尔变量,在编辑器中显示成一个CheckBox。

[Toggle] _InvertDirection ("Invert Direction?", float) = 0

  我们希望美术将这个参数勾上后,即_InvertDirection为1的情况下,流光的方向就会反过来,从魔法阵边缘流向魔法阵圆心。因此flowRadiusMax的计算方式修改为:

float timeProgress = fmod(_Time.y, _Period) / _Period;

float flowProgress = _InvertDirection * (1 - timeProgress) + (1 - _InvertDirection) * timeProgress;

float flowRadiusMax = flowProgress * (radiusMax + _FlowWidth);

  flowRadiusMin的计算不变,还是flowRadiusMax - _FlowWidth。

  如果想要调节流光的亮度,可以给shader多添加一个_FlowHighlight浮点参数:

_FlowHighlight ("Flow Highlight", float) = 1

  用它乘以光带颜色。于是finalColor变为:

fixed4 finalColor = baseColor + isInFlow * _FlowHighlight * tex2D(_FlowTex, flowTexUV) * _FlowColor * baseColor;

  这是_FlowHighlight为3的效果:

  这和本文最开头的效果已经完全一致了,除了流光的周期和颜色有点差别。文章最开头的效果动画中,光带的颜色会随着光带半径增大而变化。这一点可以通过类似_FlowTex的思路来实现:将_FlowColor改为一张贴图,用来记录从圆心到魔法阵边缘的颜色。不过我们在项目中也不想为此再多维护一张贴图了,所以就换了一种效果没那么灵活但比较简单仍可以让光带变色的方法,就是将_FlowColor换成两个颜色参数:

_FlowColorAtCenter ("Flow Color At Center", Color) = (1, 1, 1, 1)

_FlowColorAtEdge ("Flow Color At Edge", Color) = (1, 1, 1, 1)

  FlowColorAtCenter是位于圆心的光带颜色,_FlowColorAtEdge是位于圆周上的光带颜色。光带位于两者之间时,光带颜色就是这两个颜色之间的插值:

fixed4 flowColor = _FlowColorAtCenter + flowProgress * (_FlowColorAtEdge - _FlowColorAtCenter);

  另外,应美术同事的需求,shader还添加了一个_AllAlpha参数,用来控制整个plane的半透。美术想用Animation配合这个参数做出闪烁效果:

_AllAlpha ("All Alpha", Range(0, 1)) = 1

...

finalColor *= _AllAlpha;

  还有,美术希望底图的Tiling和Offset能起作用,这样将shader应用在其它地方时,就可以通过底图的重复和偏移方便地做出一些星空的效果。因此vert函数中的mainTex计算要使用Unity提供的TRANSFORM_TEX宏:

o.mainTex = TRANSFORM_TEX(v.texcoord, _MainTex);

  但这样mainTex就不再是Plane模型上的原始uv了。而我们并不希望底图的Tiling和Offset会影响流光的位置,因此我们还需要给v2f结构体添加一个flowUV成员来记录Plane的原始uv:

v2f vert (appdata_t v)

{

...

o.flowUV = v.texcoord;

return o;

}

  最终的shader代码如下:

Shader "Inception-Fx/FlowInsideOut" {Properties {_MainTex ("Base Texture (RGB)", 2D) = "white" {}_MainColor ("Base Color", Color) = (1, 1, 1, 1)_FlowTex ("Flow Texture (RGB)", 2D) = "black" {}_FlowColorAtCenter ("Flow Color At Center", Color) = (1, 1, 1, 1)_FlowColorAtEdge ("Flow Color At Edge", Color) = (1, 1, 1, 1)_Period ("Period (Seconds)", float) = 1_FlowWidth ("Flow Width", Range(0, 1)) = 0.1_FlowHighlight ("Flow Highlight", float) = 1[Toggle] _InvertDirection ("Invert Direction?", float) = 0_AllAlpha ("All Alpha", Range(0, 1)) = 1}SubShader {Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}Cull OffLighting OffZWrite OffFog { Mode Off }Blend One OnePass {CGPROGRAM#pragma vertex vert#pragma fragment frag//#pragma only_renderers gles d3d9 gles3 metal#include "UnityCG.cginc"sampler2D _MainTex;float4 _MainTex_ST;fixed4 _MainColor;sampler2D _FlowTex;fixed4 _FlowColorAtCenter;fixed4 _FlowColorAtEdge;float _Period;float _FlowWidth;float _FlowHighlight;float _InvertDirection;float _AllAlpha;struct appdata_t {float4 vertex : POSITION;half2 texcoord : TEXCOORD0;};struct v2f {float4 pos : POSITION;half2 mainTex : TEXCOORD0;half2 flowUV : TEXCOORD1;};v2f vert (appdata_t v){v2f o;o.pos = mul(UNITY_MATRIX_MVP, v.vertex);o.mainTex = TRANSFORM_TEX(v.texcoord, _MainTex);o.flowUV = v.texcoord;return o;}fixed4 frag (v2f i) : COLOR{fixed4 baseColor = tex2D(_MainTex, i.mainTex) * _MainColor;float2 center = float2(0.5, 0.5);float r = distance(i.flowUV, center);float radiusMax = 0.5;  // 从(0.5, 0.5)到(0.5, 1)的距离float timeProgress = fmod(_Time.y, _Period) / _Period;float flowProgress = _InvertDirection * (1 - timeProgress) + (1 - _InvertDirection) * timeProgress;float flowRadiusMax = flowProgress * (radiusMax + _FlowWidth);float flowRadiusMin = flowRadiusMax - _FlowWidth;float isInFlow = step(flowRadiusMin, r) - step(flowRadiusMax, r);float2 flowTexUV = float2((r - flowRadiusMin) / (flowRadiusMax - flowRadiusMin), 0);fixed4 flowColor = _FlowColorAtCenter + flowProgress * (_FlowColorAtEdge - _FlowColorAtCenter);fixed4 finalColor = baseColor + isInFlow * _FlowHighlight * tex2D(_FlowTex, flowTexUV) * flowColor * baseColor;finalColor *= _AllAlpha;return finalColor;}ENDCG}} FallBack "Diffuse"
}

从圆心向外流光的魔法阵shader相关推荐

  1. python 魔法阵

    想必很多朋友都在动漫里看过魔法阵,今天他来了 以下为代码 import turtle as t def tcyuan(x, y, r): t.fillcolor("black") ...

  2. python画魔法阵_Python编写循环的两个建议 | 鹅厂实战!

    本文系 "Python 工匠"系列的第 7 篇文章,已取得作者授权. 循环是一种常用的程序控制结构.我们常说,机器相比人类的最大优点之一,就是机器可以不眠不休的重复做某件事情,但人 ...

  3. Python的turtle模块用法及实例 六:魔法阵七:樱花树 八:小猪佩奇九:多来爱梦

    目录 turtle:基本用法 一:画圆 二:奥运五环 三:美队盾牌 四:繁星 五:星空 六:魔法阵 七:樱花树 八:小猪佩奇 九:多来爱梦 turtle:基本用法 turtle.down() #移动时 ...

  4. noip2016普及组 魔法阵vijos2012

    描述 六十年一次的魔法战争就要开始了,大魔法师准备从附近的魔法场中汲取魔法能量. 大魔法师有m个魔法物品,编号分别为1,2,...,m.每个物品具有一个魔法值,我们用x_ix​i​​表示编号为i的物品 ...

  5. NOIP2016普及组第四题——魔法阵

    题目描述 六十年一次的魔法战争就要开始了,大魔法师准备从附近的魔法场中汲取魔法能量. 大魔法师有m个魔法物品,编号分别为1,2,-,m.每个物品具有一个魔法值,我们用Xi表示编号为i的物品的魔法值.每 ...

  6. 【做题记录】[NOIP2016 普及组] 魔法阵

    P2119 魔法阵 2016年普及组T4 题意: 给定一系列元素 \(\{X_i\}\) ,求满足以下不等式的每一个元素作为 \(a,b,c,d\) 的出现次数 . \[\begin{cases}X_ ...

  7. [NOIP2016PJ]魔法阵

    今天模拟赛的题,,,唯一没有Giao出来的题(不然我就AKIOI了~) 最开始没想到数学题,把所有部分分都说一遍吧: 35分:纯暴力O(M^4)枚举,对于每一组a,b,c,d验证其是否合法. 60分: ...

  8. C++——NOIP2016普及组 t4——魔法阵

    题目描述 六十年一次的魔法战争就要开始了,大魔法师准备从附近的魔法场中汲取魔法能量. 大魔法师有 m 个魔法物品,编号分别为 1,2,...,m  .每个物品具有一个魔法值,我们用 xi 表示编号为  ...

  9. NOIP 2016 PJ T4 魔法阵

    P2119 魔法阵 题目描述 六十年一次的魔法战争就要开始了,大魔法师准备从附近的魔法场中汲取魔法能量. 大魔法师有m个魔法物品,编号分别为1,2,...,m.每个物品具有一个魔法值,我们用Xi表示编 ...

最新文章

  1. redisTemplate分布式锁演变、redission分布式锁实现!
  2. python 数值的整数次方
  3. 昨日之我,今日之我与明日之我
  4. 第01课:中文自然语言处理的完整机器处理流程
  5. JSP 简介(转载)
  6. loadrunner脚本录制为空的解决方法
  7. 基于JAVA+SpringBoot+Mybatis+MYSQL的在线动漫信息平台
  8. gitlab 邮件发送
  9. 欧盟《一般数据保护法案》(GDPR)核心要点 本文更多的是站在企业角度来思考法案对物联网行业的影响以及应对措施,一来希望与同行企业可以就GDPR进行更多的互动讨论;二来也是希望传播国际法案对于安全和
  10. Mac OS下Axure RP 8.0.0.3312安装及注册汉化
  11. ubuntu管理开机启动项
  12. 2019-9-2-win10-uwp-切换主题
  13. Xposed FrameWork v89 安装
  14. html dt和dd顺序,dl dt dd使用方法
  15. HTML实现图片360度循环旋转
  16. 广安职业技术学院计算机在那个校区,广安职业技术学院有几个校区哪个更好
  17. XPAND恩帝泵800克健美补充剂,脂肪燃烧,激素原
  18. 【积水成渊-逐步定制自己的Emacs神器】4:Emacs自动补全
  19. 2015年9月20日
  20. USCD行人异常数据集使用指南 | 快速下载

热门文章

  1. 【墨者学院】主机溢出提权漏洞分析
  2. 邓亚萍的即刻搜索:推民生产品搜索曝光不良食品
  3. 用Python和Google AppEngine开发基于Google架构的应用软件
  4. Hot 100(三)
  5. matlab handles结构体及用法
  6. Unity-奥义技能背景变黑效果
  7. Dart vs Swift
  8. CTP: SimNow , 策略模拟交易利器,赞!
  9. IC卡、ID卡、M1卡、射频卡的区别是什么
  10. java中怎么编写围棋对弈_java课程设计围棋对弈(含代码).doc