很长时间没有写博客了,一方面Sebastian大佬正在更新程序生成星球的教程,所以想等到大佬更新接近尾声的时候开始那个教程的分享。最近主要是在工作之余补一补Unityshader的基础知识,然后正好在项目中遇到这么一个问题,需要实现2D图片的描边和内发光。找了很多资料一直都没有找到实现起来比较不错的方法,而且搜出来的大部分都是3D的描边,3D描边的思想很容易掌握,但是一直没有2D的外轮廓的描边,所以就寻思着自己想一个办法,运气不错,刚好在看冯乐乐的《UnityShader入门精要》的时候她的书里讲了一章节的图片的描边shader,但是她的这个shader的描边并不是我想要的图片的外轮廓的描边,她的方法是通过卷积来求得像素的亮度,通过对比像素的亮度差来获取图片内容的中的边界区分,虽然目的不同,但是她的思想我觉得是可以学习一下的,所以就使用卷积实现了2D图片外轮廓的描边和内发光的实现,下面就进入正题。

首先讲一下冯乐乐这本书中的边缘检测的方法。她的边缘检测的原理是利用一些边缘检测算子对图像进行卷积操作,卷积的操作指的是使用一个卷积核对一张图像中的每个像素进行一系列的操作,而卷积核通常是一个四方形网格结构。这本书中用的卷积核是

Gx

-1 0 1
-2 0 2
-1 0 1

Gy

-1 2 -1
0 0 0
1 2 1

表格中的每个数字都代表周围像素的权重,通过获取每个像素的亮度乘以权重的和来获取梯度G,通过一个梯度的阈值来获取边缘。具体的实现可以去看一下书,这里就跳过不赘述了。

那么我就使用这样的卷积的思想,但是我不使用刚才所讲的卷积核,我使用一个3×3的权重都是1的表格,然后计算获得的是一个像素本身的颜色的透明度和周围8个其他像素的颜色的透明度的和Sum,然后设定一个阈值来与Sum作对比获取图片的外轮廓。下面贴出代码:

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Custom/ShaderForTest"
{Properties{_MainTex("Main Texture", 2D) = "white"{}                                   //主纹理_EdgeAlphaThreshold("Edge Alpha Threshold", Float) = 1.0                    //边界透明度和的阈值_EdgeColor("Edge Color", Color) = (0,0,0,1)                                   //边界颜色_EdgeDampRate("Edge Damp Rate", Float) = 2                                 //边缘渐变的分母_OriginAlphaThreshold("OriginAlphaThreshold", range(0.1, 1)) = 0.2          //原始颜色透明度剔除的阈值[Toggle(_ShowOutline)] _DualGrid ("Show Outline", Int) = 0                 //Toggle开关来控制是否显示边缘}SubShader{Tags{ "RenderType"="Transparent" "Queue"="Transparent" }Blend SrcAlpha OneMinusSrcAlphaPass{Ztest Always Cull Off ZWrite OffCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma shader_feature _ShowOutline#include "UnityCG.cginc"sampler2D _MainTex;half4 _MainTex_TexelSize;fixed _EdgeAlphaThreshold;fixed4 _EdgeColor;float _EdgeDampRate;float _OriginAlphaThreshold;struct v2f{float4 vertex : SV_POSITION;float2 uv[9] : TEXCOORD0;};half CalculateAlphaSumAround(v2f i){half texAlpha;half alphaSum = 0;for(int it = 0; it < 9; it ++){texAlpha = tex2D(_MainTex, i.uv[it]).w;alphaSum += texAlpha;}return alphaSum;}v2f vert(appdata_img v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);half2 uv = v.texcoord;o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);return o;}fixed4 frag(v2f i) : SV_Target{#if defined(_ShowOutline)half alphaSum = CalculateAlphaSumAround(i);float isNeedShow = alphaSum > _EdgeAlphaThreshold;float damp = saturate((alphaSum - _EdgeAlphaThreshold) * _EdgeDampRate);fixed4 orign = tex2D(_MainTex, i.uv[4]);float isOrigon = orign.a > _OriginAlphaThreshold;fixed3 finalColor = lerp(_EdgeColor.rgb, orign.rgb, isOrigon);return fixed4(finalColor.rgb, isNeedShow * damp);#endifreturn tex2D(_MainTex, i.uv[4]);}ENDCG}}
}

变量的意义都写在注释里面了。由于大部分的图片都是带有透明度的所以需要透明混合(Blend SrcAlpha OneMinusSrcAlpha)我们在结构体v2f中定义了一个二维向量的数组,长度是9,是为了拿到像素周围的其他像素点的uv偏移,然后我们就在顶点函数中去计算uv的偏移。half4 _MainTex_TexelSize;定义一个这个变量就能获取到该纹理的纹素,指的就是一个像素占uv的比,比如512*512的图片,纹素就是(1/512,1/512)。然后在片元函数中先计算获得到当前像素的透明度加上周围八个像素的透明度的和alphaSum,如果大于一个边界透明度的阈值说明当前像素点就是一个边界上的点。float damp = saturate((alphaSum - _EdgeAlphaThreshold) * _EdgeDampRate);这段代码的意义是为了在边界的外边缘做缓冲的处理,这样看一起来会柔和一点不会很生硬。然后获取原像素的颜色信息并且剔除透明度过低的边缘。然后输出颜色。

参数的设置:

实现出来的效果图:

放大可以看到边界的渐变处理:

其实内发的光的思想和外轮廓的描边的思想是差不多的,但是我这里实现的时候并没有再使用上面的权重相同的3×3的表格,而是通过精度来切分一个圆,然后通过圆心和半径来算出uv的偏移,由于内发光是要找到边界靠内的一部分地方,所以算出来的透明度的和一样要大于一个阈值,但是这个时候会发现其实不做限制的话会跟外轮廓冲突,所以这里要加一个条件就是要剔除超出原图的像素。

下面就直接贴代码,并且将过程中的原理和解释全部放在注释中:

Shader "Custom/ShaderForTest2"
{Properties{//2D描边_MainTex("Main Texture", 2D) = "white"{}     //主纹理_EdgeAlphaThreshold("Edge Alpha Threshold", Float) = 1.0        //边界透明度的阈值_EdgeColor("Edge Color", Color) = (0,0,0,1)                //边界的颜色_EdgeDampRate("Edge Damp Rate", Float) = 2                //渐变的分母_OriginAlphaThreshold("OriginAlphaThreshold", range(0.1, 1)) = 0.2    //原像素剔除的阈值[Toggle(_ShowOutline)] _ShowOutline ("Show Outline", Int) = 0      //开启外轮廓的Toggle//2D内发光_InnerGlowWidth("Inner Glow Width", Float) = 0.1            //内发光的宽度_InnerGlowColor("Inner Glow Color", Color) = (0,0,0,1)       //内发光的颜色_InnerGlowAccuracy("Inner Glow Accuracy", Int) = 2           //内发光的精度_InnerGlowAlphaSumThreshold("Inner Glow Alpha Sum Threshold", Float) = 0.5       //内发光的透明度和的阈值_InnerGlowLerpRate("Inner Glow Lerp Rate", range(0, 1)) = 0.8           //内发光颜色和原颜色的差值[Toggle(_ShowInnerGlow)] _ShowInnerGlow ("Show Inner Glow", Int) = 0       //开启外轮廓的Toggle}SubShader{Tags{ "RenderType"="Transparent" "Queue"="Transparent" }Blend SrcAlpha OneMinusSrcAlphaPass{Ztest Always Cull Off ZWrite OffCGPROGRAM#pragma vertex vert#pragma fragment frag#pragma shader_feature _ShowOutline#pragma shader_feature _ShowInnerGlow#include "UnityCG.cginc"sampler2D _MainTex;half4 _MainTex_TexelSize;fixed _EdgeAlphaThreshold;fixed4 _EdgeColor;float _EdgeDampRate;float _OriginAlphaThreshold;float _InnerGlowWidth;fixed4 _InnerGlowColor;int _InnerGlowAccuracy;float _InnerGlowAlphaSumThreshold;float _InnerGlowLerpRate;struct v2f{float4 vertex : SV_POSITION;float2 uv[9] : TEXCOORD0;};half CalculateAlphaSumAround(v2f i){half texAlpha;half alphaSum = 0;for(int it = 0; it < 9; it ++){texAlpha = tex2D(_MainTex, i.uv[it]).w;alphaSum += texAlpha;}return alphaSum;}float CalculateCircleSumAlpha(float2 orign, float radiu, int time){//通过精度来划分一个圆,然后通过一个for循环来计算偏移,然后采样机上透明度。//我本来使用的精度的平方,但是这样unityshader会报一个错误,说迭代的次数太长,不能超过1024次,//但是实际上我并没有用那么多次,但是没法解决就手动输入精度float sum = 0;float perAngle = 360 / time;for(int i = 0; i < time; i ++){float2 newUV = orign + radiu * float2(cos(perAngle * i), sin(perAngle * i));sum += tex2D(_MainTex, newUV).a;}return sum;}v2f vert(appdata_img v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);half2 uv = v.texcoord;o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);return o;}fixed4 frag(v2f i) : SV_Target{fixed4 innerGlow = fixed4(0,0,0,0);fixed4 outline = fixed4(0,0,0,0);fixed4 orignColor = tex2D(_MainTex, i.uv[4]);//2D图片外轮廓#if defined(_ShowOutline)half alphaSum = CalculateAlphaSumAround(i);float isNeedShow = alphaSum > _EdgeAlphaThreshold;float damp = saturate((alphaSum - _EdgeAlphaThreshold) * _EdgeDampRate);float isOrigon = orignColor.a > _OriginAlphaThreshold;fixed3 finalColor = lerp(_EdgeColor.rgb, fixed3(0,0,0), isOrigon);float finalAlpha = isNeedShow * damp * (1 - isOrigon);outline = fixed4(finalColor.rgb, finalAlpha);#endif//2D图片的内发光#if defined(_ShowInnerGlow)//计算透明度的和float alphaCircleSum = CalculateCircleSumAlpha(i.uv[4], _InnerGlowWidth, _InnerGlowAccuracy) / _InnerGlowAccuracy;float innerColorAlpha = 0;//这里获取到内发光的的透明度,并且做了渐变,让靠近边界的颜色更亮一些,原理的透明度的会越来越低innerColorAlpha = 1 - saturate(alphaCircleSum - _InnerGlowAlphaSumThreshold) / (1 - _InnerGlowAlphaSumThreshold);//剔除超出原图的像素的颜色。if(orignColor.a <= _OriginAlphaThreshold){innerColorAlpha = 0;}fixed3 innerColor = _InnerGlowColor.rgb * innerColorAlpha;innerGlow = fixed4(innerColor.rgb, innerColorAlpha);//return innerGlow;#endif//将外轮廓和内发光元颜色叠加输出。#if defined(_ShowOutline)float outlineAlphaDiscard = orignColor.a > _OriginAlphaThreshold;orignColor = outlineAlphaDiscard * orignColor;//乘2是为了更加突出外发光return lerp(orignColor ,innerGlow * 2, _InnerGlowLerpRate * innerGlow.a) + outline;#endifreturn lerp(orignColor ,innerGlow * 2, _InnerGlowLerpRate * innerGlow.a);//return tex2D(_MainTex, i.uv[4]) + innerGlow + outline;}ENDCG}}
}

参数设置:

实现效果:

总结一下:

看起来效果至少是我试过的好几种方法中最好的了,但是这个2D描边存在一个缺陷,就是不能服务于2D骨骼动画,因为2D骨骼动画是由一张张的图片构成,所以每张图片都做了描边就会显得非常奇怪。再说内发光,内发光的缺点也很明显,内发光的宽度不能一直放大,所以宽度的增加内发光的效果会直接将整个图片变成内发光的颜色,但是至少目前的参数设置和效果我觉得还是蛮不错的。但是我在实现过程中并没有很好的考虑性能的问题,所以其他大佬看完之后有什么批评建议希望可以告诉。

期待大佬的程序生成行星的系列视频能早点更新,期待与大家分享。如果代码有什么不理解或者不合理的都可以联系我哦~

Unity 2D图片外轮廓描边和内发光的Shader实现相关推荐

  1. Unity 2D图片的3D效果(1)——阴影

    项目为方便编辑地图,使用的是Unity自带的Tilemap功能.因为使用的是俯视视角,拼好第一个地图后发现2D图片展现的效果太平了,地面和障碍物很难分辨.所以才有了"把2d图片展现出3d效果 ...

  2. 【游戏开发创新】Unity 2D图片任意形状破碎碎裂效果,以此纪念我的牙光荣牺牲

    文章目录 一.前言 二.效果演示 三.Demo工程下载 四.操作步骤 1.牙图片:SrpiteRenderer 2.碎裂:Explodable 3.多边形碰撞体组件:PolygonCollider2D ...

  3. 关于unity 2d图片的触发与碰撞

    刚开始 跟本碰不上,不知道为什么,后来 5个unity一起看 ,哈哈哈哈.最后结论: 这个必须是这样的 !!!!!!!!!!!!!!!!!!!!!!!

  4. 【Unity】从零使用Amplify Shader - 超简单2D外轮廓

    前言: 超简单方案又来了,今天我们做的也是一个在游戏中很常见的外轮廓,实际上外轮廓的实现方案有很多种,Shader层面这里推荐冯乐乐女神<Unity Shader入门精要>,里面有比较详细 ...

  5. unity Shader Graph实现2D图片扭曲波纹效果

    先看效果,制作版本:unity2019.4.2 制作2D图片效果,不需要用到光照信息,所以创建ShaderGraph时选用Unlit Graph. 图片一般都会有透明通道,记得修改Unlit Mast ...

  6. Unity 2D Spine 外发光实现思路

    Unity 2D Spine 外发光实现思路 前言 对于3D骨骼,要做外发光可以之间通过向法线方向延申来实现. 但是对于2D骨骼,各顶点的法线没有向3D骨骼那样拥有垂直于面的特性,那我们如何做2D骨骼 ...

  7. Unity 2D教程: 滚动,场景和音效

    http://www.tairan.com/archives/7074 原文地址:http://www.raywenderlich.com/71029/unity-4-3-2d-tutorial-sc ...

  8. Unity 2D光照(2D Light)和阴影(Shadow Caster 2D)

    前言 在上一篇我们简单了了解了Unity 2D动画的实现,在这一篇中,我们来学一下Unity的2D Light,给我们的2D动画添加上光照效果,简单的效果图如下: 首先先分享一个B站上别人翻译了的视频 ...

  9. UNITY 2D入门基础教程 (一)

    如果用以前版本的Unity做2D游戏,虽然能做,但是要费很多周折. 比如你可以将一张纹理赋予一个"面片"网格,然后用脚本控制它的动画调整它的位移.如果你要使用物理引擎,那么还要将这 ...

最新文章

  1. linux命令后面常见的/dev/null 和 21 的含义
  2. boost::core模块default_allocator
  3. [翻译 EF Core in Action 2.1] 设置一个图书销售网站的场景
  4. DataFrame/Series获取列名以及更改列名(转)
  5. PLM中BOM核心技术的研究[转]
  6. 2013腾讯编程马拉松初赛(3月20日)
  7. 电影评论分类:二分类问题
  8. 小白都能看懂的干货!大数据这朵“后浪”,能卷起多大的风浪?
  9. MFC开发IM-第一篇
  10. 优化C/C++代码的小技巧
  11. MatConvnet工具箱文档翻译理解二
  12. 【邮政编码识别】基于matlab灰度二值化邮政编码识别【含Matlab源码 788期】
  13. CAS4 之 集成RESTful API
  14. union并不绝对比or的执行效率高
  15. mysql 在线ddl_MySQL5.7—在线DDL总结
  16. 电子科技大学信息与通信工程学院保研面试题
  17. 【ESP 保姆级教程】 疯狂传感器篇 —— 案例:ESP8266 + MQ3酒精传感器 + webserver(局域网内曲线变化图)+ 自定义微信告警
  18. A40i使用笔记:时区设置
  19. linux pdf翻译
  20. LaTeX排版常用字体和格式设置

热门文章

  1. KDA IBelink K1 Max 如春天的小草,迅速在宇宙大帝萌根发芽
  2. 关于MessageBox与天鹰网络战队XiaoXi系列函数的使用说明
  3. 聚焦数字孪生——盈嘉再次亮相第六届世界互联网大会
  4. value_counts()计数的用法
  5. 昆山培训java_昆山学习java要来这里
  6. 网商银行×OceanBase:首家云上银行的分布式数据库应用实践
  7. Bindiff430,Bindiff5,Bindiff6下载
  8. 日本电影《花样奇缘》:人生是一场疯狂奇妙的梦境
  9. virtualbox虚拟机NAT模式下不能连接外网
  10. impdp 并行_Oracle expdp/impdp常用性能优化方法