Vispy:基于 OpenGL 的高性能科学可视化

好的,各位观众老爷们,今天咱们来聊聊Vispy——一个让你在科学可视化领域飞起来的OpenGL小火箭。别怕OpenGL听起来高大上,有了Vispy,咱们也能轻松驾驭。

开场白:为什么选择Vispy?

想象一下,你辛辛苦苦跑了一堆数据,结果用matplotlib画出来的图慢得像蜗牛,转个角度卡成PPT。是不是想摔键盘?这时候,Vispy就是你的救星!

简单来说,Vispy的优势在于:

  • 高性能: 基于OpenGL,GPU加速,处理大数据不在话下。
  • 灵活性: 可以定制各种shader,实现各种炫酷的可视化效果。
  • 易用性: 提供Python接口,方便上手。
  • 跨平台: Windows、macOS、Linux通吃。

第一部分:Vispy基础入门

首先,安装Vispy。打开你的终端,输入:

pip install vispy

安装完成之后,咱们来创建一个简单的窗口。

import vispy
from vispy import app

class Canvas(app.Canvas):
    def __init__(self):
        app.Canvas.__init__(self, keys='interactive', size=(800,600))
        self.show()

    def on_draw(self, event):
        vispy.gloo.clear('white') # 设置背景颜色为白色

if __name__ == '__main__':
    canvas = Canvas()
    app.run()

这段代码创建了一个800×600的白色窗口。解释一下:

  • vispy.app.Canvas:Vispy的画布类,所有可视化内容都绘制在这个上面。
  • keys='interactive':允许使用键盘交互。
  • on_draw:当窗口需要重绘时调用的函数。
  • vispy.gloo.clear('white'):使用白色清除画布。
  • app.run():启动Vispy的事件循环。

运行这段代码,你应该能看到一个白色的窗口。是不是很简单?

第二部分:绘制简单的图形

接下来,咱们来画一个简单的三角形。

import vispy
from vispy import app
from vispy import gloo
import numpy as np

VERT_SHADER = """
    attribute vec4 a_position;
    void main() {
        gl_Position = a_position;
    }
"""

FRAG_SHADER = """
    void main() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
    }
"""

class Canvas(app.Canvas):
    def __init__(self):
        app.Canvas.__init__(self, keys='interactive', size=(800,600))

        # 定义三角形的顶点坐标
        vertices = np.array([[-0.5, -0.5], [0.5, -0.5], [0.0, 0.5]], dtype=np.float32)

        # 创建vertex buffer
        self.vertex_buffer = gloo.VertexBuffer(vertices)

        # 创建program
        self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
        self.program['a_position'] = self.vertex_buffer

        self.show()

    def on_draw(self, event):
        vispy.gloo.clear('white')
        self.program.draw('triangles')

if __name__ == '__main__':
    canvas = Canvas()
    app.run()

这段代码绘制了一个红色的三角形。解释一下:

  • VERT_SHADER:顶点着色器,定义了顶点的位置。
  • FRAG_SHADER:片段着色器,定义了像素的颜色。
  • vertices:三角形的顶点坐标,是一个NumPy数组。
  • gloo.VertexBuffer:顶点缓冲区,用于存储顶点数据。
  • gloo.Program:OpenGL程序,将顶点着色器和片段着色器组合在一起。
  • self.program['a_position'] = self.vertex_buffer:将顶点缓冲区绑定到顶点着色器的a_position属性。
  • self.program.draw('triangles'):绘制三角形。

重点来了:Shader!

Shader是OpenGL的核心。它们是用GLSL(OpenGL Shading Language)编写的小程序,运行在GPU上,负责处理顶点和像素。

  • 顶点着色器(Vertex Shader): 处理顶点数据,例如位置、颜色、法线等。它的输入是顶点属性,输出是顶点的最终位置。
  • 片段着色器(Fragment Shader): 处理像素数据,例如颜色、纹理等。它的输入是插值后的顶点属性,输出是像素的最终颜色。

上面的例子中,顶点着色器只是简单地将顶点位置传递给OpenGL,片段着色器将所有像素设置为红色。

第三部分:Vispy进阶技巧

  1. 使用Transformations

    Vispy提供了各种transformation,例如平移、旋转、缩放等,方便我们操作图形。

    from vispy import scene
    from vispy.scene import visuals
    
    canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
    view = canvas.central_widget.add_view()
    
    # 创建一个立方体
    cube = visuals.Cube(size=1)
    view.add(cube)
    
    # 使用MatrixTransform进行旋转
    from vispy.util import transforms
    matrix = transforms.MatrixTransform()
    matrix.rotate(30, (1, 1, 0))  # 绕(1, 1, 0)轴旋转30度
    cube.transform = matrix
    
    # 设置相机
    view.camera = scene.cameras.TurntableCamera(fov=60.0, distance=3)
    
    if __name__ == '__main__':
        scene.run()

    这段代码创建了一个旋转的立方体。MatrixTransform可以方便地进行各种矩阵变换。

  2. 使用Mesh绘制复杂图形

    Mesh是Vispy中最常用的图形类型之一,可以用来绘制各种复杂的3D模型。

    import vispy
    from vispy import app
    from vispy import gloo
    import numpy as np
    
    VERT_SHADER = """
    attribute vec4 a_position;
    attribute vec4 a_color;
    varying vec4 v_color;
    uniform mat4 u_model;
    uniform mat4 u_view;
    uniform mat4 u_projection;
    void main() {
        v_color = a_color;
        gl_Position = u_projection * u_view * u_model * a_position;
    }
    """
    
    FRAG_SHADER = """
    varying vec4 v_color;
    void main() {
        gl_FragColor = v_color;
    }
    """
    
    class Canvas(app.Canvas):
        def __init__(self):
            app.Canvas.__init__(self, keys='interactive', size=(800,600))
    
            # 创建一个立方体的顶点和颜色
            vertices, faces, normals, values = vispy.geometry.create_cube()
            colors = np.random.rand(vertices.shape[0], 4).astype(np.float32)
    
            # 创建vertex buffer
            self.vertex_buffer = gloo.VertexBuffer(vertices)
            self.color_buffer = gloo.VertexBuffer(colors)
            self.index_buffer = gloo.IndexBuffer(faces)
    
            # 创建program
            self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
            self.program['a_position'] = self.vertex_buffer
            self.program['a_color'] = self.color_buffer
    
            # 设置model, view, projection矩阵
            self.model = np.eye(4, dtype=np.float32)
            self.view = np.eye(4, dtype=np.float32)
            self.projection = np.eye(4, dtype=np.float32)
    
            self.program['u_model'] = self.model
            self.program['u_view'] = self.view
            self.program['u_projection'] = self.projection
    
            gloo.set_state(depth_test=True)
    
            self.theta = 0
            self.phi = 0
    
            self.show()
            self._timer = app.Timer('auto', connect=self.on_timer, start=True)
    
        def on_draw(self, event):
            vispy.gloo.clear('black', depth=True)
            self.program.draw('triangles', self.index_buffer)
    
        def on_timer(self, event):
            self.theta += .5
            self.phi += .5
            self.model = np.eye(4, dtype=np.float32)
            self.model = vispy.util.transforms.rotate(self.theta, (0, 0, 1))
            self.model = np.dot(self.model, vispy.util.transforms.rotate(self.phi, (0, 1, 0)))
            self.program['u_model'] = self.model
            self.update()
    
    if __name__ == '__main__':
        canvas = Canvas()
        app.run()

    这段代码创建了一个旋转的彩色立方体。注意,我们需要手动设置model, view, projection矩阵,才能正确地显示3D模型。

  3. 使用Visuals简化代码

    Vispy提供了许多现成的Visuals,例如Line, Scatter, Image等,方便我们绘制常见的图形。

    from vispy import scene
    from vispy.scene import visuals
    import numpy as np
    
    canvas = scene.SceneCanvas(keys='interactive', size=(800, 600), show=True)
    view = canvas.central_widget.add_view()
    
    # 创建一些随机点
    pos = np.random.normal(size=(100, 3))
    colors = np.random.normal(size=(100, 4))
    
    # 使用Scatter绘制散点图
    scatter = visuals.Scatter(pos, color=colors, size=10)
    view.add(scatter)
    
    # 设置相机
    view.camera = scene.cameras.TurntableCamera(fov=60.0, distance=3)
    
    if __name__ == '__main__':
        scene.run()

    这段代码创建了一个随机的散点图。使用visuals.Scatter可以方便地绘制散点图。

第四部分:Vispy实战案例

  1. 绘制大规模点云

    import vispy
    from vispy import app
    from vispy import gloo
    import numpy as np
    
    VERT_SHADER = """
    attribute vec4 a_position;
    attribute vec4 a_color;
    varying vec4 v_color;
    uniform mat4 u_model;
    uniform mat4 u_view;
    uniform mat4 u_projection;
    void main() {
        v_color = a_color;
        gl_Position = u_projection * u_view * u_model * a_position;
        gl_PointSize = 2.0; // 设置点的大小
    }
    """
    
    FRAG_SHADER = """
    varying vec4 v_color;
    void main() {
        gl_FragColor = v_color;
    }
    """
    
    class Canvas(app.Canvas):
        def __init__(self):
            app.Canvas.__init__(self, keys='interactive', size=(800,600))
    
            # 创建大规模点云 (100万个点)
            n_points = 1000000
            vertices = np.random.normal(size=(n_points, 3), scale=0.5).astype(np.float32)
            colors = np.random.rand(n_points, 4).astype(np.float32)
    
            # 创建vertex buffer
            self.vertex_buffer = gloo.VertexBuffer(vertices)
            self.color_buffer = gloo.VertexBuffer(colors)
    
            # 创建program
            self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
            self.program['a_position'] = self.vertex_buffer
            self.program['a_color'] = self.color_buffer
    
            # 设置model, view, projection矩阵
            self.model = np.eye(4, dtype=np.float32)
            self.view = np.eye(4, dtype=np.float32)
            self.projection = np.eye(4, dtype=np.float32)
    
            self.program['u_model'] = self.model
            self.program['u_view'] = self.view
            self.program['u_projection'] = self.projection
    
            gloo.set_state(depth_test=True)
    
            self.theta = 0
            self.phi = 0
    
            self.show()
            self._timer = app.Timer('auto', connect=self.on_timer, start=True)
    
        def on_draw(self, event):
            vispy.gloo.clear('black', depth=True)
            self.program.draw('points') # 绘制点云
    
        def on_timer(self, event):
            self.theta += .5
            self.phi += .5
            self.model = np.eye(4, dtype=np.float32)
            self.model = vispy.util.transforms.rotate(self.theta, (0, 0, 1))
            self.model = np.dot(self.model, vispy.util.transforms.rotate(self.phi, (0, 1, 0)))
            self.program['u_model'] = self.model
            self.update()
    
    if __name__ == '__main__':
        canvas = Canvas()
        app.run()

    这段代码绘制了一个包含100万个点的旋转点云。注意,使用gl_PointSize可以设置点的大小。

  2. 绘制体数据(Volume Rendering)

    import vispy
    from vispy import app
    from vispy import gloo
    import numpy as np
    
    VERT_SHADER = """
    void main() {
        gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
    }
    """
    
    FRAG_SHADER = """
    uniform sampler3D u_volume;
    uniform vec3 u_data_shape;
    uniform mat4 u_model;
    uniform mat4 u_view;
    uniform mat4 u_projection;
    uniform float u_alpha;
    const int n_samples = 200; //采样点数
    void main() {
        // 计算光线方向
        vec3 ray_origin = (inverse(u_view) * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
        vec3 ray_direction = normalize((inverse(u_model) * inverse(u_projection) * vec4(gl_FragCoord.xy / vec2(800.0, 600.0) * 2.0 - 1.0, 0.0, 1.0)).xyz - ray_origin);
    
        // 计算步长
        float step_size = 1.0 / float(n_samples);
    
        // 从前向后采样
        float alpha = 0.0;
        vec4 color = vec4(0.0);
        for (int i = 0; i < n_samples; ++i) {
            // 计算采样点位置
            vec3 sample_position = ray_origin + ray_direction * step_size * float(i);
    
            // 将采样点位置归一化到[0, 1]
            vec3 normalized_position = (sample_position + u_data_shape / 2.0) / u_data_shape;
    
            // 采样体数据
            float value = texture(u_volume, normalized_position).r;
    
            // 使用传递函数 (transfer function)
            float alpha_value = value * u_alpha;
            vec4 sample_color = vec4(value, value, value, alpha_value);
    
            // 前向合成
            color = color + sample_color * (1.0 - color.a);
            alpha = color.a;
    
            // 如果alpha接近1,停止采样
            if (alpha >= 0.95) {
                break;
            }
        }
    
        gl_FragColor = color;
    }
    """
    
    class Canvas(app.Canvas):
        def __init__(self):
            app.Canvas.__init__(self, keys='interactive', size=(800,600))
    
            # 创建体数据 (例如,一个3D高斯分布)
            data_shape = (64, 64, 64)
            x, y, z = np.mgrid[-1:1:data_shape[0]*1j, -1:1:data_shape[1]*1j, -1:1:data_shape[2]*1j]
            volume_data = np.exp(-5 * (x**2 + y**2 + z**2)).astype(np.float32)
    
            # 创建texture
            self.volume_texture = gloo.Texture3D(volume_data, internalformat='r32f')
    
            # 创建program
            self.program = gloo.Program(VERT_SHADER, FRAG_SHADER)
            self.program['u_volume'] = self.volume_texture
            self.program['u_data_shape'] = data_shape
            self.program['u_alpha'] = 0.1 # 控制透明度
    
            # 设置model, view, projection矩阵
            self.model = np.eye(4, dtype=np.float32)
            self.view = np.eye(4, dtype=np.float32)
            self.projection = np.eye(4, dtype=np.float32)
    
            # 设置透视投影
            self.projection = vispy.util.transforms.perspective(60.0, 800.0/600.0, 0.1, 100.0)
    
            # 设置相机位置
            self.view = vispy.util.transforms.translate((0, 0, -3))
    
            self.program['u_model'] = self.model
            self.program['u_view'] = self.view
            self.program['u_projection'] = self.projection
    
            gloo.set_state(depth_test=False)
    
            self.theta = 0
            self.phi = 0
    
            self.show()
            self._timer = app.Timer('auto', connect=self.on_timer, start=True)
    
        def on_draw(self, event):
            vispy.gloo.clear('black')
            #绘制一个覆盖整个窗口的四边形
            gloo.gl.glDrawArrays(gloo.gl.GL_TRIANGLE_STRIP, 0, 4)
    
        def on_timer(self, event):
            self.theta += .5
            self.phi += .5
            self.model = np.eye(4, dtype=np.float32)
            self.model = vispy.util.transforms.rotate(self.theta, (0, 0, 1))
            self.model = np.dot(self.model, vispy.util.transforms.rotate(self.phi, (0, 1, 0)))
            self.program['u_model'] = self.model
            self.update()
    
    if __name__ == '__main__':
        canvas = Canvas()
        app.run()

    这段代码使用光线投射(Ray Casting)技术渲染了一个3D高斯分布。注意,我们需要使用sampler3D类型的uniform变量来访问体数据。

总结:Vispy的优势与局限

特性 优势 局限
性能 基于OpenGL,GPU加速,处理大数据效率高 需要一定的OpenGL基础才能发挥最大性能
灵活性 可以定制各种shader,实现各种炫酷的可视化效果 Shader编写需要一定的GLSL知识
易用性 提供Python接口,方便上手,提供许多现成的Visuals 相比matplotlib,学习曲线稍陡峭
跨平台 Windows、macOS、Linux通吃
社区支持 社区活跃,文档完善

最后的温馨提示:

  • Vispy虽然强大,但也不是万能的。对于简单的绘图任务,matplotlib可能更方便。
  • 学习Vispy需要一定的OpenGL基础,建议先了解一些OpenGL的基本概念。
  • 多看Vispy的官方文档和示例代码,可以帮助你更快地掌握Vispy。
  • 遇到问题不要慌,Google一下,或者到Vispy的社区提问。

好了,今天的Vispy讲座就到这里。希望大家能够掌握Vispy的基本用法,并在科学可视化领域取得更大的成就! 祝各位 coding 愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注