游戏中的角色快速移动或者进行动作时,会留下一道闪烁的残影。这个效果是不是非常酷?
那么,这个酷炫的效果是如何实现的呢?
在本文中,我们将揭开人物残影效果的秘密,告诉你它是如何在游戏中实现的,以及为什么它对游戏的吸引力如此之大。
1.思路
说起残影,大家都应该能想象到他的产生原因。对应到游戏开发过程中就是将角色当前的图像与一定时间间隔之前的图像叠加起来,再进行对应透明度的变化来实现最终的效果;通常情况想要实现残影的效果有两种方案:
1.通过去获取指定帧角色网格数据,生成一个新的网格模型,最终的效果是通过同时显示n个网格模型来实现
2.通过屏幕后处理方式,获取指定帧数的角色图像效果,最终的效果是通过n张获取的角色效果叠加实现
2.实现
1.RenderTexture
「确定了我们需要添加残影的对象是角色,所以我们只需要拿到角色的渲染图像信息」
项目设置中新增 Layers 命名为 Player
将角色节点以及其所有子节点 Layer 设置为 Layer
设置主相机 Main Camera 的 Visibility 将 Player 设置为不可见
新增摄像机,命名 Ghost Camera 设置其 Visibility 仅可见 Player
设置相机 Ghost Camera Clear Flags 为 SOLID_COLOR
设置相机 Ghost Camera Clear Color 的颜色为黑色,透明度为 0
「通过以上步骤,我们已经创建了一个只渲染指定角色的摄像机,那么接下来我们将渲染的结果输出到屏幕上」
Canvas 想创建 Sprite 命名 Ghost Sprite 节点 size 和设计分辨率保持一致
创建 RT 并且将 rt 渲染到 Sprite 上
private initRenderTexture(): void {
this._renderTexture = new RenderTexture();
this._renderTexture.reset({
width: this._mainCamera.camera.width,
height: this._mainCamera.camera.height,
});
this._ghostCamera.targetTexture = this._renderTexture;
let spriteFrame: SpriteFrame = new SpriteFrame();
spriteFrame.texture = this._renderTexture;
this.ghostSprite.spriteFrame = spriteFrame;
console.log("初始化rt", this._renderTexture);
}
「这个时候我们已经将角色通过rt的方式显示到了屏幕上」
2.相机
「想在场景中有两个摄像机,Main Camera和Ghost Camera;我们需要保持Ghost Camera节点的属性与Main Camera的保持一致」
private updateCameraTransform(): void {
if (!this.ghostCameraNode || !this.mainCameraNode) return;
this.ghostCameraNode.worldPosition = this.mainCameraNode.worldPosition;
this.ghostCameraNode.worldScale = this.mainCameraNode.worldScale;
this.ghostCameraNode.worldRotation = this.mainCameraNode.worldRotation;
}
3.残影
「接下来就是重点了...」
1.保存rt渲染图像
「上边已经提过了,咱们的实现方式是通过将n张保存的渲染图像再次进行叠加去实现残影的效果,那么首要就是去保存指定帧rt的数据;引擎是没有提供直接的接口的,通过查看源码和翻阅资料,可以通过以下代码去实现」
let textureBuffer: Uint8Array = this._renderTexture.readPixels();
//通过texture buffer 创建texture
let img = new ImageAsset();
img.reset({
_data: textureBuffer,
width: this._renderTexture.width,
height: this._renderTexture.height,
format: Texture2D.PixelFormat.RGBA8888,
_compressed: false
});
let texture: Texture2D = new Texture2D();
texture.image = img;
2.图像队列
「通过上边代码我们可以拿到指定帧rt的数据,并将其转化为Texture2D 格式,但是想要实现残影,仅仅只有一张是不行的,我们需要多张图像;」
1.定义图像队列
private _ghostTexures: Array<Texture2D> = null;
2.指定数量
@property
_ghostNum: number = 5;
@property({ type: CCInteger })
public get ghostNum() {
return this._ghostNum;
};
3.指定获取图像间隔
@property
_ghostInterval: number = 0.1;
@property({ type: CCFloat })
public get ghostInterval() {
return this._ghostInterval;
};
public set ghostInterval(val) {
this._ghostInterval = val;
};
4.获取图像队列
private updateGhostTexures(deltaTime: number): void {
this._ghostIntervalDt += deltaTime;
if (this._ghostIntervalDt >= this.ghostInterval) {
this._ghostIntervalDt = 0;
} else {
return;
}
if (!this.ghostCameraNode || !this.mainCameraNode) return;
if (this._ghostTexures.length >= this._ghostNum) {
// 删除最后一个
let texture: Texture2D = this._ghostTexures.splice(this._ghostNum - 1, 1)[0];
texture.destroy();
}
let textureBuffer: Uint8Array = this._renderTexture.readPixels();
//通过texture buffer 创建texture
let img = new ImageAsset();
img.reset({
_data: textureBuffer,
width: this._renderTexture.width,
height: this._renderTexture.height,
format: Texture2D.PixelFormat.RGBA8888,
_compressed: false
});
let texture: Texture2D = new Texture2D();
texture.image = img;
this._ghostTexures.unshift(texture);
}
「那么到此咱们已经成功的获取到了我们残影实现所需要的图像队列,接下来就需要通过自定义的材质将图像进行叠加」
4.shader
1.复制引擎自带的builtin-sprite到项目中
2.创建材质文件,指定effect为复制出来的builtin-sprite
3.将材质文件绑定到Ghost Sprite 节点,Sprite组件的CustomMaterial;
「进行完上边的操作,接下来我们就可以通过编写shader 进行图像的叠加」
定义属性
CCEffect %{
techniques:
- passes:
- vert: sprite-vs:vert
....
properties:
alphaThreshold: { value: 0.5 }
ghostTexure_0: {value: white }
ghostTexure_1: {value: white }
ghostTexure_2: {value: white }
ghostTexure_3: {value: white }
ghostTexure_4: {value: white }
}%
片元着色器进行叠加
CCProgram sprite-fs %{
...
#if USE_TEXTURE
in vec2 uv0;
#pragma builtin(local)
layout(set = 2, binding = 12) uniform sampler2D cc_spriteTexture;
uniform sampler2D ghostTexure_0;
uniform sampler2D ghostTexure_1;
uniform sampler2D ghostTexure_2;
uniform sampler2D ghostTexure_3;
uniform sampler2D ghostTexure_4;
#endif
vec4 frag () {
vec4 o = vec4(1, 1, 1, 1);
#if USE_TEXTURE
//残影采样,并且计算颜色值以及透明度
vec4 col0=CCSampleWithAlphaSeparated(ghostTexure_0, uv0);
vec4 col1=CCSampleWithAlphaSeparated(ghostTexure_1, uv0);
vec4 col2=CCSampleWithAlphaSeparated(ghostTexure_2, uv0);
vec4 col3=CCSampleWithAlphaSeparated(ghostTexure_3, uv0);
vec4 col4=CCSampleWithAlphaSeparated(ghostTexure_4, uv0);
vec4 col= vec4(1, 1, 1, 1);
col=(col0+col1+col2+col3+col4)/5.0;
o=col;
#endif
o *= color;
ALPHA_TEST(o);
return o;
}
}%
「此时一个简单的残影的效果就可以正常显示了」
「虽然此时的效果已经显示出来了,但是在显示上已经存在一些问题」
1.上边的材质中只对传入的图像队列进行了叠加输出,但是没有包含原有的角色对象
2.残影应该有透明度减淡的效果体现
3.原角色显示区域不需要进行叠加处理
「基于以上两点,对片元着色器进行优化」
vec4 frag () {
vec4 o = vec4(1, 1, 1, 1);
#if USE_TEXTURE
o *= CCSampleWithAlphaSeparated(cc_spriteTexture, uv0);
//残影采样,并且计算颜色值以及透明度
vec4 col0=CCSampleWithAlphaSeparated(ghostTexure_0, uv0);
vec4 col1=CCSampleWithAlphaSeparated(ghostTexure_1, uv0);
vec4 col2=CCSampleWithAlphaSeparated(ghostTexure_2, uv0);
vec4 col3=CCSampleWithAlphaSeparated(ghostTexure_3, uv0);
vec4 col4=CCSampleWithAlphaSeparated(ghostTexure_4, uv0);
col0.a*=0.3;
col1.a*=0.4;
col2.a*=0.3;
col3.a*=0.1;
col4.a*=0.05;
float a=max(col0.a,col1.a);
a=max(col2.a,a);
a=max(col3.a,a);
a=max(col4.a,a);
vec4 col= vec4(1, 1, 1, 1);
col.rgb=(col0.rgb+col1.rgb+col2.rgb+col3.rgb+col4.rgb)/5.0;
col.a=a;
float r=step(o.a,a);
o=mix(o,col,r);
#endif
o *= color;
ALPHA_TEST(o);
return o;
}
写在结尾
以上就是宗宝通过后处理 rt实现残影的大致流程,当然这种实现方式可能会在某些情况下无法满足需求,大家有更好的方式欢迎分享
宗宝微信:「Carlos13207」
源码示例获取方式
「关注宗宝公众号:穿越的杨宗宝, 回复:"残影"」
往期精彩