3D 着色器:RimLight

    RimLight 也称为“内发光”/“轮廓光”/“边缘光”(本文统一使用边缘光),是一种通过使物体的边缘发出高亮,让物体更加生动的技术。

    RimLight 是菲涅尔现象1的一种应用,通过计算物体法线和视角方向的夹角的大小,调整发光的位置和颜色,是一种简单且高效的提升渲染效果的着色器。

    在边缘光的计算中,视线和法线的夹角越大,则边缘光越明显。

    首先参考 新建一个名为 rimlight.effect 的着色器,并创建一个使用该着色器的材质 rimlight.mtl

    create rimlight

    Cocos Effect 使用 YAML 作为解析器,因此 CCEffect 的写法需要遵守 YAML 的语法标准,对于这块的内容可以参考 YAML 101

    在本示例中,将暂时不考虑半透明效果,此时可以将 部分删掉。

    opaque 部分的 frag 函数修改为: rimlight-fs:frag,这是接下来要实现的边缘光的片元着色器部分。

    1. - name: opaque
    2. passes:
    3. - vert: general-vs:vert # builtin header
    4. frag: rimlight-fs:frag

    为了方便调整边缘光的颜色,增加一个用于调整边缘光颜色的属性 rimLightColor,由于不考虑半透明,只使用该颜色的 RGB 通道:

    1. rimLightColor: { value: [1.0, 1.0, 1.0], # RGB 的默认值
    2. target: rimColor.rgb, # 绑定到 Uniform rimColor 的 RGB 通道上
    3. editor: { # 在 material 的属性检查器内的样式定义
    4. displayName: Rim Color, # 显示 Rim Color 作为显示名称
    5. type: color } } # 该字段的类型为颜色值

    此时的 CCEffect 代码:

    1. CCEffect %{
    2. techniques:
    3. - name: opaque
    4. passes:
    5. - vert: general-vs:vert # builtin header
    6. frag: rimlight-fs:frag
    7. properties: &props
    8. mainTexture: { value: white }
    9. mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
    10. # Rim Light 的颜色,只依赖 RGB 三个通道的分量
    11. rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } }
    12. }%

    这个绑定意味着着色器的 rimLightColor 的 RGB 分量的值会通过引擎传输到 Uniform rimColorrgb 三个分量里。

    通常引擎内置的顶点着色器可以满足大部分的开发需求,因此可以直接采用引擎内置的通用顶点着色器:

    1. - vert: general-vs:vert # builtin header

    修改通过 资源管理器 创建的着色器中的片元着色器代码,将 CCProgram unlit-fs 修改为 CCProgram rimlight-fs

    修改前:

    1. CCProgram unlit-fs %{
    2. precision highp float;
    3. ...
    4. }%

    修改后:

    1. CCProgram rimlight-fs %{
    2. precision highp float;
    3. ...
    4. }%

    在光照计算中,通常都需要计算法线和视线的夹角,而视线的计算和 摄像机位置 紧密相关。

    如上图所示,如果要计算视线,需要通过 摄像机位置 减去 物体的位置。在着色器内,想要获取 摄像机位置,需要使用 中的 cc_cameraPos,该变量存放于 cc-global 着色器片段内。通过 include 关键字,可以方便的引入整个着色器片段。

    1. #include <cc-global> // 包含 Cocos Creator 内置全局变量

    着色器代码:

    1. CCProgram rimlight-fs %{
    2. precision highp float;
    3. #include <cc-global> // 包含 Cocos Creator 内置全局变量
    4. #include <output>
    5. #include <cc-fog-fs>
    6. ...
    7. }
    1. vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向

    我们不关心视线向量的长度,因此得到 viewDirection 后,通过 normalize 方法进行归一化处理:

    cc_cameraPos 的 xyz 分量表示了相机的位置。

    此时的片元着色器代码:

    1. vec4 frag(){
    2. vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向
    3. vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化
    4. vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色
    5. CC_APPLY_FOG(col, v_position);
    6. return CCFragOutput(col);
    7. }

    接下来需要计算法线和视角的夹角,由于使用的是内置标准顶点着色器 ,法线已由顶点着色器传入到片元着色器,但是没有被声明,若要在片元着色器里面使用,只需在代码中增加:

    1. in vec3 v_normal;

    此时的片元着色器:

    1. CCProgram rimlight-fs %{
    2. precision highp float;
    3. #include <cc-global>
    4. #include <output>
    5. in vec2 v_uv;
    6. in vec3 v_normal;
    7. in vec3 v_position;
    8. ....
    9. }

    法线由于管线的插值,不再处于归一化状态,因此需要对法线进行归一化处理,使用 normalize 函数进行归一化:

    1. vec3 normal = normalize(v_normal); // 重新归一化法线。

    此时的 frag 函数:

    1. vec4 frag(){
    2. vec3 normal = normalize(v_normal); // 重新归一化法线。
    3. vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向
    4. vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化
    5. vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色
    6. CC_APPLY_FOG(col, v_position);
    7. return CCFragOutput(col);
    8. }

    这时可计算法线和视角的夹角,在线性代数里面,点积表示为两个向量的模乘以夹角的余弦值:

    1. a·b = |a|*|b|*cos(θ)

    通过简单的交换律可得出:

    1. cos(θ) = a·b /(|a|*|b|)

    由于法线和视角方向都已经归一化,因此他们的模为 1,点积的结果则表示为法线和视角的 cos 值。

    1. cos(θ) = a·b

    将其转化为代码:

    1. dot(normal, normalizedViewDirection)

    注意点积的计算可能会出现小于 0 的情况,而颜色是正值,通过 max 函数将其约束在 [0, 1] 这个范围内:

    1. max(dot(normal, normalizedViewDirection), 0.0)

    此时可根据点积的结果来调整 RimLight 的颜色:

    着色器代码如下:

    1. vec4 frag(){
    2. vec3 normal = normalize(v_normal);// 重新归一化法线。
    3. vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向
    4. vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化
    5. float rimPower = max(dot(normal, normalizedViewDirection), 0.0); // 计算 RimLight 的亮度
    6. vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色
    7. col.rgb += rimPower * rimColor.rgb; // 增加边缘光
    8. CC_APPLY_FOG(col, v_position);
    9. return CCFragOutput(col);
    10. }

    可观察到物体中心比边缘更亮,这是因为边缘顶点的法线和视角的夹角更大,得到的余弦值更小。

    dot result

    要调整这个结果,只需用 1 减去点积的结果即可,删除下面的代码:

    float rimPower = max(dot(normal, normalizedViewDirection), 0.0);

    并增加:

    1. float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);

    片元着色器代码:

    1. vec4 frag(){
    2. vec3 normal = normalize(v_normal); // 重新归一化法线。
    3. vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向
    4. vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化
    5. float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);
    6. vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色
    7. col.rgb += rimPower * rimColor.rgb; // 增加边缘光
    8. CC_APPLY_FOG(col, v_position);
    9. return CCFragOutput(col);
    10. }

    在 CCEffect 内增加如下代码:

    1. rimInstensity: { value: 1.0, # 默认值为 1
    2. target: rimColor.a, # 绑定到 rimColor 的 alpha 通道
    3. editor: { # 属性检查器的样式
    4. slide: true, # 使用滑动条来作为显示样式
    5. range: [0, 10], # 滑动条的值范围
    6. step: 0.1 } # 每次点击调整按钮时,数值的变化值

    此时的 CCEffect 代码:

    1. CCEffect %{
    2. techniques:
    3. - name: opaque
    4. passes:
    5. - vert: general-vs:vert # builtin header
    6. frag: rimlight-fs:frag
    7. properties: &props
    8. mainTexture: { value: white }
    9. # Rim Light 的颜色,只依赖 rgb 三个通道的分量
    10. rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } }
    11. # rimLightColor 的 alpha 通道没有被用到,复用该通道用来描述 rimLightColor 的强度。
    12. }%

    增加此属性后,材质 属性检查器 上会增加可调整的 RimIntensity:

    intensity

    通过 pow 函数调整边缘光,使其范围不是线性变化,可体现更好的效果,删除如下代码:

    col.rgb += rimPower * rimColor.rgb;

    新增下列代码:

    1. float rimInstensity = rimColor.a; // alpha 通道为亮度的指数
    2. col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 使用 ‘pow’ 函数对点积进行指数级修改

    pow 是 GLSL 的内置函数,其形式为:pow(x, p),代表以 x 为底数,p 为指数的指数函数。

    最终片元着色器代码:

    1. vec4 frag(){
    2. vec3 normal = normalize(v_normal); // 重新归一化法线。
    3. vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向
    4. vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化
    5. float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);// 计算 RimLight 的亮度
    6. vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色
    7. float rimInstensity = rimColor.a; // alpha 通道为亮度的指数
    8. col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 增加边缘光
    9. CC_APPLY_FOG(col, v_position);
    10. return CCFragOutput(col);
    11. }

    之后将材质 属性检查器 上的 rimIntensity 的值修改为 3:

    此时可观察到边缘光照更自然:

    增加亮度调整后

    通过 Rim ColorrimIntensity 可方便的调整边缘光的颜色和强度:

    调整颜色结果

    完整的着色器代码:

    1. CCEffect %{
    2. techniques:
    3. - name: opaque
    4. passes:
    5. - vert: general-vs:vert # builtin header
    6. frag: rimlight-fs:frag
    7. properties: &props
    8. mainTexture: { value: white }
    9. mainColor: { value: [1, 1, 1, 1], editor: { type: color } }
    10. # Rim Light 的颜色,只依赖 rgb 三个通道的分量
    11. rimLightColor: { value: [1.0, 1.0, 1.0], target: rimColor.rgb, editor: { displayName: Rim Color, type: color } }
    12. # rimLightColor 的 alpha 通道没有被用到,复用该通道用来描述 rimLightColor 的强度。
    13. rimInstensity: { value: 1.0, target: rimColor.a, editor: {slide: true, range: [0, 10], step: 0.1}}
    14. }%
    15. CCProgram rimlight-fs %{
    16. precision highp float;
    17. #include <cc-global>
    18. #include <output>
    19. #include <cc-fog-fs>
    20. in vec2 v_uv;
    21. in vec3 v_normal;
    22. in vec3 v_position;
    23. uniform sampler2D mainTexture;
    24. uniform Constant {
    25. vec4 mainColor;
    26. vec4 rimColor;
    27. };
    28. vec4 frag(){
    29. vec3 normal = normalize(v_normal); // 重新归一化法线。
    30. vec3 viewDirection = cc_cameraPos.xyz - v_position; // 计算视线的方向
    31. vec3 normalizedViewDirection = normalize(viewDirection); // 对视线方向进行归一化
    32. float rimPower = 1.0 - max(dot(normal, normalizedViewDirection), 0.0);// 计算 RimLight 的亮度
    33. vec4 col = mainColor * texture(mainTexture, v_uv); // 计算最终的颜色
    34. float rimInstensity = rimColor.a; // alpha 通道为亮度的指数
    35. col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 增加边缘光
    36. CC_APPLY_FOG(col, v_position);
    37. return CCFragOutput(col);
    38. }
    39. }%

    若要让边缘光的颜色受纹理颜色的影响,可将下列代码:

    1. col.rgb += pow(rimPower, rimInstensity) * rimColor.rgb; // 增加边缘光

    改为:

    1. col.rgb *= 1.0 + pow(rimPower, rimInstensity) * rimColor.rgb; // 边缘光受物体着色的影响

    此时的边缘光则会受到最终纹理和顶点颜色的影响: