基于 Blinn-Phong 的高性能 Shader,支持阴影和环境反射
共 9602字,需浏览 20分钟
·
2023-08-30 14:24
引言:社区高产大户孙二喵同学今天给大家带来了全套的传统光照模型 Shader,并集成了 Cocos Creator 的光照、阴影和环境反射能力,让你在渲染效果和性能之间自由权衡。
正文开始
Cocos Creator 引擎的 3D 渲染功能,从一开始就支持了标准的现代化渲染流程,如 PBR材质、HDR 渲染等等。但对于一些算力紧张或者对发热耗电控制严格的平台,我们需要一些更为低功耗的渲染方式来降低渲染开销。基于这个需求,我基于传统光照模型重写了一套高性能的 Cocos Shader,并且能够利用 Cocos Creator 内置的光照、阴影和环境反射能力。希望能给有需要的开发者带来帮助。
- 体验地址:
-
源码地址:
https://store.cocos.com/app/detail/5256
在计算机图形学中,光照模型(Lighting Model)用于模拟物体表面在光线照射下的反射效果。本文使用 Cocos Creator 来演示常用的光照模型的效果和示例代码(使用GLSL语言)。
Unlit(无光照)
无光照模型并不考虑光照影响,只将物体的颜色或纹理直接渲染到屏幕。这种模型适用于不需要光照影响的场景,例如广告牌或者地面指引等特效。
实现代码如下:
void main()
{
vec4 o = mainColor; //材质颜色
return CCFragOutput(o);
}
Lambert(兰伯特)
兰伯特光照模型是一种描述漫反射的光照模型,其假设物体表面对光的反射不依赖于观察者的位置。这种模型常用于模拟非金属、非镜面的物体表面。
实现代码如下:
void Lambert(inout vec4 diffuseColor,in vec3 normal)
{
vec3 N = normalize(normal);
vec3 L = normalize(cc_mainLitDir.xyz * -1.0);
float NL = max(dot(N, L), 0.0);
vec3 diffuse = NL * (diffuseColor.rgb * cc_mainLitColor.xyz);
vec3 ambient = cc_ambientGround.rgb * diffuseColor.rgb * cc_ambientSky.w;
diffuseColor.rgb = ambient + diffuse;
}
Half Lambert(半兰伯特)
半兰伯特光照模型是兰伯特光照模型的一个变体,它改变了对光线反射的解释,使得在光线与法线成 90 度角时,反射强度为 0.5 而非 0 ,从而使阴影部分不那么暗,这里做了下优化增加了 diffuseWrap 的参数,用 pow(NL * diffuseWrap + (1.-diffuseWrap),2.0)
代替 pow(NL *0.5 +00.5,2.)
。此光照模型常用于卡通或非真实感渲染。
相比兰伯特模型,半兰伯特模型的阴影部分不那么暗(下图左侧),我们也可以通过 diffuseWrap 去控制暗部的阴影强度。
实现代码如下:
void HalfLambert(inout vec4 diffuseColor,in vec3 normal)
{
vec3 N = normalize(normal);
vec3 L = normalize(cc_mainLitDir.xyz * -1.0);
float NL = max(dot(N, L), 0.0);
vec3 diffuse = pow(NL * diffuseWrap + (1.-diffuseWrap),2.0) * (diffuseColor.rgb * cc_mainLitColor.xyz);
vec3 ambient = cc_ambientGround.rgb * diffuseColor.rgb * cc_ambientSky.w;
diffuseColor.rgb = ambient + diffuse;
}
Blinn-Phong(布林-冯)
Blinn-Phong 光照模型是 Phong 光照模型的改进版,它引入了 "半向量" 的概念,使得镜面高光的计算更加高效。适用于模拟有光泽的物体表面。
实现代码如下:
void void blinnPhong(inout vec4 diffuseColor,in vec3 normal)
{
vec3 N = normalize(normal);
vec3 L = normalize(cc_mainLitDir.xyz * -1.0);
float NL = max(dot(N, L), 0.0);
vec3 diffuse = NL * diffuseColor.rgb * cc_mainLitColor.xyz;
vec3 position;
HIGHP_VALUE_FROM_STRUCT_DEFINED(position, v_position);
vec3 cameraPosition = cc_cameraPos.xyz / cc_cameraPos.w;
vec3 V = normalize(cameraPosition- position);
vec3 H = normalize(L + V);
float specularFactor = pow(max(0.0, dot(H,N)), bpParams.x*50.);
vec3 specular = (specularFactor * cc_ambientSky.rgb * cc_mainLitColor.xyz);
float shadowCtrl = 1.0;
#if CC_RECEIVE_SHADOW && CC_SHADOW_TYPE == CC_SHADOW_MAP
if (NL > 0.0) {
#if CC_DIR_LIGHT_SHADOW_TYPE == CC_DIR_LIGHT_SHADOW_CASCADED
shadowCtrl = CCCSMFactorBase(position, N, v_shadowBias);
#endif
#if CC_DIR_LIGHT_SHADOW_TYPE == CC_DIR_LIGHT_SHADOW_UNIFORM
shadowCtrl = CCShadowFactorBase(CC_SHADOW_POSITION, N, v_shadowBias);
#endif
}
#endif
diffuse = (diffuse + specular) * (shadowCtrl);
}
Toon(卡通着色)
卡通模型或称 Cel-Shading,它通过将光照强度量离散化为几个等级,模拟出手绘动画的效果。适用于卡通或者艺术风格的渲染。
实现代码如下:
void ToonShading (inout vec4 diffuseColor,in vec3 normal) {
vec3 position;
HIGHP_VALUE_FROM_STRUCT_DEFINED(position, v_position);
vec3 V = normalize(cc_cameraPos.xyz - position);
vec3 N = normalize(normal);
vec3 L = normalize(-cc_mainLitDir.xyz);
float NL = 0.5 * dot(N, L) + 0.5;
float NH = 0.5 * dot(normalize(V + L), N) + 0.5;
vec3 lightColor = cc_mainLitColor.rgb * (cc_mainLitColor.w * shadeParams.x);
float shadeFeather = shadeParams.y;
float shadeCtrl = mix(1., (1.-shadeParams.z), clamp(1.0 + (shadeParams.x - shadeFeather - NL) / shadeFeather, 0.0, 1.0));
shadeCtrl *= mix(1., (1.-shadeParams.z*0.5), clamp(1.0 + (shadeParams.w - shadeFeather - NL) / shadeFeather, 0.0, 1.0));
float specularWeight = 1.0 - pow(specularParams.x, 5.0);
float specularMask = 1.0-smoothstep( NH, NH+ specularParams.y, specularWeight + EPSILON_LOWP);
float shadowCtrl = 1.0;
#if CC_RECEIVE_SHADOW && CC_SHADOW_TYPE == CC_SHADOW_MAP
if (NL > 0.0) {
#if CC_DIR_LIGHT_SHADOW_TYPE == CC_DIR_LIGHT_SHADOW_CASCADED
shadowCtrl = CCCSMFactorBase(position, N, v_shadowBias+0.1);
#endif
#if CC_DIR_LIGHT_SHADOW_TYPE == CC_DIR_LIGHT_SHADOW_UNIFORM
shadowCtrl = CCShadowFactorBase(CC_SHADOW_POSITION, N, v_shadowBias+0.1);
#endif
}
#endif
float diffuseCtrl = (shadowCtrl+specularMask*specularParams.z)*shadeCtrl;
vec3 envColor = cc_ambientGround.rgb*cc_ambientSky.w;
diffuseColor.rgb *= (envColor + (lightColor*diffuseCtrl));
}
我们还可以通过边缘光(Rim Light)实现不同风格化渲染风格。
实现代码如下:
#if USE_RIM_LIGHT
float fRim = (1.0 - dot(v_view_normal,vec3(0,0,1.0))) * rimColor.w;
color.rgb = mix(color.rgb,rimColor.rgb,fRim);
#endif
PBR vs. Blinn-Phong
PBR - Physically Based Rendering, 是最新的光照模型,它试图更真实地模拟光线与物体表面的相互作用,包括漫反射和镜面反射。PBR 模型通常包括能量守恒和菲涅耳等效等物理原理,适用于模拟真实世界的渲染。
PBR 是几乎是所有现代标准图形引擎默认的光照模型,但 PBR 由于涉及过多的公式计算和贴图采样,它的 Shader 代码非常复杂,对用户设备算力要求较高。对这块有兴趣的同学可以直接查看 Cocos 引擎最新版本的内置 Shader 源码。
https://github.com/cocos/cocos-engine/tree/develop/editor/assets/chunks
但在很多情况下,我们不用 PBR 也能渲染出可接受的效果。
如下图所示:
左:Blinn-Phong,右:PBR
不同的光照模型适用于不同的渲染风格,可以根据具体的需求和场景来选择使用。例如,无光照模型适合广告牌或者地面指引,兰伯特光照模型适合无光泽的表面,卡通模型适合卡通或手绘风格的渲染,而 PBR 则适合模拟真实世界的高质量渲染。
常用术语
在 Shader 中,无论我们使用哪一种光照模型,都有一些通用的技术和术语,它们各自承担着不同的功能和目的。以下是一些概念的解释:
颜色(Color):这通常是一个 RGBA 值,表示一个像素的基本颜色。R、G、B分别代表红色、绿色和蓝色,A 代表透明度。这些值通常在 0 到 1 之间。
Albedo Map:不带任何光照信息的颜色贴图,主要表示物体表面的固有颜色,不受光照影响,通常用在 PBR 光照模型中。
Diffuse Map:漫反射贴图,可能会携带一些光照信息,比如 AO,Shading 等。一般用于传统光照模型。
Alpha Test:Alpha 测试是一种通过比较像素的 Alpha 值和预设阈值来决定是否丢弃像素的技术。这种技术常常用于实现透明和半透明效果。
实现代码如下:
#if USE_ALPHA_TEST
if (color.ALPHA_TEST_CHANNEL < colorScaleAndCutoff.w) discard;
#endif
Normal Map:Normal Mapping 是一种用于模拟表面细节的技术。它使用一张贴图来存储向量,这个向量描述了表面在每一点上的法线方向,使得物体表面看起来有更多的细节。
Emissive Map:Emissive Map 是一种纹理贴图,用于表示物体在没有外部光照的情况下自发的颜色和亮度。
Fog:雾是一种用于模拟大气效果的技术,它可以使远离观察者的物体看起来更模糊,颜色也会向雾的颜色过渡。
Image-Based Lighting (IBL):IBL 是一种使用环境反射贴图来模拟环境光照的技术。它可以产生更真实的反射和光照效果。
实现代码如下:
#if CC_USE_IBL && USE_IBL
vec3 cameraPosition = cc_cameraPos.xyz / cc_cameraPos.w;
vec3 V = normalize(cameraPosition- position);
vec3 env = vec3(1.);
vec3 R = normalize(reflect(-V, N));
vec3 rotationDir = RotationVecFromAxisY(R.xyz, cc_surfaceTransform.z, cc_surfaceTransform.w);
vec4 envmap = fragTextureLod(cc_environment, rotationDir, bpParams.y * (cc_ambientGround.w - 1.0));
#if CC_USE_IBL == IBL_RGBE
env = unpackRGBE(envmap);
#else
env = SRGBToLinear(envmap.rgb);
#endif
diffuse = mix(env, diffuse, bpParams.x);
#endif
vec3 ambient = cc_ambientGround.rgb * diffuseColor.rgb * cc_ambientSky.w;
diffuseColor.rgb = ambient + diffuse;
}
Rim Light: 边缘光是一种模拟物体边缘被背光照亮的效果的技术,可以增加3D模型的立体感。
作用原理
上面提到的这些技术,步骤通常在 Fragment Shader 中以以下的顺序进行:
1. 颜色:首先,你需要知道物体的基本颜色,这通常通过读取Albedo贴图来实现。
2. Normal Map:然后,你可以应用Normal Map来改变物体表面的法线,从而模拟出更多的细节。
3. 光照计算:接着,你可以进行光照计算,这通常包括环境光、漫反射光、镜面反射光等的计算。在计算过程中,你可能会用到Rim Light来增加边缘的亮度。
4. IBL:然后,你可以根据全景图片来计算IBL,使得环境的反射和阴影效果更真实。 5. Emissive Map:然后,你可以加上Emissive Map,使物体能在没有光源的情况下发光。
6. Alpha Test:最后,你可以进行Alpha测试,根据测试结果决定是否丢弃像素。
7. Fog:在所有的颜色和光照计算完毕后,你可以应用Fog效果,使远离观察者的物体颜色向雾的颜色过渡。
性能分析
由于 PC 的 GPU 运算能力和带宽都比较强大,在处理这些光照模型时候,性能几乎相差不大。手机中的高端机型也不会受很大影响,只有少数低端机上 PBR 性能会弱于 Lambert。
同时我们观察到,使用阴影和描边(Outline)会使得顶点数翻倍,这是性能下降的主要原因,这主要有以下两个原因:
阴影生成:阴影通常是通过生成阴影映射(Shadow Map)来实现的。在这个过程中,场景需要从光源的视角进行一次额外的渲染。这意味着每个顶点需要被再次处理和光栅化,使得顶点数翻倍。
描边生成额外的几何体:在原始模型的基础上生成一个稍大的版本,然后渲染这个大版本的反面,形成描边效果。这种方法会导致顶点数翻倍,因为你实际上是渲染了两个模型。
通常我们可以通过开启 GPU Instancing
来提升游戏性能。
如果模型有骨骼动画,建议启用烘焙动画来配合 GPU Instancing
使用。
所以建议大家还是基于游戏风格去选择光照模型,实测下来,PBR 性能弱的主要原因是开启了 IBL。
源码免费获取
针对 Blinn-Phong,Lambert 模型内也写了简化版的 IBL,大家可以从 Cocos Store 下载全套源码包进行了解:
地址 : https://store.cocos.com/app/detail/5256
点击【阅读原文】可快速跳转,免费的哟。
关注 Cocos 引擎官方公众号,你会变得更强!