GPU粒子爆发#

../../_images/explosions.gif

在此示例中,我们将演示如何使用粒子创建爆炸。粒子由GPU跟踪,显著提高了性能。

步骤1:打开空白窗口#

首先,让我们从一个空白窗口开始。

gpu_particle_burst_01.py#
 1"""
 2Example showing how to create particle explosions via the GPU.
 3"""
 4import arcade
 5
 6SCREEN_WIDTH = 1024
 7SCREEN_HEIGHT = 768
 8SCREEN_TITLE = "GPU Particle Explosion"
 9
10
11class MyWindow(arcade.Window):
12    """ Main window"""
13    def __init__(self):
14        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
15
16    def on_draw(self):
17        """ Draw everything """
18        self.clear()
19
20    def on_update(self, dt):
21        """ Update everything """
22        pass
23
24    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
25        """ User clicks mouse """
26        pass
27
28
29if __name__ == "__main__":
30    window = MyWindow()
31    window.center_window()
32    arcade.run()

步骤2:为每次单击创建一个粒子#

../../_images/gpu_particle_burst_02.png

在下一节中,我们将在用户每次在屏幕上单击鼠标时绘制一个点。

对于每一次单击,我们将创建一个 Burst 类,这些类最终将转变为完全爆炸。每个突发实例都将被添加到一个列表中。

进口商品#

首先,我们将为我们的程序导入更多项目:

from array import array
from dataclasses import dataclass

import arcade
import arcade.gl

突发数据类#

接下来,我们将创建一个数据类来跟踪每个猝发的数据。对于每个猝发,我们需要跟踪一个顶点数组对象(VAO),该对象存储有关猝发的信息。在这里面,我们将有一个顶点缓冲区对象(VBO),它将是一个高速内存缓冲区,我们将在其中存储位置、颜色、速度等。

@dataclass
class Burst:
    """ Track for each burst. """
    buffer: arcade.gl.Buffer
    vao: arcade.gl.Geometry

初始化方法#

接下来,我们将创建一个名为 burst_list 。我们还将创建我们的OpenGL着色器程序。该程序将是两个着色器程序的集合。这些文件将存储在单独的文件中,保存在同一目录中。

备注

除了通过 load_program() 一种方法 ArcadeContext 如图所示,还可以通过使用以下命令将GLSL程序保留在三引号字符串中 program()Context

MyWindow.__init__#
    def __init__(self):
        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
        self.burst_list = []

        # Program to visualize the points
        self.program = self.ctx.load_program(
            vertex_shader="vertex_shader_v1.glsl",
            fragment_shader="fragment_shader.glsl",
        )

        self.ctx.enable_only()

OpenGL着色器#

OpenGL着色语言(GLSL)是在图形卡(GPU)上运行的C样式语言,而不是在CPU上运行。遗憾的是,对该语言的完整解释超出了本教程的范围。然而,我希望该教程能让您开始了解它是如何工作的。

我们将拥有两个着色器。一个 顶点着色器 ,和一个 片段着色器 。顶点着色器针对要渲染的几何体的每个顶点运行,碎片着色器针对每个像素运行。例如,顶点着色器可能为矩形上的每个点运行四次,片段着色器将为屏幕上的每个像素运行。

顶点着色器采用顶点的位置。我们会设置 in_pos 并将该数据传递给该着色器。

顶点着色器输出顶点的颜色。颜色是红-绿-蓝-阿尔法(RGBA)格式,浮点数的范围从0到1。在下面的程序中,我们将颜色设置为(1,1,1),即白色,第四个1表示完全不透明。

vertex_shader_v1.glsl#
 1#version 330
 2
 3// (x, y) position passed in
 4in vec2 in_pos;
 5
 6// Output the color to the fragment shader
 7out vec4 color;
 8
 9void main() {
10
11    // Set the RGBA color
12    color = vec4(1, 1, 1, 1);
13
14    // Set the position. (x, y, z, w)
15    gl_Position = vec4(in_pos, 0.0, 1);
16}

碎片着色器没有太多功能,它只是接收 color 从顶点着色器中,并将其作为像素颜色传回。在本教程中,我们将为每个版本使用相同的片段着色器。

fragment_shader.glsl#
 1#version 330
 2
 3// Color passed in from the vertex shader
 4in vec4 color;
 5
 6// The pixel we are writing to in the framebuffer
 7out vec4 fragColor;
 8
 9void main() {
10
11    // Fill the point
12    fragColor = vec4(color);
13}

按下鼠标#

每次我们按下鼠标按键时,我们都会在该位置创建一个爆炸。

该猝发的数据将存储在 Burst 班级。

这个 Burst 类需要我们的数据缓冲区。数据缓冲区包含有关每个粒子的信息。在这种情况下,我们只有一个粒子,只需要将该粒子的x,y存储在缓冲区中。然而,最终我们将有数百个粒子,每个粒子都有位置、速度、颜色和衰减率。为了便于创建该数据,我们创建了一个生成器函数 _gen_initial_data 。在这一点上,它完全是夸大其词,但我们将在本教程中对其进行补充。

这个 buffer_description 表示每个顶点都有两个浮动数据点 (2f ),这些数据点将以引用名称进入着色器 in_pos 我们在上面的 OpenGL着色器

MyWindow.on_mouse_press#
    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
        """ User clicks mouse """

        def _gen_initial_data(initial_x, initial_y):
            """ Generate data for each particle """
            yield initial_x
            yield initial_y

        # Recalculate the coordinates from pixels to the OpenGL system with
        # 0, 0 at the center.
        x2 = x / self.width * 2. - 1.
        y2 = y / self.height * 2. - 1.

        # Get initial particle data
        initial_data = _gen_initial_data(x2, y2)

        # Create a buffer with that data
        buffer = self.ctx.buffer(data=array('f', initial_data))

        # Create a buffer description specifying the buffer's data format
        buffer_description = arcade.gl.BufferDescription(
            buffer,
            '2f',
            ['in_pos'])

        # Create our Vertex Attribute Object
        vao = self.ctx.geometry([buffer_description])

        # Create the Burst object and add it to the list of bursts
        burst = Burst(buffer=buffer, vao=vao)
        self.burst_list.append(burst)

绘图#

最后,画出来。

MyWindow.on_draw#
    def on_draw(self):
        """ Draw everything """
        self.clear()

        # Set the particle size
        self.ctx.point_size = 2 * self.get_pixel_ratio()

        # Loop through each burst
        for burst in self.burst_list:

            # Render the burst
            burst.vao.render(self.program, mode=self.ctx.POINTS)

节目单#

步骤3:多个移动的粒子#

../../_images/gpu_particle_burst_03.png

下一步是拥有多个粒子,并让这些粒子移动。我们将通过创建粒子,并根据自创建以来的时间计算它们应该位于的位置来完成此操作。这与我们移动精灵的方式略有不同,因为在每次更新调用期间会逐位手动重新定位精灵。

进口商品#

首先,我们将为两个 randomtime 库:

import random
import time

常量#

然后,我们需要创建一个包含要创建的粒子数量的常量:

PARTICLE_COUNT = 300

突发数据类#

我们需要在突发数据中添加一个时间。这将是一个浮点数,表示创建猝发时的开始时间。

@dataclass
class Burst:
    """ Track for each burst. """
    buffer: arcade.gl.Buffer
    vao: arcade.gl.Geometry
    start_time: float

更新猝发创建#

现在,当我们创建一个爆发时,我们需要多个粒子,每个粒子也需要一个速度。在……里面 _gen_initial_data 我们为每个粒子添加一个循环,并输出增量x和y。

注意:由于我们设置增量x和增量y的方式,粒子将扩展为矩形而不是圆形。我们将在稍后的步骤中解决这一问题。

因为我们添加了一个速度,所以我们的缓冲区现在需要两对浮点数 2f 2f 已命名 in_posin_vel 。我们将稍微更新着色器以使用新值。

最后,我们的拆分对象需要跟踪我们创建拆分的时间。

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                dx = random.uniform(-.2, .2)
 8                dy = random.uniform(-.2, .2)
 9                yield initial_x
10                yield initial_y
11                yield dx
12                yield dy
13
14        # Recalculate the coordinates from pixels to the OpenGL system with
15        # 0, 0 at the center.
16        x2 = x / self.width * 2. - 1.
17        y2 = y / self.height * 2. - 1.
18
19        # Get initial particle data
20        initial_data = _gen_initial_data(x2, y2)
21
22        # Create a buffer with that data
23        buffer = self.ctx.buffer(data=array('f', initial_data))
24
25        # Create a buffer description specifying the buffer's data format
26        buffer_description = arcade.gl.BufferDescription(
27            buffer,
28            '2f 2f',
29            ['in_pos', 'in_vel'])
30
31        # Create our Vertex Attribute Object
32        vao = self.ctx.geometry([buffer_description])
33
34        # Create the Burst object and add it to the list of bursts
35        burst = Burst(buffer=buffer, vao=vao, start_time=time.time())
36        self.burst_list.append(burst)

在ON_DRAW中设置时间#

当我们绘制时,我们需要设置“统一数据”(对所有点都相同的数据),它表示自猝发开始以来已经过了多少秒。着色器将使用它来计算粒子位置。

    def on_draw(self):
        """ Draw everything """
        self.clear()

        # Set the particle size
        self.ctx.point_size = 2 * self.get_pixel_ratio()

        # Loop through each burst
        for burst in self.burst_list:

            # Set the uniform data
            self.program['time'] = time.time() - burst.start_time

            # Render the burst
            burst.vao.render(self.program, mode=self.ctx.POINTS)

更新顶点着色器#

我们的顶点着色器需要更新。我们现在收看一部 uniform float 时间到了。统一数据设置一次,程序中的每个顶点都可以使用它。在我们的例子中,我们不需要为爆发中的每个粒子单独复制爆发的开始时间,因此它是统一的数据。

我们还需要添加另一个包含两个浮点的向量,这将包含我们的速度。我们定好了 in_vel 在……里面 更新猝发创建

最后我们根据时间和粒子的速度计算出一个新的位置。我们在设置环境时使用新的位置 gl_Position

vertex_shader_v2.glsl#
 1#version 330
 2
 3// Time since burst start
 4uniform float time;
 5
 6// (x, y) position passed in
 7in vec2 in_pos;
 8
 9// Velocity of particle
10in vec2 in_vel;
11
12// Output the color to the fragment shader
13out vec4 color;
14
15void main() {
16
17    // Set the RGBA color
18    color = vec4(1, 1, 1, 1);
19
20    // Calculate a new position
21    vec2 new_pos = in_pos + (time * in_vel);
22
23    // Set the position. (x, y, z, w)
24    gl_Position = vec4(new_pos, 0.0, 1);
25}

节目单#

步骤4:随机角度和速度#

../../_images/gpu_particle_burst_04.png

第三步没有很好地选择速度,因为我们的粒子膨胀成了矩形,而不是圆形。我们需要选择一个随机的方向和速度,而不是选择一个随机的增量x和y。然后用它来计算增量x和y。

更新导入#

导入数学库,以便我们可以执行一些触发器:

import math

更新猝发创建#

现在,选择一个从0到2圆周率弧度的随机方向。另外,选择一个随机的速度。然后用正弦和余弦来计算增量x和y。

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                angle = random.uniform(0, 2 * math.pi)
 8                speed = random.uniform(0.0, 0.3)
 9                dx = math.sin(angle) * speed
10                dy = math.cos(angle) * speed
11                yield initial_x
12                yield initial_y
13                yield dx
14                yield dy
15

节目单#

第5步:高斯分布#

../../_images/gpu_particle_burst_05.png

将速度设置为随机量有助于扩大圆。另一种选择是使用高斯函数来产生更多的“Splat”外观:

                speed = abs(random.gauss(0, 1)) * .5

节目单#

步骤6:添加颜色#

../../_images/gpu_particle_burst_06.png

到目前为止,我们的粒子都是白色的。我们如何添加颜色?我们需要为每个粒子生成它。着色器采用RGB浮点形式的颜色,因此我们将为红色生成一个随机数,并添加一些绿色以获得黄色。不要加太多的绿色,否则你会得到绿色的色彩。

最后,确保更新着色器缓冲区描述(VBO)以接受三个颜色通道浮点 (3f )在名称下 in_color

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                angle = random.uniform(0, 2 * math.pi)
 8                speed = abs(random.gauss(0, 1)) * .5
 9                dx = math.sin(angle) * speed
10                dy = math.cos(angle) * speed
11                red = random.uniform(0.5, 1.0)
12                green = random.uniform(0, red)
13                blue = 0
14                yield initial_x
15                yield initial_y
16                yield dx
17                yield dy
18                yield red
19                yield green
20                yield blue
21
22        # Recalculate the coordinates from pixels to the OpenGL system with
23        # 0, 0 at the center.
24        x2 = x / self.width * 2. - 1.
25        y2 = y / self.height * 2. - 1.
26
27        # Get initial particle data
28        initial_data = _gen_initial_data(x2, y2)
29
30        # Create a buffer with that data
31        buffer = self.ctx.buffer(data=array('f', initial_data))
32
33        # Create a buffer description specifying the buffer's data format
34        buffer_description = arcade.gl.BufferDescription(
35            buffer,
36            '2f 2f 3f',
37            ['in_pos', 'in_vel', 'in_color'])
38
39        # Create our Vertex Attribute Object
40        vao = self.ctx.geometry([buffer_description])
41
42        # Create the Burst object and add it to the list of bursts
43        burst = Burst(buffer=buffer, vao=vao, start_time=time.time())
44        self.burst_list.append(burst)

然后,更新着色器以使用该颜色,而不是始终使用白色:

vertex_shader_v3.glsl#
 1#version 330
 2
 3// Time since burst start
 4uniform float time;
 5
 6// (x, y) position passed in
 7in vec2 in_pos;
 8
 9// Velocity of particle
10in vec2 in_vel;
11
12// Color of particle
13in vec3 in_color;
14
15// Output the color to the fragment shader
16out vec4 color;
17
18void main() {
19
20    // Set the RGBA color
21    color = vec4(in_color[0], in_color[1], in_color[2], 1);
22
23    // Calculate a new position
24    vec2 new_pos = in_pos + (time * in_vel);
25
26    // Set the position. (x, y, z, w)
27    gl_Position = vec4(new_pos, 0.0, 1);
28}

节目单#

第七步:淡出#

../../_images/gpu_particle_burst_07.png

现在,爆炸粒子会永远持续下去。让他们淡出吧。一旦爆发消失,让我们将其从 burst_list

常量#

首先,让我们添加几个常量来控制粒子褪色的最小和最大次数:

MIN_FADE_TIME = 0.25
MAX_FADE_TIME = 1.5

更新初始化#

接下来,我们需要更新OpenGL上下文以支持Alpha混合。返回到 __init__ 方法,并更新 enable_only 请致电:

self.ctx.enable_only(self.ctx.BLEND)

将衰落速率添加到缓冲区#

接下来,将淡入率浮点添加到VBO:

 1    def on_mouse_press(self, x: float, y: float, button: int, modifiers: int):
 2        """ User clicks mouse """
 3
 4        def _gen_initial_data(initial_x, initial_y):
 5            """ Generate data for each particle """
 6            for i in range(PARTICLE_COUNT):
 7                angle = random.uniform(0, 2 * math.pi)
 8                speed = abs(random.gauss(0, 1)) * .5
 9                dx = math.sin(angle) * speed
10                dy = math.cos(angle) * speed
11                red = random.uniform(0.5, 1.0)
12                green = random.uniform(0, red)
13                blue = 0
14                fade_rate = random.uniform(
15                    1 / MAX_FADE_TIME, 1 / MIN_FADE_TIME)
16
17                yield initial_x
18                yield initial_y
19                yield dx
20                yield dy
21                yield red
22                yield green
23                yield blue
24                yield fade_rate
25
26        # Recalculate the coordinates from pixels to the OpenGL system with
27        # 0, 0 at the center.
28        x2 = x / self.width * 2. - 1.
29        y2 = y / self.height * 2. - 1.
30
31        # Get initial particle data
32        initial_data = _gen_initial_data(x2, y2)
33
34        # Create a buffer with that data
35        buffer = self.ctx.buffer(data=array('f', initial_data))
36
37        # Create a buffer description specifying the buffer's data format
38        buffer_description = arcade.gl.BufferDescription(
39            buffer,
40            '2f 2f 3f f',
41            ['in_pos', 'in_vel', 'in_color', 'in_fade_rate'])
42
43        # Create our Vertex Attribute Object
44        vao = self.ctx.geometry([buffer_description])
45
46        # Create the Burst object and add it to the list of bursts
47        burst = Burst(buffer=buffer, vao=vao, start_time=time.time())
48        self.burst_list.append(burst)

更新着色器#

更新着色器。计算阿尔法。如果小于0,则使用0。

vertex_shader_v4.glsl#
 1#version 330
 2
 3// Time since burst start
 4uniform float time;
 5
 6// (x, y) position passed in
 7in vec2 in_pos;
 8
 9// Velocity of particle
10in vec2 in_vel;
11
12// Color of particle
13in vec3 in_color;
14
15// Fade rate
16in float in_fade_rate;
17
18// Output the color to the fragment shader
19out vec4 color;
20
21void main() {
22
23    // Calculate alpha based on time and fade rate
24    float alpha = 1.0 - (in_fade_rate * time);
25    if(alpha < 0.0) alpha = 0;
26
27    // Set the RGBA color
28    color = vec4(in_color[0], in_color[1], in_color[2], alpha);
29
30    // Calculate a new position
31    vec2 new_pos = in_pos + (time * in_vel);
32
33    // Set the position. (x, y, z, w)
34    gl_Position = vec4(new_pos, 0.0, 1);
35}

消除褪色脉冲串#

一旦我们的爆裂完全消退,就没有必要保留它了。所以在我们的 on_update 淡入淡出后,从Burst_List中删除猝发。

 1    def on_update(self, dt):
 2        """ Update game """
 3
 4        # Create a copy of our list, as we can't modify a list while iterating
 5        # it. Then see if any of the items have completely faded out and need
 6        # to be removed.
 7        temp_list = self.burst_list.copy()
 8        for burst in temp_list:
 9            if time.time() - burst.start_time > MAX_FADE_TIME:
10                self.burst_list.remove(burst)

节目单#

第8步:添加重力#

您还可以通过基于重力常数调整速度来将重力添加到粒子。(在本例中为1.1。)

// Adjust velocity based on gravity
vec2 new_vel = in_vel;
new_vel[1] -= time * 1.1;

// Calculate a new position
vec2 new_pos = in_pos + (time * new_vel);

节目单#