Unity 2D Spine 外发光实现思路

Unity 2D Spine 外发光实现思路


前言

对于3D骨骼,要做外发光可以之间通过向法线方向延申来实现。

但是对于2D骨骼,各顶点的法线没有向3D骨骼那样拥有垂直于面的特性,那我们如何做2D骨骼的外发光效果呢?

理论基础

我们要知道,要实现外发光效果,首先得先实现外描边效果。对于2D图片的描边实现有很多种方案。

内描边:

思路:对于任意像素,如果其四周存在透明像素,则说明是边缘。

简单实现的效果如下图:

在这里插入图片描述

这样的边缘会非常锯齿化,因为这样做非常绝对地判断了是或不是边缘来进行上色。

如果我们不那么绝对,采取以下这种策略来进行上色:

对于任意像素,其四周的像素alpha值之积越小,则说明越靠近边缘。根据计算出的积,来使原像素颜色和边缘颜色做个线性插值(Lerp函数),以作为最后的输出颜色。

简单实现的效果如下图:

在这里插入图片描述

这样的边缘会比上面的更加柔和。

可以看得出来,这样的策略会占用图片的非透明像素,也就是人们所说的内描边。

外描边:

思路:对于透明像素,如果四周存在不透明像素,则说明是边缘。

在这里插入图片描述

和内描边一样,如果采用非常绝对的边缘判断方式,那么绘制出来的边缘就会非常锯齿化。

这里我们可以采用另一种思路:对于透明像素,如果四周像素的alpha之和越小,则说明离边缘越远。最终的边缘像素的alpha为周围像素alpha的平均值。

简单实现的效果如下图:

在这里插入图片描述

这样绘制出来的边缘,离原图像内容越远,越透明。有了alpha的渐变,也就有了初步的外发光效果了。

可以看得出来,这样的策略不会占用图片的非透明像素,也就是人们所说的外描边。但是却会受到图片本身绘制区域大小的影响。

图像膨胀和腐蚀:

实际上,上面的外描边和内描边的思想,就是图像的膨胀和腐蚀。

外描边说高深了,就是图像膨胀;内描边说高深了,就是图像腐蚀。

这里做个简单的科普介绍,感兴趣的小伙伴自行深入研究。

膨胀算法:

所谓膨胀算法,即使用一个n*n的矩阵去扫描图像中的每一个像素。用矩阵每一个值与其覆盖的周围一圈像素值做“与”操作,只要有任意1,那么该像素值为1。(“与”操作中都是1才是1)

膨胀之后,图像边界会向外扩大。

例子:

原图像:

00000
00000
00100
00000
00000

膨胀算子:

010
111
010

最终结果:

00000
00100
01110
00100
00000

腐蚀算法:

所谓腐蚀算法,即使用一个n*n的矩阵去扫描图像中的每一个像素。矩阵每一个值与其覆盖的周围一圈像素值做“或”操作,只要有任意0,那么该像素值为0。(“或”操作中都是0才是0)

腐蚀之后,图像边界会向内收缩。

腐蚀算子例子:

101
000
101
卷积:

具体定义请参考百度百科 - 卷积,这里做个简单的科普介绍,感兴趣的小伙伴自行深入研究。

简单来说就是分别乘加,最终输出各乘积之和。

实际上,上面说到的对周围像素的alpha求和取平均和后面会说到的模糊效果,说高深了都是卷积的思想。图像领域常用的边缘检测方式还包括利用Sobel算子对图像进行卷积。

在这里插入图片描述

上图就是4x4的矩阵应用3x3的卷积核,在步长为1的情况下,不做边缘扩展策略,最终输出为2x2的矩阵。具体计算原理及过程过程可参考Convolutional Neural Networks - Basics · Machine Learning Notebook。

遇到问题

有了上述的理论基础之后,我们再来看如何实现2D骨骼外发光,以及实现过程中需要注意和会面临的问题。

  1. 2D骨骼是由多张图片组成的,这意味着每张图片骨骼在渲染流程中会分别进行绘制,并且每张图片都存在绘制区域的限制。
  2. 要达到美术的发光效果,不仅要有描边,还要有光晕效果。
  3. 对每个像素进行操作,需要时刻考虑计算量,性能和美术效果会存在制衡。

初步方案

一开始打算在Shader直接实现外发光效果。

对于上述问题1,分别绘制的图片骨骼来说,我们可以采用多个Pass来避免对每个图片骨骼都进行了描边。

但是受困于每张图片骨骼存在绘制区域限制,导致最终效果光晕无法延展过长,不然会出现被图片大小截断的现象。

于是,为了扩展绘制区域,解决该问题,我们尝试使用后处理。

中间方案

既然采用图像后处理,那么肯定就需要先获得渲染出来的图像,之后再对图像逐像素进行先前的策略。

一开始想到的是用相机单独渲染目标,然后获取其渲染的RenderTexture,对它进行逐像素处理。

这里没有使用Shader,而是直接在C#中读取像素,并修改颜色。关键代码如下:

private Sprite ProcessTexture()
{tempColors = tempTexture.GetPixels(); // 读取像素,这一步操作非常耗时,尽可能减少像素数量for (i=0;i<textureSize;i++) {if (tempColors[i].a <= ALPHA_LIMIT){showColors[i] = new Color(0.95f, 1f, 0.17f, GetAlpha(tempColors, i));}else{showColors[i] = tempColors[i];}}tempTexture.SetPixels(showColors); // 设置像素颜色tempTexture.Apply();return Sprite.Create(tempTexture, rect, new Vector2(0.5f, 0.5f));
}private float GetAlpha(Color[] colors, int index)
{alpha = 0;num = 0;for (p = -LENGTH; p <= LENGTH; p++) // 步长过长计算量也会非常大,非常耗时,但是效果会更好{for (q = -LENGTH; q <= LENGTH; q++){thisIndex = index + p + (int)rect.width * q;if (thisIndex >= 0 && thisIndex < textureSize){alpha += colors[thisIndex].a;num++;}}}alpha /= num;return alpha;
}

这种方式是使用Texture2D的接口来进行像素遍历,虽然能实现想要的效果,但是如果要每帧都渲染的话,计算会非常非常非常耗!最终简单实现效果图如下:

在这里插入图片描述

进阶方案

后来我发现可以使用Shader做一个后处理,把相机渲染出来的图像再经过这个后处理的Shader渲染一次,把结果绘制在最终的屏幕上。

使用了Shader之后,考虑到更好的光晕效果,我们可以很轻易地利用多个Pass对外描边做一个Bloom处理。

Bloom的原理是什么?

本质上就是渲染两张图。首先,我们在第一张图里像平常一样正常地渲染场景。然后,把明亮的区域渲染到第二张图里。在这之后我们把第二张图模糊,并且加到第一张图上,就得到了最终的结果。

具体实现思路,就是先绘制出外描边部分,然后对外描边部分做一个模糊效果,这样就得到了一个Bloom图。把这个Bloom图和原图进行叠加,得到最后的效果图。

下面是后处理C#部分的关键代码:

void OnRenderImage(RenderTexture source, RenderTexture dest)
{RenderTexture rtTemp = RenderTexture.GetTemporary(1000, 1000, 0); // 中间RenderTexturertTemp.filterMode = FilterMode.Bilinear;Graphics.Blit(source, rtTemp, bloomMaterial, 0); // 第一个Pass,绘制外描边bloomMaterial.SetTexture("_BloomTex", rtTemp); // 把渲染出的外描边传到第二个Pass中Graphics.Blit(source, dest, bloomMaterial, 1); // 第二个Pass,Bloom效果以及最终成像RenderTexture.ReleaseTemporary(rtTemp);img.texture = dest;
}

后处理Shader第一个Pass的片段着色器:

fixed4 frag(OutoutVertex i) : COLOR
{fixed4 col = tex2D(_MainTex, i.uv);float alphaAdd = 0;// 采样周围8个点float2 up_uv = i.uv + float2(0, 1) * _lineWidth * _MainTex_TexelSize.xy;float2 down_uv = i.uv + float2(0, -1) * _lineWidth * _MainTex_TexelSize.xy;float2 left_uv = i.uv + float2(-1, 0) * _lineWidth * _MainTex_TexelSize.xy;float2 right_uv = i.uv + float2(1, 0) * _lineWidth * _MainTex_TexelSize.xy;float2 upleft_uv = i.uv + float2(-1, 1) * _lineWidth * _MainTex_TexelSize.xy;float2 upright_uv = i.uv + float2(1, 1) * _lineWidth * _MainTex_TexelSize.xy;float2 downleft_uv = i.uv + float2(-1, -1) * _lineWidth * _MainTex_TexelSize.xy;float2 downright_uv = i.uv + float2(1, -1) * _lineWidth * _MainTex_TexelSize.xy;// 累加alphaalphaAdd += tex2D(_MainTex, up_uv).a + tex2D(_MainTex, down_uv).a + tex2D(_MainTex, left_uv).a + tex2D(_MainTex, right_uv).a+ tex2D(_MainTex, upleft_uv).a + tex2D(_MainTex, upright_uv).a + tex2D(_MainTex, downleft_uv).a + tex2D(_MainTex, downright_uv).a+ col.a;if (alphaAdd > 0 && col.a <= _alphaThreshold) // 只要周围存在非透明像素,且自身透明度小于阈值{col.rgb = _lineColor;col.a = 1;}else{col = float4(0, 0, 0 ,0); // 这个Pass只需要获得描边}return col;
}

第一个Pass的作用是为了绘制描边,渲染出来的图如下:

后处理Shader第二个Pass的片段着色器:

fixed4 frag(OutoutVertex i) : COLOR
{fixed4 col = tex2D(_MainTex, i.uv);fixed4 bloomCol = tex2D(_BloomTex, i.uv); // 上一个Pass传入的_BloomTex// 采样周围8个点float2 up_uv = i.uv + float2(0, 1) * _bloomWidth * _MainTex_TexelSize.xy;float2 down_uv = i.uv + float2(0, -1) * _bloomWidth * _MainTex_TexelSize.xy;float2 left_uv = i.uv + float2(-1, 0) * _bloomWidth * _MainTex_TexelSize.xy;float2 right_uv = i.uv + float2(1, 0) * _bloomWidth * _MainTex_TexelSize.xy;float2 upleft_uv = i.uv + float2(-1, 1) * _bloomWidth * _MainTex_TexelSize.xy;float2 upright_uv = i.uv + float2(1, 1) * _bloomWidth * _MainTex_TexelSize.xy;float2 downleft_uv = i.uv + float2(-1, -1) * _bloomWidth * _MainTex_TexelSize.xy;float2 downright_uv = i.uv + float2(1, -1) * _bloomWidth * _MainTex_TexelSize.xy;fixed4 color = tex2D(_BloomTex, up_uv) + tex2D(_BloomTex, down_uv) + tex2D(_BloomTex, left_uv) + tex2D(_BloomTex, right_uv) +tex2D(_BloomTex, upleft_uv) + tex2D(_BloomTex, upright_uv) + tex2D(_BloomTex, downleft_uv) + tex2D(_BloomTex, downright_uv) +bloomCol;color /= 9; // 均值模糊return bloomCol + color + col * _weight; // 模糊结果与原像素一定比例求和
}

第二个Pass对之前获得的描边做了一次简单的模糊,之后再与原图像颜色进行叠加,就实现了一个简单的描边Bloom效果。渲染出来的图如下:

在这里插入图片描述

颜色叠加之后,最终效果如下图:

在这里插入图片描述
在这里插入图片描述

这种策略在开销相对较小的情况下实现了较好的效果,整体性价比比较高。如果想要更宽的光晕效果,而且还要做到合理不穿帮,可以增加计算量或者优化策略。

小结

总的来说,上面提到的几种方式,并不是一套完完整整的项目代码,只是一系列解决问题的思路和策略,相当于是抛砖引玉,一旦带入到项目中就需要具体问题具体分析了。当然,肯定还有各种各样的优化方法,以及一些更好计算方法。但无论采用何种策略,最终都会是性能和美术效果的平衡。

参考

Convolutional Neural Networks - Basics · Machine Learning Notebook

百度百科 - 卷积

【Unity学习心得】Sprite外发光的制作

Unity实现bloom效果

Created a Spine Edge Shader

https://developer.unity.cn/projects/cel-shading-trick

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/685946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

书生·浦语-模型评测opencompass

大预言模型评测 模型评测包括主管评测与客观评测 测试模型对提示词的敏感性&#xff0c;或通过提示词获得更准确地答案 主流评测框架 opencompass评测平台 作业

智慧公厕:让智慧城市的公共厕所焕发“智慧活力”

智慧城市的建设已经进入了一个新的阶段&#xff0c;不仅仅是智慧交通、智慧环保&#xff0c;如今甚至连公厕都开始迎来智慧化时代。智慧公厕作为智慧城市的神经末梢&#xff0c;正在通过信息化、数字化和智慧化的方式&#xff0c;实现全方位的精细化管理。本文以智慧公厕源头专…

Flink理论—Flink架构设计

Flink架构设计 Flink 是一个分布式系统&#xff0c;需要有效分配和管理计算资源才能执行流应用程序。它集成了所有常见的集群资源管理器&#xff0c;例如Hadoop YARN&#xff0c;但也可以设置作为独立集群甚至库运行,例如Spark 的 Standalone Mode 本节概述了 Flink 架构&…

IBM公司已经创立百年

不知道有谁还知道或记得蓝快。 我在2003年前后因为编程需要买的IBM笔记本电脑&#xff0c;慢慢接触到了IBM蓝快的技术服务人员&#xff0c;他们算是那个年代超级专业性的代名词。因为装Linux&#xff0c;当时流行的系统是Redhat8&#xff0c;Mandrake,slackware,因为Lilio2的引…

初识最短路径

一.最短路径的介绍 最短路径是图论和网络分析中一个重要的概念&#xff0c;它指的是在一个图或网络中连接两个节点或顶点的路径中&#xff0c;具有最小权重总和的路径。这个权重可以表示为路径上边或弧的长度、耗费、时间等&#xff0c;具体取决于问题的背景和应用场景。 如果你…

【STM32 CubeMX】SPI层次结构SPI协议与SPI控制器结构

文章目录 前言一、SPI 程序层次1.1 硬件原理图1.2 硬件框图1.3 软件层次 二、SPI协议2.1 硬件连线2.2 如何访问SPI设备2.3 SPI 框图 总结 前言 随着嵌入式系统的迅猛发展&#xff0c;STM32系列微控制器在各种应用中得到广泛应用。在嵌入式系统设计中&#xff0c;串行外设接口&…

Anaconda、conda、pip、virtualenv的区别

① Anaconda Anaconda是一个包含180的科学包及其依赖项的发行版本。其包含的科学包包括&#xff1a;conda, numpy, scipy, ipython notebook等。 Anaconda具有如下特点&#xff1a; ▪ 开源 ▪ 安装过程简单 ▪ 高性能使用Python和R语言 ▪ 免费的社区支持 其特点的实现…

牛客网SQL进阶123:高难度试卷的得分的截断平均值

官网链接&#xff1a; SQL类别高难度试卷得分的截断平均值_牛客题霸_牛客网牛客的运营同学想要查看大家在SQL类别中高难度试卷的得分情况。 请你帮她从exam_。题目来自【牛客题霸】https://www.nowcoder.com/practice/a690f76a718242fd80757115d305be45?tpId240&tqId2180…

matplotlib从起点出发(13)_Tutorial_13_Autoscaling

0 自动放缩 轴上的限制可以手动设置&#xff08;例如ax.set_xlim(xmin, xmax))&#xff0c;或者Matplotlib可以根据Axes上已有的数据自动设置它们。此种放缩行为有许多选项&#xff0c;如下所述。 我们将从一个简单的折线图开始&#xff0c;显示自动缩放将轴限制扩展到数据的…

08MARL深度强化学习——模型种类

文章目录 前言1、训练与执行的模型2、Centralised Training and Execution2.1 Example——central learning2.2 局限性 3、Decentralised Training and Execution3.1 Example——independent learning3.2局限性 4、Centralised Training with Decentralised Execution4.1 Examp…

vue实现多个下拉框联动(二)

在Vue3的组件中&#xff0c;定义多个下拉框的数据和选中的值。例如&#xff1a; data() {return {firstSelectValue: ,secondSelectValue: ,thirdSelectValue: ,// 其他下拉框的数据和选中的值} }在模板中&#xff0c;使用v-model指令绑定下拉框的选中值&#xff0c;并使用cha…

精读《Function Component 入门》

1. 引言 如果你在使用 React 16&#xff0c;可以尝试 Function Component 风格&#xff0c;享受更大的灵活性。但在尝试之前&#xff0c;最好先阅读本文&#xff0c;对 Function Component 的思维模式有一个初步认识&#xff0c;防止因思维模式不同步造成的困扰。 2. 精读 什…

P1090 [NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G题解

题目 在一个果园里&#xff0c;多多已经将所有的果子打了下来&#xff0c;而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。 每一次合并&#xff0c;多多可以把两堆果子合并到一起&#xff0c;消耗的体力等于两堆果子的重量之和。可以看出&#xff0c;所…

如何将浏览器设置为黑色

步骤 1 设置边框颜色&#xff1a; 找到浏览器的 设置 选项在里面选择浏览器的 外观 选项把 主题 相关的改成 黑色 即可 步骤 2 设置页面背景颜色&#xff1a; 在浏览器搜索栏中输入指令&#xff0c;并确认 浏览器类型指令edgeedge://flags/chromechrome://flags/ 在出现…

绿色化 数据库 MongoDB 和 mysql 安装

绿色化 数据库 MongoDB 和 mysql 安装 【1.1】 前言 为什么要绿色化 安装呢&#xff1f;因为系统老升级&#xff0c;老重装&#xff01;&#xff01;也方便了解下数据库配置和库在那 绿色软件喜欢一般放在 D盘tools目录里 D:\tools\ 数据库 MongoDB D:\tools\MongoDB 数…

Mysql第二关之存储引擎

简介 所有关于Mysql数据库优化的介绍仿佛都有存储引擎的身影。本文介绍Mysql常用的有MyISAM存储引擎和Innodb存储引擎&#xff0c;还有常见的索引。 Mysql有两种常见的存储引擎&#xff0c;MyISAM和Innodb&#xff0c;它们各有优劣&#xff0c;经过多次优化和迭代&#xff0c;…

代码随想录算法训练营第十九天|654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树

654.最大二叉树 刷题https://leetcode.cn/problems/maximum-binary-tree/description/文章讲解https://programmercarl.com/0654.%E6%9C%80%E5%A4%A7%E4%BA%8C%E5%8F%89%E6%A0%91.html视频讲解https://www.bilibili.com/video/BV1MG411G7ox/?vd_sourceaf4853e80f89e28094a5fe…

洛谷题解 - B2015 计算并联电阻的阻值

目录 题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 代码 题目描述 对于阻值为 r 1 r_1 r1​ 和 r 2 r_2 r2​ 的电阻&#xff0c;其并联电阻阻值公式计算如下&#xff1a; R 1 1 r 1 1 r 2 R\dfrac{1}{\dfrac{1}{r_1}\dfrac{1}{r_2}} Rr1​1​r2​1​1​ 输入…

软件工程师,OpenAI Sora驾到,快来围观

概述 近期&#xff0c;OpenAI在其官方网站上公布了Sora文生视频模型的详细信息&#xff0c;展示了其令人印象深刻的能力&#xff0c;包括根据文本输入快速生成长达一分钟的高清视频。Sora的强大之处在于其能够根据文本描述&#xff0c;生成长达60秒的视频&#xff0c;其中包含&…

【Spring1】Spring容器的本质就是两个HashMap

做为Java程序员,我们就是Spring的用户,Spring的影子在我们的代码里几乎是无处不在,那Spring到底是什么,带给我们什么,如何工作的呢?这个简单问题貌似又不太好讲。例如如果要问一下什么是Spring框架, 可以找到大量的解答,基本内容如下: Spring是一个开源的应用程序框架…