![9bece45dab3cef655e1f4d273af249f0.png](https://img-blog.csdnimg.cn/img_convert/9bece45dab3cef655e1f4d273af249f0.png)
在前两篇中,我们完成了 基于纹理的边缘检测
以及 水彩纸边缘效果 的制作,接下来就是最后一道工序,为图像的暗部加阴影线
添加阴影线同样有基于纹理和基于模型的几种不同方法,实际可以根据喜好和需求选择。
首先在properties中添加一些用得上的元素:阴影纹理、添加阴影的阈值,以及未来让效果调节起来容易一些,增加一个阴影强度的参数。
_MainTexB ("Sketch Texture", 2D) = "black" {}
_ShadowThreshold("Threshold",Range(0, 1))=0
_ShadowS("Strength",Range(0, 1))=0
基于纹理的方法很简单,直接使用之前计算亮度的函数,并直接将其与ShadowThreshold进行比较,就可以得到一个比较生硬的效果。
fixed4 frag(v2f i) : SV_Target {
fixed4 texMain=tex2D(_MainTex, i.uvTA[0]);
fixed4 texBack=tex2D(_MainTexB, i.uvTB);
if(sh>_ShadowThreshold){
return texMain;
}else{
return texBack;
}
}
![f0532da0bd724f4d12b24bc44fb667ab.gif](https://img-blog.csdnimg.cn/img_convert/f0532da0bd724f4d12b24bc44fb667ab.gif)
以上就是通过某个纹理来控制一张图是否需要显示的方法,市面上的很多消融效果使用的就是同样的原理。但是这样简单粗暴的方法显然是达不到一个好看的效果的,于是我们做一些修改。任务有二,第一要让阴影融合更平滑,第二点是要阴影的叠加更加自然。
fixed4 sum=lerp (texMain,texBack,saturate(_ShadowThreshold-luminance(texMain)) );
return sum;
把刚才的简单判断替换成上一章使用的方法,果然自然了一些。不过虽然效果看起来可以,但是实际并不正确。通过上面的式子计算下来,直到在threshold到达1的时候,亮度为0的部分才是完全显示阴影也纹理的,虽然可能也没什么必要。
于是这里换一种计算方法,首先定义 一个property来表示柔化边缘的宽度。
_SoftEdge ("Soft Edge length", Range(0, 1)) = 1
之后,在亮度低于threshold的部分,引入softedge的宽度进行计算。
fixed soft=saturate((_ShadowThreshold-sh)/_SoftEdge) ;
sh=step(sh,_ShadowThreshold)*soft;
fixed4 sum=lerp(texMain,texBack,sh);
return sum;
看看效果,果然好了很多吧。
![d39f88ecce48925387a0d524e033dfc8.gif](https://img-blog.csdnimg.cn/img_convert/d39f88ecce48925387a0d524e033dfc8.gif)
接下来解决第二点的问题。
在上面的内容中,我们实际上做到了使用透明度进行融合,这种简单的融合效果在制作两张图片的切换时是比较合适的,但是用作阴影还是不够。
如果有画图或使用图形处理工具的经验的话,应该对图层的混合模式不太陌生,例如常见的柔光、正片叠底、叠加等等。
关于 混合模式 ,在知乎上就能搜到很多对其介绍的文章。
关于 混合模式的算法 ,能搜索到的文章也有很多。
由于这是一篇用于激发人们学习兴趣的文章,所以这里就以简单相加或相乘为例。
先增加一个表示阴影整体强度的property,和一个表示两种混合方式的keyword。
_ShadowStrength("Strength",Range(0, 1))=0
[KeywordEnum(multiply, lighten)] _TYPE ("Type", Float) = 0
接着在frag中做一点修改
fixed4 frag(v2f i) : SV_Target {
fixed4 texMain=tex2D(_MainTex, i.uvTA[0]);
fixed4 texBack=tex2D(_MainTexB, i.uvTB);
fixed sh=luminance(texMain);
//fixed soft=_SoftEdge>0? clamp(_ShadowThreshold-sh,0,_SoftEdge)/_SoftEdge : 1 ;
fixed soft=saturate((_ShadowThreshold-sh)/_SoftEdge) ;
sh=step(sh,_ShadowThreshold)*soft;
fixed4 sum=texMain;
#if _TYPE_MULTIPLY
//Multiply
fixed4 Csketch=texMain*lerp(1,texBack,_ShadowStrength);
sum=lerp(texMain,Csketch,sh);
#endif
#if _TYPE_LIGHTEN
//Lighten
fixed4 Csketch=max( texMain,lerp(0,texBack,_ShadowStrength));
sum=lerp(texMain,Csketch,sh);
#endif
return sum;
}
并且不要忘了添加这一条来让两个keyword生效。
#pragma multi_compile _TYPE_LIGHTEN _TYPE_MULTIPLY
于是来看看效果
![4381eec8cbdb8e1bf715356d74ba7e30.gif](https://img-blog.csdnimg.cn/img_convert/4381eec8cbdb8e1bf715356d74ba7e30.gif)
这样,一个基于纹理的暗部阴影效果就完成了。
如果把计算亮度的部分换成跟一个颜色的对比,再增加一个表示暗部颜色的property,甚至还可以将阴影叠加到特定颜色上哦。
fixed4 xx =texMain-_Darkcolor;
sh=dot(xx,xx);
![659d665dc7abffb52d7d119d0afe664c.gif](https://img-blog.csdnimg.cn/img_convert/659d665dc7abffb52d7d119d0afe664c.gif)
基于纹理的方法虽然看起来效果比较细腻,但它只能贴在物体上或挂在摄像机上,识别出的暗部也只是颜色比较深的部分。那么如何让它能够找出真正的阴影部分呢,这就需要使用基于模型的方法。
由于不再需要通过颜色计算描边,在结构体v2f中已经不需要再保存周围点的uv坐标了,于是我们把uvTA只保留一个。同时,由于是基于模型来添加阴影线,阴影的强度是可以在vert函数中完成的,添加一个sketchWeights,用来将阴影线的强度传递到下一工序中。当然,这个数值存到其中某个uv的z通道里也没问题。
struct v2f {
float4 pos : SV_POSITION;
fixed2 uvTB:TEXCOORD0;
fixed2 uvTA[1] : TEXCOORD2;
fixed sketchWeights : TEXCOORD3;
};
在vert函数中,计算完主要纹理和阴影纹理的uv之后,就可以开始进行阴影的计算了。
v2f vert(appdata_full v) {
v2f o;
//o.pos = UnityObjectToClipPos(v.vertex);//mul(UNITY_MATRIX_MVP, v.vertex);
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uvTB=(v.texcoord-_MainTexB_ST.zw)*_MainTexB_ST.xy ;
half2 uvTA = (v.texcoord-_MainTex_ST.zw)*_MainTex_ST.xy ;
o.uvTA[0] = uvTA;// + _MainTex_TexelSize.xy * half2(0, 0) * size;
//……
return o;
}
首先使用WorldSpaceLightDir获取当前顶点到光源的方向,接着使用UnityObjectToWorldNormal将当前的法线从模型空间转换为世界空间。然后使用点乘的方法计算光源和法线这两个向量之间的夹角。
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed lightDifference =dot(worldLightDir, worldNormal); //from -1 to 1
两个normalize过的向量点乘,得出的结果是在-1到1之间,那么我们只要把它映射到0和1之间,后面的计算就可以和之前的一样了。
lightDifference=(lightDifference+1)/2; //from 0 to 1
fixed soft=saturate((_ShadowThreshold-lightDifference)/_SoftEdge) ;
lightDifference=step(lightDifference,_ShadowThreshold)*soft;
o.sketchWeights=saturate(lightDifference);
![a66bdbebd14063bff61035b1b072bd85.gif](https://img-blog.csdnimg.cn/img_convert/a66bdbebd14063bff61035b1b072bd85.gif)
接下来加上描边部分。
我们使用将顶点沿着法线方向外扩的方法来实现描边的效果,这一步骤需要新增加一个pass来完成。
v2f vertOutline (appdata_base v) {
v2f o;
float4 offset = float4(v.normal, 0) * _OutlineSize;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex+offset);
return o;
}
float4 fragOutline(v2f i) : SV_Target {
return _OutlineColor;
}
增加一对vert和frag函数来供负责描边的新pass使用,在vert中,使用顶点的法线方向计算出描边外扩的偏移量,再与原本的顶点位置相加得出偏移后的位置,之后在frag函数中直接绘制设定好的描边颜色。
这个时候,可能会弹出一条说vert函数没有完全初始化的warning,不过众所周知大家都不在乎warning所以放着不管就好了。那么再加一个新的结构体,只保留描边用frag函数需要的顶点位置就解决了~
struct v2foutline {
float4 pos : SV_POSITION;
};
最后,添加新的pass。
Pass {
Cull Front
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vertOutline
#pragma fragment fragOutline
ENDCG
}
制作描边的时候,大家通常会使用Cull Front把正面剔除,不过这里对效果的的影响并不大,要不要剔除或者怎么剔除都可以随需求来决定。
看一下成果。
![af2691035b1e1d424cdca3b9fbdcaf9c.gif](https://img-blog.csdnimg.cn/img_convert/af2691035b1e1d424cdca3b9fbdcaf9c.gif)
方块的描边似乎断了,没有办法,这是unity提供的默认方块实际上是由6个独立的平面构成所导致的。也正是因为这一点,才让构成每个平面的4个顶点的法线方向能够一致。
![765f1cdd405c140547f7d5faede38257.png](https://img-blog.csdnimg.cn/img_convert/765f1cdd405c140547f7d5faede38257.png)
此外,添加一条blend命令来控制描边的混合方式,这里选择了通过透明度混合的方法,这样就可以调节描边的浓度了。
![cf250dcfe72d4c42d2f578b4b5d75d8a.gif](https://img-blog.csdnimg.cn/img_convert/cf250dcfe72d4c42d2f578b4b5d75d8a.gif)
好了,重现女武神系列的canvas效果系列文章到此结束,希望能给大家带来一些启发哦。
![0a0ff62b9bd0225fab9fca47e1590d1f.gif](https://img-blog.csdnimg.cn/img_convert/0a0ff62b9bd0225fab9fca47e1590d1f.gif)
这一章中使用到的资源仍然在这里获取:
sakuraplus/make-terrain-with-google-elevationgithub.com获取更多特效,Buy 松鼠 a coffee(由于不太常用,只在插件中作为一个例子收录,跟卡比这篇一样):
slideshow effectassetstore.unity.com如果有其他感兴趣的特效,大家一定要告诉松鼠哦~