是否想了解如何在3D游戏中添加纹理,光照,阴影,法线贴图,发光物体,环境光遮挡和其他效果? 太好了! 本文介绍了一组可将游戏图形级别提高到新高度的着色技术。 我将以一种您可以在任何工具堆栈(无论是Godot,Unity还是其他工具)上应用/移植此信息的方式来解释每种技术。
作为着色器之间的“胶水”,我决定使用出色的游戏引擎Panda3D和OpenGL着色语言(GLSL)。 如果使用相同的堆栈,则将获得额外的优势-您将学习如何在Panda3D和OpenGL中专门使用着色技术。
准备工作
下面是我用来开发和测试示例代码的系统。
星期三
示例代码是在以下环境中开发和测试的:
- Linux manjaro 4.9.135-1-MANJARO
- OpenGL渲染器字符串:GeForce GTX 970 / PCIe / SSE2
- OpenGL版本字符串:4.6.0 NVIDIA 410.73
- g ++(GCC)8.2.1 20180831
- 熊猫3D 1.10.1-1
用料
用于创建
mill-scene.egg
每种
Blender材料都有两个纹理。
第一个纹理是法线贴图,第二个纹理是漫反射贴图。 如果对象使用其顶点的法线,则使用“纯蓝色”法线贴图。 由于所有模型在相同位置具有相同的卡,因此可以将着色器通用化并将其应用于场景图的根节点。
请注意,场景图是Panda3D引擎
实现的功能 。
这是仅包含颜色
[red = 128, green = 128, blue = 255]
的单色法线贴图。
该颜色表示单位法线,沿z轴
[0, 0, 1]
的正方向指示。
[0, 0, 1] = [ round((0 * 0.5 + 0.5) * 255) , round((0 * 0.5 + 0.5) * 255) , round((1 * 0.5 + 0.5) * 255) ] = [128, 128, 255] = [ round(128 / 255 * 2 - 1) , round(128 / 255 * 2 - 1) , round(255 / 255 * 2 - 1) ] = [0, 0, 1]
在这里,我们看到单位法线
[0, 0, 1]
转换为纯蓝色
[128, 128, 255]
,而实心蓝色转换为单位法线。
有关法线贴图技术的部分将对此进行详细说明。
熊猫3d
在此代码示例中,
Panda3D用作着色器之间的“胶水”。 这不会影响下面介绍的技术,也就是说,您可以在任何选定的堆栈或游戏引擎中使用此处研究的信息。 Panda3D提供了某些便利设施。 在我谈论它们的文章中,因此您可以在堆栈中找到它们的对应对象,或者如果它们不在堆栈中,则可以自己重新创建它们。
值得考虑的是,在
config.prc
中添加了
gl-coordinate-system default
,
textures-power-2 down
和
textures-auto-power-2 1
的添加。 它们不包含在标准
Panda3D配置中 。
默认情况下,Panda3D使用带有向上z轴的右手坐标系,而OpenGL使用带有带有y向上轴的右手坐标系。
gl-coordinate-system default
可让您摆脱着色器内部两个坐标系之间的变换。
如果系统支持,
textures-auto-power-2 1
允许我们使用不是2的幂的纹理大小。
当执行SSAO或在屏幕/窗口内实现其他技术时,这很方便,因为屏幕/窗口大小通常不是2的幂。
如果系统仅支持大小等于
textures-power-2 down
会将纹理的大小减小为2的幂。
构建示例代码
如果要运行示例代码,则必须首先对其进行构建。
Panda3D可在Linux,Mac和Windows上运行。
的Linux
首先,
安装要分发
的 Panda3D SDK 。
查找Panda3D标头和库的位置。 它们最有可能分别位于
/usr/include/panda3d/
和
/usr/lib/panda3d/
。
然后克隆此存储库并导航到其目录。
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
现在将源代码编译成输出文件。
g++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-O2 \
-I/usr/include/python2.7/ \
-I/usr/include/panda3d/
创建输出文件后,通过将输出文件与其依赖项相关联来创建可执行文件。
g++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/usr/lib/panda3d \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread
有关更多信息,请参见
Panda3D手册 。
Mac电脑
首先安装Mac版
Panda3D SDK 。
查找Panda3D的标头和库在哪里。
然后克隆存储库并导航到其目录。
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
现在将源代码编译成输出文件。 您需要找到包含目录在Python 2.7和Panda3D中的位置。
clang++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-g \
-O2 \
-I/usr/include/python2.7/ \
-I/Developer/Panda3D/include/
创建输出文件后,通过将输出文件与其依赖项相关联来创建可执行文件。
您需要找到Panda3D库所在的位置。
clang++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/Developer/Panda3D/lib \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread
有关更多信息,请参见
Panda3D手册 。
窗户
首先
安装适用于Windows
的 Panda3D SDK 。
查找Panda3D标头和库的位置。
克隆此存储库并导航到其目录。
git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners
有关更多信息,请参见
Panda3D手册 。
启动演示
构建示例代码后,您可以运行可执行文件或演示。 这就是它们在Linux或Mac上运行的方式。
./3d-game-shaders-for-beginners
因此它们在Windows上运行:
3d-game-shaders-for-beginners.exe
键盘控制
该演示具有键盘控制,可让您移动相机并切换各种效果的状态。
机芯
w
深入场景。- a-顺时针旋转场景。
s
远离场景。d
逆时针旋转场景。
切换效果
y
启用SSAO。Shift
+ y
禁用SSAO。u
包含电路。Shift
+ u
禁用轮廓。i
-启用绽放。Shift
+ i
禁用绽放。o
启用法线贴图。Shift
+ o
禁用法线贴图。p
包含雾。Shift
+ p
关闭雾。h
景深的包含。Shift
+ h
禁用景深。j
启用后代化。Shift
+ J-禁用后代化k
启用像素化。Shift
+ k
禁用像素化。l
锐化。Shift
+ l
禁用清晰度。n
包含薄膜颗粒。Shift
+ n
禁用胶片颗粒。
参考系统
在开始编写着色器之前,您需要熟悉以下参考系统或坐标系。 所有这些都归结为从
(0, 0, 0)
中获取参考原点的当前坐标的内容。 一旦发现,就可以使用某种矩阵或其他向量空间对其进行转换。 通常,如果着色器的输出看起来不正确,则原因是坐标系统混乱。
型号
模型或对象的坐标系相对于模型的原点。 在三维建模程序中,例如在Blender中,通常将其放置在模型的中心。
世界
世界空间是相对于您创建的场景/关卡/宇宙的原点的。
复习
视图的坐标空间是相对于活动摄像机的位置的。
剪裁
相对于相机框架中心的剪切空间。 其中的所有坐标都是均匀的,并且在
(-1, 1)
区间内。 X和y平行于相机胶卷,z坐标为深度。
不在可见度金字塔范围内或相机可见度范围内的所有顶点都将被切除或丢弃。 我们将看到这种情况是如何发生的,一个立方体在相机远处的平面后面被截断,一个立方体在侧面。
萤幕
屏幕空间(通常)相对于屏幕的左下角。 X从零变为屏幕的宽度。 Y从零变为屏幕高度。
GLSL
与其使用固定功能的流水线,不如使用可编程的GPU渲染流水线。 由于它是可编程的,因此我们自己必须以着色器的形式将其传递给程序代码。 着色器是一个(通常很小的)程序,使用类似于C语言的语法创建。 不同类型的着色器包括顶点着色器,曲面细分着色器,几何着色器,片段着色器和计算着色器。 要使用本文中描述的技术,对于我们来说,使用顶点和片段就足够了
阶段。
#version 140 void main() {}
这是最小的GLSL着色器,由GLSL版本号和主要功能组成。
#version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec4 p3d_Vertex; void main() { gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }
这是截断的顶点着色器GLSL,它将输入的顶点转换为剪切空间,并将此新位置显示为统一的顶点位置。
main
过程什么也不返回,因为它是
void
,并且
gl_Position
变量是内联输出。
值得一提的两个关键词是:
uniform
和
in
。
uniform
关键字表示此全局变量对于所有顶点都是相同的。 Panda3D本身会设置
p3d_ModelViewProjectionMatrix
并且对于每个顶点,它都是相同的矩阵。
in
关键字意味着将该全局变量传递给着色器。 顶点着色器获取几何组成的每个顶点,顶点着色器将附加到该顶点。
#version 140 out vec4 fragColor; void main() { fragColor = vec4(0, 1, 0, 1); }
这是修剪的GLSL片段着色器,显示不透明的绿色作为片段的颜色。
不要忘记一个片段仅影响一个屏幕像素,但是几个片段可以影响一个像素。
注意out关键字。
out
关键字表示此全局变量由着色器设置。
名称
fragColor
可选的,因此您可以选择其他任何名称。
这是上面显示的两个着色器的输出。
纹理渲染
该示例代码不是直接在屏幕上渲染/绘制,而是使用一种技术
名称“渲染到纹理”(渲染到纹理)。 要渲染到纹理,您需要配置帧缓冲区并将纹理绑定到它。 您可以将多个纹理绑定到单个帧缓冲区。
绑定到帧缓冲区的纹理存储片段着色器返回的向量。 通常,这些向量是颜色向量
(r, g, b, a)
,但它们可以是位置向量或法线向量
(x, y, z, w)
。 对于每个绑定纹理,片段着色器可以输出单独的矢量。 例如,我们可以一遍推导顶点的位置和法线。
与Panda3D一起使用的大部分示例代码与设置
帧缓冲区纹理有关 。 为简单起见,示例代码中的每个片段着色器只有一个输出。 但是,为了确保高帧速率(FPS),我们需要在每个渲染过程中输出尽可能多的信息。
这是示例代码中帧缓冲区的两个纹理结构。
第一种结构使用各种顶点和片段着色器将水车场景渲染为帧缓冲区纹理。 该结构通过磨机穿过舞台的每个顶点并沿着相应的片段。
在这种结构中,示例代码如下工作。
- 保存几何数据(例如,位置或顶点法线)以备将来使用。
- 保存材料数据(例如漫反射颜色)以备将来使用。
- 创建不同纹理(漫射,法线贴图,阴影贴图等)的UV绑定。
- 计算环境,漫射,反射和发射的光照。
- 渲染雾。
第二结构是瞄准屏幕形状的矩形的正交相机。
该结构仅穿过四个峰及其相应的片段。
在第二个结构中,示例代码执行以下操作:
- 处理另一个帧缓冲区纹理的输出。
- 将不同的帧缓冲区纹理合并为一个。
在代码示例中,我们可以看到一帧缓冲区纹理的输出,将相应的帧设置为true,将所有其他帧设置为false。
纹理化
纹理化是使用UV坐标将颜色或某些其他矢量与片段结合。 U和V的值从零到一变化。 每个顶点接收一个UV坐标,并显示在顶点着色器中。
片段着色器获取插值的UV坐标。 插值表示片段的UV坐标在组成三角形面的顶点的UV坐标之间。
顶点着色器
#version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec2 p3d_MultiTexCoord0; in vec4 p3d_Vertex; out vec2 texCoord; void main() { texCoord = p3d_MultiTexCoord0; gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; }
在这里,我们看到顶点着色器将纹理的坐标输出到片段着色器。 请注意,这是一个二维向量:U的一个值和V的一个值。
片段着色器
#version 140 uniform sampler2D p3d_Texture0; in vec2 texCoord; out vec2 fragColor; void main() { texColor = texture(p3d_Texture0, texCoord); fragColor = texColor; }
在这里,我们看到片段着色器在其UV坐标中搜索颜色并将其显示为片段的颜色。
屏幕填充纹理
#version 140 uniform sampler2D screenSizedTexture; out vec2 fragColor; void main() { vec2 texSize = textureSize(texture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; texColor = texture(screenSizedTexture, texCoord); fragColor = texColor; }
渲染为纹理时,网格是具有与屏幕相同的纵横比的扁平矩形。 因此,我们可以计算UV坐标,仅知道
A)使用UV坐标将纹理的宽度和高度与屏幕尺寸叠加在矩形上,以及
B)片段的x和y坐标。
要将x绑定到U,您需要将x除以传入纹理的宽度。 同样,要将y绑定到V,您需要将y除以传入纹理的高度。 您将看到在示例代码中使用了此技术。
灯饰
为了确定照明,有必要计算并组合环境照明,漫射照明,反射照明和发射照明的各个方面。 示例代码使用Phong照明。
顶点着色器
对于每种光源,除了环境光之外,Panda3D都为我们提供了方便的结构,可用于顶点着色器和片段着色器。 最方便的是阴影贴图和用于查看阴影的矩阵,以将顶点转换为阴影或照明空间。
从顶点着色器开始,我们必须将顶点从观察空间变换和移除,变成场景中每个光源的阴影或照明空间。 将来,这对于片段着色器渲染阴影会派上用场。 阴影或照明空间是每个坐标都相对于光源位置(原点是光源)的空间。
片段着色器
片段着色器执行大部分光照计算。
材质
Panda3D为我们当前正在渲染的网格或模型提供了材料(以结构形式)。
多种光源
在研究场景的照明源之前,我们将创建一个包含漫反射和反射颜色的驱动器。
现在,我们可以循环移动光源,计算每种光源的漫反射和反射颜色。
照明相关矢量图
这是计算每个光源引入的漫反射和反射颜色所需的四个基本向量。 照明方向矢量是指向光源的蓝色箭头。 法线向量是垂直向上指向的绿色箭头。 反射向量是反映光的方向向量的蓝色箭头。 眼睛或视野矢量是指向相机的橙色箭头。
照明方向是从顶点位置到光源位置的向量。
如果这是定向照明,则Panda3D
p3d_LightSource[i].position.w
为零。 定向照明没有位置,只有方向。 因此,如果这是定向照明,则照明方向将是与光源相反或相反的方向,因为对于定向照明,Panda3D将
p3d_LightSource[i].position.xyz
设置为
p3d_LightSource[i].position.xyz
。
顶点的法线必须是单位向量。 单位向量的值等于1。
接下来,我们需要另外三个向量。
我们需要具有照明方向参与的标量产品,因此最好对其进行标准化。 这给了我们等于单位(单位矢量)的距离或大小。
视线方向与顶点/片段的位置相反,因为顶点/片段的位置相对于摄影机的位置。 不要忘记顶点/片段的位置在查看空间中。 因此,我们从顶点/片段移动到相机(眼睛),而不是从相机(眼睛)移动到顶点/片段。
反射矢量是垂直于表面的照明方向的反射。 当“光线”接触表面时,它以与落下时相同的角度反射。 照明的方向向量与法线之间的角度称为“入射角”。 反射矢量和法线之间的角度称为“反射角度”。
您需要更改反射光矢量的符号,因为它应指向与眼睛矢量相同的方向。 别忘了眼睛的方向是从顶部/片段到照相机的位置。 我们将使用反射向量来计算反射光的亮度。
漫射照明
漫射照明的亮度是表面法线和单个矢量的照明方向的标量积。 标量积的范围为负一到一。 如果两个向量都指向同一方向,则亮度为1。 在所有其他情况下,它将小于统一。
如果照度矢量接近与法线相同的方向,则漫射照度的亮度趋于统一。
如果漫射照明的亮度小于或等于零,则需要转到下一个光源。
现在,我们可以计算此源引入的漫反射颜色。
如果漫射照明的亮度等于1,则漫射颜色将是漫射纹理的颜色和照明颜色的混合。在任何其他亮度下,漫反射颜色将更暗。请注意,我限制了漫反射颜色,以使其不比漫反射纹理的颜色更亮。这样可以防止场景过度曝光。反射光
漫射照明后,将计算反射。
反射光的亮度是眼睛向量和反射向量之间的标量积。与漫射照明的亮度一样,如果两个向量指向同一方向,则反射照明的亮度等于1。任何其他亮度将减少此光源引入的反射颜色量。材料的光泽决定了反射光的照明将被散射多少。通常,它是在模拟程序中设置的,例如在Blender中。在Blender中,它称为镜面硬度。聚光灯
此代码不允许照明影响聚光灯锥或金字塔外部的碎片。幸运的是,Panda3D中可以定义 spotDirection
和spotCosCutoff
工作有方向性和射灯。聚光灯既有位置又有方向。但是,定向照明仅具有方向,而点光源仅具有位置。但是,此代码可用于所有三种类型的照明,而无需混淆if语句。 spotCosCutoff = cosine(0.5 * spotlightLensFovAngle);
如果在投影照明的情况下,矢量``照明的片段源''与投影仪的方向矢量的标量乘积小于投影仪视场角的一半的余弦值,则着色器不会考虑该源的影响。请注意,您必须更改符号unitLightDirection
。unitLightDirection
从片段到探照灯,我们需要从探照灯移到片段,因为它spotDirection
直接到达探照灯金字塔的中心,且距离探照灯的位置一定距离。在定向照明和点光源的情况下,Panda3D将该spotCosCutoff
值设置为-1。回想一下,标量积在-1到1的范围内变化。因此,它的大小无关紧要unitLightDirectionDelta
,因为它始终大于或等于-1。
像代码一样unitLightDirectionDelta
,该代码也适用于所有三种类型的光源。对于聚光灯,当它接近聚光灯金字塔的中心时,它将使片段变亮。对于定向和点光源,光源spotExponent
为零。回想一下,零幂的任何值都等于1,所以漫反射颜色等于其自身乘以1,即不变。暗影
Panda3D简化了阴影的使用,因为它为场景中的每个光源创建了一个阴影贴图和阴影变换矩阵。要自己创建转换矩阵,您需要收集一个矩阵,该矩阵将查看空间的坐标转换为照明空间(坐标相对于光源的位置)。要自己创建阴影贴图,您需要从光源的角度将场景渲染到帧缓冲区纹理中。帧缓冲区纹理应包含从光源到片段的距离。这称为“深度图”。最后,您需要将自己的深度图uniform sampler2DShadow
和阴影转换矩阵分别手动传输到着色器uniform mat4
。因此,我们将重新创建Panda3D为我们自动执行的操作。使用了所示的代码段textureProj
,该代码段与上面显示的功能不同texture
。textureProj
第一分歧vertexInShadowSpaces[i].xyz
上vertexInShadowSpaces[i].w
。然后,她使用它vertexInShadowSpaces[i].xy
来查找存储在阴影贴图中的深度。然后,她使用vertexInShadowSpaces[i].z
来比较顶部的深度和中的阴影贴图的深度vertexInShadowSpaces[i].xy
。如果比较成功,则textureProj
返回1。否则,它返回零。零表示此顶点/片段在阴影中,而一表示该顶点/片段不在阴影中。请注意,textureProj
它也可以返回从零到一的值,具体取决于阴影贴图的配置方式。在这个例子中textureProj
根据相邻深度执行多次深度测试,并返回加权平均值。该加权平均值可以使阴影平滑。衰减度
到光源的距离只是照明方向矢量的大小或长度。请注意,我们不使用归一化的照明方向,因为这样的距离将等于1。到光源的距离对于计算衰减是必需的。衰减意味着远离光源的光的影响减小。参数constantAttenuation
,linearAttenuation
并quadraticAttenuation
可以指定任意值。它应该下手constantAttenuation = 1
,linearAttenuation = 0
和quadraticAttenuation = 1
。使用这些参数,在光源的位置它等于1,并且在远离光源时趋于零。最终色彩照明
要计算照明的最终颜色,您需要添加漫反射和反射颜色。有必要在绕过场景中的光源的周期中将其添加到驱动器中。周围环境
照明模型中的环境照明组件基于材质的环境颜色,环境照明的颜色以及漫反射纹理的颜色。与每个光源累积的漫反射和反射颜色的计算相比,周围的光源绝不能超过一个,因此该计算应该只执行一次。请注意,执行SSAO时,环境光的颜色会派上用场。全部放在一起
最终颜色是环境颜色,漫反射颜色,反射颜色和发射颜色的总和。源代码
法线贴图
使用法线贴图可以将新零件添加到曲面,而无需其他几何图形。通常,在3D建模程序中工作时,会创建网格的高多边形版本和低多边形版本。然后从高多边形网格获取顶点的法线并将其烘焙到纹理中。此纹理是法线贴图。然后,在片段着色器内部,用烘焙到法线贴图中的高多边形网格的法线替换低多边形网格的顶点的法线。因此,在照亮网格时,看起来它的多边形比实际更多。这样,您可以保持较高的FPS,同时传输高多边形版本的大多数详细信息。在这里,我们看到了从高多边形模型到低多边形模型的过渡,然后是具有法线贴图叠加的低多边形模型的过渡。但是,不要忘记覆盖法线贴图只是一种幻想。在某个角度下,表面再次开始变得平坦。顶点着色器
从顶点着色器开始,我们需要将法线向量,双法线向量和切线向量输出到片段着色器。这些向量在片段着色器中用于将法线贴图的法线从切线空间转换为可视空间。p3d_NormalMatrix
将顶点,双法线和切线向量的法线向量转换为视图空间。请不要忘记,在查看空间中所有坐标都相对于摄像机的位置。[p3d_NormalMatrix]是ModelViewMatrix的顶部3x3反向转置元素。此结构用于将法线向量转换为查看空间的坐标。
来源
我们还需要将法线贴图的UV坐标输出到片段着色器。片段着色器
回想一下,顶点法线用于计算光照。但是,要计算光照,法线贴图会为我们提供其他法线。在片段着色器中,我们需要将顶点的法线替换为法线贴图中的法线。
使用由顶点着色器传输的法线贴图的坐标,我们从贴图中提取相应的法线。
上面,我展示了如何将法线转换为颜色以创建法线贴图。现在我们需要逆转此过程,以便可以将原始法线烘焙到地图上。 [ r, g, b] = [ r * 2 - 1, g * 2 - 1, b * 2 - 1] = [ x, y, z]
这是从法线贴图中解开法线的过程。
从法线贴图获得的法线通常在切线空间中。但是,它们可以在另一个空间中。例如,Blender允许您在切线空间,对象空间,世界空间和摄影机空间中烘焙法线。要将法线贴图的法线从切线空间转移到查看空间,请基于切线向量,双法线向量和顶点法线创建3x3矩阵。将法线乘以该矩阵并将其标准化。这就是我们以法线结束的地方。所有其他照明计算仍在执行。源代码