本文讲解2D游戏中,如何利用法线贴图来实现有材质特性、全角度、且受时间影响的接近真实的光照效果。
本文链接 游戏shader(6):利用法线贴图实现动态2D光照效果
一、制作背景
2D游戏中,场景中有多个火把、蜡烛、灯光等贴图,让场景非常真实绚丽。但遗憾的是,周围的物体和经过的游戏角色,并不受这些光的影响。
我们要做的就是让周围的物体和经过的游戏角色,实时地受到这些灯光的影响,实现仿3D效果,增强2D游戏的体验和真实性。
二、效果动态图对比
以下展示了不受光照影响和受光照影响的效果对比图。很明显,对旁边的火把光作出反馈时,角色更加真实。
三、实现原理
实现基本原理是,我们将环境光源的坐标、颜色、强度实时传入角色的片元shader,据此重新计算角色的颜色。
那么如何确定角色身上哪些像素接受光照、哪些不接受、又接受多少呢?这个就要用到法线贴图了。在法线贴图上,每个纹理像素的RGB不再代表颜色分量,而是代表角色身上每个像素的法向量信息。关于法线贴图定义请自行查阅相关资料。
步骤如下:
1、定义光照影响因子。这里用光源坐标、颜色、强度、时间变量几个uniform变量。
2、准备法线贴图。一种方法是自定义每个像素的法向量信息,最后导出贴图。一种是利用法线贴图生成工具,这里推荐一个在线法线贴图生成工具 https://cpetry.github.io/NormalMap-Online/ ,上传贴图,调整参数,并生成和下载的法线贴图。这里用的是在线生成。
3、将法线贴图纹理,作为shader的uniform参数传入供采样。
四、关键实现和代码
一些算法做简化处理。思想粗略描述如下:
1、强度距离衰减因子 = 光照强度 × ((感光距离最大值常量 - 角色纹理像素到光源的距离 )/ 感光距离最大值常量);
2、法向量2D平面分量 = vec2(法线贴图颜色.R,法线贴图颜色.B)* 2.0; // 2d游戏中忽略Z分量
3、光照反馈强度因子 = 点积(光源到角色像素的向量, 法向量2D平面分量)
4、时间因子。动态变化以实现强弱闪烁效果。更真实地,应该以光源实际强度的值为准,在CPU去计算。这里简便起见,用时间因子模拟
4、角色像素最终颜色RGB = 角色纹理原始颜色 × (1-光照原始透明度) + 光照原始颜色RGB × 强度距离衰减因子 × 光照反馈强度因子 × 时间影响因子 × (光照原始透明度)
以上混合透明度可以根据自己想要的效果自定义即可。
shader代码如下:
u_timeValue // 传入的时间值,数值自定义,可每帧随机变化,来模拟闪烁效果 。更真实地,应该以光源实际强度的值为准,在CPU去计算。这里简便起见,用时间因子模拟。
vec4 normalColor = texture2D(u_normalTexture, uvn); // 该片元坐标在法线贴图上的采样颜色
vec2 lightPos = u_lightPos; // 光源的位置
vec4 lightColor = u_lightColor; // 光的原始颜色
float lightRadius = u_lightRadius; // 光能照到的最大半径
float px = u_nodePos.x + (uvx -u_nodeAnchor.x) * u_nodeWidth; // 计算角色身上某像素的x位置
float py = u_nodePos.y + (u_nodeAnchor.y - uvy) * u_nodeHeight; // 计算角色身上某像素的y位置
vec2 vecLight = vec2(px - lightPos.x, py - lightPos.y); // 光源到角色身上某像素的向量
vec2 vecLightN = normalize(vecLight); // 光源到角色身上某像素向量的法向量
float dis = length(vecLight); // 光源到角色身上某像素的距离
vec2 normalVec = vec2(normalColor.r - 0.5, normalColor.g - 0.5) * 2.0; // 法线贴图坐标空间转到颜色空间(-1~1转0~1)
if (u_nodeScaleX == -1.0) { // 可能的x翻转
normalVec.r = -normalVec.r;
}
if (u_texture_flipY > 0.0) { // 可能的y翻转
normalVec.g = -normalVec.g;
}
float strength2 = max(dot(vecLightN, normalVec), 0.0); // 关键点:法向量和vecLightN 向量做点积运算,得到反馈强度值
float strength = smoothstep(0.0, 1.0, 1.0 - dis/lightRadius); // 计算距离衰减因子,这里采用平滑插值函数
float time = u_timeValue / timeRatio; // 时间影响因子
float timeYu = time - float(int(time));// 计算要用到的时间余数
timeYu = timeYu * 0.5 + 0.5 * timeRatio;// 时间转换到0~timeRatio范围
float strength3 = timeYu / timeRatio; // 时间因子转换 0~1
strength3 = strength3 * 0.5 + 0.5; // 时间因子转换 0.5~1 防止闪烁过于强烈很突兀
vec3 mixColor = color.rgb + lightColor.rgb * strength * strength2 * strength3 * lightColor.a; // 最终颜色混合公式.混合方式和参数可以自定义 这里采用颜色直接相加
color = vec4(mixColor, color.a); // 角色像素最终颜色