PyOpenGL代码实战(四):着色器
一、理论基础
我们在上一章已经介绍过着色器。着色器主要有顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)两种。
实际上,着色器程序并不只有这两种,还有几何着色器、曲面细分着色器等。因为应用较少,此处不再介绍。
顶点着色器:对顶点进行处理(三角形中的每一个顶点都会运行一次顶点着色器程序)。主要用于将顶点的模型坐标转换为NDC坐标。
片元着色器:对像素进行处理(三角形中的每一个像素都会运行一次片元着色器程序)。主要用于像素的着色。
二、着色器编程
在OpenGL中,着色器是由一门名为GLSL的编程语言写成的。GLSL是一种与C语言十分相似的GPU编程语言。
着色器还可以使用 HLSL 编写,在 Unity 中常用。
本文主要以上一章提供的着色器代码为例。
顶点着色器:
#version 330 corein vec3 aPos;
in vec3 aColor;
out vec3 VertexColor;void main()
{gl_Position = vec4(aPos, 1.0f);VertexColor = aColor;
}
片元着色器:
#version 330 corein vec3 VertexColor;
out vec4 FragColor;void main()
{FragColor = vec4(VertexColor, 1.0f);
}
1、变量
1)变量类型
在GLSL中有以下类型的变量:
| 类型 | 说明 |
|---|---|
| int | 整型数 |
| float | 浮点数 |
| bool | 布尔类型 |
| vecX | 浮点型向量,X表示元素个数(X可取2,3,4) |
| ivecX | 整数型向量 |
| bvecX | 布尔型向量 |
| matX | X×X的浮点型矩阵 |
| sampler2D | 2D纹理 |
| samplerCube | 立方体纹理 |
上面的表格中,列出了GLSL的所有变量类型,GLSL中的大部分数据类型与其它编程语言无异,sampler2D和samplerCube将在后续章节中详细介绍。
除此之外,GLSL支持数组与结构体类型的变量,语法与C语言完全一致,此处不再介绍。
2)变量的传递
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,参数以数组的形式传递 |
2、功能实现
1)顶点着色器
#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 坐标。
2)片元着色器
#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 timeglUseProgram(program) # 使用着色器
tLoc = glGetUniformLocation(program, "t")
glUniform1f(tLoc, (math.sin(time()) + 1) / 2)
注意这段代码应该写在渲染函数里,因为t的值每次渲染都会发生变化。
至此,实现这一功能的代码已经完成。完整代码如下:
from time import timefrom OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.arrays.vbo import VBO
from csdn.window import Window
import numpy as npw = 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 npclass 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.sizedtype = value.dtypefuncs = {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)returnelif 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 timeimport numpy as np
from OpenGL.GL import *
from OpenGL.arrays.vbo import VBO# 导入Window类和Shader类
from shader import Shader
from window import Windoww = 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) / 2shader.setUniform("t", t)glBindVertexArray(vao)glDrawArrays(GL_TRIANGLES, 0, 3)w.loop(render)
五、结语
本文初步介绍了着色器代码的编写,关于着色器的更多细节,将会在后续章节中逐步介绍。在下一章中,将会介绍纹理的相关内容。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
