cocos creator3d教程

注:本文既有经验上的总结,又有实现方式上的讲解。既有流程上的描述,又有代码细节上的剖析。 全文字数5000+,看的时候最好带上笔和纸。 零、你的序 感谢大家的厚爱,KylinsPostEffects 上架Cocos Store 两星期销量破百。 现在应广大用户的要求,

注:本文既有经验上的总结,又有实现方式上的讲解。既有流程上的描述,又有代码细节上的剖析。

全文字数5000+,看的时候最好带上笔和纸。

零、你的序

感谢大家的厚爱,KylinsPostEffects上架Cocos Store两星期销量破百。

现在应广大用户的要求,写一篇关于这个3D后期效果框架的源码剖析文章,以供大家学习和文档查阅之用。

说实话,篇文章重写了好几稿,但左思右想觉得不妥,所以那几稿都没有发出来。

一开始给它的定位是源码剖析。所谓源码剖析的意思就是说,只需要写清楚一些关键的源码是什么意思,需要注意的点就行了。

如果只讲源码,那和附带的文档没有区别,并不能为一些想要了解这方面的朋友带来帮助。

并且,那种很干的文章阅读起来点都不丝滑,也不是麒麟子的风格。

因此,本文将从故事说起,逐步涵盖需要讲解的内容。

  • 1、它的故事
  • 2、为何选择这样的实现方案
  • 3、此方案的优缺点
  • 4、方案中的若干技术细节
  • 5、后期规划与可能提升的方向

同时,这篇文章在阅读后,还能获得一些可以高度复用到其它场合的知识点:

  • 1、顶点坐标系变换流程
  • 2、使用Shader渲染屏幕空间的Quad
  • 3、使用代码创建Mesh
  • 4、节点的Layer与摄像机的CullingMask
  • 5、使用代码创建、管理RenderTexture

没错,整了这么长的序,就是怕你跑了

昨天也写了一个序,但光是那个序就有5000+字,只好作为单独的文章发布了,名字叫《麒麟子带你快速进入Cocos Creator的3D世界》,有兴趣的朋友可以从公众号:麒麟子随笔 中查阅。

一、它的故事

其实这个后期效果框架,我也等了好久。

一方面是等Cocos Creator 的3D渲染管线自定义流程更轻便,另一方面是等社区中有大佬来写。

可能让大家想不到的是,这个后期处理框架第一版的完成时间是在2020年10月10日,当时做3D项目还是用的Cocos Creator 3D 1.2,是为一个3D MMORPG项目写的。

时隔一年之多,Cocos Creator早已今非昔比,版本号已经v3.3.2了,马上就出v3.4。并且它的3D渲染原生性能等能力早已甩掉一年前的版本好几条街,然而还是没有人提交后期效果解决方案。

基于社区童鞋们的强烈需求,麒麟子整理了之前写的这个方案,升级到了v3.3.2,并做了易用性优化,最终形成了KylinsPostEffects

麒麟子觉得,针对目前Cocos Store上关于这方面的作品不多的情况。真正的原因或许并不是用的人少,大概率可能是,有非常多的3D项目成果,但是它们并没有被公开出来,就像这个一年多前就已经完成并用于3D MMO项目的后期效果处理框架。

所以麒麟子做了这个示范,希望可以带动大家的分享氛围。

给不熟悉的朋友简单介绍一下KylinsPostEffects

KylinsPostEffects是一个不污染场景节点树不依赖Prefab不修改引擎渲染管线使用简单极易扩展的3D后期效果处理方案。内置了GlowBloom空间扰动LUT等效果。

为了照顾广大C友的支持,双11那天,麒麟子所有作品(其实就两个)会参加Cocos Store的促销活动,要买的可以提前放入购物车了。

麒麟子为了给大家带来更多Cocos相关的3D内容,深夜伏案写作。又为了让大家阅读内容时更加顺畅,主动关闭了文中和文末的广告。这样的麒麟子,偶尔打一个广告,想必不会被吐槽吧?

二、为何选择这样的实现方案

要实现后期效果的最佳方法,当然是自定义引擎的渲染管线啦

但是有时候为了体现自己很厉害,都会先说一个但是,这是基本操作。

但是,大家都知道,一但改动引擎的渲染管线,那么引擎新版本升级的时候,将会带来合并版本的工作量。严重的情况下,会导致已修改过的管线需要全部重写,底层重写就需要对所有设备做兼容性测试,代价是巨大的。所以,一般情况下不建议改动管线。

并且,如果采用Hack的方式修改引擎渲染管线,将导致小游戏平台不能使用引擎分离功能。 原生平台由于本身就是整个引擎打包,专注于原生的项目可以不理。

但是,又来一个但是。

采用Cocos Creator引擎的项目,大部分都是原生+小游戏多端发布的,引擎分离这个特性从目前来看,大部分CP还是蛮关注的。

基于兼容性、稳定性、包体和工作量的考虑,最终选择了这个基于多个摄像机的后期效果解决方案

三、此方案的优缺点

优缺点的参照对象为自定义管线的解决方案

优点

  • 1、无需改动引擎渲染管线
  • 2、基于逻辑层制作,兼容原生小游戏H5Cosos Runtime
  • 3、基于多摄像机渲染,理解成本低,便于入手

缺点

  • 1、每一个Pass都需要一个摄像机,管理不便
  • 2、每一个Pass都需要走完整的渲染管线流程,多出许多不必要的逻辑开销
  • 3、需要消耗场景的Layer,这会导致能够同时激活的后期效果是有限的

四、方案中的若干技术细节

KylinsPostEffects是一个采用多个摄像机实现的后期效果解决方案。

麒麟子对解决方案的理解是:通过对需求和问题的分析,得出满足需求和解决问题的方法,并根据方法形成一种可实施的方案。

由于代码大家都拿到手了,过多的讲解代码细节意义不大。今天我们就从为什么要这样设计?是为了解决什么样的问题?

但不修改管线我们如何做到呢?在这里,麒麟子相到了通过渲染一个撑满屏幕的Qaud来实现。

注:Cocos Creator v3.4之后,建议采用自定义管线来做后期效果,本文描述的是多摄像机渲染Quad的方法。

这两方面来讲解。

4.1、效果初始化与使用

//获取主摄像机
let mainCamera = find('Main Camera').getComponent(Camera);
mainCamera.visibility &= ~Layer3D.UI_3D.mask;

//设置需要的后效(由于每一个后效都会占用若干个RenderTexture,消耗显存,所以如果项目中有不需要的效果,则建议不要出现在列表中
let efxList = [
    PEFX_GrapScene, //抓取主摄像机的内容,供所有后效使用  要确保它是第一个
    PEFX_Glow,
    PEFX_Distortion,
    PEFX_Bloom,
    PEFX_Lut,
    PEFX_Final //最后呈现到屏幕上 要确保它是最后一个
];

PostEFXMgr.inst.setup(efxList, mainCamera, () => {
  //在这里对效果进行开关和参数调节。
  //开启Glow效果
  PostEFXMgr.inst.setEfxEnable(PEFX_Glow.NAME,true);
  let glow = PostEFXMgr.inst.getPEFX(PEFX_Glow.NAME) as PEFX_Glow;
  //设置Glow效果参数
  //设置模糊范围 值越大,GLOW的溢出边缘越大, 建议不要大于6.0
  glow.blurRadius = 4.5;
  //设置混合强度 值越大越亮
  glow.blendIntensity = 1.5;
});

上面的代码,是位于TestApp.ts文件,start函数中的初始化代码,效果使用的代码由于太多,这里只贴了glow作为示例。

问题:为什么setup函数要传递一个main Camera?

因为不同的项目,需求不同。有的项目只需要一个一个摄像机,有的项目需要多个,或者主摄像机的名字不叫Main Camera。*


这里传递的mainCamera就是用于最后呈现效果的Camera,以方便各类项目在不修改源码的情况下直接使用本方案。

问题:mainCamera.visibility &= ~Layer3D.UI_3D.mask的作用

答:这是一个渲染冲突引起的,我们发现,如果主摄像机渲染了UI_3D这个Layer的话,就会引起渲染异常。


由于目前版本(Cocos Creator v3.3.2)并不支持3D空间中的UI,所以最快的解决办法就是让主摄像机去掉UI_3D这个Layer

问题:为什么使用代码需要在setup的回调函数里调用?

1、这是因为所有后期效果需要用到的资源会先进行预加载,为了确保所有调用是在资源加载完成后执行,所以设计了这个完成回调



2、每一个PEFX都会继承自PEFX_Base并实现getRes函数,这函数返回的是这个PEFX依赖的图片和材质等素材。



3、PEFXMgrsetup函数会收集完所有依赖的素材并进行加载,加载完成后触发这个回调。

问题:efxList中的顺序影响效果吗

影响的

  • 1、由于PEFX_GrapScenePEFX_Final具有特殊性,因此必须保证PEFX_GrapScene是第一个,PEFX_Final是最后一个。
  • 2、PEFX_Lut由于是校色功能,因此推荐放到PEFX_Final之前。
  • 3、PEFX_Bloom由于是全屏泛光,因此推荐放到PEFX_Lut之前。
  • 4、PEFX_Glow由于是单个物体发光,因此推荐放到PEFX_Bloom之前。
  • 5、PEFX_Distortion由于是单个物体导致的扰动效果,同时对物体的自发光应该也要产生影响才对,因此推荐放到PEFX_Glow之后。

大家在做顺序编排的时候,要根据效果的作用进行放置,才能达到最佳效果。全局的靠后,针对单个物体的靠前。

4.2、整个方案的渲染流程是什么?

  • step 1:将主摄像机渲染的内容输出到一张RenderTexture(为了方便描述,我们给它取一个名字 full_scene_rt
  • step 2:对full_scene_rt效果处理P1
  • step 3:对full_scene_rt效果处理P2
  • step 4:对full_scene_rt效果处理…
  • step 5:对full_scene_rt效果处理Pn
  • step 6:将处理过的结果显示到屏幕上

问题:PEFX_GrapScenePEFX_Final的作用是什么?

PEFX_GrapScene就是负责将主摄像机渲染的3D场景内容抓取到RenderTexture上的。

虽然它不做任何的后期处理,但为了保持架构的一致性,也把它抽象为了一个PEFX,方便管理。

需要承认一个错误是,PEFX_GrapScene应该为PEFX_GrabScene,我打错单词了。但本文需要和源码保持一致,暂时就不动它。

PEFX_Final就是负责将最终的那张处理过的RenderTexture渲染到屏幕上。

因此,我们必须保证PEFX_GrapScene是第一个,PEFX_Final是最后一个。

问题:PEFX_Pass是什么意思?

PEFX_Pass就是执行的一次后期处理Shader的最小单位。

每一个PEFX_Pass有一个单独的Camera,一个单独的Quad

为了让Camera只渲染对应的Qaud,我们就需要为这个Quad分配一个独立的Layer,且将此CameraCulling Mask设置为这个Layer值。

一个PEFX_Pass大概包含以下内容:

PEFX_Pass:{
  Camera, //摄像机
  Quad, //用于渲染到屏幕大小的内容
  Material, //材质文件
  Effect, //对应的Shader文件
  Uniforms, //对应的参数
  Textures, //对应的纹理
}

问题:PEFX是什么意思?

PEFXPost Effect的缩写。
一个PEFX会包含一个或多个PEFX_Pass

比如PEFX_Lut它只有一个PEFX_Pass,而PEFX_Bllom则有4个PEFX_Pass。这个可以通过查看对应的源码文件,数一下它的setupPasses函数中有多少个addPass

问题:新增一个PEFX需要做哪些工作

我们PEFX_Glow为例。

  • 1、新建一个TS脚本文件,取名为PEFX_Glow.ts
  • 2、编写一个类PEFX_Glow并继承自PEFX_Base。
  • 3、新建Material相关文件, post-material-glow****
  • 4、新建Effect相关文件,post-effect-glow*****
  • 5、完成PEFX_Glow类的getRessetPasses函数。
  • 6、由于只有特殊标记的对象才会进行GLOW渲染,因此需要在Layer3D.ts中新增一个GLOWlayer,以用作特殊用途。
  • 7、编写GlowObject.ts,用来方便用户标记需要Glow的对象。
  • 8、将PEFX_Glow添加到setup的efxList表的适合位置。

由此,新增一个PEFX的工作就做完了。

4.3、实现过程中的一些技术要点

要点1:如何确保渲染的内容能撑满屏幕

由于是采用摄像机的方式渲染内容,那我们需要做的就是确保我们渲染的模型刚好与屏幕一样大。

但用户机型的分辨率是完全不一样的,难道我们需要针对不同机型分辨率动态生成对应的四边形吗?

也不是不可以的,但在这里,我们还有更简单的方式:

利用3D图形管线中NDC Space设备分辨率无关性

如上图所示,对图形管线熟悉的同学应该明白,当我们的顶点经过Projection Transformation(投影变换)之后,会进入Clipping Space(裁剪坐标系)。

Clipping Space中的顶点经过Perspective Divison(透视除法)后会进入NDC Space(标准化设备坐标系)。

NDC Space是一个 x,y,z的值域为(-1.0,1.0)的边长为2.0的立方体。

透视除法NDC:顶点坐标 P(x,y,z,1.0)经过MVP变换之后,会变成Clipping Space中的点Pc(x',y',z',w)。 点Pc,除以Pc.w后,就得到了NDC中的点 Pndc(x'/w,y'/w,z'/w,1.0)

NPC Space与设备分辨率的无关性是指NPC Space中的点经过Viewport Mapping(视口映射)后,才会转到屏幕坐标系。 而视口映射时,渲染目标才会参与运算。如果一个顶点在NDC Space中的x或者y的值为-1或者1,则表示它渲染出来的位置是在屏幕边缘。

基于上面的背景知识,我们就知道,不管我们是通过代码手工构建Mesh,还是通过美术建模Mesh,又或者通过Vertex Shader特殊处理Mesh,最终我们要的是让它撑满整个屏幕。

在介绍这个高端操作之前,我们先看一下对应的Vertex Shader代码:

  vec4 vert () {
    vec4 position;
    CCVertInput(position);
    
    v_uv = a_texCoord * tilingOffset.xy + tilingOffset.zw;
    //flip v_uv.y
    v_uv.y = 1.0 - v_uv.y;

    //z=0,w=1
    vec4 outPos = vec4(sign(position.x),sign(position.y),0,1);
    
    return outPos;
  }

你没看错,它确实就这么简单。

在vs中,使用了sign函数来让一个值变成-1或者1。这是麒麟子10年前学会的骚操作。

不管你的模型是什么形状,只要其顶点满足以下4种情况:

  • 1、有一个顶点的x小于0
  • 2、有一个顶点的y小于0
  • 3、有一个顶点的x小于0
  • 4、有一个顶点的y小于0

它就可以渲染出基于屏幕对齐的QUAD

注意,我们将其z=0,w=1。 这就能使QUADClipping SpaceNDC Space中的顶点值是一样的。

而w为何要强行给1,通过前面的顶点变换流程讲解我们可以理解,w=1的时候,整个运算过程将会变得非常简便,不易出错。

最终我们可以得到一个非常简单的运算过程,能非常容易地使得结果符合预期。

要点2:如何用代码生成一个带Quad Mesh的结点

当走到这一步的时候,我们随便扔一个Sphere或者Cube,甚至是其它Mesh给我们上面说的Shader,它都可以渲染出我们想要的撑满屏幕的Quad。但硬件运算能力能不浪费就不浪费,通过下面两个方案我们可以拿到一个Quad Mesh

  • 1、新建Quad节点,通过代码访问使用
  • 2、复制引擎internal中的Quad Meshresources目录采用resources.load进行动态加载
  • 3、用utils.createMesh构建一个Quad Mesh

前面两种方式,不需要特别讲解,基本上都会。我主要提一下第三种,因为创建Mesh的接口不是那么直观。

private static _quadMesh: Mesh = null;
public static get quadMesh(): Mesh {
    if (!this._quadMesh) {
        let positions = [-1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0, 1.0, 1.0, 0.0];
        let indices = [0, 1, 2, 0, 2, 3];
        let uvs = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0];
        this._quadMesh = utils.createMesh({ positions: positions, uvs: uvs, indices: indices });
    }
    return this._quadMesh;
}

如上面代码所示,我们构建了一个-1.0,1.0的四边形,并采用utils.createMesh接口进行创建。

注:utils.createMesh会重新开辟VAO,对显存性能的影响蛮大,不适合每帧调用。 千,万,不,要用来在update中动态更新模型。

使用这个QuadMesh的代码也非常简单,只需要将它赋值给MeshRenderer即可

let quadNode = new Node();
pass.quad = quadNode.addComponent(MeshRenderer);
pass.quad.mesh = PEFXResources.quadMesh;
let mat = PEFXResources.getRes(materialFilePath);
pass.quad.setMaterial(mat, 0);
pass.node.addChild(quadNode);

要点3:RenderTextureUsage的作用

RenderTextureUsage用来设置Camera输出的目标纹理信息。

当一个PEFX_Pass被添加RenderTextureUsage组件时,表示它需要将结果输出到一张RenderTexture上。

很显然,PEFX_Final是没有这个RenderTextureUsage组件的。

我们来看看RenderTextureUsage的属性:

  • 1、widthScale:number,heightScale:number:此RenderTexture相对于设备分辨宽、高的缩放,假如设备分辨率为1280x640,widthScaleheightScale设置为0.5就表示,它需要一张640x320大小的RenderTexture

  • 2、renderTextureUsage:stringRenderTexture的用途、名字。 假如赋值为full_scene_rt,则我们可以通过RTMgr.inst.getRenderTexture('full_scene_rt')获取到它。

  • 3、camera:Camera:此RenderTexture需要操作的目标摄像机,对于普通的PEFX来说,操作的是属于自己的PEFX_Pass的摄像机,对于GrabScene,或者深度图PEFX则直接使用主摄像机,这个在addPass的时候可进行设置。

要点4:RTAsSampler的作用

后期效果是对RenderTexture的连续处理,因此RTAsSampler的作用就是将为PEFX_Pass配置需要的RenderTexure

我们来看看RenderTexture的属性:

  • 1、model: MeshRenderer:每一个PEFX_Pass自己的那个Mesh Quad。持有它的引用是为了方便我们对其材质参数进行设置。

  • 2、samplerMap: string[]:用于配置需要的RenderTextureShaderTexture Sampler映射关系。

    PEFX_Glow最后一个Pass为例:['prev_rt:mainTexture','glow_rt2:bluredTexture']
    它表示,glow shader中的mainTexture需要使用prev_rt对应的那张RenderTextureglow shader中的bluredTexture需要使用glow_rt2对应的这张Render Texture

注:prev_rt是一个特殊的标记,它表示需要取获取上一个PEFX的结果。这是由于,我们的效果会按需开启,我们无法知道上一个开启的是谁。通过prev_rt这个特殊的名字,进行查找。源码请看:PostEFXMgr类中的getPrevRT函数

!!特别注意!!:一个Render Texture 不,能,同,时作为纹理参数渲染目标

要点5:SyncWidthMainCamera的作用

由于此方案未修改管线,目前无法做到利用MRT来做特效标记。

因此我们的Glow效果Distortion效果只能将需要的处理对象额外渲染一次到对应的Render Texture上。

而这个额外渲染一次用到的摄像机,则需要和主摄像机做同步。SyncWidthMainCamera的作用就是自动同步摄像机相关的参数。

要点6:RenderTexture的大小

let dpr = view.getDevicePixelRatio();
let size = view.getFrameSize();
width = widthArg * size.width * dpr;
height = heightArg * size.height * dpr;

上面的代码来自RTMgr.ts,想强调的是,在做RenderTexture分配的时候,getFrameSize需要乘以getDevicePixelRatio的值,才能保证效果清晰。在高分屏上,可能会分配出特别大的RenderTexture,造成显存开销大和像素填充率压力。

从实际测试来看,dpr >= 1.5 就可以满足需求。如果项目画面效果能接受,不妨加上let dpr = Math.min(view.getDevicePixelRatio(),1.5),以限制最大分辨率。

还有一个点就是,在窗口大小变化时,我们得响应resize事件,并调用所有Render Texturereset接口,重新设置宽高。

五、后期规划与可能提升的方向

    1. Cocos Creator v3.4即将发布,据说可以很方便地自定义后期效果管线,那样的话就可以通过管线来实现,减少多个Camera的开销,以及对Layer的依赖。
    1. 目前的效果无法在编辑器中预览,只能在运行时查看,有兴趣的朋友可以考虑改为编辑器中也能实时查看效果。
    1. 通过管线拿到深度图,实现更多有趣的效果,如TAADoF等。
    1. 借助MRT,优化GlowDistortion,减少特殊Pass的开销,RT可合用1张,正确的深度遮挡关系。


六、总结

以上就是关于KylinsPostEffects的故事、流程与源码剖析,相信看完这篇文章的小伙伴,就能够轻松的使用和修改它了。

如果还有疑惑的地方,请大家添加私信麒麟子。

关于诸如BLOOM,GLOW,DISTORTION,LUT等实现原理。麒麟子会在后面的文章中逐一和大家讲解。请大家关注麒麟子的自媒体账号即可。


长按二维码,即可关注麒麟子的公众号

关注转发是对一个作者最高的认可。

各位的认可,又将会成为作者写作的动力

知秋君
上一篇 2024-07-24 15:48
下一篇 2024-07-24 15:12

相关推荐