mirror of
https://github.com/ifengzp/cocos-awesome.git
synced 2024-12-26 03:39:22 +00:00
114 lines
6.6 KiB
Markdown
114 lines
6.6 KiB
Markdown
|
# 效果演示
|
|||
|
|
|||
|
早些时候在社区见到一个元宇宙的项目,做了一个前后场景方块碎片过渡的效果,十分有科技感,异名就有了复刻的想法;加上两个场景切换的过渡效果比较实用,社区上已经有了开源的方案,异名复刻的成本很低,但是收获不少
|
|||
|
![demo](http://cdn.blog.ifengzp.com/switch-scene/SquaresWire.gif)
|
|||
|
|
|||
|
# 实现思路
|
|||
|
|
|||
|
因为最终效果需要应用到两个场景切换中,所以借助了常驻节点来实现全局共享的能力。常驻节点是一种特殊类型的节点,它们在场景切换时不会被销毁,而是保持在内存中。常驻节点在整个游戏生命周期中都存在,可以用于实现一些需要在多个场景之间共享的功能或数据,它的开启很简单:`cc.game.addPersistRootNode(this.node)`
|
|||
|
|
|||
|
在常驻节点中,我们创建一个内部Camera和Sprite来渲染场景的过渡画面
|
|||
|
```c++
|
|||
|
// 同样的方式创建一个内部的渲染Sprite,
|
|||
|
let cameraNode = cc.find('TRANSITION_CAMERA', this.node);
|
|||
|
if (!cameraNode) {
|
|||
|
cameraNode = new cc.Node('TRANSITION_CAMERA');
|
|||
|
this._camera = cameraNode.addComponent(cc.Camera);
|
|||
|
cameraNode.parent = this.node;
|
|||
|
} else {
|
|||
|
this._camera = cameraNode.getComponent(cc.Camera);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
这样我们就拥有了三个Camera,当场景A切换到场景B的时候,我们把 `TRANSITION_CAMERA` 激活,用这个相机来渲染过渡的画面,具体细节看核心的过渡逻辑
|
|||
|
|
|||
|
```c++
|
|||
|
_loadScene(sceneUrl, fromCameraPath, toCameraPath, onSceneLoaded, onTransitionFinished) {
|
|||
|
this._spriteNode.active = true;
|
|||
|
this._cameraNode.active = true;
|
|||
|
let fromCameraNode = cc.find(fromCameraPath);
|
|||
|
let fromCamera = fromCameraNode && (fromCameraNode.getComponent(cc.Camera) as any);
|
|||
|
|
|||
|
let originTargetTexture1 = fromCamera.targetTexture;
|
|||
|
fromCamera.cullingMask &= ~this._camera.cullingMask;
|
|||
|
fromCamera.targetTexture = this._texture1;
|
|||
|
fromCamera.render(cc.director.getScene());
|
|||
|
fromCamera.targetTexture = originTargetTexture1;
|
|||
|
|
|||
|
return cc.director.loadScene(sceneUrl, (arg) => {
|
|||
|
onSceneLoaded && onSceneLoaded(...arg);
|
|||
|
|
|||
|
let toCameraNode = cc.find(toCameraPath);
|
|||
|
let toCamera = toCameraNode && (toCameraNode.getComponent(cc.Camera) as any);
|
|||
|
|
|||
|
toCamera.cullingMask &= ~this._camera.cullingMask;
|
|||
|
let originTargetTexture2 = toCamera.targetTexture;
|
|||
|
toCamera.targetTexture = this._texture2;
|
|||
|
toCamera.render(cc.director.getScene());
|
|||
|
|
|||
|
this._camera.depth = toCamera.depth;
|
|||
|
this._camera.clearFlags = toCamera.clearFlags;
|
|||
|
|
|||
|
this._onLoadFinished = () => {
|
|||
|
toCamera.targetTexture = originTargetTexture2;
|
|||
|
|
|||
|
this._spriteNode.active = false;
|
|||
|
this._cameraNode.active = false;
|
|||
|
};
|
|||
|
});
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
这里面有一个"Render-To-Texture(RTT)"的操作,它将图形渲染的结果直接绘制到一个纹理(Texture)上,而不是直接渲染到屏幕上,核心代码是这三行
|
|||
|
|
|||
|
```c++
|
|||
|
// 设置渲染目标:将渲染目标设置为一个纹理,而不是默认的屏幕缓冲区。这样,所有的渲染操作都会直接渲染到这个纹理上
|
|||
|
fromCamera.targetTexture = this._texture1;
|
|||
|
// 进行渲染:执行需要渲染的图形绘制操作,渲染结果会直接绘制到纹理上
|
|||
|
fromCamera.render(cc.director.getScene());
|
|||
|
// 恢复渲染目标:渲染完成后,将渲染目标恢复为默认的屏幕缓冲区,以便后续的渲染操作正常进行
|
|||
|
fromCamera.targetTexture = originTargetTexture1;
|
|||
|
```
|
|||
|
|
|||
|
这里面还有一个我们不常用的位运算 `fromCamera.cullingMask &= ~this._camera.cullingMask;`,其目的是通过对摄像机的 cullingMask 属性进行位操作,来控制哪些图层的对象会被摄像机忽略。cullingMask 是在图形编程中用于决定摄像机渲染哪些图层的一个掩码(mask),它通常用于指定摄像机在渲染时应该考虑哪些图层中的对象,哪些图层中的对象应该被忽略
|
|||
|
|
|||
|
其中 `~this._camera.cullingMask` 使用了位求反操作符 ~,它会将 `this._camera.cullingMask` 的每个位都取反,即将 0 变为 1,将 1 变为 0通过按位与运算符;然后通过 `&=` 将 fromCamera.cullingMask 和 ~this._camera.cullingMask 的结果进行按位与操作,以得到新的掩码。这个新的掩码会在不影响其他位的情况下,将与参考摄像机的 cullingMask 相关的位设置为 0,从而排除这些层级的对象不被当前摄像机渲染
|
|||
|
|
|||
|
获得了前后场景的画面,接下来就可以基于此实现两张Texture的切换效果;在这个demo中,我们要实现一个渐进的、基于小方块的过渡动效,方块从起始颜色逐渐变化到目标颜色,我们定义三个参数:progress(time) 控制过渡的进度,smoothness 控制过渡边缘的平滑度,squares 控制方块的数量,核心函数如下
|
|||
|
![demo](http://cdn.blog.ifengzp.com/switch-scene/canshu.png)
|
|||
|
|
|||
|
```c++
|
|||
|
vec4 transition(vec2 p) {
|
|||
|
const vec2 center = vec2(0.5, 0.5);
|
|||
|
// 根据进度计算当前方向
|
|||
|
vec2 v = normalize(direction);
|
|||
|
v /= abs(v.x) + abs(v.y);
|
|||
|
float d = v.x * center.x + v.y * center.y;
|
|||
|
float offset = smoothness;
|
|||
|
|
|||
|
// 计算当前进度
|
|||
|
float pr = smoothstep(-offset, 0.0, v.x * p.x + v.y * p.y - (d - 0.5 + progress * (1.0 + offset)));
|
|||
|
|
|||
|
// 按照 squares 划分成小方块的坐标
|
|||
|
vec2 squarep = fract(p * squares);
|
|||
|
vec2 squaremin = vec2(pr / 2.0);
|
|||
|
vec2 squaremax = vec2(1.0 - pr / 2.0);
|
|||
|
|
|||
|
// 通过 step 函数计算当前像素位置是否在当前方块内,并返回一个混合系数 a
|
|||
|
float a = (1.0 - step(progress, 0.0)) * step(squaremin.x, squarep.x) * step(squaremin.y, squarep.y) * step(squarep.x, squaremax.x) * step(squarep.y, squaremax.y);
|
|||
|
return mix(getFromColor(p), getToColor(p), a);
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
切分小方块代码是 `vec2 squarep = fract(p * squares)`, 这行代码的作用是将输入坐标 p 乘以 squares 后,取其小数部分,这种操作通常用于在纹理坐标或屏幕空间坐标上创建重复的图案或效果
|
|||
|
|
|||
|
举例如果 p 是 (0.3, 0.7) 且 squares 是 4,则 p * squares 是 (1.2, 2.8),而 fract(1.2, 2.8) 是 (0.2, 0.8),这样,原始坐标 (0.3, 0.7) 被映射到一个在 [0, 1) 范围内的新坐标。通过这种方式,就能实现将图像或屏幕空间分割成多个小方块的效果,然后通过step函数算出每个像素点是否处于方块内,最后进行色彩的插值,从而实现画面的切换,最终效果如下
|
|||
|
|
|||
|
![demo](http://cdn.blog.ifengzp.com/switch-scene/SquaresWire.gif)
|
|||
|
|
|||
|
# 效果预览
|
|||
|
|
|||
|
**源码**获取请点击**查看原文**,长按二维码**查看效果**👇
|
|||
|
|
|||
|
![ewm](http://cdn.blog.ifengzp.com/switch-scene/ewm.png)
|