公司对项目的UI出了一些看起来科技风的效果,于是要靠shader来实现了。几个月没写shader了有点手生。
这个还适用于在PC端游戏的一个大的透明背景中,背景根据鼠标的轨迹来实时做一些轨迹跟随,例如五角星或者心形跟随。
拖尾效果因为太难需要的时间长被砍掉了,后面是觉得有点兴趣和感觉以后有用,闲暇时间自己捣鼓的。
后面只是试验到将鼠标点轨迹输入shader,shader能沿着这条轨迹绘制线条,虽然与我预想的不一样。后面的图片采样跟随就没做了,感觉还要花很多时间。
缺点
主要想到的缺点是在移动端,使用这个效果的Image面积不能过大,因为这个shader里面,一个片源在片元着色器中至少有几次或者十几次的tex2D操作,为了达到较长且密度较高的拖尾,可能会有几十次的tex2D操作。如果Image占了整个屏幕,相当于整个屏幕每个像素都会执行多次的tex2D操作,tex2D消耗的是显卡的带宽,移动端显卡比较吃紧的就是显卡带宽。PC端倒无所谓
方案确定
范围映射
想了三种方案
-
先用物体A,A挂载的材质上是一个队列靠前的shader,shader根据所给图片绘制一次正六边形,然后绘制部分写入模板缓冲,再用物体B,,B挂载的材质上是一个队列靠后的shader,shader根据模板缓冲判断范围进行圆形的淡入淡出操作,然后再将模板缓冲还原。因为每次都是A先写入,所以每次B写入时判断都会是A的缓冲修改范围,所以写完以后及时还原是正确的。然后手指点击拖动的是A。前面的做法之前没做过,可行性不确定。 这种做法我想不出拖尾效果怎么实现 -
与A的想法类似,鼠标按下的时候跟随鼠标平移正六边形的UV,然后圆形的扩散范围与正六边形进行交集操作,因为目前已经有UV平移的博客unity shader-UV平移,旋转,缩放 ,可行性应该比1的高。这种做法和1一样是用图,用图的方式一大优点是换了其他的图形一样适用,比如换成五角星。 -
直接搜索用shader绘制正六边形的办法,绘制一个正六边形的shader。在这个判断了正六边形的基础上进行与触摸点的距离判断决定透明度值。 这种做法一开始是最好实现的,直接找现成代码复制过去, 一旦出问题改起来需要先了解原理,改起来也不好改,要懂六边形的数学知识,不是一时半会。并且这种做法只适合六边形, 一旦换了其他图形所有数学理论基础要重新建立。
拖尾轨迹
轨迹拖尾这部分搜索也有不少优秀的博客,只是功能不相关,只有思路参考,以后有类似的功能可以参考
Unity Shader - Motion Trail Effect(模型运动轨迹/拖尾效果) Unity3D-Shader-人物残影效果
这部分思考了好久,最后只想到一种方法,用RenderTexture存储什么的都感觉不可行,最后思路是在c#用队列的形式,每帧检查当前鼠标点是否与上一帧的鼠标点一致,不一致则存入队列,然后队列按照从后往前的顺序传入shader,队列的顺序是后进后出, 最后的就是最新的,传入shader是一个数组或者矩阵,最新的点用不透明度最高的值,越靠后的用越低的值,所有关于触摸点的距离透明度渐变都是在此值的基础上进行变化。
以此思路为基础:
在选择了3的基础上,
一开始以为这种做法会导致点与点之间的圆形辐射范围重合部分的透明度比不重合部分高,后来想想 , 以传入的所有轨迹点作为中心绘制六边形, 每个像素从后往前判断自己属于哪个轨迹点,然后按照对应的基础值进行绘制。这样即使对于同一个像素,即使他是和其他基础值是重合的, 其上面应用到的基础值永远是所有重合基础值中最新的。这里要理解给定任意中心和半径绘制正六边形的方法,门槛高一些
在选择了2的基础上,
对相同的六边形图片,每个轨迹点进行一次对应的uv偏移,这样需要估计至少十几份的uv偏移操作, uv的平移操作不像拉伸那样表现效果神秘莫测, 可行性目测可以,只是对于同一个像素,对于每次轨迹点的偏移,在shader里面要有一下子十几次不同的uv偏移,每次偏移要采样一次六边形贴图, 采样到在六边形范围内,则用轨迹点对应基础值不透明度乘以shader最基本的透明度,如果没在六边形范围内则透明度为0,所有采样的颜色相加,乘以这十几次分之一,最后作为颜色值。
实现步骤
下面的步骤每一步都有个备份、下个步骤是在前个步骤的基础上做的
-
实现以图片中心为中心向圆形四周的透明度降低渐变,并加入蜂窝图,r值作为遮罩 -
加入六边形图,以r作为像素舍弃标志位,对圆进行裁切,要注意调整六边形的Tiling -
测试C#获取拖拽向量传入shader,对图片读取进行与鼠标移动方向一致的UV偏移: 主要是鼠标按下时鼠标点的uv坐标和鼠标点的局部坐标,以及鼠标拖拽时鼠标点相对按下时的相对移动向量。 -
C#代码重构 -
将3应用到控制正六边形的移动以及圆形渐变范围的移动 -
不拖拽时,区域的渐变消失,思路是c#检测到没有按下时,将shader的基础透明值渐渐降低为0;按下时,有种从按下点到四周扩散的效果,思路是按下时,基础透明值逐渐升高到指定值。因为本身每个片源自身的透明值是随着到点击点的距离的降低而升高的,所以在随着阈值升高时会有按下时向四周扩散和随着阈值降低时会有抬起时向中心收拢的效果 -
对shader传入任意方向的线的点的uv坐标,对图片进行任意方向的uv移动,然后读取图片,对线段中每个分段采样uv得到的效果进行正确的混合操作,测试残影效果是否可行。 -
从这里到实现残影拖尾实际上还有很多步骤要实现,shader这种写法直接一步到位出问题很难排查出来。先是测试鼠标轨迹点传入到shader里面是否正确,所以需要在shader中将轨迹点绘制出来。先做的是固定线段的绘制,如从中心点水平向右 向左 左斜上方 右斜上方等的线绘制出来。传入的还是uv,然后一个线段分成好多个点,求每两个点之间符合点到直线的距离,小于一定值并且xy都在两点范围内的就绘制出来。一个线段分好多点是为了给轨迹点做准备,因为轨迹是随机曲线,只能分成若干个很短的直线来进行记录。 -
C#中对鼠标轨迹点的记录 -
将鼠标轨迹点传入到shader中进行绘制 -
将鼠标轨迹点传入shader中,进行残影对轨迹点的跟随 -
美术调控参数简化
现象
- 在Unity2021.1.12f1c1中, 用于pivot是(0.5,0.5)的Canvas,并且Canvas是ScreenSpace-Overlay和ScreenSpace-Camera下的UI时,UI使用的shader里面的模型坐标系原点是在屏幕中心 ,并且模型坐标的单位是屏幕分辨率, 原点这个一开始是着实惊讶到,感觉是个bug,不相信完全可以写个shader测试一下,按照unity的性格,原点这个特点估计会一直保留。同样一个shader,用在3D世界中的时候单位是米,用在UI部分时候单位是像素,所以一些输入要改的特别大。所以在做UI的shader时候,最好是先用在熟悉的3D世界中,整体原理相差不会太多,差的只是一些单位和原点等参考值。 既然这样,那么只能在c#里面求到图片的中心点相对与屏幕中心的位置然后传入shader,才能保证图片不管在哪个位置,都能正确显示点击的位置。
这里UV和局部坐标系不一样的概念,UV的和3D世界的规律一致,要改的只是传入的局部坐标位置
测试代码如下:
Shader "Unlit/ClickHighLightUI"
{
Properties
{
_MaskGraphic ("MaskGraphic", 2D) = "white" {}
_ShowRange ("ShowRange", Range(500,2000)) = 1000
_HighLightColor ("HighLightColor", color) = (0,1,1,0)
_ClickPos("ClickPos",vector) = (0,1,0,0)
}
SubShader
{
Tags {
"RenderType"="Transparent"
"RenderQueue"="Transparent"
}
Pass
{
ZWrite off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float3 clickPos : TEXCOORD1;
float3 pos_OS : TEXCOORD2;
float2 maskUV : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MaskGraphic;
float4 _MaskGraphic_ST;
float _ShowRange;
float4 _HighLightColor;
float4 _ClickPos;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.maskUV = TRANSFORM_TEX(v.uv, _MaskGraphic);
o.clickPos = float3(0,0,0);
o.pos_OS = v.vertex;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
float dis = distance(i.pos_OS, i.clickPos);
float highLightLevel = min(max(_ShowRange - dis, 0) / _ShowRange, 1);
float4 col = float4(_HighLightColor.rgb, highLightLevel);
float4 maskCol = tex2D(_MaskGraphic, i.maskUV);
return col;
}
ENDCG
}
}
}
- Shader.SetGlobalXXX()方法对于在Shader里面已经声明了的变量是无效的,举个例子,有个shader
在property属性块里面声明了
_Vector ("Vector", color) = (1,1,1,1)
...
CGPROGRAM
float4 _Vector;
ENDCG
则在c#代码中,
Shader.SetGlobalVector("_Vector", new Vector4(255,0,0,1));
这样写是无效的,Shader.SetGlobalVector这个方法只针对未在property声明的属性有效。
-
Unity 2021.1.2f1c1中,如果存在这样的目录结构,即一个shader一个材质一个场景在同个文件夹并且三个同名 那么这个场景将会打不开—来自unity的bug, 虽然只是遇到了就这一个特例,但还是百思不得解,要记录(记仇)一下 -
在Canvas是ScreenSpace-Overlay的情况下,uv的offset调节变化方向与3D世界是相反的, UI中的这个方向与其旋转和缩放和位置无关,如下图所示 UI 3D世界 -
重构,写到某个阶段的时候,所有功能在一个类实现,职责扩散了, 再继续写下去和改起来都思维会混乱,因为怕影响到其他代码,开发速度会变慢很多。 因为只是个预研,于是简单用单一职责原则对类进行分化,每个类独立一个功能,数据抽取到数据类,单独小功能抽取成一个方法,工具方法抽取到工具类, 这样写的时候不用思考着会不会影响到其他代码,方便后面继续开发和维护 -
备份,包括使用git,完成了阶段性的功能以后,脚本和shader和场景要备份一份出来,这是为了在下一步中之前的功能出错的时候,能快速对比找出错误点,以及上阶段的功能下个阶段被改掉了,但是上个阶段的功能有其他的应用价值,例如下次做其他表现时候想到这个地方可以参考,或者突然发现这里有个知识点比较重要想回顾一下,这时保留起来,以后就会用到的。 -
传向量数组到shader不会了,参考 Unity 传递 Array 给 Shader 还是谷歌强,同样的搜索词一搜就出来, 百度搜了好久都找不到答案 -
另外还遇到个情况是同样一个shader,在改好之后,与改之前相同的参数,立刻运行可能看不到效果,可能要运行几次,改好之后的效果才会出现,shader是立刻编译的,和c#的转圈圈不同,这个有点不理解,只能说既然遇到了还是记录(记仇)一下
10.这种复杂的shader还是要拆解下来一步步做功能调试和实现,如果一下子做好两个功能,那么调试的时候,容易长时间找不出问题,容易沮丧,一步步实现时间短成就感能激励自己做下一步。之前在一开始是一步步来,做到靠后的步骤时,一下子做了c#轨迹点的记录和shader轨迹点采样图拖尾成像的功能,结果调试时感觉太深而比较绝望。后来还是先做好固定路径shader轨迹点采样图拖尾成像,例如横向固定路线等。
- Fail to present D3D11 swapchain due to device reset/removed. this error can hann if you draw or dispatch very …
我在写shader的时候出现了这个错误,某个shader,一旦材质选择了使用这个shader,那么就会立刻报错然后unity崩溃。删了材质重新赋值也不行,那应该是shader的锅了。因为第一次在shader中用for循环,感觉比较可能出错在那里,然后感觉可能不能在shader中使用,然后试了发现不是这样,而是shader写完之后在逻辑上出现错误了,
...
for (int currentIndex = 0;
currentIndex <= _TrailTracksCount; currentIndex --)
{
...
这是我的for循环,仔细看可以看到我初始值是0.第一次循环后就减减了,然后负数当然报错,如果for循环第二个数值是常量如下,
..
for (int currentIndex = 0;
currentIndex <= 5; currentIndex --)
{
..
那么shader直接表示不通过,但是如果是编译时不能确定的数值_TrailTracksCount,那么就直接报最开始时候的错误然后崩溃了。
-
ArgumentException: Zero-sized array is not allowed. 才发现Unityc#传数组给Shader不能传没有元素的数组。那只能保底传一个零向量数组进去了,这里记仇一下。 -
直接判断是否等于0太精确了 可以认为符合条件的是单个像素宽度的直线,或者shader中这样的判断根本不会有存在 反正表现不正常 需要改成判断是否小于某个阈值 例如0.003 -
下面是两点式方程判断点是否在直线上,(y-y2)/(y1-y2) = (x-x2)/(x1-x2),虽然最后因为什么灵异现象没有选择这种方法,还是记录一下:这两个if是一样的 但是前者不能将upright的直线画出来 后者能 感觉可能还是底层判断精度的问题
if (((i.uv.y - _lastTrailTrackDeltaUV.y) / (_currentTrailTrackDeltaUV.y - _lastTrailTrackDeltaUV.y)) -
((i.uv.x - _lastTrailTrackDeltaUV.x) / (_currentTrailTrackDeltaUV.x - _lastTrailTrackDeltaUV.x))
== 0.0f)
if (((i.uv.y - _lastTrailTrackDeltaUV.y) / (_currentTrailTrackDeltaUV.y - _lastTrailTrackDeltaUV.y) ==
(i.uv.x - _lastTrailTrackDeltaUV.x) / (_currentTrailTrackDeltaUV.x - _lastTrailTrackDeltaUV.x))
- 在这个工程中, 传入shader的是一个list, shader里面用数组接受这个list,遍历越界时,和C#遍历并不一样,shader没有报错,而是直接显示上的异常。当显示出现异常时,要注意是否有数组越界问题。因为写C#多了的原因,出现这种显示异常一般会集中于计算上的问题,然后在这方面看了很久而没有想到Shader代码的遍历上的原因。
在这样遍历时,会出现本来绘制线,但绘制了其他线的情况,例如横线
for (int currentIndex = 1;
currentIndex <= _TrailTracksCount; currentIndex ++)
{
解析
实际运行中的代码思想和构思时还是有不一样的,有些地方的代码思想值得记录一下,以后借鉴。
两点式化为一般式以及点到直线的距离
参考 直线方程两点式怎么化成一般式
然后基于此代入点到直线的距离公式求距离
参考 点到直线的距离公式
虽然这样的效果还可以用c#用对象池技术实时生成一个个的的形状物体也可以实现,并且当不透明的时候遮挡关系可以正确处理,并且一开始维护起来简单很多,比较重要的是当拖尾是透明的时候,重叠部分就会显得比较违和了,C#层面没有办法处理重叠部分。
用C#来做时,如果要求拖尾比较细腻,则生成的物体有几十个,每个间隙很小,生成物比较多时如果要考虑性能,则要考虑UI的合批和重建,减少重建的时间需要在一个canvas下单独存放这个特效物体,不同的canvas可能会引入新的层级显示的问题,需要做其他的分支情况处理。最后在商业项目中这个功能处理的分支情况可能很多,要处理好代码结构,否则容易出错不容易维护等。
而仔细想想用shader来做,一开始是很难的,后面这些透明度的混合,重建,合批,层级显示等问题都是没有的,看根据实际需要选择。
仓库地址
源工程
|