我们在上一章已经介绍过着色器。着色器主要有顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)两种。
实际上,着色器程序并不只有这两种,还有几何着色器、曲面细分着色器等。因为应用较少,此处不再介绍。
顶点着色器:对顶点进行处理(三角形中的每一个顶点都会运行一次顶点着色器程序)。主要用于将顶点的模型坐标转换为NDC坐标。
片元着色器:对像素进行处理(三角形中的每一个像素都会运行一次片元着色器程序)。主要用于像素的着色。
在OpenGL中,着色器是由一门名为GLSL的编程语言写成的。GLSL是一种与C语言十分相似的GPU编程语言。
着色器还可以使用 HLSL 编写,在 Unity 中常用。
本文主要以上一章提供的着色器代码为例。
顶点着色器:
#version 330 core
in vec3 aPos;
in vec3 aColor;
out vec3 VertexColor;
void main()
{
gl_Position = vec4(aPos, 1.0f);
VertexColor = aColor;
}
片元着色器:
#version 330 core
in vec3 VertexColor;
out vec4 FragColor;
void main()
{
FragColor = vec4(VertexColor, 1.0f);
}
在GLSL中有以下类型的变量:
类型 | 说明 |
---|---|
int | 整型数 |
float | 浮点数 |
bool | 布尔类型 |
vecX | 浮点型向量,X表示元素个数(X可取2,3,4) |
ivecX | 整数型向量 |
bvecX | 布尔型向量 |
matX | X×X的浮点型矩阵 |
sampler2D | 2D纹理 |
samplerCube | 立方体纹理 |
上面的表格中,列出了GLSL的所有变量类型,GLSL中的大部分数据类型与其它编程语言无异,sampler2D
和samplerCube
将在后续章节中详细介绍。
除此之外,GLSL支持数组与结构体类型的变量,语法与C语言完全一致,此处不再介绍。
GLSL中的变量,可以在CPU、顶点着色器、片元着色器之间传递。传递变量的语法如下:
in vec3 aPos;
out vec4 FragColor;
uniform mat4 m;
在上面的代码中,in/out/uniform
是用于表示这个变量是由谁传递来的。
关键字 | 在顶点着色器中的含义 | 在片元着色器中的含义 |
---|---|---|
in | 该变量由CPU(通过顶点数据)传入 | 该变量由顶点着色器传入 |
out | 该变量需传递给片元着色器 | 固定用于out vec4 FragColor ,设置像素颜色 |
uniform | 统一变量。该变量由CPU(在CPU代码中设置)传入。对于所有顶点,该变量的值相同 | 同顶点着色器 |
在例子中,顶点着色器定义变量的代码如下:
// aPos和aColor都是由CPU通过顶点数据传入的
in vec3 aPos;
in vec3 aColor;
// VertexColor需要传递给片元着色器
out vec3 VertexColor;
片元着色器定义变量的代码如下:
// VertexColor是由顶点着色器传入的变量,注意此变量应与顶点着色器的相应变量同名同类型
in vec3 VertexColor;
// 固定写法,后续代码设置FragColor的值以设置像素颜色
out vec4 FragColor;
上面的代码只是定义了变量的传递方式,下面介绍如何真正传递变量的值。
首先,CPU通过顶点数据向顶点着色器传递数据。这一部分的内容在上一章已经做过介绍。使用VBO传递顶点数据,使用glVertexAttribPointer
将顶点数据分配给对应的变量。
# 坐标
aPosLoc = glGetAttribLocation(program, 'aPos')
glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(0))
glEnableVertexAttribArray(aPosLoc)
# 颜色
aColorLoc = glGetAttribLocation(program, 'aColor')
glVertexAttribPointer(aColorLoc, 3, GL_FLOAT, GL_FALSE, 24, ctypes.c_void_p(12))
glEnableVertexAttribArray(aColorLoc)
注意,顶点着色器的每一个in
变量,都会有一个索引,可以通过以下语法设置索引:
in vec3 aColor; // 未设置索引,编译器自动分配索引
layout (location = 0) in vec3 aPos; // 手动设置索引
如果没有设置索引,则编译器会自动为变量分配索引。
通过glGetAttribLocation(program, name)
获取变量的索引值,通过索引值就可以将顶点数据与对应的变量绑定了。
顶点着色器向片元着色器传递变量,只需要在顶点着色器中用in
修饰变量,在片元着色器中用out
修饰变量,保证对应变量同名同类型即可。值得注意的是,顶点着色器是针对顶点而言的,而片元着色器是针对像素的,所有由顶点着色器传递给片元着色器的变量,会由GPU自动完成插值处理。
最后,是CPU通过设置统一变量来给两种着色器程序设置变量值。
在GLSL中定义统一变量:
uniform vec3 color;
在Python代码中设置统一变量的值:
loc = glGetUniformLocation(program, "color")
glUniform3f(loc, 1, 0, 0)
和顶点着色器中的in
变量一样,每个uniform
变量也有一个唯一的索引,通过glGetUniformLocation(program, name)
可以获取对应变量的索引,再通过一系列形如glUniformX(loc, *args)
的函数来设置变量的值。其中X表示要设置的变量的长度和类型。
X |
含义 |
---|---|
f |
float |
i |
int |
ui |
unsigned int |
3f |
vec3,参数以3个float类型的变量的形式传递 |
3fv |
vec3,参数以数组的形式传递 |
#version 330 core // 指定GLSL版本
// 定义变量
in vec3 aPos;
in vec3 aColor;
out vec3 VertexColor;
// 主函数
void main()
{
gl_Position = vec4(aPos, 1.0); // 设置顶点坐标
VertexColor = aColor; // 设置要传给片元着色器的变量的值
}
注意:代码中的注释仅为说明作用,请不要在 GLSL 中输入中文(包括在注释中)。
在这段代码里,第一行用于指定GLSL版本为3.30,如果没有这一句代码,则编译器默认使用GLSL1.1版本,编译会报错。
在主函数中,有这样一句代码:
gl_Position = vec4(aPos, 1.0f); // 设置顶点坐标
其中gl_Position
是GLSL自带的变量,它用于设置顶点的NDC坐标。
注意到这里的
gl_Position
是一个4维向量,而 3D 空间中的坐标应该是三维的。因为这里的坐标是齐次坐标,GPU会自动将坐标的前三个分量除以最后一个分量,得到的三维向量即为 NDC 坐标。
#version 330 core
// 定义变量
in vec3 VertexColor;
out vec4 FragColor;
// 主函数
void main()
{
FragColor = vec4(VertexColor, 1.0f); // 设置像素颜色
}
这段代码中,有这样一句代码值得注意:
FragColor = vec4(VertexColor, 1.0f); // 设置像素颜色
FragColor
变量是用户在前面定义的out
变量,用于设置像素的颜色。
接下来,我们将修改我们在前一章提供的着色器代码,来实现三角形时隐时现的效果:
由于gif的压缩原理,上图可能有些失真。
这个效果的实现非常简单,我们原来在片元着色器中有这样一段代码:
FragColor = vec4(VertexColor, 1.0f);
这段代码可以设置像素的颜色,我们只需要将VertexColor
乘上一个随时间周期性变化的变量t
,就可以实现这种效果:
// 首先在主函数之外定义uniform变量
uniform float t;
// ...
// 修改主函数的代码
FragColor = vec4(VertexColor * t, 1.0f);
接下来,我我们要通过Python传入t的值。
import math
from time import time
glUseProgram(program) # 使用着色器
tLoc = glGetUniformLocation(program, "t")
glUniform1f(tLoc, (math.sin(time()) + 1) / 2)
注意这段代码应该写在渲染函数里,因为t
的值每次渲染都会发生变化。
至此,实现这一功能的代码已经完成。完整代码如下:
from time import time
from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.arrays.vbo import VBO
from csdn.window import Window
import numpy as np
w = Window(1920, 1080, "Test")
triangle = np.array([
-0.5, -0.5, 0, 1, 0, 0,
0.5, -0.5, 0, 0, 1, 0,
0, 0.5, 0, 0, 0, 1
], dtype=np.float32)
vao = glGenVertexArrays(1)
glBindVertexArray(vao)
vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()
vs = """
#version 330 core
in vec3 aPos;
in vec3 aColor;
out vec3 VertexColor;
void main()
{
gl_Position = vec4(aPos, 1.0f);
VertexColor = aColor;
}
"""
fs = """
#version 330 core
in vec3 VertexColor;
uniform float t;
out vec4 FragColor;
void main()
{
FragColor = vec4(VertexColor * t, 1.0f);
}
"""
vsProgram = shaders.compileShader(vs, GL_VERTEX_SHADER)
fsProgram = shaders.compileShader(fs, GL_FRAGMENT_SHADER)
program = shaders.compileProgram(vsProgram, fsProgram)
aPosLoc = glGetAttribLocation(program, 'aPos')
glVertexAttribPointer(aPosLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(0))
glEnableVertexAttribArray(aPosLoc)
aColorLoc = glGetAttribLocation(program, 'aColor')
glVertexAttribPointer(aColorLoc, 3, GL_FLOAT, False, 24, ctypes.c_void_p(12))
glEnableVertexAttribArray(aColorLoc)
def render():
# 此处传入t的值
glUseProgram(program)
tLoc = glGetUniformLocation(program, "t")
glUniform1f(tLoc, (np.sin(time()) + 1) / 2)
glBindVertexArray(vao)
glDrawArrays(GL_TRIANGLES, 0, 3)
w.loop(render)
接下来,我们将把与着色器有关的代码封装成Shader类,以提高代码的复用性和可扩展性。
from OpenGL.GL import shaders
from OpenGL.GL import *
import numpy as np
class Shader:
def __init__(self, vsPath, fsPath) -> None:
""" 读取 GLSL 文件并编译 """
with open(vsPath, 'r') as f:
text = f.read()
vs = shaders.compileShader(text, GL_VERTEX_SHADER)
with open(fsPath, 'r') as f:
text = f.read()
fs = shaders.compileShader(text, GL_FRAGMENT_SHADER)
self.shader = shaders.compileProgram(vs, fs)
def use(self):
glUseProgram(self.shader)
def setUniform(self, name, value):
""" 根据传入的数据类型,自动选择 glUniformX 函数 """
self.use()
loc = glGetUniformLocation(self.shader, name)
dtype = type(value)
if dtype == np.ndarray:
size = value.size
dtype = value.dtype
funcs = {
np.int32: [glUniform1i, glUniform2i, glUniform3i, glUniform4i],
np.uint: [glUniform1ui, glUniform2ui, glUniform3ui, glUniform4ui],
np.float32: [glUniform1f, glUniform2f, glUniform3f, glUniform4f],
np.double: [glUniform1d, glUniform2d, glUniform3d, glUniform4d],
}
func = funcs[dtype][size - 1]
func(loc, *value)
return
elif dtype == int or dtype == np.int32 or dtype == np.int64:
glUniform1i(loc, value)
elif dtype == float or dtype == np.float64 or dtype == np.float32:
glUniform1f(loc, value)
else:
raise RuntimeError("未知的参数类型!")
def setAttrib(self, name, size, dtype, stride, offset):
""" 设置顶点属性链接 """
loc = glGetAttribLocation(self.shader, name)
glVertexAttribPointer(loc, size, dtype, False, stride, ctypes.c_void_p(offset))
glEnableVertexAttribArray(loc)
封装完成后,示例代码:
import math
from time import time
import numpy as np
from OpenGL.GL import *
from OpenGL.arrays.vbo import VBO
# 导入Window类和Shader类
from shader import Shader
from window import Window
w = Window(1920, 1080, "Test")
triangle = np.array([
-0.5, -0.5, 0, 1, 0, 0,
0.5, -0.5, 0, 0, 1, 0,
0, 0.5, 0, 0, 0, 1
], dtype=np.float32)
vao = glGenVertexArrays(1)
glBindVertexArray(vao)
vbo = VBO(triangle, GL_STATIC_DRAW)
vbo.bind()
# 导入顶点着色器文件和片元着色器文件
shader = Shader("base.vert", "base.frag")
shader.setAttrib("aPos", 3, GL_FLOAT, 24, 0)
shader.setAttrib("aColor", 3, GL_FLOAT, 24, 12)
def render():
shader.use()
t = (math.sin(time()) + 1) / 2
shader.setUniform("t", t)
glBindVertexArray(vao)
glDrawArrays(GL_TRIANGLES, 0, 3)
w.loop(render)
本文初步介绍了着色器代码的编写,关于着色器的更多细节,将会在后续章节中逐步介绍。在下一章中,将会介绍纹理的相关内容。