[add] init
BIN
doc/Bullet_Tracking/resources/demo.gif
Normal file
After Width: | Height: | Size: 305 KiB |
BIN
doc/Bullet_Tracking/resources/detail.png
Normal file
After Width: | Height: | Size: 316 KiB |
BIN
doc/Bullet_Tracking/resources/preview_ewm.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
46
doc/Bullet_Tracking/子弹跟踪效果.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 效果演示
|
||||
|
||||
打击的目标一直在移动,但是子弹却像长了眼睛一样在后面尾随,直到精准击中目标。这种“长了眼睛的子弹”,是打击类游戏中比较经典的武器之一。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
首先要排除掉用的动画来实现,因为我们的目标在实时地移动,所以子弹的终点位置是不确定的,同时在移动的时候,我们的子弹的方向也需要进行实时的调整,整个过程中起点和终点不确定,运动过程也不确定,因此整个过程不能用一个动画过程去模拟。
|
||||
|
||||
但是我们可以在`update`的时候去实时改变子弹的方向和处理速度变化。在每帧的回调中,我们可以拿到当前目标的位置和子弹的位置,然后我们通过向量减法,算出子弹到目标的归一化向量,然后再给子弹做位置的叠加和角度的叠加。
|
||||
|
||||

|
||||
|
||||
这里面涉及到两个向量处理,一个是向量减法,如上图所示,它的意义应该不难理解,就是表达了子弹到目标的这个过程。还有一个处理叫做`归一化向量`,简单点说就是单位长度为1的向量,因此也把它叫单位向量或者标准向量,对于大部分向量而言,我们只关心向量的方向,而不在意向量的长度,这种情况下就适合用单位向量来表示,比如光线的入射方向、反射的方向等,当然还有我们这个例子中用到的“跟踪子弹”,要实现这个功能,我们要做的就是让子弹能够朝目标的方向去运动,而运动的步长就是我们自己自定义的速度,实现代码如下:
|
||||
|
||||
```js
|
||||
bulletSpeed = 200;
|
||||
update(dt) {
|
||||
if (!this.fireFlag) return;
|
||||
|
||||
let targetPos: cc.Vec2 = this.target.getPosition();
|
||||
let bulletPos: cc.Vec2 = this.bullet.getPosition();
|
||||
let normalizeVec: cc.Vec2 = targetPos.subtract(bulletPos).normalize();
|
||||
|
||||
this.bullet.x += normalizeVec.x * this.bulletSpeed * dt;
|
||||
this.bullet.y += normalizeVec.y * this.bulletSpeed * dt;
|
||||
// 角度变化以y轴正方向为起点,逆时针角度递增
|
||||
this.bullet.angle = cc.v2(0, 1).signAngle(normalizeVec) * 180 / Math.PI;
|
||||
|
||||
let rect = this.target.getBoundingBox();
|
||||
if (rect.contains(bulletPos)) this.hitTheTarget();
|
||||
}
|
||||
```
|
||||
|
||||
因为节点的角度是以y轴正方向为起点,逆时针递增,所以在计算子弹的角度的时候,可以使用`cc.v2(0, 1).signAngle(normalizeVec)`算出当前向量和y轴夹角的弧度,然后再转化成角度赋值。
|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Circle_avatar/resources/circle.png
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
doc/Circle_avatar/resources/demo.png
Normal file
After Width: | Height: | Size: 116 KiB |
BIN
doc/Circle_avatar/resources/ellipse.png
Normal file
After Width: | Height: | Size: 158 KiB |
BIN
doc/Circle_avatar/resources/preview_ewm.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
72
doc/Circle_avatar/shader圆形头像.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 效果演示
|
||||
|
||||
圆形头像在creator中没有提供,但是这个又是个比较高频的使用功能。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
可以使用一张圆的图片,然后配合mask的反向遮罩来实现,但是这种实现的效果会有锯齿,所以一般会写一个shader。异名上篇文章中追光效果中那个shader刚好直接就可以使用了,这系列的定位是常用功能集锦,圆形头像又是高频应用,因此异名就再单独拿出来再水一篇,方面后面查看使用。
|
||||
|
||||
光圈是一个圆,假设圆心在纹理的中间,它的坐标是`vec2(0.5,0.5)`,我们只需让到圆心的距离大于半径的像素丢弃或者透明度为0,代码如下:
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 color = vec4(1, 1, 1, 1);
|
||||
color *= texture(texture, v_uv0);
|
||||
color *= v_color;
|
||||
|
||||
color.a = step(length(v_uv0 - vec2(0.5,0.5)), 0.1);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
其中`step` 是内置的规整函数 `step(a, x) = x >= a? 1 : 0`,`length`是取模。上面的代码段应用在可以在正方形的纹理中可以得出一个正圆,但是如果纹理不是正方形,上面出来的效果会是一个椭圆,因为在shader无论纹理的真实宽高是多少,它的`x,y`变化范围都是0~1,是比例的变化。如果需要产生一个正圆,还是得通过获取纹理的真实宽高,来计算真实的宽高比例,异名选择的方式是在在组件初始化的时候,输入一个`wh_ratio`比例来获取,圆的真实半径通过勾股定理来计算,异名这里就没有开方了,直接通过半径平方的比较来舍去圆外的点。
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 o = vec4(1, 1, 1, 1);
|
||||
o *= texture(texture, v_uv0);
|
||||
o *= v_color;
|
||||
|
||||
float circle = radius * radius;
|
||||
float rx = center.x * wh_ratio;
|
||||
float ry = center.y;
|
||||
float dis = (v_uv0.x * wh_ratio - rx) * (v_uv0.x * wh_ratio - rx) + (v_uv0.y - ry) * (v_uv0.y - ry);
|
||||
|
||||
o.a = step(dis, 0.1);
|
||||
gl_FragColor = o;
|
||||
}
|
||||
```
|
||||
|
||||
这样子就能在一个不同宽高比的纹理中都能够画出一个正圆。
|
||||
|
||||

|
||||
|
||||
但是这样的圆的边缘是有锯齿的,所以我们需要借助另外一个内置插值函数`smoothstep(min, max, x)`,它能够返回一个在输入值之间平稳变化的插值,以此来达到边缘羽化的效果。
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 o = vec4(1, 1, 1, 1);
|
||||
o *= texture(texture, v_uv0);
|
||||
o *= v_color;
|
||||
|
||||
float circle = radius * radius;
|
||||
float rx = center.x * wh_ratio;
|
||||
float ry = center.y;
|
||||
float dis = (v_uv0.x * wh_ratio - rx) * (v_uv0.x * wh_ratio - rx) + (v_uv0.y - ry) * (v_uv0.y - ry);
|
||||
|
||||
o.a = smoothstep(circle, circle - blur, dis);
|
||||
gl_FragColor = o;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
BIN
doc/Coin_fly_to_wallet/resourse/fly_gold.gif
Normal file
After Width: | Height: | Size: 366 KiB |
BIN
doc/Coin_fly_to_wallet/resourse/fly_gold_circle_0.png
Normal file
After Width: | Height: | Size: 172 KiB |
BIN
doc/Coin_fly_to_wallet/resourse/fly_gold_circle_1.png
Normal file
After Width: | Height: | Size: 94 KiB |
BIN
doc/Coin_fly_to_wallet/resourse/fly_gold_circle_2.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
doc/Coin_fly_to_wallet/resourse/preview_ewm.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
68
doc/Coin_fly_to_wallet/金币落袋效果.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# 效果演示
|
||||
金币从初始点散开然后逐个飞落到指定的位置,这是游戏里面很常用的一个动画,效果如下
|
||||

|
||||
|
||||
# 实现思路
|
||||
要实现这个效果,我们已知三个条件,分别是起点位置,终点位置,金币个数。
|
||||
|
||||
## 生成金币位置
|
||||
金币散开的位置看起来很随机,但是经过我们的拆解,其实它的第一步是先生成一个标准的圆。假设我们现在是8个金币,我们知道起点的坐标,如何求这8个金币的散开位置呢,这其实是一个数学问题。
|
||||
`8`个金币平分一个圆,每个金币夹角是`360度 / 8 = 45度`,假设圆的半径`r`是确定的,我们又知道圆心的坐标,结合三角函数我们就能够很轻易算出每个金币的位置。
|
||||

|
||||
|
||||
当我们拥有每一个金币的标准位置之后,再给它们每个的位置叠加一个随机偏移,这样子他们的位置看起来就是围绕着起点做随机分布
|
||||
|
||||

|
||||
|
||||
以上代码如下
|
||||
```js
|
||||
/**
|
||||
* 以某点为圆心,生成圆周上等分点的坐标
|
||||
*
|
||||
* @param {number} r 半径
|
||||
* @param {cc.Vec2} pos 圆心坐标
|
||||
* @param {number} count 等分点数量
|
||||
* @param {number} [randomScope=80] 等分点的随机波动范围
|
||||
* @returns {cc.Vec2[]} 返回等分点坐标
|
||||
*/
|
||||
getCirclePoints(r: number, pos: cc.Vec2, count: number, randomScope: number = 60): cc.Vec2[] {
|
||||
let points = [];
|
||||
let radians = (Math.PI / 180) * Math.round(360 / count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
let x = pos.x + r * Math.sin(radians * i);
|
||||
let y = pos.y + r * Math.cos(radians * i);
|
||||
points.unshift(cc.v3(x + Math.random() * randomScope, y + Math.random() * randomScope, 0));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 金币落袋
|
||||
拆分效果可以看到,金币落袋的过程中还有先后的顺序,我们在实现的时候先计算一下每个金币到袋子的距离,然后做个排序,让距离袋子近的金币先执行进袋的动画,远的后执行。
|
||||

|
||||
代码的实现如下:
|
||||
```js
|
||||
goldNodeList.sort(node => node.dis);
|
||||
|
||||
goldNodeList.forEach((node, idx) => {
|
||||
node.gold.runAction(cc.sequence(
|
||||
cc.moveTo(0.3, node.startPoint),
|
||||
cc.delayTime(idx * 0.03),
|
||||
cc.moveTo(0.6, node.endPoint),
|
||||
cc.callFunc(() => {
|
||||
this.goldPool.put(node.gold);
|
||||
})
|
||||
))
|
||||
});
|
||||
```
|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Dissolve_color/resources/demo.gif
Normal file
After Width: | Height: | Size: 289 KiB |
BIN
doc/Dissolve_color/resources/ewm.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
58
doc/Dissolve_color/溶解效果.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 效果演示
|
||||
|
||||
物体的淡入淡出是游戏当中很常见的一种状态切换效果,但是有时候我们希望`fade`切换的时候,物体能够能更有色彩层次感或者其他一些特殊的中间状态,这个时候就得自己去写着色器,这种区别于单纯的淡入和淡出的效果可以形象地叫做溶解。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
溶解效果的思路很简单,先获取到当前贴图的色彩,然后定义一个对照维度,大部分情况下就取rgb的某一色彩通道就可以了,比如异名demo中的色调偏蓝色调,异名的对比值就取rgb中的blue,然后动态去改变该维度的参考值,当纹理贴图的色块blue值小于该参考值的时候就去色。当参考值变为0的时候,我们的色彩也就完全溶解掉了。`effect`代码如下:
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 color = vec4(1, 1, 1, 1);
|
||||
|
||||
#if USE_TEXTURE
|
||||
color *= texture(texture, v_uv0);
|
||||
#if CC_USE_ALPHA_ATLAS_TEXTURE
|
||||
color.a *= texture2D(texture, v_uv0 + vec2(0, 0.5)).r;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// 当颜色小于溶解的程度,则直接抛弃
|
||||
if(color.b < fade_pct) discard;
|
||||
gl_FragColor = color;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果我们希望溶解更加有层次变化,我们还可以对溶解的边缘做一些处理,比如透明度改变,色彩改变等,这就需要代入我们自己的使用场景中,根据实际的需要去调整,比如异名的demo中,希望溶解的边缘有一些蓝色调的变化:
|
||||
|
||||
```js
|
||||
if(color.b < fade_pct + 0.1) {
|
||||
color = vec4(0.92, 0.8, 0.95, color.a);
|
||||
}
|
||||
```
|
||||
|
||||
然后我们需要在`update`的时候去动态更新并设置`fade_pct`,以达到纹理的溶解效果动态变化:
|
||||
|
||||
```js
|
||||
update(dt) {
|
||||
this.fadePct += dt * this.speed;
|
||||
this.material.setProperty('fade_pct', this.fadePct));
|
||||
}
|
||||
```
|
||||
|
||||
以上就是溶解效果的核心思路,完整的代码请通过源码查看。通过demo我们可以看出,不同的贴图的溶解效果和我们的溶解条件是直接关联的,左侧的死神色彩单一,蓝色通道较窄,它的变化过程就比较集中而且快,同时用蓝色调处理溶解的边缘效果的过渡也比较融洽。右边的骑士,色彩比较丰富,用单一的蓝色通道当成溶解条件和过渡就相对来说比较生硬,我们需要视项目的实际情况动态调整。
|
||||
|
||||
# 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
52
doc/Essays/4M_limit.md
Normal file
@@ -0,0 +1,52 @@
|
||||
微信小游戏平台上对首包的的限制是4M,超出限制之后可以采取什么样的措施呢?异名做了一下盘算,大概可以有以下操作
|
||||
|
||||
# 挤牙膏式瘦身
|
||||
如果我们能够在各种常规的瘦身手段下就可以把超出的容量压缩回到4M以内,那当然是最棒的。这一块能做的无非就是引擎的瘦身和资源的瘦身,而资源无外乎是图片,音频,字体等。
|
||||
## 引擎瘦身
|
||||
首先是引擎代码本身,在开发阶段我们的引擎默认是所有模块都包含进去的,但是在打包阶段,有一些无用的代码模块我们就可以在`项目-项目设置-模块设置`中剔除掉,比如一个普通的2D游戏,可能就没有使用到3D模块、物理模块、`EditBox`等等,我们最好根据实际的项目的需要勾选自己需要打包的模块,根据异名自己的经验,大部分情况下做了引擎瘦身和没做引擎瘦身的前后做对比,瘦身之后可能至少会帮你省掉你几百k的大小。其中有些功能模块,它的名字不是很直观,你可能不知道自己有没有用到,那就不要勾选。经常会有新手遇到,为什么打包前,在预览模式下项目跑得好好的,但是打包之后项目功能就不正常了,出现这种情况其实很大概率就是你的模块漏勾了,回去校对一下重新打包就可以了。
|
||||
|
||||
## 资源瘦身
|
||||
资源无外乎是图片,音频,字体。如果是寻常的web项目我们其实有很常用的几个构建工具,像`gulp/grunt/webpack`等等,引用相关的压缩库,然后执行构建命令就可以了,但是我个人觉得还是没有这种必要引入构建工具,因为代码压缩和名字hash引擎已经自身支持了,那音频和字体其实在我们使用之前只要使用工具一次性压缩就可以了,而图片则因为数量太多以及会涉及到自动合图,所以需要在构建之后重新压缩一遍,但是我个人会比较倾向于依赖引擎自身提供的插件机制,毕竟使用了构建工具之后起码还得去`npm install`一下,还要去设置每个包自身的一些配置,然后项目build完之后还得去敲个命令,整个一套走下来,其实效率还是不够,而且针对图片压缩,社区内其实已经有能够直接就开箱即用的相关插件,像`pngquant`我就用挺顺手的,而且在它的基础上也可以添加一些自己的发布流程进去,比如我就把文件夹改名放到里面,后续的项目直接拷贝过去使用就可以了。
|
||||
|
||||

|
||||
|
||||
音频这块我建议还是使用第三方的工具吧,我自己习惯使用`ffmpeg`。如果有用到其他字体的话,一款普通的中文字体大大几十M,但是我们使用字可能就是那么几个,所以字体提取也很有必要,其实社区内有收费的插件,但是目前中文字体提取库无非就是`Fontmin`或者字蛛,它们都可以可以通过终端命令或者客户端和web端去提取所需的字体,异名以前也写过一款小工具,大家需要的话可以去下载来用,地址放在原文链接。
|
||||
|
||||

|
||||
|
||||
# 资源远程加载
|
||||
既然本地放不下了,那就把资源放在远端吧。这块引擎的支持也很好,在打包构建的时候填写远程服务器地址,然后把打包后的res目录存放到服务器下,再删除本地的res文件夹就可以了,运行的时候如果在本地没有这个资源就会去远端获取。但是呢,异名在权衡之后,并不会选择这个方案,首先第一个问题是资源在远端,加载会有网络延迟,这个时候场景是黑屏的,解决这个问题可以做一个简单的初始场景,初始场景的资源还是保留在本地,然后在初始场景预加载真实的游戏场景,等到加载完了之后跳转过去
|
||||
|
||||
```js
|
||||
cc.director.preloadScene("Game", (completedCount, totalCount) => {
|
||||
// 在这里处理加载进度
|
||||
console.log(completedCount, totalCount);
|
||||
}, (error, asset) => {
|
||||
if (error) {
|
||||
cc.error(error);
|
||||
return;
|
||||
}
|
||||
cc.director.loadScene("Game")
|
||||
});
|
||||
```
|
||||
但是这里的骚操作就是你得在res文件夹里面挑选你的首屏资源,面对一堆嵌套的文件和无规则的文件夹名称,那是多大的效率浪费呀,为了提高效率,异名看到社区内看到有人专门写个`Python`脚本(怎么感觉发力点发错了呢...),当把资源挑选完毕之后,还得把cdn上的res文件夹删掉,然后重传,那这个发版过程也未免太过于太琐碎了。还有就是网络请求多了,万一遇到个网络不好报个`timeout`呀,或者资源更新但是你的cdn节点还没同步过来,然报个`notFound`呀什么的,那也得做个异常处理是吧。
|
||||
|
||||

|
||||
|
||||
除了上面说的,异名觉得最关键的还有费用问题,带宽都是钱啊,明明微信总包大小有8M,有多少小游戏经过合理的瘦身之后总包大小还能超过8M呢?而且微信针对网络资源的还有一套自己的缓存管理机制,几乎每次打开都会重新去拉取,cdn的流量就这么被挥霍掉了。
|
||||
|
||||

|
||||
|
||||
当然每种手段都有每种手段的应用场景,异名在社区内还看到有同学利用微信的文件系统api去拉取zip资源,然后通过unzip命令去解压的,如果你的游戏资源确实很多很大,那也难以避免的需要使用远程资源,具体场景还是得合理分析才行。
|
||||
|
||||
### 分包
|
||||
引擎对分包的支持真的非常好啦,对应的文件夹中打个勾就行了。异名的做法其实是多加了一个loading场景,然后把主场景的资源都放在分包里面,在loading场景中通过`loadSubpackage`监听下载分包,下载完成后再跳转主场景就可以了。和资源远程加载相比,整个配置步骤清晰明了,发版过程简单流畅,而且省了你的cdn流量,同时微信自身还会对代码包进行主动缓存,一次下载之后就会缓存下来方便下次使用。异名把两者一对比,觉得是分包那是真的香啊~
|
||||
|
||||
有几个小点还是需要注意一下:
|
||||
|
||||
- 老版本兼容:由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。对于老客户端,会去下载整包代码启动
|
||||
- 2.1.0 以下版本基础库不存在`wx.loadSubpackage`方法,需要通过require来加载(可以在后台屏蔽以下的用户)
|
||||
- 微信 6.6.7 以下客户端开发版/体验版因历史兼容问题无法打开分包小游戏,正式包可以
|
||||
|
||||
## 总结
|
||||
说这么多,总结下来就是,如果你的代码包超过4M,请先进行各种压缩,如果确实已经是极限压缩了,那使用分包会比远程资源相对来说成本更低也更合理。另外异名并不建议子域使用cocos构建,因为构建之后又多了一份引擎代码,首包确实很难控制下来,好的选择是使用canvas的api去绘制或者使用一个简单的模板渲染引擎,当然,这就是另外一个话题了
|
BIN
doc/Filter/resources/demo.png
Normal file
After Width: | Height: | Size: 157 KiB |
BIN
doc/Filter/resources/ewm.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
96
doc/Filter/常见滤镜.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# 效果演示
|
||||
|
||||
我们手机上有很多照片处理软件,图片滤镜是里面不可或缺的一部分,我们可以先尝试一些很简单的滤镜的算法,管中窥豹地去认识一下色彩的处理。
|
||||
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
先来看一下比较常用的褐色、老照片效果,它的算法是:
|
||||
> r = r * 0.393 + g * 0.769 + b * 0.189;
|
||||
> g = r * 0.349 + g * 0.686 + b * 0.168;
|
||||
> b = r * 0.272 + g * 0.534 + b * 0.131;
|
||||
|
||||
```js
|
||||
void main () {
|
||||
vec4 color = texture(texture, v_uv0);
|
||||
float _r = color.r * 0.393 + color.g * 0.769 + color.b * 0.189;
|
||||
float _g = color.r * 0.349 + color.g * 0.686 + color.b * 0.168;
|
||||
float _b = color.r * 0.272 + color.g * 0.534 + color.b * 0.131;
|
||||
color = vec4(_r, _g, _b, color.a);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
灰度或者去色的核心是让RGB三种色值相等即可得到不同的灰度,根据需求的不同,我们可以通过取三个色值的平均值,三个色值的最大值,最小值,加权平均值等方式来处理
|
||||
|
||||
```js
|
||||
void main () {
|
||||
vec4 color = texture(texture, v_uv0);
|
||||
float gray = (color.r + color.g + color.b)/3.0;
|
||||
color = vec4(gray, gray, gray, color.a);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
反相的算法是让RGB三种颜色分别取255的差值
|
||||
|
||||
```js
|
||||
void main () {
|
||||
vec4 color = texture(texture, v_uv0);
|
||||
float r = (255.0 - color.r * 256.0) / 256.0;
|
||||
float g = (255.0 - color.g * 256.0) / 256.0;
|
||||
float b = (255.0 - color.b * 256.0) / 256.0;
|
||||
color = vec4(r, g, b, color.a);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
让图像呈现淡蓝色,也可以形象叫做冰冻,它的算法是
|
||||
|
||||
```js
|
||||
void main () {
|
||||
vec4 color = texture(texture, v_uv0);
|
||||
float _r = (255.0 - color.r * 256.0) / 256.0;
|
||||
float _g = (255.0 - color.g * 256.0) / 256.0;
|
||||
float _b = (255.0 - color.b * 256.0) / 256.0;
|
||||
color = vec4(_r, _g, _b, color.a);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
连环画滤镜是:
|
||||
> R = |g – b + g + r| * r;
|
||||
> G = |b – g + b + r| * r;
|
||||
> B = |b – g + b + r| * g;
|
||||
|
||||
```js
|
||||
void main () {
|
||||
vec4 color = texture(texture, v_uv0);
|
||||
float _r = abs(color.g - color.b + color.g + color.r) * color.r;
|
||||
float _g = abs(color.b - color.g + color.b + color.r) * color.r;
|
||||
float _b = abs(color.b - color.g + color.b + color.r) * color.g;
|
||||
color = vec4(_r, _g, _b, color.a);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
滤镜是用来实现图片的各种特殊效果的,简单的颜色滤镜我们就通过简单的颜色叠加公式可以得出,但是复杂的滤镜效果就可能需要使用更高阶的数学处理甚至叠加多次处理才能得到,我们在图像处理APP里面看到的各种各样的滤镜其实就是人家专门针对某种效果提炼出来的公式,有兴趣的同学用前端的知识也能够打造一个专门的图像处理App
|
||||
|
||||
# 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Follow_spot/resources/circle.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
doc/Follow_spot/resources/cover.png
Normal file
After Width: | Height: | Size: 208 KiB |
BIN
doc/Follow_spot/resources/demo.gif
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
doc/Follow_spot/resources/ellipse.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
doc/Follow_spot/resources/feather.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
doc/Follow_spot/resources/mystical.gif
Normal file
After Width: | Height: | Size: 650 KiB |
BIN
doc/Follow_spot/resources/preview_ewm.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
95
doc/Follow_spot/追光效果.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 效果演示
|
||||
|
||||
追光效果是在舞台全场黑暗的情况下用光柱突出角色或其他特殊物体,通过人为操控光源跟随人物移动,主要用来突出角色主体以及主体和环境的关系。在游戏中可以用来突出氛围以及聚焦玩家视线焦点,不仅可以用来营造沉浸式氛围,也可以用在解谜或者找物品等类别的游戏中。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
根据实际效果可以提炼出3个功能点:光圈的形状和大小可控,光圈的边缘虚化,光圈可操控移动。
|
||||
|
||||
光圈是一个圆,假设圆心在纹理的中间,它的坐标是`vec2(0.5,0.5)`,我们只需让到圆心的距离大于半径的像素丢弃或者透明度为0,代码如下:
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 color = vec4(1, 1, 1, 1);
|
||||
color *= texture(texture, v_uv0);
|
||||
color *= v_color;
|
||||
|
||||
color.a = step(length(v_uv0 - vec2(0.5,0.5)), 0.1);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
其中`step` 是内置的规整函数 `step(a, x) = x >= a? 1 : 0`,`length`是取模。上面的代码段应用在可以在正方形的纹理中可以得出一个正圆,但是如果纹理不是正方形,上面出来的效果会是一个椭圆,因为在shader无论纹理的真实宽高是多少,它的`x,y`变化范围都是0~1,是比例的变化。如果需要产生一个正圆,还是得通过获取纹理的真实宽高,来计算真实的宽高比例,异名选择的方式是在在组件初始化的时候,输入一个`wh_ratio`比例来获取,圆的真实半径通过勾股定理来计算,异名这里就没有开方了,直接通过半径平方的比较来舍去圆外的点。
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 o = vec4(1, 1, 1, 1);
|
||||
o *= texture(texture, v_uv0);
|
||||
o *= v_color;
|
||||
|
||||
float circle = radius * radius;
|
||||
float rx = center.x * wh_ratio;
|
||||
float ry = center.y;
|
||||
float dis = (v_uv0.x * wh_ratio - rx) * (v_uv0.x * wh_ratio - rx) + (v_uv0.y - ry) * (v_uv0.y - ry);
|
||||
|
||||
o.a = step(dis, 0.1);
|
||||
gl_FragColor = o;
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
onload() {
|
||||
this.material.setProperty('wh_ratio', this.bg.width / this.bg.height);
|
||||
}
|
||||
```
|
||||
|
||||
这样子就能在一个不同宽高比的纹理中都能够画出一个正圆。
|
||||
|
||||

|
||||
|
||||
这样的圆的边缘是有锯齿的,而且追光需要光圈的边缘虚化,所以我们需要借助另外一个内置插值函数`smoothstep(min, max, x)`,它能够返回一个在输入值之间平稳变化的插值,以此来达到边缘羽化的效果。
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 o = vec4(1, 1, 1, 1);
|
||||
o *= texture(texture, v_uv0);
|
||||
o *= v_color;
|
||||
|
||||
float circle = radius * radius;
|
||||
float rx = center.x * wh_ratio;
|
||||
float ry = center.y;
|
||||
float dis = (v_uv0.x * wh_ratio - rx) * (v_uv0.x * wh_ratio - rx) + (v_uv0.y - ry) * (v_uv0.y - ry);
|
||||
|
||||
o.a = smoothstep(circle, circle - blur, dis);
|
||||
gl_FragColor = o;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
接下来的让光圈随着动作的移动就很简单了,在`touch`的时候去更改光圈的圆心位置就行,因为我们的shader中是比例的变化,所以我们传进去的时候也要转化成比例,同时别忘了坐标的转化:
|
||||
|
||||
```js
|
||||
touchEvent(evt: cc.Event.EventTouch) {
|
||||
let pos = evt.getLocation();
|
||||
this.material.setProperty('center', [pos.x / this.bg.width, (this.bg.height - pos.y) / this.bg.height]);
|
||||
}
|
||||
```
|
||||
|
||||
这样子我们就把追光的功能实现了,剩下的就是根据业务的需要,生成追光的路径,这个就是把圆心的位置传进来即可。除了应用到舞台追光的那种场景中,异名觉得它的应用还可以有更多的想象空间,比如在黑暗的博物馆里,在手电筒的灯光照射下,蒙娜丽莎的微笑就更加神秘了....
|
||||
|
||||

|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Infinite_bg_scroll/resourse/Infinite_bg_scroll.gif
Normal file
After Width: | Height: | Size: 375 KiB |
BIN
doc/Infinite_bg_scroll/resourse/detail.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
doc/Infinite_bg_scroll/resourse/overflow.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
doc/Infinite_bg_scroll/resourse/preview_ewm.png
Normal file
After Width: | Height: | Size: 17 KiB |
56
doc/Infinite_bg_scroll/背景无限滚动.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 效果演示
|
||||
|
||||
这是游戏里面很常用的一个功能模块,它就像你的生活,有着走不完的路程。它的实现也很简单,要么做一个很长的背景图,然后移动相机;要么就是实现一个跑马灯,像那些轮播图什么的,大家应该都有写过。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
## 背景无缝切换实现
|
||||
|
||||
这个demo的实现思路是跑马灯。背景是两张一样的背景图,第一张图在前,第二张图在后,两张图同时移动,当第一张图正好移动到出屏幕的时候,第二张图正好移进屏幕,这个时候复原两张图的初始位置。这个过程不停循环就有走不完的路程了。
|
||||
|
||||

|
||||
|
||||
代码如下
|
||||
```js
|
||||
speed: number = 500;
|
||||
update(dt) {
|
||||
const temp = dt * this.speed;
|
||||
if (this.bg2.x - temp <= 0) {
|
||||
this.bg1.x = this.bg2.x;
|
||||
this.bg2.x = this.bg1.x + this.bg1.width;
|
||||
}
|
||||
|
||||
this.bg1.x -= temp;
|
||||
this.bg2.x -= temp;
|
||||
}
|
||||
```
|
||||
|
||||
## 全屏适配
|
||||
|
||||
因为是整个背景都在跑动,所以两个背景节点都是需要做`widget`拉伸的,第一张背景上下左右都设置为0就可以了,第二种背景往右再偏移一个屏幕。但是这样是不够的,因为不是所有用户的屏幕都是按我们的设计分辨率来的,所以跑着跑着可能你的背景就露馅了
|
||||
|
||||

|
||||
|
||||
所以我们在onload的时候还是得手动去对齐一下bg2的位置,因为它在布局编辑器里面只是往右移动了我们的设计分辨率一样宽的距离。(bg1肯定是对的,因为使用了widget对齐了当前屏幕)
|
||||
|
||||
```js
|
||||
onLoad() {
|
||||
const viewSize = cc.view.getVisibleSize();
|
||||
this.bg2.getComponent(cc.Widget).left = viewSize.width
|
||||
this.bg2.getComponent(cc.Widget).right = -viewSize.width
|
||||
}
|
||||
```
|
||||
|
||||
# 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Joystick/resourse/Joystick.gif
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
doc/Joystick/resourse/calc_point_pos.png
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
doc/Joystick/resourse/handle_point.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
doc/Joystick/resourse/preview_ewm.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
64
doc/Joystick/遥控杆.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 效果演示
|
||||
|
||||
这是游戏里面很常用的一个功能模块,通过操控遥控杆控制物体的移动
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
遥控杆的构造分为中间的控制点和外层的圆形,首先给遥控杆绑定个触控事件,然后在`touch_move`的时候让触控杆保持在圆形中,同时把鼠标的位置偏移信息传给需要移动的物体。
|
||||
|
||||
## 控制杆逻辑
|
||||
|
||||
用户点击的时候分两种情况,一种是用户的点击位置能够让控制点完全落在圆形内,这个时候让控制点直接移动到用户点击的位置即可;另外一种是用户的点击位置会让控制点会落在圆形外,那就要做一个计算,让控制点的位置维持在圆形内。
|
||||
|
||||

|
||||
|
||||
这个计算也是很简单,因为我们知道外面圆形的半径,触控点的半径,那它们只要相减就能得出触控点距离圆心最远的距离R。当超过这个距离的时候我们已知斜边是R,同时可以得出用户点击点的向量夹角,那就可以通过三角函数轻易算出控制点的位置。
|
||||
|
||||

|
||||
|
||||
这个逻辑在`TOUCH_START`和`TOUCH_MOVE`中都需要,以下代码是实现:
|
||||
```js
|
||||
touchStartEvent(event) {
|
||||
let touchPos = this.node.convertToNodeSpaceAR(event.getLocation());
|
||||
const distance = touchPos.len();
|
||||
const radius = this.node.width / 2 - this.controlDot.width / 2;
|
||||
|
||||
// 以x轴正方向为基准,计算偏移量
|
||||
this.radian = cc.v2(1, 0).signAngle(touchPos);
|
||||
const offsetX = Math.cos(this.radian) * radius;
|
||||
const offsetY = Math.sin(this.radian) * radius;
|
||||
this.controlDot.setPosition(radius > distance ? touchPos : cc.v2(offsetX, offsetY));
|
||||
|
||||
this.movableFlag = true;
|
||||
}
|
||||
```
|
||||
|
||||
## 物体移动
|
||||
|
||||
在用户点击的时候我们已经拿到了用户拖动的方向`radian`,有了方向再加上物体移动的步长,这个其实也就是物体移动的速度,那就可以算出物体在x方向和y方向的移动增量,直接相加即可。
|
||||
我们可以加一个标志位`movableFlag`,当用户操控控制杆的时候就把这个标志位置为`true`,然后在`update`中根据这个标志位使物体进行位置偏移。
|
||||
|
||||
```js
|
||||
speed: number = 150;
|
||||
update(dt) {
|
||||
if (!this.movableFlag) return;
|
||||
this.movableStar.x += Math.cos(this.radian) * dt * this.speed;
|
||||
this.movableStar.y += Math.sin(this.radian) * dt * this.speed;
|
||||
}
|
||||
```
|
||||
|
||||
大多数情况下还需要添加限制条件,让物体的移动不能超出画布,那这就可以通过获取当前屏幕的宽高,然后在计算位置偏移的时候多加一个判断,超出画布就不增加偏移,不超出画布就正常偏移就可以了
|
||||
|
||||
# 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Magnifying_mirror/resources/demo.gif
Normal file
After Width: | Height: | Size: 248 KiB |
BIN
doc/Magnifying_mirror/resources/preview_ewm.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
57
doc/Magnifying_mirror/局部放大效果.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# 效果演示
|
||||
|
||||
对画面的某个位置进行放大和缩小,是某些类型游戏里面必不可少的功能,比如常见的地图缩放,局部细节放大等等。它核心是对相机应用,异名基于此实现一个放大镜的demo
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
首先要理解相机,和现实的相机一样,相机是用来拍摄画面的,然后生成照片也就是`Texture`,但是引擎内的相机相当于人眼睛,它每一帧都在拍摄画面,可能你没用留意到,我们初始化的每个场景的canvas下都默认有一个`Main Camera`,它就是用来拍摄整个场景的内容,然后投影到画布上的。异名翻了一下以前自己接触`Camera`这个概念的时候的笔记,在这里贴一下:
|
||||
|
||||
> 该选择相机那种模式?
|
||||
|
||||
根据`camera`的成像原理,一共有两种模式的相机。一种是透视投影,它是锥形的成像模式,是从某个投射中心将物体投射到单一投影面上所得到的透视图,成像效果远小近大,与人们观看物体时所产生的视觉效果非常接近,在做3D场景的时候,就必须用该模式来拍摄3D的画面。另外一种是正交投影模式,也叫"平行投影",就是把图像通过平行光线投影到画面上,可以理解成一张平面上的画,它就是用来拍摄2D画面的。
|
||||
|
||||
> 如果场景内有多个相机,可是屏幕只有一个,这时候这些相机是如何利用屏幕的?
|
||||
|
||||
引擎输出画面的时候会把一个—个的相机的图像叠加起来;
|
||||
|
||||
> 多个相机的时候,如何确定那个相机先,哪个相机后呢?
|
||||
|
||||
相机有一个`depth`属性, `depth`小的先绘制到屏幕, `depth`大的后绘制到屏幕。
|
||||
|
||||
> 有多个相机,但是物体是唯一的;多个相机的话,一个物体会被绘制多次?
|
||||
|
||||
- 可以通过合理分组给每一个相机选取拍摄具体要拍摄的物体
|
||||
- 相机会绘制属于自己渲染分组里面的物体
|
||||
|
||||
> 如何清除屏幕?
|
||||
|
||||
- 相机有—个`clearFlag`如果你设置了,它在绘制画面的时候,会清理屏幕
|
||||
- 一般只给前面的相机设置`clearFlag`;后面相机都不能再设置,不然会把前面相机绘制的内容清除掉
|
||||
|
||||
把思路拉回到放大镜demo中,要实现放大镜,其实就是多创造一个相机去拍摄需要放大的物体,然后调整相机的缩放比例,使投影的物体放大或者缩小对应的倍数,然后配合使用`Mask`组件去裁剪我们需要的局部位置,mask的编辑器设置请参考项目源码,相机初始化代码实现如下:
|
||||
|
||||
```js
|
||||
initCamera() {
|
||||
let visibleRect = cc.view.getVisibleSize();
|
||||
|
||||
let texture = new cc.RenderTexture();
|
||||
texture.initWithSize(visibleRect.width, visibleRect.height);
|
||||
let spriteFrame = new cc.SpriteFrame();
|
||||
spriteFrame.setTexture(texture);
|
||||
this.mirrorCameraNode.getComponent(cc.Camera).targetTexture = texture;
|
||||
|
||||
this.tempCameraSpriteNode.getComponent(cc.Sprite).spriteFrame = spriteFrame;
|
||||
this.tempCameraSpriteNode.scaleY = -1;
|
||||
}
|
||||
```
|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Metaball/resources/demo.gif
Normal file
After Width: | Height: | Size: 277 KiB |
BIN
doc/Metaball/resources/preview_ewm.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
40
doc/Metaball/元球效果.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 效果演示
|
||||
|
||||
元球也叫融球,它能够让两个球体产生“黏糊糊”的效果,是流体,融合等效果的实现基础,异名这次实现的demo是一个固定的大圆,然后手指控制一个游离态的小圆
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
Metaballs在数学上是一个求等势面的公式,两个球体之间的等势面为`E = R² / (△x² + △y²)`,
|
||||
|
||||
```c++
|
||||
float energy(float r, vec2 point1, vec2 point2) {
|
||||
return (r * r) / ((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y));
|
||||
}
|
||||
```
|
||||
|
||||
demo的实现很简单,固定的圆处于中心的位置,加大一下半径,求出它的等势面`energy(u_radius + 0.1, v_uv0.xy, vec2(0.5))`,然后我们在手指的落足点再生成一个等势面`energy(u_radius, v_uv0.xy, u_point)`,然后叠加它们,让处于等势面上的点的色值透明度为1,不在该等势面上的透明度为0就可以达到视觉中的球体融合效果:
|
||||
|
||||
```c++
|
||||
void main(){
|
||||
vec4 color = texture(texture, v_uv0);
|
||||
|
||||
float fragEnergy = energy(u_radius + 0.1, v_uv0.xy, vec2(0.5)) + energy(u_radius, v_uv0.xy, u_point);
|
||||
color.a = smoothstep(0.95, 1.0, fragEnergy);
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
这个demo效果异名记得是在一个记账软件上看到的,然后念念不忘,如果你是有心人,你会发现cocos Creator官网的loading动画也是两个球体之间来回改变位置的metaball动画。那如果我们的页面上有更多的小球,让它们互相叠加融球效果,那就可以产生出流体的效果了,异名这两周会抽空实现喔
|
||||
|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Mosaic/resources/demo.gif
Normal file
After Width: | Height: | Size: 488 KiB |
BIN
doc/Mosaic/resources/preview_ewm.png
Normal file
After Width: | Height: | Size: 12 KiB |
53
doc/Mosaic/马赛克像素风.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 效果演示
|
||||
|
||||
马赛克是一种常用的图像处理手段,主要功能就是使图像模糊,因为这种模糊看上去有一个个的小格子组,便形象的称这种画面为马赛克。当马赛克越来越小的时候,画面呈现出来的效果也叫像素风
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
实现思路就是把图片划分成`M * N`个小格子,格子内取同一个颜色。我们前面经常用到`texture(texture, v_uv0)`这个函数,它的作用就是把纹理贴图按uv进行取样,输出一个`vec4`类型的颜色值,现在实现马赛克效果的时候我们只需要改一下,本来是按uv进行取样的,现在我们改为按`M * N`来取样,我们需要找出每个格子的中心点,然后传入`texture`函数,这样一来问题就变成了,如何计算每个格子的中心点。
|
||||
|
||||
从水平方向说起,格子的x轴中心坐标等于当前格子的位置加上格子宽度的一半。水平方向的格子数量`x_count`通过外部传入,我们可以得出每个格子的宽`block_w = 1.0 / x_count`,格子的当前位置等于当前格子的序号乘上格子的宽度,格子的序号为`block_x_idx = floor(v_uv0.x / block_w)`,当前格子的位置为`block_x_idx * block_w`,当前格子的中心点位置为`block_w * (block_x_idx + 0.5)`。同理可以得到格子中心点的垂直方向的位置:
|
||||
|
||||
```c++
|
||||
vec2 getUvMapPos() {
|
||||
float block_w = 1.0 / x_count;
|
||||
float block_x_idx = floor(v_uv0.x / block_w);
|
||||
|
||||
float block_h = 1.0 / y_count;
|
||||
float block_y_idx = floor(v_uv0.y / block_h);
|
||||
|
||||
return vec2(block_w * (block_x_idx + 0.5), block_h * (block_y_idx + 0.5));
|
||||
}
|
||||
```
|
||||
|
||||
有了映射的坐标,我们就可以直接进行颜色取样和赋值,为了便于控制,我们还可以加上一个宏开关:
|
||||
```c++
|
||||
void main () {
|
||||
vec4 o = vec4(1, 1, 1, 1);
|
||||
vec2 realPos = v_uv0;
|
||||
|
||||
#if USE_MASAIC
|
||||
realPos = getUvMapPos();
|
||||
#endif
|
||||
|
||||
o *= texture(texture, realPos);
|
||||
o *= v_color;
|
||||
|
||||
gl_FragColor = o;
|
||||
}
|
||||
```
|
||||
|
||||
后面可以通过更改传进来的垂直和水平方向的格子数量来控制马赛克的大小,效果如下:
|
||||
|
||||

|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
72
doc/Moving_ghost/Moving_ghost.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 效果演示
|
||||
|
||||
游戏中的人物移动带起残影,用来表达速度是很有视觉表现力的。异名的实现思路是从“白玉无冰”那里照搬过来的,在具体的实现上面添加了一些异名自己的理解。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
## 投影到多个画布
|
||||
|
||||
“白玉无冰”的这张图解析得很清晰,我们在实现的时候会在移动的角色中新建一个独立摄像机的子节点,专门拍摄需要移动的角色,然后投影到五个不同透明度的Sprite中。当角色移动的时候,我们也让作为残影的五个Sprite,分别有延迟地移动到角色当前的位置,这样子在视觉上就有五个残影在跟随了。
|
||||
|
||||

|
||||
|
||||
在具体的实现有两个注意点,一个是因为摄像机仅仅只需拍摄移动的角色,所以要为角色新建一个分组,相机只拍摄这个分组;还有一个就是相机拍摄出来的画面投影在`RenderTexture`上是一个上下颠倒的镜像图像,所以要设置每个`Sprite`所在节点的`scaleY = -1`;
|
||||
|
||||
这块的具体实现上我和白玉无冰的做法不一样,白玉无冰直接在编辑器上对残影所在的`Sprite`节点做上述提到的透明处理、层级管理、颠倒处理,异名会觉得把这块的设置放到代码层面处理比较好,一来是编辑器的功能最好还是专职于布局,比如像这个残影透明参数和层级管理确实可以通过编辑器来配置,但是透明度的细微变化和变量绑定的先后顺序这些微小区别,其实是不利于后面的维护和他人接手的,后面阅读代码逻辑的时候也无法看出整个实现的思路。二来就是在代码内做设置参数可以随时在代码上做调整,比如我的残影透明度的初始值,用代码赋值的方式就会比在编辑器中设置会更加一目了然。
|
||||
|
||||
代码如下
|
||||
```js
|
||||
const roleZindex = 10;
|
||||
this.role.zIndex = roleZindex;
|
||||
|
||||
const texture = new cc.RenderTexture();
|
||||
texture.initWithSize(this.node.width, this.node.height);
|
||||
const spriteFrame = new cc.SpriteFrame();
|
||||
spriteFrame.setTexture(texture);
|
||||
this.roleCamera.targetTexture = texture;
|
||||
this.ghostCanvasList.forEach((ghost, idx) => {
|
||||
ghost.node.scaleY = -1;
|
||||
ghost.node.zIndex = roleZindex - idx;
|
||||
ghost.node.opacity = 100 - idx * 15;
|
||||
ghost.spriteFrame = spriteFrame;
|
||||
});
|
||||
```
|
||||
|
||||
## 角色移动
|
||||
因为我们的实现是把相机作为子节点绑定在角色节点下面,当角色移动的时候我们的相机也跟着移动了,我们就需要把相机投影所在的`Sprite`节点们分别做一个延时移动,带出”残影“效果。代码如下:
|
||||
|
||||
```js
|
||||
this.schedule(this.ghostFollow, 0.1, cc.macro.REPEAT_FOREVER);
|
||||
this.node.on(cc.Node.EventType.TOUCH_MOVE, this.touchMoveEvent, this);
|
||||
|
||||
touchMoveEvent(evt: cc.Event.EventTouch) {
|
||||
this.role.x += evt.getDeltaX();
|
||||
this.role.y += evt.getDeltaY();
|
||||
}
|
||||
|
||||
ghostFollow() {
|
||||
this.ghostCanvasList.forEach((ghost, i) => {
|
||||
const dis = (ghost.node.position as any).sub(this.role.position).mag();
|
||||
if (dis < 0.5) return;
|
||||
ghost.node.stopAllActions();
|
||||
ghost.node.runAction(cc.moveTo(i * 0.04 + 0.02, this.role.x, this.role.y));
|
||||
});
|
||||
}
|
||||
```
|
||||
这里注意到,我们并不是在每次`touchMoveEvent`的时候去调用`ghostFollow`函数,而是启用了一个无限重复的定时器去同步,为什么呢?大家去试一试就知道了原因了,其实`cc.Node.EventType.TOUCH_MOVE`是帮我们做了节流的,大部分时候我们受益于这个节流,但是在这个功能里面,节流调用会导致我们的位置同步不及时而导致残影不流畅,所以我们需要单开一个定时器去实时同步,同时记得在destroy之前别忘了去销毁它。
|
||||
|
||||
`ghostFollow`函数主要是同步残影和角色的位置,白玉无冰在这里也有两个小的疏忽。一个是在计算dis的时候,正确的做法是拿残影的位置去和角色的位置做距离运算的,这个失误白玉无冰有在公众号留言中提到,但是还没有在代码仓库中修正过来,大家借鉴的时候要注意。还有一个就是判断是否静止,异名的判断条件是`dis < 0.5`就认为它们已经重合了,`0.5`的像素差别在画面上人眼是看不出来的,那为什么不能是`if(dis > 0) { xxxxx }`呢?在现代的语言中,浮点数计算是有误差的,dis的结果是通过向量计算得出的,经过了加减乘除,中间的计算过程肯定产生了浮点数,它产生出来的结果肯定也是有误差的,大家可以在控制台把`dis`变量打印出来,你会发现,每次静止的时候,理论上静止了,dis的结果应该为0,但是实际上有可能每次产生的dis值都是不一样的,可能是`0`也有可能是`0.0001234`、`0.1222222`等等,但是这个值我们已经可以认为它们已经静止了。因此大家要有意识,当涉及到精确判断的时候,要做容错处理,`if(dis > 0) { xxxxx }`这种写法其实没有考虑到容错。
|
||||
|
||||
# 效果预览
|
||||
|
||||
**源码**获取请点击**查看原文**,长按二维码查看**效果**👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Moving_ghost/resource/demo.gif
Normal file
After Width: | Height: | Size: 201 KiB |
BIN
doc/Moving_ghost/resource/detail.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
doc/Moving_ghost/resource/ewm.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
doc/Photo_gallery/resources/demo.gif
Normal file
After Width: | Height: | Size: 963 KiB |
BIN
doc/Photo_gallery/resources/preview_ewm.png
Normal file
After Width: | Height: | Size: 22 KiB |
64
doc/Photo_gallery/渐变过渡的相册.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# 效果演示
|
||||
|
||||
相册是一个大家比较熟悉的场景,一般我们是实现的都是那种跑马灯式的轮播相册,这里异名给大家提供一个利用shader实现图片渐变过渡的相册思路
|
||||
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
拆分一下功能点,主要有两个:一个是实现图片的渐变,一个是实现图片的切换。
|
||||
|
||||
图片的渐变可以理解为随着时间的变化,在某一方向上的局部的像素点的透明度变化。demo中实现的效果是一个水平滚轴式的切换,水平平移在数学上的实现其实就是一个简单的关于时间变化的垂直直线`x = time`,我们只需要把每个像素点的x坐标和这个垂直直线做比较,在左边的透明度设为0,在右边的透明度设为1,然后再通过平滑取样就能够有渐变过渡的效果:
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 color = vec4(1, 1, 1, 1);
|
||||
color *= texture(texture, v_uv0);
|
||||
color *= v_color;
|
||||
|
||||
#if USE_TRAMSFORM
|
||||
color.a = smoothstep(0.0, u_fade_radius, u_fade_radius + v_uv0.x - u_time);
|
||||
#endif
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
实现了图片的渐变,接下来就是图片的切换,所有的图片其实都在一个队列当中,我们在渐变完成之后只需要把最上面的的那张图片放到最下面,就能够让这个相册一直在循环播放,在这个过程中我们再加上一些图片的状态处理就能够是实现demo中的渐变相册效果了
|
||||
|
||||
```js
|
||||
isTransforming: boolean = false;
|
||||
bgTramsform() {
|
||||
if (this.isTransforming) return;
|
||||
this.isTransforming = true;
|
||||
|
||||
let time = 0.0;
|
||||
let node = this.switchNodeList[0];
|
||||
let material = node.getComponent(cc.Sprite).getMaterial(0);
|
||||
material.setProperty('u_fade_radius', this.fadeRadius);
|
||||
material.setProperty('u_time', time);
|
||||
material.define('USE_TRAMSFORM', true, 0, true);
|
||||
|
||||
let timer = setInterval(() => {
|
||||
time += 0.03;
|
||||
material.setProperty('u_time', time);
|
||||
if (time > 1.0 + this.fadeRadius) {
|
||||
this.switchNodeList.shift();
|
||||
this.switchNodeList.push(node);
|
||||
this.switchNodeList.forEach((node, idx) => node.zIndex = this.switchNodeList.length - idx)
|
||||
material.define('USE_TRAMSFORM', false, 0, true);
|
||||
this.isTransforming = false;
|
||||
timer && clearInterval(timer);
|
||||
}
|
||||
}, 30);
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 效果预览
|
||||
|
||||
源码获取请点击**查看原文**,长按二维码查看效果👇
|
||||
|
||||

|
134
doc/Scratch_ticket/Scratch_ticket.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 效果演示
|
||||
|
||||
奖券,优惠券什么的就需要用到刮刮卡的效果了,大家生活里都使用过。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
一个完整的刮刮卡需要有这三个功能,刮开涂层、刮开比例、重置。整个功能其实是对`mask`组件的一次深入应用。
|
||||
|
||||
## 刮开涂层
|
||||
|
||||
首先创立一个mask组件,长宽都设置为0,勾选反向遮罩,然后再拖一个涂层的`sprite`节点作为它的子节点。为什么这样做呢,我先介绍一下遮罩👇
|
||||
|
||||

|
||||
|
||||
上面可以很好地看明白,其实遮罩就是一个盖板,盖住了就看不到它的子节点了,但是如果我在遮罩中间掏个洞,那我就可以透过这个洞看到里面的东西。正向遮罩就是我看不到后面的东西,反向遮罩就是我可以看到后面的东西。如果我们把遮罩的长宽都设置为0,那等于这个遮罩什么用都没有,因为它的面积是0,所以它的子节点就能够完全暴露在画面上,所以我们能够看到涂层图片。
|
||||
|
||||
然后我们把刮的操作理解为更改这个遮罩的形状,本来它的面积是0,但是当用户刮的时候,我就给它赋予一个长宽,比如我把它的长宽变成10,那这个遮罩就从一个点(长宽为0)变成了一个长宽为10的正方形。而我们设置了这个遮罩类型为反向遮罩,所以我们就能透过这个正方形看到刮刮卡底层的内容了。这就解析了为什么我们需要勾选反向遮罩。这两步的图解如下👇
|
||||
|
||||

|
||||
|
||||
|
||||
那问题来了,我在刮的时候,我生成的形状不能是矩形呀,我的动作是不规则的。其实mask是用Graphics实现的,刮的动作其实不就是在划线吗,所以每次刮的时候,我们只需要记录当前点和上一个点,调用`moveTo`和`lineTo`把用户的滑动轨迹画出来就可以了。我们在`touch_start`的时候只有一个点,那就用`circle`去画一个圆就可以了。代码如下:
|
||||
|
||||
```js
|
||||
touchStartEvent(event) {
|
||||
let point = this.ticketNode.convertToNodeSpaceAR(event.getLocation());
|
||||
this.clearMask(point);
|
||||
}
|
||||
|
||||
touchMoveEvent(event) {
|
||||
let point = this.ticketNode.convertToNodeSpaceAR(event.getLocation());
|
||||
this.clearMask(point);
|
||||
}
|
||||
|
||||
tempDrawPoints: cc.Vec2[] = [];
|
||||
clearMask(pos) {
|
||||
let mask: any = this.maskNode.getComponent(cc.Mask);
|
||||
let stencil = mask._graphics;
|
||||
const len = this.tempDrawPoints.length;
|
||||
this.tempDrawPoints.push(pos);
|
||||
|
||||
if (len <= 1) {
|
||||
// 只有一个点,用圆来清除涂层
|
||||
stencil.circle(pos.x, pos.y, CLEAR_LINE_WIDTH / 2);
|
||||
stencil.fill();
|
||||
|
||||
} else {
|
||||
// 存在多个点,用线段来清除涂层
|
||||
let prevPos = this.tempDrawPoints[len - 2];
|
||||
let curPos = this.tempDrawPoints[len - 1];
|
||||
|
||||
stencil.moveTo(prevPos.x, prevPos.y);
|
||||
stencil.lineTo(curPos.x, curPos.y);
|
||||
stencil.lineWidth = CLEAR_LINE_WIDTH;
|
||||
stencil.lineCap = cc.Graphics.LineCap.ROUND;
|
||||
stencil.lineJoin = cc.Graphics.LineJoin.ROUND;
|
||||
stencil.strokeColor = cc.color(255, 255, 255, 255);
|
||||
stencil.stroke();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 统计刮开比例
|
||||
|
||||
上面解决了如何刮涂层,但是刮纯粹是图像上的操作,如何统计刮开比例呢。我采用的方法就是把遮罩分出N个小正方体,然后每次画的时候不是要动态去画圆和画线段吗,那顺便遍历一下这N个小正方体,如果正方体和里面包括圆心或者和线段有相交,那就把这个正方体标记一下。最后在`touch_end`的时候去统计已经标记过的正方体数量就可以统计出比例了。我还在代码里面留了个`debug`开关,开发的时候可以去实时查看是否有相交喔。
|
||||
|
||||

|
||||
|
||||
圆心点在正方形内是直接计算,线段和正方形相交是调用了`cc.Intersection.lineRect`方法,这块的代码如下:
|
||||
```js
|
||||
// 生成小格子,用来辅助统计涂层的刮开比例
|
||||
for (let x = 0; x < this.ticketNode.width; x += CALC_RECT_WIDTH) {
|
||||
for (let y = 0; y < this.ticketNode.height; y += CALC_RECT_WIDTH) {
|
||||
this.polygonPointsList.push({
|
||||
rect: cc.rect(x - this.ticketNode.width / 2, y - this.ticketNode.height / 2, CALC_RECT_WIDTH, CALC_RECT_WIDTH),
|
||||
isHit: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 记录点所在的格子
|
||||
this.polygonPointsList.forEach((item) => {
|
||||
if (item.isHit) return;
|
||||
const xFlag = pos.x > item.rect.x && pos.x < item.rect.x + item.rect.width;
|
||||
const yFlag = pos.y > item.rect.y && pos.y < item.rect.y + item.rect.height;
|
||||
if (xFlag && yFlag) item.isHit = true;
|
||||
});
|
||||
|
||||
// 记录线段经过的格子
|
||||
this.polygonPointsList.forEach((item) => {
|
||||
item.isHit = item.isHit || cc.Intersection.lineRect(prevPos, curPos, item.rect);
|
||||
});
|
||||
```
|
||||
|
||||
## 重置
|
||||
|
||||
重置格子,直接再生成一批就可以了。然后清除已画区域直接调用`Graphics`的`clear`方法就可以了
|
||||
|
||||
```js
|
||||
polygonPointsList: { rect: cc.Rect; isHit: boolean }[] = [];
|
||||
reset() {
|
||||
let mask: any = this.maskNode.getComponent(cc.Mask);
|
||||
mask._graphics.clear();
|
||||
|
||||
this.tempDrawPoints = [];
|
||||
this.polygonPointsList = [];
|
||||
this.progerss.string = '已经刮开了 0%';
|
||||
this.ticketNode.getComponent(cc.Graphics).clear();
|
||||
|
||||
// 生成小格子,用来辅助统计涂层的刮开比例
|
||||
for (let x = 0; x < this.ticketNode.width; x += CALC_RECT_WIDTH) {
|
||||
for (let y = 0; y < this.ticketNode.height; y += CALC_RECT_WIDTH) {
|
||||
this.polygonPointsList.push({
|
||||
rect: cc.rect(x - this.ticketNode.width / 2, y - this.ticketNode.height / 2, CALC_RECT_WIDTH, CALC_RECT_WIDTH),
|
||||
isHit: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 效果预览
|
||||
|
||||
**源码**获取请点击**查看原文**,长按二维码**查看效果**👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Scratch_ticket/resource/Scratch_ticket.gif
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
doc/Scratch_ticket/resource/calc.gif
Normal file
After Width: | Height: | Size: 272 KiB |
BIN
doc/Scratch_ticket/resource/mask_intro.png
Normal file
After Width: | Height: | Size: 290 KiB |
BIN
doc/Scratch_ticket/resource/preview_ewm.png
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
doc/Scratch_ticket/resource/step_1&2.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
doc/Specular_gloss/resources/demo.gif
Normal file
After Width: | Height: | Size: 192 KiB |
BIN
doc/Specular_gloss/resources/ewm.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
54
doc/Specular_gloss/镜面光泽.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 效果演示
|
||||
|
||||
镜面上扫过一道光泽是UI里面很常用的一种特效,通常用来强调某个物体或者凸显物体的“稀有”价值,比如卡片中扫过一道光芒等
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
根据这个效果的实际需求,可以提取到两个关键点,一个是光路的生成,一个是光路随着时间进行偏移。直观可以看出光路由两根斜率一样的直线组成,其中一根在x轴上偏移一定的距离,两根斜线就能够组成一个倾斜的区域,这个区域用数学来表达就是:两根斜线形成的不等式组。直线的斜截式方程是`y = kx + b`,假设斜率k为1,那光路的区域就可以表示为:`x >= -y` 和 `x <= -y + width`,其中`width`就是我们定义的光路的宽度,有了区域之后我们只需要让符合该区域的像素点色彩叠加点变化就可以实现光路的效果。
|
||||
|
||||
```c++
|
||||
void main () {
|
||||
vec4 color = vec4(1, 1, 1, 1);
|
||||
color *= texture(texture, v_uv0);
|
||||
|
||||
if (v_uv0.x >= -v_uv0.y && v_uv0.x <= -v_uv0.y + width) {
|
||||
color *= strength;
|
||||
}
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
```
|
||||
|
||||
光路随着时间的偏移效果,其实就是让光路的斜距随着时间的变化增加就可以了。这里可以通过脚本的方式在每帧的回调中把偏移的距离动态传进来,但是这种传递其实挺耗性能的,还有一种方式就是我们可以引入`cc-global`,然后通过`cc_time.x`拿到累积的时间参数,然后加上我们的偏移限制来实现光路的循环播放。其中我们光路的起点应该是`0.0 - width`,光路的偏移长度应该是`width + 1.0 + width`:
|
||||
|
||||
```c++
|
||||
#include <cc-global>
|
||||
void main () {
|
||||
vec4 color = vec4(1, 1, 1, 1);
|
||||
color *= texture(texture, v_uv0);
|
||||
|
||||
float time_step = -width;
|
||||
time_step += mod(cc_time.x, 1.0 + 2.0 * width);
|
||||
|
||||
if (v_uv0.x >= -v_uv0.y + time_step && v_uv0.x <= -v_uv0.y + width + time_step) {
|
||||
color *= strength;
|
||||
}
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
|
||||
为了让效果呈现更加完美,我们还可以去调整一下光路的斜率,如果需要多条光路的话,也可以多复制几个不等式组加上不同的偏移距离和宽度就可以了。另外引擎是默认启用了动态合图,它会自动将合适的贴图动态合并到一张大图上来减少drawcall,这样子就会导致我们在effect中拿到的uv坐标不准确,我们可以通过 `cc.dynamicAtlasManager.enabled = false` 把合图给关掉,但是这是个全局开关,所以更好的方法是在资源管理面板中把该资源的`packable`勾选掉,这样子它就不会被打包到合图中了。
|
||||
|
||||
# 效果预览
|
||||
|
||||
**源码**获取请点击**查看原文**,长按二维码**查看效果👇**
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Typer/resource/demo.gif
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
doc/Typer/resource/ewm.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
doc/Typer/resource/result.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
doc/Typer/resource/typer_detail.gif
Normal file
After Width: | Height: | Size: 95 KiB |
70
doc/Typer/打字机效果.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 效果演示
|
||||
|
||||
打字机效果也就是让文字逐个在屏幕中显示,直到把整段话说完,常常被应用到人物对话,角色旁白甚至引导教程等高频场景中。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
文字逐一显示,其核心思路就是定时器的应用,对`label`组件而言,我们只需要在定时器的每次回调里面把字符逐一添加进去即可
|
||||
|
||||
```js
|
||||
typerTimer: number = null; // 计时器Id
|
||||
makeLaberTyper(str: string) {
|
||||
let charArr = str.split('');
|
||||
let charIdx = 0;
|
||||
|
||||
this.typerTimer && clearInterval(this.typerTimer);
|
||||
this.typerTimer = setInterval(() => {
|
||||
if (charIdx >= charArr.length) {
|
||||
this.typerTimer && clearInterval(this.typerTimer);
|
||||
} else {
|
||||
charIdx += 1;
|
||||
this.label.string = charArr.slice(0, charIdx).join('');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
```
|
||||
|
||||
但是对于富文本组件而言,我们就不能单纯地截取每个字符串然后再动态添加补充,因为富文本的字符串里面带有标签,单纯截取字符的方式无法保证标签的闭合,如果标签无法闭合,那呈现的字体效果就不是带有格式的富文本字段。所以要实现富文本的打字机效果,首先需要针对富文本进行文字提取,保留标签,然后再把文字动态塞到它原本存在的位置当中,这就转化成了一个简单的“找位置”问题,假定我们当前的富文本字符串如下:
|
||||
|
||||
`我是<color=#1B262E>异名</c>\n这是<color=#1B262E>富文本打字机</color>效果`
|
||||
|
||||
我们需要把字符串里面的标签找出来,然后替换成一个个容器,然后按顺序地往每个容器内添加文字,提炼和动态添加的过程如下所示
|
||||
|
||||

|
||||
|
||||
剩下的就是代码层面上的实现了,异名的做法是先一次性生成不同的字符串放在数组里面,然后在定时器的回调里面出栈:
|
||||
|
||||
```js
|
||||
let str = '我是<color=#1B262E>异名</c>\n这是<color=#1B262E>富文本打字机</color>效果';
|
||||
let charArr = str.replace(/<.+?\/?>/g, '').split('');
|
||||
let tempStrArr = [str];
|
||||
|
||||
for (let i = charArr.length; i > 1; i--) {
|
||||
let curStr = tempStrArr[charArr.length - i];
|
||||
let lastIdx = curStr.lastIndexOf(charArr[i - 1]);
|
||||
let prevStr = curStr.slice(0, lastIdx);
|
||||
let nextStr = curStr.slice(lastIdx + 1, curStr.length);
|
||||
|
||||
tempStrArr.push(prevStr + nextStr);
|
||||
}
|
||||
console.log(tempStrArr)
|
||||
```
|
||||
|
||||

|
||||
|
||||
可以留意到字符片段里面也有换行符`\n`,虽然它是由两个字符组成,但是反斜杠是js里面的特殊字符,在字符串的处理中像`\n`、`\"`、`\\`等等都会被算作一个字符。
|
||||
|
||||
|
||||
# 效果预览
|
||||
|
||||
**源码**获取请点击**查看原文**,长按二维码**查看效果**👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/Water_spread/resource/demo.gif
Normal file
After Width: | Height: | Size: 834 KiB |
BIN
doc/Water_spread/resource/demo2.gif
Normal file
After Width: | Height: | Size: 644 KiB |
BIN
doc/Water_spread/resource/demo3.gif
Normal file
After Width: | Height: | Size: 475 KiB |
BIN
doc/Water_spread/resource/demo4.gif
Normal file
After Width: | Height: | Size: 356 KiB |
BIN
doc/Water_spread/resource/ewm.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
doc/Water_spread/resource/stretch.png
Normal file
After Width: | Height: | Size: 346 KiB |
91
doc/Water_spread/水波扩散效果.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 效果演示
|
||||
|
||||
水波扩散是一个比较好看的交互效果,特别是在某些以水为故事发生场景的游戏中,扩散的水波会让场景更加栩栩如生。
|
||||
|
||||

|
||||
|
||||
# 实现思路
|
||||
|
||||
如果水波静止,我们看到的其实是像素点围绕着某个中心点的拉伸效果,我们只需让每个像素点叠加上它和中心点的向量差,就能够呈现出画面上的所有像素围绕中心点的拉伸感。
|
||||
```c++
|
||||
void main() {
|
||||
vec2 uv = normalize(vec2(0.5, 0.5) - v_uv0) * 0.2 + v_uv0;
|
||||
gl_FragColor = texture(texture, uv);
|
||||
}
|
||||
```
|
||||

|
||||
|
||||
这个时候如果我们加上时间参数,我们就可以得到一个往外不停井喷的“黑洞”:
|
||||
|
||||
```c++
|
||||
void main() {
|
||||
vec2 uv = normalize(vec2(0.5, 0.5) - v_uv0) * 0.2 * cc_time.x + v_uv0;
|
||||
gl_FragColor = texture(texture, uv);
|
||||
}
|
||||
```
|
||||

|
||||
|
||||
|
||||
但是水波往外扩散是呼吸灯式的一波波往外涌,而且不是这种无尽式的一直把东西往外掏的感觉,所以我们要给`cc_time.x`加上一个周期性的变化,让它能表现出这种周期性的往外扩散的感觉。
|
||||
|
||||
```c++
|
||||
void main() {
|
||||
vec2 uv = normalize(vec2(0.5, 0.5) - v_uv0) * 0.2 * sin(cc_time.x) + v_uv0;
|
||||
gl_FragColor = texture(texture, uv);
|
||||
}
|
||||
```
|
||||

|
||||
|
||||
这种呼吸灯式的涌动其实和我们的最终效果有很大区别,因为它永远在循环涌动,但是我们的水波是从中心扩散出去之后,中间部分就不再动了的,怎么让中间的像素不再多次涌动呢?如果把一圈水波比作圆,那水波的扩散行为其实就是这个圆的半径在不断的增大,圆外面的波纹有效,圆里面的波纹静止。因此我们可以多加一个距离取样,像素离扩散中心的距离大于半径才保留否则丢弃,而这个半径从零开始逐渐增大。
|
||||
|
||||
```c++
|
||||
void main() {
|
||||
vec2 distance_vec = vec2(0.5, 0.5) - v_uv0;
|
||||
float sin_factor = sin(cc_time.x) * 0.2;
|
||||
|
||||
float wave_radius = 0.3;
|
||||
float distance = sqrt(distance_vec.x * distance_vec.x + distance_vec.y * distance_vec.y);
|
||||
// 其中waveOffset是随时间增长的,通过外部传入
|
||||
float dis_factor = clamp(wave_radius - abs(distance - wave_offset), 0.0, 1.0);
|
||||
|
||||
vec2 uv = v_uv0 + normalize(distance_vec) * sin_factor * dis_factor;
|
||||
gl_FragColor = texture(texture, uv);
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
接下来的就是参数的调试,主要是三角函数的采样那里,我们希望水波能够产生多个波动,所以我们需要乘上一定的倍数,让函数的作用范围足够大,才能有足够多的波峰谷底。另外就是sin函数的输出值域在`(-1, 1)`之间,所以我们的输出也需要缩小一定的倍数,才能让函数的峰值变化处于一个合理的范围。
|
||||
|
||||
```c++
|
||||
void main() {
|
||||
vec2 distance_vec = center - v_uv0;
|
||||
distance_vec = distance_vec * vec2(canvas_size.x / canvas_size.y, 1.0);
|
||||
float distance = sqrt(distance_vec.x * distance_vec.x + distance_vec.y * distance_vec.y);
|
||||
|
||||
// distance小于1,但是我们希望能有多个波峰波谷,所以在sin的内部乘上一个比较大的倍数
|
||||
// sin函数的值在-1到1之间,我们希望偏移值很小,所以输出的时候需要缩小一定的倍数倍
|
||||
float sin_factor = sin(distance * 100.0 + cc_time.x) * 0.05;
|
||||
float discard_factor = clamp(wave_radius - abs(wave_offset - distance), 0.0, 1.0);
|
||||
|
||||
// 计算总的uv的偏移值
|
||||
vec2 offset = normalize(distance_vec) * sin_factor * discard_factor;
|
||||
vec2 uv = offset + v_uv0;
|
||||
|
||||
gl_FragColor = texture(texture, uv);
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
# 效果预览
|
||||
|
||||
**源码**获取请点击**查看原文**,长按二维码**查看效果**👇
|
||||
|
||||

|
||||
|
||||
我是异名,你的阅读是我的动力
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/demo.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
25
doc/footer.md
Normal file
@@ -0,0 +1,25 @@
|
||||
我是异名,你的阅读是我的动力,其他文章链接:
|
||||
|
||||
- [融球效果(shader)](https://mp.weixin.qq.com/s/BiTQLHvxgXbZidJdbDniig)
|
||||
- [颜色滤镜(shader)](https://mp.weixin.qq.com/s/cLHyZnOCmjLGek4HPjlOyw)
|
||||
- [水波扩散效果(shader)](https://mp.weixin.qq.com/s/wrZx8J8d8hcm2A1Qs9T36A)
|
||||
- [镜面光泽效果(shader)](https://mp.weixin.qq.com/s/QJ6eYYT4yhlI2yzZXWveJg)
|
||||
- [圆形头像(shader)](https://mp.weixin.qq.com/s/dTNJEb5Mn09I9DXXrN9Ynw)
|
||||
- [追光效果(shader)](https://mp.weixin.qq.com/s/Qnln8_3r_soYzQRMp1QBOw)
|
||||
- [溶解效果(shader)](https://mp.weixin.qq.com/s/JUlGzPvDeWayHr2YVYhhjQ)
|
||||
- [富文本打字机效果](https://mp.weixin.qq.com/s/w-ohIyWrdSmMlPs6Kek5MQ)
|
||||
- [放大镜效果](https://mp.weixin.qq.com/s/ITeUvJbcJ7XOuedUhFbawg)
|
||||
- [子弹跟踪效果](https://mp.weixin.qq.com/s/nzO4nL65uUYlaD1pDnSHwg)
|
||||
- [微信小游戏超出4M之后](https://mp.weixin.qq.com/s/4c5J9VRb0uP8gbRuUFoc7w)
|
||||
- [残影移动](https://mp.weixin.qq.com/s/xgmH4QIQPQAyzjErhCOkMw)
|
||||
- [刮刮卡实现](https://mp.weixin.qq.com/s/yZ1Lq-L-BxbaChADqC1tDQ)
|
||||
- [金币落袋效果](https://mp.weixin.qq.com/s/Ssz-3GHnDJVMHbJSPD4-Pw)
|
||||
- [遥控杆实现](https://mp.weixin.qq.com/s/hgbONZQH3N_UnF3eUagcRQ)
|
||||
- [背景无限滚动](https://mp.weixin.qq.com/s/-k2l86R7zCYCgUNTln82XA)
|
||||
- [Cocos游戏开发入门最佳实践](https://mp.weixin.qq.com/s/UU28K7rDPX1TMaa7UtCVag)
|
||||
- [使用Cocos进行2D和3D混合开发](https://mp.weixin.qq.com/s/4NhEMDlb3hbbnIayW6D2Gw)
|
||||
|
||||

|
||||
|
||||
|
||||
**源码地址**:https://github.com/ifengzp/cocos-awesome/ [直接跳转](https://github.com/ifengzp/cocos-awesome/)
|