本章为大家带来内发光特效。

2d-sprite-glow-inner.gif

一、内发光原理

学习 Shader 过程中,偶然在网上看到一句的内发光原理,十分精辟受用:

采样周边像素alpha取平均值,叠加发光效果

事实上,根据这句精辟的原理,就可以实现内发光了,你也试试吧?

以下为我的实现过程。

二、采样周边像素Alpha取平均值

怎么采集某个点的周边像素呢?这里我们可以用 「按圆采样」 算法

2.1 采样圆边上某点的 Alpha 值

如果我们已知圆的半径 radius ,已经某个角度 angle ,那么这个点的坐标就很好计算了,其上的 Alpha 值就不再话下 :

Step 1

x = radius * cos(angle);

y = radius * sin(angle);

在 Cocos Creator 的 Shader 中,代码如下:

/**

* 获取指定角度方向,距离为xxx的像素的透明度

*

* @param angle 角度 [0.0, 360.0]

* @param dist 距离 [0.0, 1.0]

*

* @return alpha [0.0, 1.0]

*/

float getColorAlpha(float angle, float dist) {

// 角度转弧度,公式为:弧度 = 角度 * (pi / 180)

// float radian = angle * 0.01745329252; // 这个浮点数是 pi / 180

float radian = radians(angle);

vec4 color = getTextureColor(texture, v_uv0 + vec2(dist * cos(radian), dist * sin(radian)));

return color.a;

}

PS:

这里我们用到了 sin 和 cos 函数,函数接受的参数是弧度制,因此我们要实现角度转弧度

// 角度转弧度,公式为:弧度 = 角度 * (pi / 180)

float radian = angle * 0.01745329252; // 这个浮点数是 pi / 180

但实际上,GLSL ES 语言已然存在内置函数 radians(float degree):将角度值转化为弧度值,因此我们就用内置函数即可。

2.2 采样圆边上所有点的 Alpah 平均值

上面我们已经实现了获取圆上某点的颜色 Alpha 值。那么,我们只需要来一个 for 循环,遍历 0 到 360 度,那这个圆上所有点的颜色 Alpha 平均值就很容易算出来了。

但是,这样子可能会有两个问题:

计算量可能会太多,导致我们的性能低下

半径很少的时候,相邻的两个或多个角度的点可能很近,或者甚至重合,此时这两个点的 Alpha 值有可能相差不大,那么此时分别计算这些角度的 Alpha 值,就可能显得有点冗余了,取其一即可

基于以上考虑,最终我采用的是圆采样方式为:

以某个角度作为间隔,遍历由此产生的各个方向的Alpha值,将这些值的和的平均值近似看作这个圆的Alpha值

比如:

假设以 45° 角间隔,那么我只需要计算下图的 [10, 17] 共计 8 个点的 Alpha 平均值,那么这个值我就可以近似看作这个圆上所有点的 Alpha 平均值了

Step2

在 Cocos Creator 的 Shader 中,代码如下:

/**

* 获取指定距离的周边像素的透明度平均值

*

* @param dist 距离 [0.0, 1.0]

*

* @return average alpha [0.0, 1.0]

*/

float getAverageAlpha(float dist) {

float totalAlpha = 0.0;

// 以30度为一个单位,那么「周边一圈」就由0到360度中共计12个点的组成

totalAlpha += getColorAlpha(0.0, dist);

totalAlpha += getColorAlpha(30.0, dist);

totalAlpha += getColorAlpha(60.0, dist);

totalAlpha += getColorAlpha(90.0, dist);

totalAlpha += getColorAlpha(120.0, dist);

totalAlpha += getColorAlpha(150.0, dist);

totalAlpha += getColorAlpha(180.0, dist);

totalAlpha += getColorAlpha(210.0, dist);

totalAlpha += getColorAlpha(240.0, dist);

totalAlpha += getColorAlpha(270.0, dist);

totalAlpha += getColorAlpha(300.0, dist);

totalAlpha += getColorAlpha(330.0, dist);

return totalAlpha * 0.0833; // 1 / 12 = 0.08333

}

2.3 采样点周边像素 Alpha 平均值

上面两个步骤,我们已经实现了 近似采样一个圆上所有点的 Alpha 平均值 。

而如果我们把「周边」这个词语理解为由很多个半径不同的圆组合起来,那么现在我们只需要采样多几个圆,那么就可以实现我们的最终需求了—— 采样周边像素Alpha取平均值 。

Step3

那么,那么我们要采样多少个圆呢?采集少了,效果可能粗糙,采集多了,可能计算量过多导致性能降低

一般而言,这种可变的属性,我们应该交给上层去传入,但是如果上层要用内发光特效,你暴露的一个参数名字叫 采样多少个圆 ,那使用者一般会很茫然。

事实上,更加贴合上层使用者理解的属性名应该为 发光宽度 glowColorSize。

那我们又如何在程序上,在这个发光宽度上,控制采样多少个圆呢?

划分方案有很多种,这里我们采用按照发光宽度,等比划分10个圆,只采样这10个圆。(当然你可以改动这里的划分方案)

在 Cocos Creator 的 Shader 中,代码如下:

/**

* 获取发光的透明度

*/

float getGlowAlpha() {

// 如果发光宽度为0,直接返回0.0透明度,减少计算量

if (glowColorSize == 0.0) {

return 0.0;

}

// 将传入的指定距离,平均分成10圈,求出每一圈的平均透明度,

// 然后求和取平均值,那么就可以得到该点的平均透明度

float totalAlpha = 0.0;

totalAlpha += getAverageAlpha(glowColorSize * 0.1);

totalAlpha += getAverageAlpha(glowColorSize * 0.2);

totalAlpha += getAverageAlpha(glowColorSize * 0.3);

totalAlpha += getAverageAlpha(glowColorSize * 0.4);

totalAlpha += getAverageAlpha(glowColorSize * 0.5);

totalAlpha += getAverageAlpha(glowColorSize * 0.6);

totalAlpha += getAverageAlpha(glowColorSize * 0.7);

totalAlpha += getAverageAlpha(glowColorSize * 0.8);

totalAlpha += getAverageAlpha(glowColorSize * 0.9);

totalAlpha += getAverageAlpha(glowColorSize * 1.0);

return totalAlpha * 0.1;

}

2.4 调试发光

Ok,有了上面的采样手段,现在我们可以来调试了。

首先,那么发光颜色选什么好呢?

交给上层控制吧,我们只需要定义一个 发光颜色 glowColor 即可。

float alpha = getGlowAlpha();

gl_FragColor = glowColor * alpha;

先来个内发红光看下: glowColor = vec4(1.0, 0.0, 0.0, 1.0);

Test 1

可以看到右边的调试结果还是挺符合我们的输出预期,周边点明显是有一个渐变透明过程

但是,此时我们得到的是内部透明度为1,靠近边缘的为接近0的透明度,其他位置为0的透明度。而内发光效果的话,恰恰相反,我们需要的是一个内部透明度为0,靠近内边缘透明度为1的效果。

那么我们尝试反转一下

float alpha = getGlowAlpha();

// 内发光是从边缘发光的,是需要内部透明度为0,靠近边缘的接近1的透明度

// 因此我们需要反转一下透明度

alpha = 1.0 - alpha;

gl_FragColor = glowColor * alpha;

Test 2

现在是反转了,但是图像外边的其他位置却上色了,而在反转之前,图像外边的其他位置是透明的,为了应用这部分过来,在反转之前,我们判断一下,透明度大于某个 阈值 ,我们才反转 alpha 值。

那么这里的 阈值 要怎么定义呢?

为了更加深入理解这个问题,我们先来放大一下 Cocos 的 Logo 上方的那个角,先看清楚一个问题:

glowThreshold

可以看到图像的边缘黑色并不是立即切换到完全透明的,而是一个过渡效果,从黑色开始慢慢变透明直到完全透明,透明从1 -> 0 慢慢过渡。事实上大部分的图像边缘都差不多类似这样子,甚至部分图片的设计,本身就是有一个很长的渐变过渡带。

那么问题来了,针对这种有渐变过渡带的纹理,在我们实现的内发光特效中,我们的发光边缘要怎么定义呢?

从图像边缘最外边的透明度为0.0开始发光?

从图像边缘往内,不透明(即透明度为1.0)的地方开始发光?

从图像 0.0 到 1.0 之间的某个值开始发光?

不好取舍,不同图片可能是需要不同处理,效果才好。

既然如此,我们就可以将这几种定义抽象一下,比如叫 发光阈值 glowThreshold,范围[0.0, 1.0]。我们暴露给上层使用者,交由上层使用者自行根据纹理去控制此值的大小即可。

现在我们的代码就可以修改为这样子了:

float alpha = getGlowAlpha();

if (alpha > glowThreshold) {

// 内发光是从边缘发光的,是需要内部透明度为0,靠近边缘的接近1的透明度

// 因此我们需要反转一下透明度

alpha = 1.0 - alpha;

}

gl_FragColor = glowColor * alpha;

在 glowThreshold 为 0.2 时,效果如下:

Test 3

OK,看上去差不多的样子了,现在我们试着简单手动混合一下,看起来内发光效果就有了

Test 4

???

好像还并不是内发光的效果,看上去上方尖角的光源有点扩边了?这是那里出问题了呢?

因为我们是要做内发光,所以如果点本来是透明的或者小于我们设立的阈值,那么其实这个点是没必要进行采样周边Alpha平均值的,否则就会有上面这种 扩边 的问题,那么我们在取发光透明度的时候,在判断一下即可

/**

* 获取发光的透明度

*/

float getGlowAlpha() {

// 如果发光宽度为0,直接返回0.0透明度,减少计算量

if (glowColorSize == 0.0) {

return 0.0;

}

// 因为我们是要做内发光,所以如果点本来是透明的或者接近透明的

// 那么就意味着这个点是图像外的透明点或者图像内透明点(如空洞)之类的

// 内发光的话,这些透明点我们不用处理,让它保持原样,否则就是会有内描边或者一点扩边的效果

// 同时也是提前直接结束,减少计算量

vec4 srcColor = getTextureColor(texture, v_uv0);

if (srcColor.a <= glowThreshold) {

return srcColor.a;

}

// 将传入的指定距离,平均分成10圈,求出每一圈的平均透明度,

// 然后求和取平均值,那么就可以得到该点的平均透明度

float totalAlpha = 0.0;

totalAlpha += getAverageAlpha(glowColorSize * 0.1);

totalAlpha += getAverageAlpha(glowColorSize * 0.2);

totalAlpha += getAverageAlpha(glowColorSize * 0.3);

totalAlpha += getAverageAlpha(glowColorSize * 0.4);

totalAlpha += getAverageAlpha(glowColorSize * 0.5);

totalAlpha += getAverageAlpha(glowColorSize * 0.6);

totalAlpha += getAverageAlpha(glowColorSize * 0.7);

totalAlpha += getAverageAlpha(glowColorSize * 0.8);

totalAlpha += getAverageAlpha(glowColorSize * 0.9);

totalAlpha += getAverageAlpha(glowColorSize * 1.0);

return totalAlpha * 0.1;

}

现在看下来效果差不多了,是内发光了!

Test 5

但是好像发光强度不够得样子?没关系,我们给它加点料,来个一元四次方程加强,让靠近边缘的地方更加亮

flavour

对应代码如下:

float alpha = getGlowAlpha();

if (alpha > glowThreshold) {

// 内发光是从边缘发光的,是需要内部透明度为0,靠近边缘的接近1的透明度

// 因此我们需要反转一下透明度

alpha = 1.0 - alpha;

// 给点调料,让靠近边缘的更加亮

alpha = -1.0 * (alpha - 1.0) * (alpha - 1.0) * (alpha - 1.0) * (alpha - 1.0) + 1.0;

}

gl_FragColor = glowColor * alpha;

现在大概效果已经出来了:

Glow Inner

三、混合颜色

在上面动图中,实际上为了演示,我是有两个 Sprite, 一个用内置材质,一个用在调试中的内发光材质,通过手动移动的方式,我们已经大概看到将内发光叠加到原图上方,看起来就是内发光特效了。

Test 5

那么这一步,我们要怎么实现一步到位,直接就将内发光叠加在原图上,形成最终效果。

实际上,这也叫 混合模式 ,混合模式主要解决的是两种颜色之间,该如何混合,比如叠加、覆盖等等。

混合模式在我们平时开发中也是经常在使用着的,比如,Sprite 组件:

Blend

理解不同的组合,对于我们实现不同混合效果,是基础中的基础。

关于这部分,官方在 UI渲染批次合并指南的 Blend 模式章节 一文中有说到,觉得纯文字比较难以理解的,可以参考网上 2dx 关于混合模式的相关文章。

回归我们的主题,要实现在原图上叠加我们的内发光特效,那么

// 源颜色就是内发光颜色

vec4 color_dest = o;

// 目标颜色就是图案颜色色

vec4 color_src = glowColor * alpha;

// 按照官方的混合颜色介绍和规则

//

// 要在图案上方,叠加一个内发光,将两者颜色混合起来,那么最终选择的混合模式如下:

//

// (内发光)color_src: GL_SRC_ALPHA

// (原图像)color_dest: GL_ONE

//

// 即最终颜色如下:

// color_src * GL_SRC_ALPHA + color_dest * GL_ONE

gl_FragColor = color_src * color_src.a + color_dest;

混合后的最终效果:

Glow Inner

四、编辑器 texture 函数问题

在对比 浏览器 和 Cocos Creator 编辑器 的预览结果的后,你可能会发现编辑器的发光效果,相比起浏览器的没有那么好,比如编辑器左右两边的发光很窄。

texture function problem

这是因为

在 Cocos Creator 2.2.1 的编辑器中,超出边界的uv并不是返回 vec4(0.0, 0.0, 0.0, 0.0),实际返回为

超出左边界的uv,返回 v_uv0.x = 0 的颜色

超出右边界的uv,返回 v_uv0.x = 1 的颜色

超出上边界的uv,返回 v_uv0.y = 1 的颜色

超出下边界的uv,返回 v_uv0.y = 0 的颜色

而这样子的处理,会导致我们获取图像边缘位置的周边像素的的 alpha 值有可能偏低。

比如:在我们这个例子上,以图像中间左边缘为例,采样周边平均 Alpha 的时候,因为超出图像边界的都是 1.0 ,因此这个图像左边缘的 平均 Alpha 就是1.0,相当于没有内发光了,光不起来,同理图像其他边缘也是。

要修复这个问题其实也很简单,我们只需要封装一层获取 uv 像素的函数

vec4 getTextureColor(sampler2D texture, vec2 v_uv0) {

if (v_uv0.x > 1.0 || v_uv0.x < 0.0 || v_uv0.y > 1.0 || v_uv0.y < 0.0) {

return vec4(0.0, 0.0, 0.0, 0.0);

}

return texture(texture, v_uv0);

}

然后将原来所有的 texture() 函数的地方直接替换为 getTextureColor() 即可

PS:上面用到的静图、动图都是修复后的效果图

五、总结

5.1 采样算法

在实现 采样周边像素Alpha取平均值 的时候,我们采用了 「按圆采样」 算法去进行采样,实际上,这里有很多种采样方式,比如: 矩形偏移采样

矩形偏移采样:

取右、右上、上、左上、左、左下、下、右下共计8个方向的点作为周边

按照上一步的定义去扩大「周边」,从而实现收集

大概步骤如下图:

Total

不过,你也可以看到,这种方案的收集方式存在一个问题:

随着收集距离的扩大,会出现越来越多的点不会收集到,因为收集方向就只有8个,方向夹角之间的点是收集不了的(比如 23 -> 24, 33 -> 34 之间的点)

那是不是这个方案就不好呢,其实也不是,这个方案的最大优点是减少了很多 sin , cos 的计算,因为就收集的8个方向,而这8个方向恰好只需要加法和减法就可以的出来了,因此性能上会更好,对于部分图片,如果发光宽度很短,那么此采集方案可能更优。

那么,简单总结下现在讨论的两种「周边采样算法」的优劣:

采样算法

优点

缺点

适用场合

按圆采样

覆盖面相对较全,效果相对细腻

计算量相对偏多

绝大部分场合

矩形偏移采样

覆盖面相对少,效果相对粗糙,且由于方向固定,可能存在特殊情况下,效果不理想

计算量相对较少

发光宽度较少,比较少大转折弯的纹理

当然,还有其他很多采样算法,如果你有想法,不妨自己动手试下吧,试完之后记得分析下优劣和使用场合,这会让你有更多收获。

5.2 关于发光强度

为了实现边缘更加光亮,我直接写死了一个 一元四次方程,实际上这可能不好控制。另外一些好的公式可以使用 二次贝塞尔 或者 三次贝塞尔 可以很方便操作控制点,从而实现不同曲度。

5.3 其他

当然,在操作一遍下来后,说不准你也觉得这种实现不好,xxx地方有哪些地方可以优化,如果有更好的方案,我们不妨留言交流一下吧。

OK,本章完,完整代码在我的 Github 仓库 或 Gitee 仓库 中可以找到。

cocos2dx 字体外发光_Cocos Creator Shader Effect 系列 - 6 - 内发光特效相关推荐

  1. cocos中如何让背景模糊_Cocos Creator Shader Effect 系列 - 8 - 高斯模糊

    本章为大家带来高斯模糊的实现 2d-sprite-gaussian-blur-v1 首先,来点简单的,比如:高斯模糊的英文名叫 Gaussian Blur. 关于高斯模糊的原理,在我学习过程中,下面两 ...

  2. cocos2dx 字体外发光_《Cocos2d-x游戏开发实战精解》学习笔记2--在Cocos2d-x中显示一行文字...

    在Cocos2d-x中要显示文字就需要用到Label控件.在3.x版本的Cocos2d中,舍弃了之前版本所使用的LabelTTF.LabelAtlas.LabelBMFont 3个用于显示文字的类,而 ...

  3. cocos2dx 字体外发光_Ps教程:只需4个图层!即可制作出超炫酷的荧光字体

    前段时间的霓虹灯火爆一时,频繁出现在各大商场里,成为众多游客拍照打卡的网红灯.相信有去过商场的朋友们肯定都有见过这些霓虹灯,反正小编是去打卡过了,拍起照来简直不要太好看,又酷又炫,小编可真是太喜欢了. ...

  4. cocos2dx 字体外发光_cocos2d-x位图字体生成工具bmfont使用图文教程 美术字使用

    bmfont工具1.14 官方最新版 类型:编程辅助大小:358KB语言:英文 评分:10.0 标签: 立即下载 在看别人的代码的时候,有时候会发现.fnt文件,这个文件是如何产生的呢,其实是使用位图 ...

  5. cocos2dx 字体外发光_在电致发光研发领域,选择有机材料是基于哪些原因?

    阅读本文前,请您先点击上面的蓝色字体,再点击"关注",这样您就可以免费收到最新内容了.每天都有分享,完全是免费订阅,请放心关注. 声明:本文转载自网络,如有侵权,请在后台留言联系我 ...

  6. cocos2dx 字体外发光_亚克力发光字制作流程有哪些,你知道吗?遵义制作厂家

    一.材料: 1.亚克力字面板 2.亚克力字边条 3.支撑块(5mm透明亚克力) 4.瞬间502胶水 5.透明建筑结构胶及胶枪 6.建筑防水密封膏及胶枪 7.厚4mmPVC底板 8.LED发光模组 9. ...

  7. cocos2dx 字体外发光_cocos2dx 3.2--裁剪节点ClippingNode

    学习cocos2dx 3.2确实比较吃力,因为网上关于最新版的v3.2的资料十分稀少,或者是讲解的确实不是很详细.大部分人都是根据官方文档照样画瓢,而对于有些比较抽象的概念及函数都是照着官方文档来讲解 ...

  8. cocos creator shader实现汽车氮气加速特效

    1:材质和shader Shader 是一种給GPU执行的代码,GPU的渲染流水线,为了方便开发人员定制效果,开放出接口給程序员编写代码来控制,这种程序叫作shader, shader开发语言,coc ...

  9. creator shader:从零开始,用shader画个彩虹

    creator shader:从零开始,用shader画个彩虹 从创建shader和材质开始 分别创建名为 rainbow的effect和material,创建一个场景,新建一张Sprite精灵,使用 ...

最新文章

  1. [动态dp]线段树维护转移矩阵
  2. android摄像头代码,Android摄像头
  3. intellij 常用设置
  4. 出现“ORA-28000:the account is locked”的解决办法
  5. 转载:IBM红米连接wifi的方法
  6. vim中taglist无法显示问题
  7. 什么是bcd码数据传输通讯_传输障碍| 数据通讯
  8. 【Level 08】U07 Mixed Feelings L1 Day trip
  9. Python面向对象之结构与成员
  10. matlab imdilate
  11. 惠普服务器ssa找不到控制卡,DL380 Gen10服务器Vmware ESXi 6.0 系统SSACLI工具
  12. XAMPP汉化教程指南
  13. Copy On Write(写时复制)
  14. 医院叫号系统与his系统对接(二)
  15. 大麦网抢票程序(一)之大麦网网站分析
  16. 东大22春实用写作X《实用写作》在线平时作业1百分非答案
  17. 11.1.4 子线程与主线程通信实例
  18. 2020年阿里云服务器租用价格表(实时更新)
  19. F - Fairy, the treacherous mailman
  20. 【论文笔记】Camera Style Adaption for Person Re-identification

热门文章

  1. 广东人被冻哭?羽绒服热潮来袭 论国民品牌是如何成功破圈的呢?
  2. 南京邮电大学C语言实验报告三
  3. 网页版音乐播放器 下载音乐 教程
  4. 换手率是否需要中性化?
  5. 用原生安卓 做一个“套壳”APP、混合开发、安卓H5加壳开发
  6. 王者服务器维护公告2月,王者天下2月17日服务器调整公告
  7. instanceof用法
  8. Mybatis源码学习(三)SqlSession详解
  9. 2016双十二淘宝推出“二次元日”
  10. TCP、RPC与HTTP到底是何方神圣?!