文章目录
前向渲染管线和延迟渲染管线
- 前向渲染:基于对几何体和光源的单独计算,当场景中存在大量光源和模型时,性能会有问题
- 延迟渲染:先使用gbuffer对几何体构造阶段的数据进行缓冲,再根据gbuffer中顶点的深度信息,剔除没必要的计算
- GBuffer 由一堆 RenderTexture 构成,保存了用于光照计算的信息,如位置、法线、颜色、材质参数等等
开发Cocos迷失岛和死亡地牢 回顾收获
-
统一管理数据
,使用全局单例的数据中心,来管理游戏数据,使用get/set方法访问数据,数据更新时触发事件,然后通知视图层进行渲染更新。 -
事件驱动视图更新,事件驱动,
on
/off
/emit
/once
等事件方法,简化视图层渲染更新逻辑。 -
用户操作类事件,推荐使用代码进行绑定,而非在编辑器中绑定。
-
property
装饰器,简化属性绑定,方便代码逻辑优化。 -
getCompoent
获取节点组件,诸如Button,Sprite,UITransform等,设置对应组件属性(Butto.interactable),调用方法。 -
prefab
预制体的使用,简化节点复用 instaniate 方法实例化 -
获取父子节点,兄弟节点,
find
/getChildByName
/getChildByPath
-
resources.load
方法加载资源,异步加载,回调中获取资源,统一使用promise封装异步加载 -
重复利用资源放入节点池,避免反复创建节点
-
动画组件
animationClip
,动画状态机,动画事件回调,可使用director.loadScene
方法进行场景切换 -
构建随机棋盘,计算棋盘网格线,计算移动路径,计算是否成功
-
摄像头设置震动状态,利用sin函数计算震动幅度,在update函数中更新摄像机位置
Cocos编辑器相关
SkeletalAnimation
动画组件,专门用于控制动画的播放和设置。
模型节点:
Body模型节点拥有一个蒙皮网格渲染器组件 SkinnedMeshRenderer
:
- Materials: 模型使用的材质;
- LightingMap: 光照烘培相关;
- Mesh: 网格对象
- Skeleton: 骨骼信息
- SkinningRoot: 动画组件所在节点;
骨骼动画组件 SkeletalAnimation
组件:
- 预烘焙动画是提前把模型的顶点在动画中不同的位置对应到一张纹理,播放动画时无需计算,直接查寻对应纹理即可
- 实时计算: Animation +动画信息Clip +蒙皮网格的节点,计算出来我们节点的位置,交由GPU绘制
- 实时计算的有点:表现力更好,节约内存空间,性能计算量大;
- 预烘培:性能好,表现力没有那么好,消耗一张纹理空间
可在FBX模型上进行动画剪辑:
const anim = this.node.getComponent(SkeletalAnimation);
anim包含一系列可用于操作动画的方法,也只是订阅对应的事件。
绑定碰撞模块节点到骨骼:
- 需要在SkeletalAnimation节点的sockets字段新增挂点。
- 挂点path属性:表示当前挂点要放在哪个骨骼节点下。
- 挂点target属性:表示场景中需要挂载的碰撞模块节点。
摄像机设置
天空盒:在场景节点中打开天空盒开关同时,还需将摄像机的 ClearFlags
设为 SKYBOX
。
摄像机属性:
Visibility
- 多选,用于设置需要显示的层级
- 每个节点都有Layer属性,通过位运算定义的层级,如Default即 1 << 30
- Visibility与Layer做与运算,得到1即代表层级显示
Priority
:场景中值越小的摄像机,拥有绘制权Projection
:- 透视摄像机符合近大远小的规律
- 正交摄像机不会根据摄像机与物体的距离,改变物体绘制大小
Fov Axis
:- Vertical:相机的视场角是沿着水平方向进行测量和调整的。这意味着你可以控制画面左右两侧能够看到的范围
- Horizontal:设置为垂直时,视场角的测量和调整是沿着垂直方向进行的。这对于需要关注上下范围的场景比较有用
Fov
:视场角影响摄像机投影屏幕的大小,角度越大,可视范围越大Near&Far
:最近和最远距离,只会显示在这两个值之间的内容Rect
:- x,y若都设置为0.5,表示绘制从屏幕中心开始
- w,h表示与屏幕的宽高比,默认为1,表示绘制大小与屏幕尺寸保持一致
Target Texture
:将一张纹理绘制到屏幕,可用于进行摄像机截图操作
光照设置及阴影设置
光照需要注意对于设置了受光材质
的物体才有效:
- 平行光:只对支持光的shader材质才有效,所以要支持阴影的shader才对光有反应
- unlit材质:cocos内置有这类shader,不受光照影响
阴影的生成:
- 打开场景节点的
Shadows Enabled
开关。 - 接受阴影的物体,要打开接受阴影开关。
- 被光照的物体,要打开投射阴影开关。
- 光照的视锥体影响范围也要注意,超出范围,也可能导致阴影消失。
字体缓冲模式
Label
文本的缓冲模式:
NONE
: 每个文本的内容都会绘制在一个纹理里面,每个label都有这样的纹理BITMAP
: 将文本作为静态图像加入动态图集进行批次合并,但是不能频繁动态修改文本内容。频繁修改反而会导致性能下降,因为批处理的是所有图集CHAR
: 将文本拆分为字符并且把字符纹理缓存到一张字符图集中进行复用,适用于字符内容重复并且频繁更新的文本内容。
分辨率
UI系统中分辨率分为:
屏幕像素分辨率
:不同设备的屏幕像素点不同逻辑分辨率
:图片资源分辨率与屏显分辨率的比值(其实没啥意义,内部计算会用到的一个比例值),资源在与屏幕尺寸不一致的情况下,如果想要资源继续平铺到全屏幕,可在项目设置中取消适配屏幕宽度和高度,这样资源就会进行缩放和拉伸,例如19201080在960640的屏幕上显示,显示高度540的内容要在640的范围做平铺。
适配各类屏幕分辨率,项目设置中设置固定高度,使用脚本获取屏幕宽度,然后对组件宽度进行缩放,或设置锚点,始终定义在屏幕的左右两侧。
Layout组件和Widget组件
Layout
组件:
- NONE,不会对子节点进行自动布局
- HORIZONTAL,横向自动排布子物体
- VERTICAL,垂直自动排布子物体
- GRID, 采用网格方式对子物体自动进行布局
- resizeMode缩放模式,包括:
- NONE,不会对子节点和容器进行大小缩放
- CONTAINER, 对容器的大小进行缩放
- CHILDREN, 对子节点的大小进行缩放
- resizeMode缩放模式,包括:
Widget
组件:
- target属性:指定一个对齐目标,默认为当前父节点
- 设置水平和竖向的对齐方式
Cocos3D场景DrawCall优化
- 静态合批:
- 调用
BatchingUtility.batchStaticModel
可进行静态合批 - 只有满足以下条件的节点才能进行静态合批:
- 子节点中只能包含
MeshRenderer
; - 子节点下的
MeshRenderer
的 Mesh 的顶点数据结构必须一致; - 子节点下的
MeshRenderer
的材质必须相同。
- 子节点中只能包含
- 如果可以,请让美术直接合并一些静态场景的模型,如地图
- 调用
- 动态合批
gpu instancing
合批:通过 Instancing 的合批适用于绘制大量顶点数据完全相同的动态模型,启用后绘制时会根据材质和顶点数据分组,每组内组织 instanced attributes 信息,然后一次性完成绘制。- vb合批:动态合批会增加CPU负担,由于CPU侧每帧(或者标记为需要重新合并时)都会进行Mesh合并,所以不适合合并的顶点数过多的情况,否则可能得不偿失。vb合批只要材质相同就会合并成一个模型渲染,因为顶点合并操作导致Cpu占用提升,已移除,
Cocos2D的DrawCall优化
- 2D相当于3D的一个面片 + 一张纹理,所以可使用
gpu instancing
合批 - 尽可能做到同一张纹理的物体在一起,不要让其他图集打乱合批
- Label = 使用Sprite的shader,使其生成一张纹理
- Label会打断DrawCall
- A1A2A3L1A4: L1会打断DrawCall -> 3个DrawCall
- 调整为:A1A2A3A4L1 -> 2个DrawCall
- 类似节点请放在一起,方便合批
- 如果有层级问题,可使用带有深度测试的贴图,使用z坐标进行控制(因为2D摄像机都是正交相机,z值不会影响缩放)
ts脚本定义枚举值属性:
import {Enum} from 'cc'
const Position = Enum({
left: "left",
right: "right",
center: "center"
})
@property({ type: Position })
public alignType = Position.left; // 对齐类型,默认为左对齐
3D项目开发2D游戏时需要注意
在制作卡牌游戏时,操作卡牌的多个节点时,在图层z属性
相差不多时,需要注意操作scale属性
时,不要忘记同时操作z维度,这个问题导致卡牌的图层出现了一些细微的错位,因为我只设置了x和y,但是其实我也有使用z来控制卡牌的多层级结构。
基于asset bundle的加载
- 将一些资源划入一个资源包里面 -> 资产包,又叫ab包;
- 资源包的划分是给开发者任意来划分;
- 每个资源文件夹都可配置为一个资源包; 资源包的名字默认是文件夹的名字; 我们的资源都在资源包里面; 步骤1:加载资源包; Step2:从资源包里面加载资源:; 第002节】加载/释放资源包: 资产管理器:全局单例,类型是资产管理器类型;
项目打包
- 安卓打包
-
安卓环境搭建:
- 安装JDK
- 安装Android Studio
- 安装SDK NDK
- 创建项目专属的密钥签名,注意记录密码和别名
-
游戏项目设置
- 横屏游戏记得在项目设置里面勾选适配宽度和高度
- 在cocos构建目录选择NDK和SDK路径
-
游戏项目cocos打包
- 选择安卓平台
- 选择主场景
- 选择在上面创建的密钥,设置好密钥库路径/密码/别名/别名密码
-
游戏项目安卓打包
- 打开Android Studio
- 打开项目文件夹
- 等待项目构建完成,获取apk文件
- 微信小游戏打包
- 安装微信开发者工具,注意在安全设置打开服务端口,否则cocos无法调起微信小游戏调试器
- 申请AppID
- cocos构建配置,填写AppID
- 构建微信小游戏
- 微信开发者工具注意选择小游戏来打开项目,调试游戏
着色器
顶点着色器
顶点着色器之后,并不会直接传递给像素着色器,而是会先把顶点着色器输出的东西进行插值、像素化。
这个过程有一个术语叫:光栅化
,三角形经过光栅化后,变成了一个个像素
除了顶点位置信息
,顶点法线
、颜色
、纹理坐标
等都会先经过光栅化,再传递给像素着色器。
由于所有vert输出的值都会被光栅化,所以顶点着色器传递到像素着色器的法线向量,在使用的时候,记得先normalize,否则会有意想不到的效果。
宝石特效
片元着色器使用varying变量的时候要保证,与顶点着色器要一一对应,有时候不同的pass的顶点着色器,可能对应同一个片元着色器函数。
// Effect Syntax Guide: https://docs.cocos.com/creator/manual/zh/shader/index.html
CCEffect %{
techniques:
- name: opaque
passes:
- vert: unlit-vs:vert
frag: unlit-fs:frag
properties: &props
mainTexture: { value: white }
mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
power: { value: 0.5}
- name: transparent
passes:
- vert: unlit-vs:vert # builtin header
frag: unlit-fs:frag
blendState:
targets:
- blend: true
blendSrc: src_alpha
blendDst: one_minus_src_alpha
blendSrcAlpha: src_alpha
blendDstAlpha: one_minus_src_alpha
properties: *props
}%
CCProgram unlit-vs %{
precision highp float;
#include <legacy/input-standard>
#include <builtin/uniforms/cc-global>
#include <legacy/local-batch>
#include <legacy/input-standard>
#include <legacy/fog-vs>
#include <legacy/shadow-map-vs>
in vec4 a_color;
#if HAS_SECOND_UV
in vec2 a_texCoord1;
#endif
out vec3 v_position;
out vec3 v_normal;
out vec3 v_dir;
out vec3 v_tangent;
out vec3 v_bitangent;
out vec2 v_uv;
out vec2 v_uv1;
out vec4 v_color;
vec4 vert () {
StandardVertInput In;
CCVertInput(In);
mat4 matWorld, matWorldIT;
CCGetWorldMatrixFull(matWorld, matWorldIT);
vec4 pos = matWorld * In.position;
v_dir = normalize((cc_matView * pos).xyz);
v_position = pos.xyz;
v_normal = normalize((matWorldIT * vec4(In.normal, 0.0)).xyz);
v_tangent = normalize((matWorld * vec4(In.tangent.xyz, 0.0)).xyz);
v_bitangent = cross(v_normal, v_tangent) * In.tangent.w; // note the cross order
v_uv = a_texCoord;
#if HAS_SECOND_UV
v_uv1 = a_texCoord1;
#endif
v_color = a_color;
CC_TRANSFER_FOG(pos);
CC_TRANSFER_SHADOW(pos);
return cc_matProj * (cc_matView * matWorld) * In.position;
}
}%
CCProgram unlit-fs %{
precision highp float;
#include <legacy/output>
#include <legacy/fog-fs>
in vec2 v_uv;
in vec3 v_position;
in vec3 v_normal;
in vec3 v_dir;
uniform sampler2D mainTexture;
uniform Constant {
vec4 mainColor;
float power;
};
vec4 frag () {
vec4 col = mainColor;
float stren = dot(v_dir, v_normal); // 通过单位视向量与单位法向量获取夹角cos值
col *= pow(abs(stren), power); // abs取绝对值,通过可控参数,控制强度
return CCFragOutput(col);
}
}%
Cocos uniform在shader中定义的规范
Cocos Shader规定,所有非 sampler 类型的 uniform 都应以 UBO(Uniform Buffer Object/Uniform Block)形式声明。
- 不应出现 vec3 成员;
- 对数组类型成员,每个元素 size 不能小于 vec4;
- 不允许任何会引入 padding 的成员声明顺序。
所有成员实际的偏移量(即成员在整体结构中的位置偏离起始位置的距离),是以自身所占用字节数为单位进行对齐的。例如,若一个成员占 4 个字节,那么它在 UBO 中的偏移量会是 4 的倍数,这样做通常是为了提高数据访问效率和内存管理的合理性,让硬件在读取数据时能更高效地定位和处理各个成员。 对于 32 位处理器,字长为 8 字节。如果一个 8字节的变量未对齐存储,可能需要两次内存访问才能读取完整的变量值,而对齐存储时只需一次内存访问。
UBO对内存管理的优化:
uniform IncorrectUBOOrder {
float f1_1; // offset 0, length 4 (aligned to 4 bytes)
vec2 v2; // offset 8, length 8 (aligned to 8 bytes) [IMPLICIT PADDING!]
float f1_2; // offset 16, length 4 (aligned to 4 bytes)
}; // total of 32 bytes
uniform CorrectUBOOrder {
float f1_1; // offset 0, length 4 (aligned to 4 bytes)
float f1_2; // offset 4, length 4 (aligned to 4 bytes)
vec2 v2; // offset 8, length 8 (aligned to 8 bytes)
}; // total of 16 bytes
aligned to 4 bytes
意味着数据项的起始地址必须是 4 的倍数,aligned to 8 bytes
则表示起始地址必须是 8 的倍数。vec2 v2前面是两个四字节数,刚好是从8开始的,所以后一种定义方式更好
f1_1 和 f1_2 对齐到相同位置的原因
- float 类型通常占用 4 个字节。根据字节对齐规则,它的偏移量(offset)为 0,并且是按 4 字节对齐的。这是因为在 UBO 中,它是第一个成员,所以从偏移量 0 开始存储,而 0 是 4 的倍数,满足 4 字节对齐的要求。
- 同样是 float 类型,占用 4 个字节。由于 f1_1 已经占用了 4 个字节,所以 f1_2 的起始偏移量为 4。4 也是 4 的倍数,因此 f1_2 也满足 4 字节对齐的要求。
Shader练习题
要求横向分三块,0-0.25为粉色,0.25-0.75为黑色,0.75-1为粉色。代码如下,注释代码是我写的,标准答案更简洁:
uniform vec2 iResolution;
void main() {
// Normalized pixel coordinates (from 0 to 1)
vec2 uv = gl_FragCoord.xy / iResolution.xy;
vec3 color = vec3(1.0, 0.3, 0.3);
// vec3 color = vec3(step(0.25, abs(uv.x-0.5)), step(0.25, abs(uv.x-0.5)), step(0.25, abs(uv.x-0.5)));
// color = vec3(min(color.x, 1.0),min(color.x, 0.3),min(color.x, 0.3));
float t1 = 1.0 - step(0.25, uv.x);
float t2 = step(0.75, uv.x);
gl_FragColor = vec4(color * max(t1, t2), 1.0);
}