Unity 2D Spine 外发光实现思路

news2024/11/26 10:36:20

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); // 中间RenderTexture
    rtTemp.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;

    // 累加alpha
    alphaAdd += 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.coloradmin.cn/o/1451223.html

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

相关文章

蒙特卡罗模拟 python Monte Carlo Simulation

1. 蒙特卡罗模拟 与普通预测模型不同&#xff0c;蒙特卡罗模拟根据估计值范围与一组固定输入值来预测一组结果。换句话说&#xff0c;蒙特卡洛模拟通过利用概率分布&#xff08;例如均匀分布或正态分布&#xff09;&#xff0c;为任何具有固有不确定性的变量构建可能结果的模型…

leetcode hot 100最小花费爬楼梯

本题和之前的爬楼梯类似&#xff0c;但是需要考虑到花费的问题&#xff01;**注意&#xff0c;只有在爬的时候&#xff0c;才花费体力&#xff01;**那么&#xff0c;我们还是按照动态规划的五部曲来思考。 首先我们要确定dp数组的含义&#xff0c;那么就是我们爬到第i层所花费…

基于蓄电池和飞轮混合储能系统的SIMULINK建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 蓄电池储能原理 4.2 飞轮储能原理 4.3 混合储能系统原理 5.完整工程文件 1.课题概述 基于蓄电池和飞轮混合储能系统的SIMULINK建模与仿真。蓄电池和飞轮混合储能&#xff0c;蓄电池可以用SIMULINK…

【C++】类和对象(五)友元、内部类、匿名对象

前言&#xff1a;前面我们说到类和对象是一个十分漫长的荆棘地&#xff0c;今天我们将走到终点&#xff0c;也就是说我们对于&#xff23;算是正式的入门了。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质量&#xff23;学习 &…

C++入门篇(5)——类和对象(2)

目录 1.类的6个默认成员函数 2.构造函数 2.2 概念 2.3 特性 3.析构函数 3.1 概念 3.2 特性 1.类的6个默认成员函数 如果一个类一个成员都没有&#xff0c;那么这个类就是空类。但空类并非什么都没有&#xff0c;编译器会对任何一个类都生成六个默认成员函数。 2.构造…

安装 Windows Server 2003

1.镜像安装 镜像安装:Windows Server 2003 2.安装过程(直接以图的形式呈现) 按Enter(继续),继续后F8继续 直接Enter安装 下一步 秘钥:GM34K-RCRKY-CRY4R-TMCMW-DMDHM 等待安装成功即可

FreeRTOS 延迟中断处理

采用二值信号量同步 二值信号量可以在某个特殊的中断发生时&#xff0c;让任务解除阻塞&#xff0c;相当于让任务与中断 同步。这样就可以让中断事件处理量大的工作在同步任务中完成&#xff0c;中断服务例程(ISR) 中只是快速处理少部份工作。如此&#xff0c;中断处理可以说是…

实现MainActivity转到其他界面的功能实现

#安卓 实现MainActivity转到其他界面的功能实现 实现步骤&#xff1a; 1.添加两个界面及&#xff1b;layout&#xff0c;分别为fullsreen和dialog 2.mainifest中注册两个antivity 3.向Mainactivity中代码。用intent简单的跳转 package com.example.myapplication;import an…

《数电》理论笔记-第3章-常用组合逻辑电路及MSI组合电路模块的应用

一&#xff0c;编码器和译码器 1&#xff0c;编码器 编码:用由0和1组成的代码表示不同的事物。 编码器:实现编码功能的电路&#xff0c; 常见编码器:普通编码器、优先编码器、二进制编码器二-十进制编码器等等 1.1 三位二进制普通编码器和三位二进制优先编码器 1分58秒开始 …

第五节 zookeeper集群与分布式锁_2

1.分布式锁概述 1.1 什么是分布式锁 1&#xff09;要介绍分布式锁&#xff0c;首先要提到与分布式锁相对应的是线程锁。 线程锁&#xff1a;主要用来给方法、代码块加锁。当某个方法或代码使用锁&#xff0c;在同一时刻仅有一个线程执行该方法或该代码段。 线程锁只在同一J…

Swift Combine 级联多个 UI 更新,包括网络请求 从入门到精通十六

Combine 系列 Swift Combine 从入门到精通一Swift Combine 发布者订阅者操作者 从入门到精通二Swift Combine 管道 从入门到精通三Swift Combine 发布者publisher的生命周期 从入门到精通四Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五Swift Com…

【算法设计与分析】搜索旋转排序数组

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;算法分析与设计 ⛺️稳中求进&#xff0c;晒太阳 题目 整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff…

【C++】 为什么多继承子类重写的父类的虚函数地址不同?『 多态调用汇编剖析』

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 前言 本篇文章主要是为了解答有…

MATLAB计算极限和微积分

一.函数与极限 计算极限&#xff1a;lim(3*x^2/(2x1))&#xff0c;x分别趋于0和1&#xff0c;代码如下&#xff1a; syms x; limit(3*x*x/(2*x1),x,0) limit(3*x*x/(2*x1),x,1) 结果分别为0和1&#xff1a; 1.计算双侧极限 计算极限&#xff1a;lim(3*x^2/(2x1))&#xff0…

哈希表哈希桶(C++实现)

哈希的概念 顺序结构以及平衡树中&#xff0c;元素关键码与其存储位置之间没有对应的关系&#xff0c;因此在查找一个元素 时&#xff0c;必须要经过关键码的多次比较。顺序查找时间复杂度为O(N)&#xff0c;平衡树中为树的高度&#xff0c;即 O( l o g 2 N log_2 N log2​N)&…

JavaScript Let 块级作用域

JavaScript Let 学习手记 最近在学习 JavaScript ES6 (2015) 标准时&#xff0c;我发现了let这个关键字&#xff0c;它为声明变量提供了一种新的方式&#xff0c;而且这种方式具有块级作用域的特点&#xff0c;真的很有趣呢&#xff01; 理解块作用域 在 ES6 之前的版本中&a…

【html学习笔记】2.基本元素

1.标题 标题会自动粗体其中大写的内容&#xff0c;并带有换行的效果会使用<h1>到<h6>表示不同大小的标题 <h1>标题1</h1> <h2>标题2</h2> <h3>标题3</h3> <h4>标题4</h4> <h5>标题5</h5> <h6>…

【Web】从零开始的js逆向学习笔记(上)

目录 一、逆向基础 1.1 语法基础 1.2 作用域 1.3 窗口对象属性 1.4 事件 二、浏览器控制台 2.1 Network Network-Headers Network-Header-General Network-Header-Response Headers Network-Header-Request Headers 2.2 Sources 2.3 Application 2.4 Console 三、…

基于3种机器学习法的黄土高原农业干旱监测比较研究_王晓燕_2022

基于3种机器学习法的黄土高原农业干旱监测比较研究_王晓燕_2022 摘要关键词1 引言2 研究区与数据6 结论#pic_center =x260) 摘要 本文集成 MODIS、TRMM、GLDAS 和再分析等多源数据,选取了 13 个与干旱有关的变量,并与基于气象数据的 3 个月时间尺度的标准化降水蒸发指数(SP…

算法沉淀——优先级队列(堆)(leetcode真题剖析)

算法沉淀——优先级队列 01.最后一块石头的重量02.数据流中的第 K 大元素03.前K个高频单词04.数据流的中位数 优先队列&#xff08;Priority Queue&#xff09;是一种抽象数据类型&#xff0c;它类似于队列&#xff08;Queue&#xff09;&#xff0c;但是每个元素都有一个关联的…