VisPy是一个基于Python的高性能可视化库,旨在实现快速的数据可视化和科学计算。
安装Python VisPy
要开始使用Python VisPy,需要先安装它。可以使用pip来安装VisPy:
pip install vispy
以下是一些可能需要安装的包
pip install pyyaml
pip install glfw
pip install scipy
这里我主要想要学习面向gloo的代码,这个是最底层的一种编码方式。在正式学习vispy的使用前,我们需要先对GLSL(OpenGL Shading Language)有一个初步的认识,这个语言可以理解成对显卡的编程,能够处理scalar (float, int, bool), vector (vec2, vec3, vec4, bvec2, bvec3, bvec3, ivec2, ivec3, ivec4), matrices (mat2, mat3, mat4)等形式的数据,也内置了包括 max, min, pow, sin, dot, normalize, clamp 等函数。
使用GLSL构建着色器
1. 变量
变量类别 | 变量类型 | 描述 |
---|---|---|
空 | void | 用于无返回值的函数或空的参数列表 |
标量 | float, int, bool | 浮点型,整型,布尔型的标量数据类型 |
浮点型向量 | float, vec2, vec3, vec4 | 包含1,2,3,4个元素的浮点型向量 |
整数型向量 | int, ivec2, ivec3, ivec4 | 包含1,2,3,4个元素的整型向量 |
布尔型向量 | bool, bvec2, bvec3, bvec4 | 包含1,2,3,4个元素的布尔型向量 |
矩阵 | mat2, mat3, mat4 | 尺寸为2x2,3x3,4x4的浮点型矩阵 |
纹理句柄 | sampler2D, samplerCube | 表示2D,立方体纹理的句柄 |
除上述之外,着色器中还可以将它们构成数组或结构体,以实现更复杂的数据类型。
PS:GLSL 中没有指针类型。
标量对应 C 语言的基础数据类型,它的构造和 C 语言一致,如下:
float myFloat = 1.0;
bool myBool = true;
myFloat = float(myBool); // bool -> float
myBool = bool(myFloat); // float -> bool
向量
当构造向量时,向量构造器中的各参数将会被转换成相同的类型(浮点型、整型或布尔型)。往向量构造器中传递参数有两种形式:
- 如果向量构造器中只提供了一个标量参数,则向量中所有值都会设定为该标量值。
- 如果提供了多个标量值或提供了向量参数,则会从左至右使用提供的参数来给向量赋值,如果使用多个标量来赋值,则需要确保标量的个数要多于向量构造器中的个数。
向量构造器用法如下:
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5}
vec3 temp = vec3(myVec3); // temp = myVec3
vec2 myVec2 = vec2(myVec3); // myVec2 = {myVec3.x, myVec3.y}
myVec4 = vec4(myVec2, temp, 0.0); // myVec4 = {myVec2.x, myVec2.y , temp, 0.0 }
矩阵
矩阵的构造方法则更加灵活,有以下规则:
- 如果对矩阵构造器只提供了一个标量参数,该值会作为矩阵的对角线上的值。例如
mat4(1.0)
可以构造一个 4 × 4 的单位矩阵 - 矩阵可以通过多个向量作为参数来构造,例如一个 mat2 可以通过两个 vec2 来构造
- 矩阵可以通过多个标量作为参数来构造,矩阵中每个值对应一个标量,按照从左到右的顺序
除此之外,矩阵的构造方法还可以更灵活,只要有足够的组件来初始化矩阵,其构造器参数可以是标量和向量的组合。在 OpenGL ES 中,矩阵的值会以列的顺序来存储。在构造矩阵时,构造器参数会按照列的顺序来填充矩阵,如下:
mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 1.0, 1.0); // 第三列
向量和矩阵的分量
单独获得向量中的组件有两种方法:即使用 "."
符号或使用数组下标方法。依据构成向量的组件个数,向量的组件可以通过 {x, y, z, w}
, {r, g, b, a}
或 {s, t, r, q}
等 swizzle 操作来获取。之所以采用这三种不同的命名方法,是因为向量常常会用来表示数学向量、颜色、纹理坐标等。其中的x
、r
、s
组件总是表示向量中的第一个元素,如下表:
分量访问符 | 符号描述 |
---|---|
(x,y,z,w) | 与位置相关的分量 |
(r,g,b,a) | 与颜色相关的分量 |
(s,t,p,q) | 与纹理坐标相关的分量 |
不同的命名约定是为了方便使用,所以哪怕是描述位置的向量,也是可以通过 {r, g, b, a}
来获取。但是在使用向量时不能混用不同的命名约定,即不能使用 .xgr
这样的方式,每次只能使用同一种命名约定。当使用 "."
操作符时,还可以对向量中的元素重新排序,如下
vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0}
vec3 temp;
temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0}
temp = myVec3.zyx; // temp = {2.0, 1.0, 0.0}
除了使用 "."
操作符之外,还可以使用数组下标操作。在使用数组下标操作时,元素 [0]
对应的是 x
,元素 [1]
对应 y
,以此类推。值得注意的是,在 OpenGL ES 2.0 中的某些情况下,数组下标不支持使用非常数的整型表达式(如使用整型变量索引),这是因为对于向量的动态索引操作,某些硬件设备处理起来很困难。在 OpenGL ES 2.0 中仅对 uniform 类型的变量支持这种动态索引。
矩阵可以认为是向量的组合。例如一个 mat2 可以认为是两个 vec2,一个 mat3 可以认为是三个 vec3 等等。对于矩阵来说,可以通过数组下标 “[]”
来获取某一列的值,然后获取到的向量又可以继续使用向量的操作方法,如下:
mat4 myMat4 = mat4(1.0); // Initialize diagonal to 1.0 (identity)
vec4 col0 = myMat4[0]; // Get col0 vector out of the matrix
float m1_1 = myMat4[1][1]; // Get element at [1][1] in matrix
float m2_2 = myMat4[2].z; // Get element at [2][2] in matrix
向量和矩阵的操作
绝大多数情况下,向量和矩阵的计算是逐分量进行的(component-wise)。当运算符作用于向量或矩阵时,该运算独立地作用于向量或矩阵的每个分量。
以下是一些示例:
vec3 v, u;
float f;
v = u + f;
等价于:
v.x = u.x + f;
v.y = u.y + f;
v.z = u.z + f;
再如:
vec3 v, u, w;
w = v + u;
等价于:
w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;
对于整型和浮点型的向量和矩阵,绝大多数的计算都同上,但是对于向量乘以矩阵、矩阵乘以向量、矩阵乘以矩阵则是不同的计算规则。这三种计算使用线性代数的乘法规则,并且要求参与计算的运算数值有相匹配的尺寸或阶数。
例如:
vec3 v, u;
mat3 m;
u = v * m;
等价于:
u.x = dot(v, m[0]); // m[0] is the left column of m
u.y = dot(v, m[1]); // dot(a,b) is the inner (dot) product of a and b
u.z = dot(v, m[2]);
再如:
u = m * v;
等价于:
u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
再如:
mat m, n, r;
r = m * n;
等价于:
r[0].x = m[0].x * n[0].x + m[1].x * n[0].y + m[2].x * n[0].z;
r[1].x = m[0].x * n[1].x + m[1].x * n[1].y + m[2].x * n[1].z;
r[2].x = m[0].x * n[2].x + m[1].x * n[2].y + m[2].x * n[2].z;
r[0].y = m[0].y * n[0].x + m[1].y * n[0].y + m[2].y * n[0].z;
r[1].y = m[0].y * n[1].x + m[1].y * n[1].y + m[2].y * n[1].z;
r[2].y = m[0].y * n[2].x + m[1].y * n[2].y + m[2].y * n[2].z;
r[0].z = m[0].z * n[0].x + m[1].z * n[0].y + m[2].z * n[0].z;
r[1].z = m[0].z * n[1].x + m[1].z * n[1].y + m[2].z * n[1].z;
r[2].z = m[0].z * n[2].x + m[1].z * n[2].y + m[2].z * n[2].z;
对于2阶和4阶的向量或矩阵也是相似的规则。
2. 结构体
与 C 语言相似,除了基本的数据类型之外,还可以将多个变量聚合到一个结构体中,下边的示例代码演示了在GLSL中如何声明结构体:
struct customStruct
{
vec4 color;
vec2 position;
} customVertex;
首先,定义会产生一个新的类型叫做 customStruct
,及一个名为 customVertex
的变量。结构体可以用构造器来初始化,在定义了新的结构体之后,还会定义一个与结构体类型名称相同的构造器。构造器与结构体中的数据类型必须一一对应,如下:
customVertex = customStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
vec2(0.5, 0.5)); // position
结构体的构造器是基于类型的名称,以参数的形式来赋值。获取结构体内元素的方法和C语言中一致:
vec4 color = customVertex.color;
vec4 position = customVertex.position;
3. 数组
除了结构体外,GLSL 中还支持数组。 语法与 C 语言相似,创建数组的方式如下代码所示:
float floatArray[4];
vec4 vecArray[2];
与C语言不同,在GLSL中,关于数组有两点需要注意:
- 除了 uniform 变量之外,数组的索引只允许使用常数整型表达式。
- 在 GLSL 中不能在创建的同时给数组初始化,即数组中的元素需要在定义数组之后逐个初始化,且数组不能使用 const 限定符。
4. 语句
运算符
下表展示了 GLSL 中支持的运算符:
绝大多数的运算符与 C 语言中一致。与 C 语言不同的是:GLSL 中对于参与运算的数据类型要求比较严格,即运算符两侧的变量必须有相同的数据类型。对于二目运算符(*,/,+,-),操作数必须为浮点型或整型,除此之外,乘法操作可以放在不同的数据类型之间如浮点型、向量和矩阵等。
前面矩阵的行数就是结果矩阵的行数,后面矩阵的列数就是结果矩阵的列数。
比较运算符仅能作用于标量,对于向量的比较,GLSL 中有内置的函数,稍后会介绍。
流程控制语句
流程控制语句与 C 语言非常相似,以下示例代码是 if-else
的使用:
if (color.a < 0.25) {
color *= color.a;
} else {
color = vec4(0.0);
}
判断的内容必须是布尔值或布尔表达式,除了基本的 if-else
语句,还可以使用 for
循环,在使用 for
循环时也有一些约束,如循环变量的值必须是编译时已知。如下:
for (int i = 0; i < 3; i++) {
sum += i;
}
在 GLSL 中使用循环时一定要注意:只有一个循环变量,循环变量必须使用简单的语句来增减(如 i++, i–, i+=constant, i-=constant等),循环终止条件也必须是循环变量和常量的简单比较,在循环内部不能改变循环变量的值。
以下代码是 GLSL 中不支持的循环用法的示例:
float myArr[4];
for (int i = 0; i < 3; i++) {
// 错误, [ ]中只能为常量或 uniform 变量,不能为整数量变量(如:i,j,k)
sum += myArr[i];
}
...
uniform int loopIter;
// 错误, 循环变量 loopIter 的值必须是编译时已知
for (int i = 0; i < loopIter; i++) {
sum += i;
}
5. 函数
GLSL 函数的声明与 C 语言中很相似,无非就是返回值,函数名,参数列表。
GLSL 着色器同样是从 main 函数开始执行。另外, GLSL 也支持自定义函数。当然,如果一个函数在定义前被调用,则需要先声明其原型。
值得注意的一点是,GLSL 中函数不能够递归调用,且必须声明返回值类型(无返回值时声明为void)。如下:
vec4 getPosition(){
vec4 v4 = vec4(0.,0.,0.,1.);
return v4;
}
void doubleSize(inout float size){
size= size*2.0 ;
}
void main() {
float psize= 10.0;
doubleSize(psize);
gl_Position = getPosition();
gl_PointSize = psize;
}
限定符
存储限定符
在声明变量时,应根据需要使用存储限定符来修饰,类似 C 语言中的说明符。GLSL 中支持的存储限定符见下表:
限定符 | 描述 |
---|---|
< none: default > | 局部可读写变量,或者函数的参数 |
const | 编译时常量,或只读的函数参数 |
attribute | 由应用程序传输给顶点着色器的逐顶点的数据 |
uniform | 在图元处理过程中其值保持不变,由应用程序传输给着色器 |
varying | 由顶点着色器传输给片段着色器中的插值数据 |
- 本地变量和函数参数只能使用 const 限定符,函数返回值和结构体成员不能使用限定符。
- 数据不能从一个着色器程序传递给下一个阶段的着色器程序,这样会阻止同一个着色器程序在多个顶点或者片段中进行并行计算。
- 不包含任何限定符或者包含 const 限定符的全局变量可以包含初始化器,这种情况下这些变量会在 main() 函数开始之后第一行代码之前被初始化,这些初始化值必须是常量表达式。
- 没有任何限定符的全局变量如果没有在定义时初始化或者在程序中被初始化,则其值在进入 main() 函数之后是未定义的。
- uniform、attribute 和 varying 限定符修饰的变量不能在初始化时被赋值,这些变量的值由 OpenGL ES 计算提供。
默认限定符
如果一个全局变量没有指定限定符,则该变量与应用程序或者其他正在运行的处理单元没有任何联系。不管是全局变量还是本地变量,它们总是在自己的处理单元被分配内存,因此可以对它们执行读和写操作。
const 限定符
任意基础类型的变量都可以声明为常量。常量表示这些变量中的值在着色器中不会发生变化,声明常量只需要在声明时加上限定符 const 即可,声明时必须赋初值。
const float zero = 0.0;
const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);
- 常量声明过的值在代码中不能再改变,这一点和 C 语言或 C++ 一样。
- 结构体成员不能被声明为常量,但是结构体变量可以被声明为常量,并且需要在初始化时使用构造器初始化其值。
- 常量必须被初始化为一个常量表达式。数组或者包含数组的结构体不能被声明为常量(因为数组不能在定义时被初始化)。
attribute 限定符
GLSL 中另一种特殊的变量类型是 attribute 变量。attribute 变量只用于顶点着色器中,用来存储顶点着色器中每个顶点的输入(per-vertex inputs)。attribute 通常用来存储位置坐标、法向量、纹理坐标和颜色等。注意 attribute 是用来存储单个顶点的信息。如下是包含位置,色值 attribute 的顶点着色器示例:
// 顶点着色器 .vsh
attribute vec4 position;
attribute vec4 color;
varying vec4 colorVarying;
void main(void) {
colorVarying = color;
gl_Position = position;
}
着色器中的两个 attribute 变量 position
和 color
由应用程序加载数值。应用程序会创建一个顶点数组,其中包含了每个顶点的位置坐标和色值信息。可使用的最大 attribute 数量也是有上限的,可以使用 gl_MaxVertexAttribs
来获取,也可以使用内置函数 glGetIntegerv
来询问 GL_MAX_VERTEX_ATTRIBS
。OpenGL ES 2.0 实现支持的最少 attribute 个数是8个。
uniform 限定符
uniform 是 GLSL 中的一种变量类型限定符,用于存储应用程序通过 GLSL 传递给着色器的只读值。uniform 可以用来存储着色器需要的各种数据,如变换矩阵、光参数和颜色等。传递给着色器的在所有的顶点着色器和片段着色器中保持不变的的任何参数,基本上都应该通过 uniform 来存储。uniform 变量在全局区声明,以下是 uniform 的一些示例:
uniform mat4 viewProjMatrix;
uniform mat4 viewMatrix;
uniform vec3 lightPosition;
需要注意的一点是,顶点着色器和片段着色器共享了 uniform 变量的命名空间。对于连接于同一个着色程序对象的顶点和片段着色器,它们共用同一组 uniform 变量,因此,如果在顶点着色器和片段着色器中都声明了 uniform 变量,二者的声明必须一致。当应用程序通过 API 加载了 uniform 变量时,该变量的值在顶点和片段着色器中都能够获取到。
另一点需要注意的是,uniform 变量通常是存储在硬件中的”常量区”,这一区域是专门分配用来存储常量的,但是由于这一区域尺寸非常有限,因此着色程序中可以使用的 uniform 的个数也是有限的。可以通过读取内置变量 gl_MaxVertexUniformVectors
和 gl_MaxFragmentUniformVectors
来获得,也可以使用 glGetIntegerv
查询 GL_MAX_VERTEX_UNIFORM_VECTORS
或者 GL_MAX_FRAGMENT_UNIFORM_VECTORS
。OpenGL ES 2.0 的实现必须提供至少 128 个顶点 uniform 向量及 16 片段 uniform 向量。
varying 限定符
GLSL 中最后一个要说的存储限定符是 varying。varying 存储的是顶点着色器的输出,同时作为片段着色器的输入,通常顶点着色器都会把需要传递给片段着色器的数据存储在一个或多个 varying 变量中。这些变量在片段着色器中需要有相对应的声明且数据类型一致,然后在光栅化过程中进行插值计算。以下是一些 varying 变量的声明:
varying vec2 texCoord;
varying vec4 color;
顶点着色器和片段着色器中都会有 varying 变量的声明,由于 varying 是顶点着色器的输出且是片段着色器的输入,所以两处声明必须一致。与 uniform 和 attribute 相同,varying 也有数量的限制,可以使用 gl_MaxVaryingVectors
获取或使用 glGetIntegerv
查询 GL_MAX_VARYING_VECTORS
来获取。OpenGL ES 2.0 实现中的 varying 变量最小支持数为 8。
回顾下最初那个着色器对应的 varying 声明:
// 顶点着色器 .vsh
attribute vec4 position;
attribute vec4 color;
varying vec4 colorVarying;
void main(void) {
colorVarying = color;
gl_Position = position;
}
// 片段着色器 .fsh
varying lowp vec4 colorVarying;
void main(void) {
gl_FragColor = colorVarying;
}
我按照下面的examples的顺序进行学习。
1. vispy/examples/gloo/colored_cube.py
import numpy as np
from vispy import app, gloo
from vispy.gloo import Program, VertexBuffer, IndexBuffer
from vispy.util.transforms import perspective, translate, rotate
from vispy.geometry import create_cube
vertex = """
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
attribute vec3 position;
attribute vec2 texcoord;
attribute vec3 normal;
attribute vec4 color;
varying vec4 v_color;
void main()
{
v_color = color;
gl_Position = projection * view * model * vec4(position,1.0);
}
"""
fragment = """
varying vec4 v_color;
void main()
{
gl_FragColor = v_color;
}
"""
class Canvas(app.Canvas):
def __init__(self):
app.Canvas.__init__(self, size=(512, 512), title='Colored cube',
keys='interactive')
# Build cube data
V, I, _ = create_cube()
vertices = VertexBuffer(V)
self.indices = IndexBuffer(I)
# Build program
self.program = Program(vertex, fragment)
self.program.bind(vertices)
# Build view, model, projection & normal
view = translate((0, 0, -5))
model = np.eye(4, dtype=np.float32)
self.program['model'] = model
self.program['view'] = view
self.phi, self.theta = 0, 0
gloo.set_state(clear_color=(0.30, 0.30, 0.35, 1.00), depth_test=True)
self.activate_zoom()
self.timer = app.Timer('auto', self.on_timer, start=True)
self.show()
def on_draw(self, event):
gloo.clear(color=True, depth=True)
self.program.draw('triangles', self.indices)
def on_resize(self, event):
self.activate_zoom()
def activate_zoom(self):
gloo.set_viewport(0, 0, *self.physical_size)
projection = perspective(45.0, self.size[0] / float(self.size[1]),
2.0, 10.0)
self.program['projection'] = projection
def on_timer(self, event):
self.theta += .5
self.phi += .5
self.program['model'] = np.dot(rotate(self.theta, (0, 0, 1)),
rotate(self.phi, (0, 1, 0)))
self.update()
if __name__ == '__main__':
c = Canvas()
app.run()
下面对代码进行详细解释
在 vertex shader 的代码中,attribute 限定符的变量是存储顶点着色器中每个顶点的输入的,通常是位置坐标/法向量/纹理坐标/颜色等。
在代码中,uniform限定符的变量是传递给着色器的只读值,在顶点着色器和片段着色器都保持不变,在全局区声明,例如各种变换矩阵,光参数等。 uniform会同时作用给顶点着色器和片段着色器。
varing限定的变量是顶点着色器的输出,同时作为片段着色器的输入。在两者代码需要分别声明。
以“gl_"开头的变量是opengl的内置变量,gl_Position和gl_FragColor的顶点着色器和片段着色器的输出。
vertex = """
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
attribute vec3 position;
attribute vec2 texcoord;
attribute vec3 normal;
attribute vec4 color;
varying vec4 v_color;
void main()
{
v_color = color;
gl_Position = projection * view * model * vec4(position,1.0);
}
"""
在Canvas类中,把attribute传入使用
vertices = VertexBuffer(V) # 24个点
self.program.bind(vertices)
把uniform传入使用
# view = array([[ 1., 0., 0., 0.],
# [ 0., 1., 0., 0.],
# [ 0., 0., 1., 0.],
# [ 0., 0., -5., 1.]])
view = translate((0, 0, -5))
model = np.eye(4, dtype=np.float32)
# projection = array([[ 2.4142137, 0. , 0. , 0. ],
# [ 0. , 2.4142137, 0. , 0. ],
# [ 0. , 0. , -1.5 , -1. ],
# [ 0. , 0. , -5. , 0. ]], dtype=float32)
# perspective(fovy, aspect, znear, zfar)
projection = perspective(45.0, self.size[0] / float(self.size[1]), 2.0, 10.0)
self.program['model'] = model
self.program['view'] = view
self.program['projection'] = projection
2. vispy/examples/gloo/textured_cube.py
学习如何传入纹理uniform sampler2D texture;,并且使用函数texture2d得到gl_FragColor。 具体的区别大家可以自己探索。
更多有趣的例子在vispy/examples/demo/gloo下面