这一系列依赖于最小规模的用于着色器和渲染工具的lwjgl-basics API. 代码已经被移植到 LibGDX. 这些概念是足够通用的, 它们能被应用于Love2D, GLSL Sandbox, iOS
, 或者其余支持 GLSL
的平台.php
本文聚焦于 3D
光照和法线贴图技术, 以及咱们如何把它们应用到 2D
游戏中, 示范下图所示, 左边是纹理贴图, 右边实时应用了光照:html
一旦你理解了光照的概念, 把它应用于任何设置都是很是直截了当的. 这里是一个 Java4K
示例中的法线贴图的例子, 例如, 经过软件渲染:java
效果跟这个 YouTube流行视频 和这个 Love2D示例 中展现的同样, 你还能够在 [GLSL] Using Normal Maps to Illuminate a 2D Texture (LibGDX) 看到效果, 其中包括一个可执行的示例.ios
正如咱们在以前的教程中讨论过的, 一个 GLSL
向量是一个浮点数的容器, 一般保存诸如位置(x,y,z)
之类的值. 在数学中,向量意味着至关多的内容,以及用于表示长度(即大小)和方向. 若是你对向量很陌生而且想要学习关于它们更多一些的知识, 查看下面这些连接:git
为了计算光照, 咱们须要使用网格的"法线
". 一个表面法线是一个垂直于切线平面的向量. 简单来讲, 它是一个向量, 垂直于给定顶点处的网格. 下面咱们会看到一个网格, 每一个顶点都有一条法线.github
每一个向量都指向外面, 遵循着网格的弯曲形状. 下面是另外一个例子, 此次是一个简单的 2D
边沿视图:算法
法线贴图
(Normal Mapping
)是一个游戏编程技巧, 它容许咱们渲染相同数目的多边形(例如低解析度的网格模型), 可是在计算光照时使用高解析度网格模型的法线. 这为咱们带来更好的感觉, 关于深度, 真实性和光滑度.编程
(图像来自于这个出色的博客文章Making Worlds 3 - That's no Moon...)app
高面数网格模型或者说精雕模型的法线被编码到一个纹理贴图(即法线图)中, 当咱们渲染低面数网格模型时会从片断着色器中对它进行取样. 结果以下:less
译者注: 左侧是4百万个三角形的高模, 中间是500个三角形的低模, 右侧是在500个三角形的低模上使用法线贴图后的效果
咱们的表面法线是单位向量, 一般位于范围 -1.0
到 1.0
之间. 咱们能够经过把法线范围转换为 0.0
到 1.0
之间来把法线向量(x, y, z)
存储到一个 RGB
纹理贴图中. 下面是伪码:
Color.rgb = Normal.xyz / 2.0 + 0.5;
例如, 一个法线 (-1, 0, 1)
会被做为 RGB
编码为 (0, 0.5, 1)
. x
轴(左/右)被保存到红色通道, y
轴(上/下)被保存到绿色通道, z
轴(前/后)被保存到蓝色通道. 最终的法线图(normal map
)看起来就是下面这个样子:
典型地, 咱们使用程序来生成法线图, 而不是手动绘制.
理解法线图, 把每一个通道独立出来查看会更清楚:
看着,绿色通道,咱们看到更亮的部分(值更接近于 1.0
) 定义了法线指向上方的区域,而更暗的区域(值更接近为 0.0
) 定义了法线指向下方的区域. 大多数的法线图会是蓝色,由于Z
轴(蓝色通道)一般指向咱们(即值为 1.0
).
在咱们游戏的片断着色器中, 咱们能够把法线解码, 经过执行跟以前编码时相反的操做, 把颜色值展开为范围 -1.0
到 1.0
之间:
//sample the normal map NormalMap = texture2D(NormalMapTex, TexCoord); //convert to range -1.0 to 1.0 Normal.xyz = NormalMap.rgb * 2.0 - 1.0;
注意: 要记住不一样的引擎和软件会使用不一样的坐标系, 绿色通道可能须要翻转.
在计算机图形学中, 咱们有大量的算法,能够结合起来打造 3D
对象的不一样渲染效果. 在这篇文章咱们将专一于 Lambert
着色,没有任何反射(诸如"光泽"或"发光"). 其余的技术,像Phong
, Cook-Torrance
, 和 Oren–Nayar
, 能够用来产生不一样的视觉效果(粗糙表面、 有光泽的表面等等)。
咱们整个光照模型看起来像这样:
N = normalize(Normal.xyz) L = normalize(LightDir.xyz) Diffuse = LightColor * max(dot(N, L), 0.0) Ambient = AmbientColor * AmbientIntensity Attenuation = 1.0 / (ConstantAtt + (LinearAtt * Distance) + (QuadraticAtt * Distance * Distance)) Intensity = Ambient + Diffuse * Attenuation FinalColor = DiffuseColor.rgb * Intensity.rgb
说实话,你不须要从数学角度理解为何这个能够起做用,但若是你有兴趣, 能够阅读更多有关"N dot L
"的内容, 在这里GLSL Tutorial – Directional Lights per Vertex I和这里Lambertian reflectance.
一些关键的术语:
Normal-法线
: 从法线图中解码获得的法线向量 XYZ.LightDir-光线方向
: 从物体表面到光源位置的向量, 咱们将会简单解释.Diffuse Color-漫射颜色
: 纹理贴图的 RGB 颜色, 没有光.Diffuse-漫射
: 跟Lambertian
反射相乘的光线颜色, 这是咱们光照等式的主要部分.Ambient-环境光
: 处于阴影中的颜色和强度, 例如, 一个户外场景会有一个更亮的环境光强度, 比起一个暗淡灯光下的户内场景.Attenuation-衰减
: 这是光线的随距离而下降
, 例如, 当咱们远离点光源时强度/亮度
的损失. 有多种方法来计算衰减--对于咱们的目标而言, 咱们将会使用常量-线性-二次方
衰减. 这里用3
个系数来计算衰减, 咱们能够改变它们来影响光线衰减的视觉效果.Intensity-强度
: 咱们阴影算法的强度--离1.0
越近意味着有光, 离0.0
越近意味着没有光.下面的图有助于你对咱们的光照模型有个直观的理解:
正如你所见, 感受它是至关模块化的, 咱们能够拿走那些不须要的部分, 就像衰减(attenuation
) 或光线颜色(light colors
).
如今, 让咱们把它们应用到 GLSL
模型上. 注意咱们只处理 2D
, 在 3D
中还有一些额外的考虑在这篇教程没有覆盖到(译者注:就是空间变换, 在 3D
场景下, 法线图中的法线所在的空间为正切空间, 光线所在的空间为世界空间, 须要统一到同一个空间计算才有意义). 咱们将把模型分解为多个单独部分, 每个都创建在下面的基础上.
你能够在这里看到Java代码示例. 它是相对直截了当的, 并不会介绍过多的在在前面的课程中尚未讨论过的内容. 咱们将使用如下两种纹理贴图︰
咱们的示例根据鼠标位置(归一化到分辨率)调整 LightPos.xy
, 根据鼠标滚轮(点击则重置光线的 Z
值)调整 LightPos.z
(深度). 在特定的坐标系中, 就像 LibGDX
, 你可能须要翻转 Y
值.
注意, 咱们的例子使用了以下这些常量, 你能够调整它们来得到不一样的视觉效果:
public static final float DEFAULT_LIGHT_Z = 0.075f; ... //Light RGB and intensity (alpha) public static final Vector4f LIGHT_COLOR = new Vector4f(1f, 0.8f, 0.6f, 1f); //Ambient RGB and intensity (alpha) public static final Vector4f AMBIENT_COLOR = new Vector4f(0.6f, 0.6f, 1f, 0.2f); //Attenuation coefficients for light falloff public static final Vector3f FALLOFF = new Vector3f(.4f, 3f, 20f);
下面是咱们的渲染代码, 就像 教程4 同样, 咱们会在渲染时使用多重纹理:
... //update light position, normalized to screen resolution float x = Mouse.getX() / (float)Display.getWidth(); float y = Mouse.getY() / (float)Display.getHeight(); LIGHT_POS.x = x; LIGHT_POS.y = y; //send a Vector4f to GLSL shader.setUniformf("LightPos", LIGHT_POS); //bind normal map to texture unit 1 glActiveTexture(GL_TEXTURE1); rockNormals.bind(); //bind diffuse color to texture unit 0 glActiveTexture(GL_TEXTURE0); rock.bind(); //draw the texture unit 0 with our shader effect applied batch.draw(rock, 50, 50);
阴影贴图的结果:
下面对光线使用了更低的 Z
值:
这里是咱们完整的片断着色器
//attributes from vertex shader varying vec4 vColor; varying vec2 vTexCoord; //our texture samplers uniform sampler2D u_texture; //diffuse map uniform sampler2D u_normals; //normal map //values used for shading algorithm... uniform vec2 Resolution; //resolution of screen uniform vec3 LightPos; //light position, normalized uniform vec4 LightColor; //light RGBA -- alpha is intensity uniform vec4 AmbientColor; //ambient RGBA -- alpha is intensity uniform vec3 Falloff; //attenuation coefficients void main() { //RGBA of our diffuse color vec4 DiffuseColor = texture2D(u_texture, vTexCoord); //RGB of our normal map vec3 NormalMap = texture2D(u_normals, vTexCoord).rgb; //The delta position of light vec3 LightDir = vec3(LightPos.xy - (gl_FragCoord.xy / Resolution.xy), LightPos.z); //Correct for aspect ratio LightDir.x *= Resolution.x / Resolution.y; //Determine distance (used for attenuation) BEFORE we normalize our LightDir float D = length(LightDir); //normalize our vectors vec3 N = normalize(NormalMap * 2.0 - 1.0); vec3 L = normalize(LightDir); //Pre-multiply light color with intensity //Then perform "N dot L" to determine our diffuse term vec3 Diffuse = (LightColor.rgb * LightColor.a) * max(dot(N, L), 0.0); //pre-multiply ambient color with intensity vec3 Ambient = AmbientColor.rgb * AmbientColor.a; //calculate attenuation float Attenuation = 1.0 / ( Falloff.x + (Falloff.y*D) + (Falloff.z*D*D) ); //the calculation which brings it all together vec3 Intensity = Ambient + Diffuse * Attenuation; vec3 FinalColor = DiffuseColor.rgb * Intensity; gl_FragColor = vColor * vec4(FinalColor, DiffuseColor.a); }
如今, 把它分解. 首先, 咱们从两个纹理贴图中取样:
//RGBA of our diffuse color vec4 DiffuseColor = texture2D(u_texture, vTexCoord); //RGB of our normal map vec3 NormalMap = texture2D(u_normals, vTexCoord).rgb;
接着, 咱们须要从当前的片断(译者注:即像素)肯定光线向量, 而且纠正它的纵横比例(aspect ratio
). 而后在归一化(normalize
)以前肯定 LightDir
向量的值(长度):
//Delta pos vec3 LightDir = vec3(LightPos.xy - (gl_FragCoord.xy / Resolution.xy), LightPos.z); //Correct for aspect ratio LightDir.x *= Resolution.x / Resolution.y; //determine magnitude float D = length(LightDir);
在咱们的光照模型中, 咱们须要从 NormalMap.rgb
中解码 Normal.xyz
, 而且归一化咱们的向量:
vec3 N = normalize(NormalMap * 2.0 - 1.0); vec3 L = normalize(LightDir);
下一步是计算 Diffuse
(漫射) 项. 为了这个, 咱们须要使用 LightColor
. 在咱们的例子中, 咱们将会把光线颜色(RGB
)和强度(alpha
)相乘: LightColor.rgb * LightColor.a
. 所以, 全部这些看起来以下:
//Pre-multiply light color with intensity //Then perform "N dot L" to determine our diffuse term vec3 Diffuse = (LightColor.rgb * LightColor.a) * max(dot(N, L), 0.0);
接着, 咱们预相乘(pre-multiply)环境颜色(ambient color
)和强度:
vec3 Ambient = AmbientColor.rgb * AmbientColor.a;
下一步是用咱们的 LightDir
的值(前面计算好的)来肯定衰减
(Attenuation
). 统一变量降低系数
(Falloff
) 定义了咱们的常量, 线性和2次方的衰减系数:
float Attenuation = 1.0 / ( Falloff.x + (Falloff.y*D) + (Falloff.z*D*D) );
接着, 计算光强度
(Intensity
)和最终颜色
(FinalColor
), 而且把它们传递给 gl_FragColor
. 注意, 咱们机智地保留了 DiffuseColor
的 alpha
值:
vec3 Intensity = Ambient + Diffuse * Attenuation; vec3 FinalColor = DiffuseColor.rgb * Intensity; gl_FragColor = vColor * vec4(FinalColor, DiffuseColor.a);
LightDir
和 attenuation
依赖于分辨率. 这意味着更改分辨率会影响咱们的光的衰减. 根据你的游戏,不一样的实现上分辨率无关多是必需的.Y
坐标系和你所采用的法线图生成程序(例如 CrazyBump
)之间的差别. 一些程序容许你导出一个翻转了Y
轴的法线图. 下面的图片展现了这个问题:实现多光源, 咱们只要简单地调整一下算法, 以下:
vec3 Sum = vec3(0.0); for (... each light ...) { ... calculate light using our illumination model ... Sum += FinalColor; } gl_FragColor = vec4(Sum, DiffuseColor.a);
注意, 这样会在你的着色器中引入更多分支(译者注:也就是这个循环), 它会致使性能下降.
这有时被称为"N 照明
"(N lighting
), 由于咱们的系统仅支持一个固定数目 N
的光源. 若是你计划包括大量的光源, 你可能想要调查多个绘制调用(例如 additive blending
), 或延迟渲染Deferred shading.
在某个时间点, 你可能会问本身:"为何我不直接作一个3D
游戏?". 比起试着把这些概念应用到 2D
精灵来讲, 这是个正当的问题而且可能会带来更好的性能和更少的开发时间.
这里有各类从一张图片生成法线图的方法. 用于转换2D
图像为法线图的经常使用程序和滤镜包括以下:
注意, 不少程序都会产生锯齿和错误, 阅读这篇文章How NOT To Make Normal Maps From Photos Or Images来得到更多细节.
你也能使用 3D
建模软件, 如 Blender 或 ZBrush 来精心雕琢出高质量的法线图.
一个工做流的想法是, 生成一个低面数,很是粗糙的 3D
对象在你的艺术资源中. 而后你可使用这个 Blender Template: Normal Map Pass 把你的对象渲染为一个 2D
正切空间内的法线图. 而后你就能在 PhotoShop
中打开这个法线图而且处理这个漫射(diffuse)颜色图了.
下面是一个 Blender
模板的样子:
在建立个人 WebGL
的 法线图像素艺术演示时, 有一堆我不得不考虑的事项. 你能够从这里查看源码和细节.
效果以下图:
在这个示例中, 我想让衰减做为一个风格元素变得可见. 典型的作法带来很是平滑的衰减, 它和块状像素艺术风格冲突. 相反, 我使用 cel shading
的光线, 给它一个阶梯状的衰减. 经过片断着色器中的 if-else
语句实现了简单的卡通着色.
下一步的考虑是, 咱们但愿光线的边缘像素的比例随着精灵(sprites)的像素变化. 实现这个目标的一个方法是经过光照着色器把咱们的场景绘制到一个 FBO
中, 而后用一个默认的着色器以一个较大的尺寸把它渲染到屏幕上. 在咱们的块状像素艺术中这种照明方式影响整个"纹素
"(texels
).