表面着色器(Surface Shader)
Surface Shader 使用统一渲染流程和结构,可以让用户以简洁的代码创建表面材质信息,指定用于组合的光照和着色模型。相比旧版(Legacy Shader)的优点是更易书写和维护,有更好的版本兼容性,也更不容易产生渲染错误。并且可以从统一流程中获取很多公共特性,如统一的全场景光照和 Debug View 调试功能等。
Creator 也更易扩展出多种常见的复杂材质提供给用户,未来还会支持 Shader Graph 自动生成 Effect 代码,可以极大提高 Shader 开发者的效率。但代价是无法定制光照和着色运算的具体内容,一旦指定了光照和着色模式,流程将按照既定路径来进行,不支持临时屏蔽或更改一些内部计算。如有这样的需求,请使用 Legacy Shader。
Surface Shader 仍然是基于 Cocos Effect 的语法,以前的材质参数、technique、pass 及渲染状态等的定义完全可以复用。
在了解 Surface Shader 之前,有几个概念需要说明一下:
说明了物体需要被渲染到哪里。
我们有很多渲染 Pass 用于渲染同一个物体到不同的纹理数据上,这些数据分别有内置的用途。比如说最常用的 渲染到场景 可以直接用于屏幕显示,或者把阴影投射物 渲染到阴影贴图,或者 渲染到动态环境反射 来生成反射贴图等。
这部分内容可以在 资源管理器 -> internal -> chunk -> shading-entries -> main-functions 内找到。
2、光照模型
说明物体表面的微观结构与固有光学属性是如何对光线产生影响和作用的。
比如说塑料会产生各向同性的圆形高光,头发会产生各向异性的条纹状高光,光线在皮肤上会发生散射,而在镜子这种更接近理想光学表面的物体上,绝大多数光线只会沿着反射角发生反射等。
光照模型名称 | 说明 |
---|---|
standard | PBR 光照,支持 GGX BRDF 分布的各向同性与各向异性光照,支持卷积环境光照 |
toon | 简单的卡通光照,阶梯状的光照效果 |
说明物体表面的一些物理参数(反照率、粗糙度等)是如何影响光照结果的。
通常材质与光照模型必须关联使用,我们会逐渐扩展常用的材质与光照模型。
材质模型名称 | 说明 |
---|---|
standard | 粗糙度和金属性描述的标准 PBR 材质,和 SP、Blender、Maya 等软件中的材质节点类似 |
toon | 简单的卡通材质,有多种 shade 颜色处理 |
4、Shader Stage
渲染是由不同的着色器来完成的,有处理顶点、像素或通用计算的不同阶段,如下表所示:
除了和 Cocos Effect 相同的 CCEffect 参数、technique 和 UBO 内存布局等定义之外,再也无需考虑各种内部宏定义的处理、着色器输入输出变量、Instancing、繁琐的顶点变换、全局像素效果和每种渲染用途的细节计算等。
典型的 Surface Shader 代码通常由三个部分组成:
- :将用户在 Effect 中声明或使用的宏名(部分)映射为 Surface 内部宏名。
Surface Functions
:用于声明表面材质信息相关的 Surface 函数。Shader Assembly
:用于组装每个顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)的代码模块。
此处以内置着色器 surfaces/standard.effect
为例,说明 Surface Shader 的代码框架。
Surface Shader 内部计算时会用到一些宏开关,需要根据 Effect 中对应含义的宏名来指定。考虑到 Effect 中的宏名会直接显示在材质面板上,这样做的好处是 Effect 中的名称可以自由开放给用户而不影响 Surface 内部计算。
宏名 | 类型 | 含义 |
---|---|---|
CC_SURFACES_USE_VERTEX_COLOR | BOOL | 是否使用顶点色 |
CC_SURFACES_USE_SECOND_UV | BOOL | 是否使用2uv |
CC_SURFACES_USE_TWO_SIDED | BOOL | 是否使用双面法线 |
CC_SURFACES_USE_TANGENT_SPACE | BOOL | 是否使用切空间(使用法线图或各向异性时必须开启) |
CC_SURFACES_TRANSFER_LOCAL_POS | BOOL | 是否在 FS 中访问模型空间坐标 |
CC_SURFACES_LIGHTING_ANISOTROPIC | BOOL | 是否开启各向异性材质 |
CC_SURFACES_LIGHTING_ANISOTROPIC_ENVCONVOLUTION_COUNT | UINT | 各向异性环境光卷积采样数,为 0 表示关闭卷积计算,仅当各向异性开启时有效 |
CC_SURFACES_USE_REFLECTION_DENOISE | BOOL | 是否开启环境反射除噪 |
CC_SURFACES_USE_LEGACY_COMPATIBLE_LIGHTING | BOOL | 是否开启 legacy 兼容光照模式,可使渲染效果和 legacy/standard.effect 完全一致,便于升级 |
搜索 CCProgram macro-remapping
一段,可以看到内容有如下三部分组成:
1、在 Surface 函数中未使用过的宏
由于 Surface Shader 精简了很多不必要的公共流程代码,如 VS FS 传参的定义等等,之前存在于旧流程中的 #if HAS_SECOND_UV
这样的代码也就不存在了。对于此类宏,必须要在此处预先定义 #pragma define-meta MACRONAME
,这样才可以显示在材质面板上。 定义好之后,下一行就可以使用标准 GLSL 预定义 #define CC_SURFACES_MACRONAME MACRONAME
。
2、在 Surface 函数中使用过的宏
// ui displayed macros used in this effect file
#define CC_SURFACES_USE_VERTEX_COLOR USE_VERTEX_COLOR
#if IS_ANISOTROPY || USE_NORMAL_MAP
#define CC_SURFACES_USE_TANGENT_SPACE 1
#endif
这部分简单多了,直接按照 #define CC_SURFACES_MACRONAME MACRONAME 定义即可。 不过 CC_SURFACES_USE_TANGENT_SPACE 宏要特别注意,通常开了法线贴图或各向异性,都要开启该宏,否则可能会出现编译错误。
3、内部功能性的宏
// functionality for each effect
直接定义想要的值即可。
Surface Function
每个材质函数的功能类似于 DCC(Digital Content Creation) 软件的材质编辑器中输出一个材质参数到指定的材质节点。类似于:
1、定义
可以使用 CCProgram
或单独的 chunk 来定义 Surface 材质函数块。
Surface Shader 在内部提供了简单的默认函数,所以 这些函数并不是必须定义的,如果你想重载某函数,需要预定义该函数对应的宏来完成。这些函数命名以 Surfaces + ShaderStage 名
打头,后跟功能描述。可以在 中查看不同材质模型中各 Surface 函数的具体定义与实现,如:
预先定义 CC_SURFACES_VERTEX_MODIFY_WORLD_POS
宏,可以忽略掉内部默认函数,让 Surface Shader 使用你定义的函数来计算材质参数。
2、VS 对应的函数列表
VS 中的处理和材质模型关系相对比较小,所以这里都使用通用函数,函数参数均为 SurfacesStandardVertexIntermediate
结构体,存放的是 VS 输入输出的数据。用户无需再关心具体的顶点输入输出流程处理,只需要聚焦到某个数据是否需要及如何修改。
预先定义宏 | 对应的函数定义 | 对应的材质模型 | 功能说明 |
---|---|---|---|
CC_SURFACES_VERTEX_MODIFY_LOCAL_POS | vec3 SurfacesVertexModifyLocalPos | Common | 返回修改后的模型空间坐标 |
CC_SURFACES_VERTEX_MODIFY_WORLD_POS | vec3 SurfacesVertexModifyWorldPos | Common | 返回修改后的世界空间坐标(世界空间动画) |
CC_SURFACES_VERTEX_MODIFY_CLIP_POS | vec4 SurfacesVertexModifyClipPos | Common | 返回修改后的剪裁(NDC)空间坐标(通常用于修改深度) |
CC_SURFACES_VERTEX_MODIFY_UV | void SurfacesVertexModifyUV | Common | 修改结构体内的 UV0 和 UV1 (使用 tiling 等) |
CC_SURFACES_VERTEX_MODIFY_WORLD_NORMAL | vec3 SurfacesVertexModifyWorldNormal | Common | 返回修改后的世界空间法线(世界空间动画) |
3、FS 对应的函数列表
FS 中的函数大部分是只修改一项,在 Surface 函数中直接返回即可。有些函数可能会修改多项(如 UV 和切空间向量),此时会在参数列表中传入多个值用于修改。具体属于哪种情况请参考函数定义。
4、VS 输入值的获取
VS 输入值都在 SurfacesStandardVertexIntermediate
结构体中,作为 Surface 函数参数传入
Vertex Shader 输入值 | 类型 | 使用时需要对应宏开启 | 含义 |
---|---|---|---|
position | vec4 | N/A | Local Position 局部坐标 |
normal | vec3 | N/A | Local Normal 局部法线 |
tangent | vec4 | CC_SURFACES_USE_TANGENT_SPACE | Local Tangent and Mirror Normal Sign 局部切线和镜像法线标记 |
color | vec4 | CC_SURFACES_USE_VERTEX_COLOR | Vertex Color 顶点色 |
texCoord | vec2 | N/A | UV0 |
texCoord1 | vec2 | CC_SURFACES_USE_SECOND_UV | UV1 |
clipPos | vec4 | N/A | Clip(NDC) Position 归一化设备坐标 |
worldPos | vec3 | N/A | World Position 世界坐标 |
worldNormal | vec4 | N/A | World Normal and Two Side Sign 世界法线和双面材质标记 |
worldTangent | vec3 | CC_SURFACES_USE_TANGENT_SPACE | World Tangent 世界切线 |
worldBinormal | vec3 | CC_SURFACES_USE_TANGENT_SPACE | World Binormal 世界副法线 |
5、FS 输入值的获取
FS 的输入值目前作为宏来使用,大部分输入值在内部做了容错处理,可以无视对应宏条件随意访问。
Fragment Shader 输入值 | 类型 | 使用时需要对应宏开启 | 含义 |
---|---|---|---|
FSInput_worldPos | vec3 | N/A | World Position 世界坐标 |
FSInput_worldNormal | vec3 | N/A | World Normal 世界法线 |
FSInput_faceSideSign | float | N/A | Two Side Sign 双面材质标记 |
FSInput_texcoord | vec2 | N/A | UV0 |
FSInput_texcoord1 | vec2 | N/A | UV1 |
FSInput_vertexColor | vec4 | N/A | Vertex Color 顶点颜色 |
FSInput_worldTangent | vec3 | N/A | World Tangent 世界切线 |
FSInput_mirrorNormal | float | N/A | Mirror Normal Sign 镜像法线标记 |
FSInput_localPos | vec4 | CC_SURFACES_TRANSFER_LOCAL_POS | Local Position 局部坐标 |
搜索 standard-fs
一段,可以看到整个 Fragment Shader 的组装过程分为 6 个部分
1、宏
首先需要包含必要的内部宏映射和通用宏定义。
宏映射使用在 Macro Remapping 一段中描述的自定义 CCProgram 代码块或 chunk 文件。
接下来需要包含通用宏定义文件 common-macros
,如下所示:
Pass standard-fs:
#include <surfaces/effect-macros/common-macros>
对于特殊渲染用途的 Pass 而言,很多 Shader 功能是关掉的,因此无需包含 common-macros
,直接包含对应渲染用途的宏定义文件即可:
Pass shadow-caster-fs:
#include <surfaces/effect-macros/render-to-shadowmap>
2、Shader 通用头文件
根据 当前的 Shader Stage 名称 来选择对应的通用头文件,如下所示:
3、用户 Surface 功能函数
使用在 Surface Function 一段中描述的自定义 CCProgram 代码块或 chunk 文件。
由于 Surface 功能函数可能还会用到 Effect 参数相关的 UBO 内存布局,因此它也要提前被 Include,否则会编译出错。
如下所示:
#include <shared-ubos>
#include <surface-fragment>
4、光照模型
此部分为可选项,只限渲染到场景的默认用途及 Fragment Shader 使用。
使用 光照模型名称 来选择对应的头文件,如下所示:
Standard PBR Lighting:
#include <lighting-models/includes/standard>
Toon Lighting:
#include <lighting-models/includes/toon>
5、表面材质和着色模型
此部分为 可选项,只限渲染到场景的默认用途使用。
材质模型名称 + Shader Stage 名称 来选择对应的头文件,如下所示:
6、Shader 主函数
使用当前 Pass 的渲染用途名称 + Shader Stage 名称 来选择对应的主函数头文件。
Pass standard-fs:
#include <shading-entries/main-functions/render-to-scene/fs>
Pass shadow-caster-fs:
#include <shading-entries/main-functions/render-to-shadowmap/fs>
使用 Surface Shader 框架后,内置的 Debug View 功能即可生效,通过在界面上选择对应的 Debug 模式即可同屏查看模型、材质、光照及其他计算数据,在渲染效果异常的时候可以快速定位问题。
此功能预计在 v3.6 开放。
- 自行添加 vs 输出与 fs 输入:VS新定义 varying 变量之后在某个 Surface 函数中计算并输出该值
- FS 新定义 varying 变量之后在某个 Surface 函数中获取并使用该值
可以在 资源管理器 -> internal -> chunks -> common 文件夹下找到不同分类的函数库头文件。
库中的函数不依赖任何内部数据(引擎相关 uniform 和贴图等),可以当作工具函数直接使用。