GLSL 编程/Blender/视图空间着色
本教程介绍了制服变量。它建立在最小着色器教程、RGB 立方体教程和着色器调试教程的基础之上。
在本教程中,我们将研究一个着色器,它根据片段在“视图空间”中的位置改变片段颜色,“视图空间”只是世界空间的旋转版本,其中相机位于原点并指向负 轴,而 轴与相机的垂直轴对齐;请参阅“顶点变换”中的描述。这个概念并不复杂;然而,它有极其重要的应用,例如使用灯光和环境贴图进行着色。我们也将看看现实世界中的着色器;例如,如何让非程序员使用你的着色器?
正如着色器调试教程中提到的,属性gl_Vertex
指定了对象坐标,即网格局部对象(或模型)空间中的坐标。对象空间(或对象坐标系)对每个对象都是唯一的;但是,所有对象都变换到一个共同的坐标系中——视图空间。从概念上讲,它们首先被变换到共同的世界空间(通过模型变换),然后变换到视图空间(通过视图变换)。在实践中,这两个变换被组合成一个变换(模型视图变换)。
如果一个对象直接放入世界空间,那么对象到世界的变换由 Blender 中对象的属性中的变换部分指定。要查看它,请在3D 视图中选择该对象,然后打开属性窗口的对象选项卡,并展开变换部分。这里有“位置”、“旋转”和“缩放”参数,它们指定了顶点如何从对象坐标变换到世界坐标。顶点通过平移、旋转和缩放的变换,以及变换的组合及其作为 4×4 矩阵的表示,在“顶点变换”中进行了讨论。该页面还讨论了从世界空间到视图空间的变换,它只依赖于相机的 position 和方向(可以通过选择相机并打开属性窗口进行检查;可以通过在3D 视图菜单中选择视图 > 相机来激活它)。
在以下示例中,从对象空间到视图空间的变换被放入一个 4×4 矩阵中,它也被称为“模型视图矩阵”(因为它结合了“模型变换”和“视图变换”)。此矩阵在内置制服变量gl_ModelViewMatrix
中可用(类似于我们在前面教程中使用的gl_ModelViewProjectionMatrix
)。此制服在以下顶点着色器中使用,该着色器应该分配给最小着色器教程中 Python 脚本的变量VertexShader
varying vec4 position_in_view_space;
void main()
{
position_in_view_space = gl_ModelViewMatrix * gl_Vertex;
// transformation of gl_Vertex from object coordinates
// to view coordinates;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
片段着色器应该分配给变量FragmentShader
varying vec4 position_in_view_space;
void main()
{
float dist = distance(position_in_view_space,
vec4(0.0, 0.0, 0.0, 1.0));
// computes the distance between the fragment position
// and the origin (4th coordinate should always be 1
// for points). The origin in view space is actually
// the camera position.
if (dist < 5.0)
{
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
// color near origin
}
else
{
gl_FragColor = vec4(0.3, 0.3, 0.3, 1.0);
// color far from origin
}
}
通常,OpenGL 应用程序必须设置制服变量的值;但是,Blender 负责设置预定义制服变量的值,例如gl_ModelViewMatrix
和gl_ModelViewProjectionMatrix
(实际上是矩阵乘积gl_ProjectionMatrix * gl_ModelViewMatrix
);因此,我们不必担心它。(然而,在某些情况下,除非用3D 视图 > 视图 > 相机 > 活动相机激活相机视图,否则 Blender 似乎会错误地设置其中一些矩阵(特别是gl_ModelViewMatrix
)。)
此着色器将顶点位置变换到视图空间,并将其以 varying 变量的形式提供给片段着色器。对于片段着色器,varying 变量包含片段在视图坐标中的插值位置。根据此位置到视图坐标系原点的距离,将设置两种颜色之一。因此,如果你使用此着色器移动一个对象,它将在靠近相机的地方变成绿色。离相机越远,它就会变成深灰色。
着色器中的许多计算可以在视图空间中执行。由于光源的位置是在视图空间中指定的,并且默认情况下没有提供到世界空间的变换,因此这通常是最方便和最有效的坐标系。但是,在某些情况下,需要在世界空间中工作(例如,使用位于世界空间中的环境贴图时)。在这些情况下,需要在制服变量中为着色器提供从视图空间到世界空间的变换。制服变量必须在使用它的任何着色器的 main 函数之外定义
uniform mat4 viewMatrixInverse;
mat4
只是 4×4 矩阵的数据类型,而uniform
表示它对所有顶点和片段的值都相同,它必须在外部设置。实际上,在 Blender 中,它是在 Python 脚本中使用以下行设置的
shader.setUniformMatrix4('viewMatrixInverse', value_of_uniform)
其中shader
是着色器程序的 Python 对象,'viewMatrixInverse'
是着色器中制服变量的名称,而value_of_uniform
是应该设置的值。setUnifomMatrix4
用于设置 4×4 矩阵制服,如Blender 的 Python API中所述。更多函数可用于设置其他类型制服。
这里,我们需要一个 4×4 矩阵,它将点从视图空间(也称为眼睛或相机空间)变换到世界空间,可以通过以下方式获得
value_of_uniform = bge.logic.getCurrentScene().active_camera.camera_to_world
即当前场景提供了一个活动相机,它提供了我们需要的变换。(仅仅为了完整性:相机还提供了world_to_camera
变换的矩阵,即视图矩阵本身。)
我们现在可以重写示例,使其在世界空间中工作而不是视图空间,即如果片段在世界空间中靠近原点,则将它们着色为绿色
import bge
cont = bge.logic.getCurrentController()
VertexShader = """
uniform mat4 viewMatrixInverse; // view to world
// transformation (the view matrix corresponds to the
// world to view transformation; thus, the inverse
// matrix corresponds to the view to world trafo)
varying vec4 position_in_world_space;
void main()
{
vec4 position_in_view_space =
gl_ModelViewMatrix * gl_Vertex;
// transformation of gl_Vertex from object coordinates
// to view coordinates;
position_in_world_space = viewMatrixInverse *
position_in_view_space;
// transformation of gl_Vertex from view coordinates
// to world coordinates;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
"""
FragmentShader = """
varying vec4 position_in_world_space;
void main()
{
float dist = distance(position_in_world_space,
vec4(0.0, 0.0, 0.0, 1.0));
// computes the distance between the fragment position
// and the origin (the 4th coordinate should always
// be 1 for points).
if (dist < 5.0)
{
gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
// color near origin
}
else
{
gl_FragColor = vec4(0.3, 0.3, 0.3, 1.0);
// color far from origin
}
}
"""
mesh = cont.owner.meshes[0]
for mat in mesh.materials:
shader = mat.getShader()
if shader != None:
if not shader.isValid():
shader.setSource(VertexShader, FragmentShader, 1)
value_of_uniform = \
bge.logic.getCurrentScene().active_camera.camera_to_world
shader.setUniformMatrix4('viewMatrixInverse', value_of_uniform)
请注意,我们应该在每一帧中设置制服的当前值,而我们只设置一次着色器的源代码。
还要注意,顶点着色器中还有一个额外的变换。这是一个矩阵向量乘法,它包含 16 次乘法,因此会消耗一些性能。如果可能,应该避免这种额外的变换。在实践中,因此我们将在视图空间中执行灯光计算,这样就不必将光源的视图空间位置变换到世界空间。
光源的位置实际上是作为内置制服提供的,类似于gl_ModelViewMatrix
和gl_ProjectionModelViewMatrix
。所有这些内置制服都是为 OpenGL 兼容性配置文件定义的。相应的变换在“顶点变换”中进行了详细描述。
正如你在上面的着色器中看到的,这些制服不必定义;它们始终在 Blender 的 GLSL 着色器中可用。如果你必须定义它们,定义将如下所示
uniform mat4 gl_ModelViewMatrix;
uniform mat4 gl_ProjectionMatrix;
uniform mat4 gl_ModelViewProjectionMatrix;
uniform mat4 gl_TextureMatrix[gl_MaxTextureCoords];
uniform mat3 gl_NormalMatrix;
// transpose of the inverse of gl_ModelViewMatrix
uniform mat4 gl_ModelViewMatrixInverse;
uniform mat4 gl_ProjectionMatrixInverse;
uniform mat4 gl_ModelViewProjectionMatrixInverse;
uniform mat4 gl_TextureMatrixInverse[gl_MaxTextureCoords];
uniform mat4 gl_ModelViewMatrixTranspose;
uniform mat4 gl_ProjectionMatrixTranspose;
uniform mat4 gl_ModelViewProjectionMatrixTranspose;
uniform mat4 gl_TextureMatrixTranspose[gl_MaxTextureCoords];
uniform mat4 gl_ModelViewMatrixInverseTranspose;
uniform mat4 gl_ProjectionMatrixInverseTranspose;
uniform mat4 gl_ModelViewProjectionMatrixInverseTranspose;
uniform mat4 gl_TextureMatrixInverseTranspose[
gl_MaxTextureCoords];
struct gl_MaterialParameters {
vec4 emission; // = Properties > Material tab > Diffuse Color *
// Shading > Emit (alpha = 1)
vec4 ambient; // = black
vec4 diffuse; // = black
// if Properties > Material tab > Diffuse Color activated;
// diffuse = Properties > Object tab > Object Color
vec4 specular; // = Properties > Material tab > Specular Color
// * Intensity (alpha = Intensity!)
float shininess;
// = Properties > Material tab > Specular > Hardness / 4.0
};
uniform gl_MaterialParameters gl_FrontMaterial; // see above
uniform gl_MaterialParameters gl_BackMaterial;
// same as gl_FrontMaterial
struct gl_LightSourceParameters {
vec4 ambient; // = black
vec4 diffuse; // = Properties > Object Data tab > Color *
// Energy (alpha = 1)
vec4 specular; // = Properties > Object Data tab > Color *
// Energy (alpha = 1)
vec4 position; // in view space, w = 0 for Sun (directional),
// w = 1 otherwise
vec4 halfVector; // = average of vectors to light and viewer
vec3 spotDirection; // in view space
float spotExponent; // = Properties > Object Data tab >
// Spot Shape > Blend * 128.0
float spotCutoff; // = Properties > Object Data tab >
// Spot Shape > Size / 2.0 (180.0 if not spotlight)
float spotCosCutoff; // derived: cos(spotCutoff)
// (range: [1.0,0.0] or -1.0 if not spotlight)
float constantAttenuation; // = 1.0
float linearAttenuation; // = 0.0
float quadraticAttenuation;// = 0.0
};
uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];
// see above for each light
struct gl_LightModelParameters
{
vec4 ambient; // = Properties > World tab > World > Ambient Color
// * Material tab > Shading > Ambient
};
uniform gl_LightModelParameters gl_LightModel; // see above
...
实际上,OpenGL 的兼容性配置文件定义了更多(不太有趣的)制服;请参阅可从Khronos 的 OpenGL 页面获得的“OpenGL 着色语言 4.10.6 规范”的第 7 章。显然 Blender 支持其中很多,但并非全部。
其中一些制服是数组,例如gl_TextureMatrix
。实际上,一个矩阵数组gl_TextureMatrix[0]
、gl_TextureMatrix[1]
、gl_TextureMatrix[2]
、...、gl_TextureMatrix[gl_MaxTextureCoords - 1]
是可用的,其中gl_MaxTextureCoords
是一个内置整数。
其他的是结构体,例如gl_LightModel
;因此,你必须使用点符号来访问它的成员,例如gl_LightModel.ambient
用于环境场景颜色。
正如我们上面所见,可以使用 Python 脚本访问视图矩阵 和逆视图矩阵 。另一方面,OpenGL 提供了模型矩阵 和视图矩阵 的乘积,即模型视图矩阵 ,它可以在 uniform gl_ModelViewMatrix
中获取。不太容易获取的是模型矩阵 。
但是,我们可以很容易地计算它。数学公式如下
换句话说,模型矩阵是逆视图矩阵和模型视图矩阵的乘积。假设我们已经定义了 uniform viewMatrixInverse
,我们可以用 GLSL 中的这种方法来计算模型矩阵
mat4 modelMatrix = viewMatrixInverse * gl_ModelViewMatrix;
Uniform 变量有一个重要的应用:用户可以设置的 uniform。实际上,这些在 Blender 中被称为游戏属性。你可以把它们看作是对象的参数,更准确地说,是它们着色器的参数。没有参数的着色器通常只被它的程序员使用,因为即使是最小的必要更改也需要一些编程。另一方面,使用具有描述性名称的参数的着色器可以被其他人使用,即使是非程序员,例如 CG 艺术家。想象一下,你是一个游戏开发团队,一个 CG 艺术家要求你为 100 次设计迭代中的每一次都调整你的着色器。显而易见,几个参数,即使是 CG 艺术家也可以玩弄它们,可能会为你节省大量的时间。此外,想象一下你想要出售你的着色器:参数通常会极大地提高你的着色器的价值。
有关游戏属性的 Blender 文档 描述了如何设置一个新的游戏属性:选择 Python 脚本附加到的对象。打开逻辑编辑器,从菜单中选择视图 > 属性(或者按n或点击左上角的小图标)。点击添加游戏属性,将名称设置为my_uniform
,类型设置为Float
。然后,我们可以在着色器中定义一个 uniform(如果我们想自找麻烦,它可以有不同的名称),并使用 Python 函数设置它
shader.setUniform1f('my_uniform', cont.owner.get('my_uniform'))
其中第一个'my_uniform'
指的是着色器中的名称,第二个'my_uniform'
指的是游戏属性的名称。setUniform1f
用于设置一维浮点数 uniform,而其他函数可用于设置其他类型的 uniform,如 Blender 的 Python API 文档 中所述。
一个简单的例子可以使用下面的 Python 脚本,它使用游戏属性的值来设置片段颜色红色分量的强度
import bge
cont = bge.logic.getCurrentController()
VertexShader = """
void main()
{
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
"""
FragmentShader = """
uniform float my_uniform;
void main()
{
gl_FragColor = vec4(my_uniform, 0.0, 0.0, 1.0);
}
"""
mesh = cont.owner.meshes[0]
for mat in mesh.materials:
shader = mat.getShader()
if shader != None:
if not shader.isValid():
shader.setSource(VertexShader, FragmentShader, 1)
shader.setUniform1f('my_uniform', cont.owner.get('my_uniform'))
恭喜你,你成功了!我们讨论了
- 如何将顶点转换到视图坐标系。
- 如何将顶点转换到世界坐标系。
- Blender 支持的最重要的 OpenGL 特定 uniform。
- 如何通过添加游戏属性来使着色器更有用、更有价值。
如果你想了解更多信息
- 关于向量和矩阵运算(例如
distance()
函数),你应该阅读“向量和矩阵运算”。 - 关于标准顶点变换,例如模型矩阵和视图矩阵,你应该阅读“顶点变换”。
- 关于将变换矩阵应用于点和方向,你应该阅读“应用矩阵变换”。
- 关于在 Blender 的 Python API 中设置着色器 uniform,你应该阅读 Blender 的 关于 bge.types.BL_Shader 类别的文档。