Introduction
Thinking of the tutorial mentioned in the original text , I went to see it and found that it is very helpful to understand some logic. By the way, I translated and recorded it.
text
My GPGPU The next project in the series is a particle physics engine that computes the entire physics simulation on the GPU. Particles are affected by gravity and bounce off scene geometry. This WebGL demo uses shader functionality and is not strictly required by the OpenGL ES 2.0 specification, so it may not work on some platforms, especially mobile devices. This will be discussed later in this article.
It is interactive. The mouse cursor is a circular obstacle that makes particles bounce, and clicking will place a permanent obstacle in the simulation. You can draw structures through which particles can flow.
This is an HTML5 video presentation of the example, recorded at a high bitrate of 60 frames per second out of necessity, so it's quite large. Video codecs don't handle full screen all particles well, and lower frame rates don't capture the effect well. I also added some sounds that are not heard in the actual demo.
- Video playback address: https://nullprogram.s3.amazonaws.com/particles/particles.mp4
On modern GPUs, it can simulate and drawing over 4 million particles at 60 frames per second. Keep in mind that this is a JavaScript application, I didn't really spend time optimizing the shaders, it's bound by WebGL, not something more suitable for general computing like OpenCL or at least desktop OpenGL.
Particle states are encoded as colors
Like Game of Life and path finding projects, the simulation state is stored in pairs of textures, and most of the work is done in the fragment shader by mapping between them pixel by pixel. I won't repeat this setup detail, so if you need to understand how it works, please refer to Game of Life article.
For this simulation, there are four of these textures instead of two: a pair of position textures and a pair of velocity textures. Why paired textures? There are 4 channels, so each part of it (x, y, dx, dy) can be packed into its own color channel. This seems to be the easiest solution.
The problem with this scheme is the lack of precision. For the R8G8B8A8 internal texture format, each channel is one byte. There are a total of 256 possible values. The display area is 800×600 pixels, so every position on the display area cannot be displayed. Fortunately, two bytes (65536 values in total) are enough for us.
The next question is how to encode values across these two channels. It needs to cover negative values (negative velocity) and should try to make the most of the dynamic range, eg try to use all 65536 values in the range.
To encode a value, multiply the value by a scalar, extending it to the dynamic range of the encoding. When choosing a scalar, the highest value desired (dimension displayed) is the highest value encoded.
Next, add half of the dynamic range to the scaling value. This converts all negative values to positive, with 0 being the minimum value. This notation is called Excess-K . The downside is that clearing the texture with transparent black ( glClearColor
) does not set the decoded value to 0.
Finally, treat each channel as a base 256 number. The OpenGL ES 2.0 shader language has no bitwise operators, so this is done using normal division and modulo. I made an encoder and decoder using JavaScript and GLSL. JavaScript needs it to write initial values, and for debugging purposes, it can read back particle positions.
vec2 encode(float value) {
value = value * scale + OFFSET;
float x = mod(value, BASE);
float y = floor(value / BASE);
return vec2(x, y) / BASE;
}
float decode(vec2 channels) {
return (dot(channels, vec2(BASE, BASE * BASE)) - OFFSET) / scale;
}
JavaScript differs from the normalized GLSL values above (0.0-1.0), which produces a one-byte integer (0-255) for packing into a typed array.
function encode(value, scale) {
var b = Particles.BASE;
value = value * scale + b * b / 2;
var pair = [
Math.floor((value % b) / b * 255),
Math.floor(Math.floor(value / b) / b * 255)
];
return pair;
}
function decode(pair, scale) {
var b = Particles.BASE;
return (((pair[0] / 255) * b +
(pair[1] / 255) * b * b) - b * b / 2) / scale;
}
The fragment shader that updates each particle samples the position and velocity textures at that particle's "index", decodes their values, operates on them, and encodes them back to a color for writing to the output texture. Since I'm using WebGL, which lacks multiple render targets (despite support for gl_FragData
), the fragment shader can only output one color. The position is updated in one pass and the velocity is updated in another pass as two separate plots. The buffers n't swap until the two passes have finished , so the velocity shader (intentionally) doesn't use the updated position value.
There is a limit to the maximum texture size, usually 8192 or 4096, so the textures are not arranged in one-dimensional textures, but remain square. Particles are indexed by 2D coordinates.
It's interesting to see position or velocity textures drawn directly to the screen instead of being displayed normally. This is another area of viewing simulations, and it even helped me spot issues that were otherwise hard to see. The output is a set of flashing colors, but with a clear pattern, showing many states of the system (or states that are not among them). I want to share a video, but the encoding is more impractical than normal display. Here's a screenshot: position, then velocity. The alpha component is not captured here.
state hold
One of the biggest challenges of running a simulation like this on a GPU is the lack of random values. There is no rand()
function in the shader language, so the whole thing is deterministic by default. All state comes from the initial texture state populated by the CPU. When particles aggregate and match states, possibly flowing together through an obstacle, it is difficult to separate them again because the simulation treats them the same way.
To alleviate this problem, the first rule is to keep state as much as possible. When a particle leaves the bottom of the display area, it is moved back to the top to "reset". If this is done by setting the particle's Y value to 0, the information will be destroyed. This must be avoided! Despite exiting during the same iteration, the Y values tend to show slightly different particles below the bottom edge. Instead of resetting to 0, add a constant: the height of the display area. The Y values are still different, so these particles are more likely to follow different routes when they collide with obstacles.
The next technique I used was to give each iteration a new random value via uniform, which was added to reset the particle's position and velocity. The same value is used for all particles for that particular iteration, so this doesn't help overlapping particles, but helps separate "flows". These are clearly visible particle lines, all following the same path. Each will exit the bottom of the display in a different iteration, so random values will separate them slightly. Ultimately, this adds some new state to the simulation at each iteration.
Alternatively, a texture containing random values can be provided to the shader. The CPU has to fill and upload textures frequently, plus there is the problem of choosing where to sample the texture, the texture itself needs a random value.
Finally, to handle completely overlapping particles, scale the particle's unique 2D index on reset and add it to the position and velocity to separate them. The sign of the random value is multiplied by the index to avoid bias in any particular direction.
To see all of this in the demo, make a big circle to catch all the particles and let them flow into a point. This will remove all state from the system. Now clear the obstacles. They will all become a tight blob. On top reset, it still has some clumping, but you'll see that they're slightly separated (with particle indices added). They will leave the bottom at slightly different times, so the random value comes into play to keep them more separated. After a few rounds, the particles should be evenly distributed again.
The last source of state is your mouse. When you move it around the scene, it interferes with the particles and introduces some noise into the simulation.
Textures as vertex attribute buffers
The idea for this project came to me while reading the OpenGL ES Shader Language Specification (PDF). I've been wanting to make a particle system but I'm stuck on how to draw the particles. The texture data representing the position needs to be fed back into the pipeline as vertices in some way. Typically, buffer texture - a texture backed by an array buffer - or pixel buffer object - async texture data copy - can be used for this operation, but WebGL does not have these capabilities. It is not possible to extract texture data from the GPU and reload it as an array buffer on each frame.
However, I came up with a cool trick that's better than both. The shader function texture2D
is used to sample the pixels in the texture. Typically, fragment shaders use this as part of the process of computing a pixel's color. But the shader language specification mentions that texture2D
can also be used in vertex shaders. That's when an idea hit me. vertex shader itself can perform a texture-to-vertex conversion .
It works by passing the aforementioned 2D particle indices as vertex attributes and using them to find particle positions from the vertex shader. The shader will run in GL_POINTS
mode, emitting point particles. Here is the abbreviated version:
attribute vec2 index;
uniform sampler2D positions;
uniform vec2 statesize;
uniform vec2 worldsize;
uniform float size;
// float decode(vec2) { ...
void main() {
vec4 psample = texture2D(positions, index / statesize);
vec2 p = vec2(decode(psample.rg), decode(psample.ba));
gl_Position = vec4(p / worldsize * 2.0 - 1.0, 0, 1);
gl_PointSize = size;
}
The real version also samples the speed because it adjusts the color (slow moving particles are brighter than fast moving particles).
However, there is a potential problem: implementations are allowed to limit the number of vertex shader texture bindings to 0 (GL_MAX_vertex_texture_IMAGE_UNITS). So technically vertex shaders must always support texture2D, but they don't need to support actual textures. It's a bit like a meal service on an airplane that doesn't carry passengers. Some platforms do not support this technology. I've only had this problem on some mobile devices so far.
Apart from the lack of support on some platforms, this allows every part of the simulation to stay on the GPU and paves the way for pure GPU particle systems.
obstacle
An important observation is that the particles do not interact with each other. This is not an n-body simulation. However, they do interact with the rest of the world: they bounce off these stationary circles intuitively. The environment is represented by another texture, which is not updated during normal iterations. I call it obstacle texture.
The color on the obstacle texture is the surface normal. That is, each pixel has a direction pointing towards it, and a flow that directs particles in a certain direction. The void has a special constant value (0, 0). This is not a unit vector (length is not 1), so it is an out-of-band value that has no effect on the particles.
Particles simply sample the obstacle texture to check for collisions. If the normal is found at its position, then use the shader function reflect
to change its velocity. This function is typically used to reflect light in a 3D scene, but it works equally well for slowly moving particles. The effect is that the particles bounce off the circle in a natural way.
Sometimes particles land on or in obstacles at low or zero speed. To move them away from the obstacle, push them slightly in the normal direction. You'll see this on slopes, where slow particles jiggle like jumping beans and move freely downward.
To make obstacle textures user-friendly, the actual geometry is maintained on the CPU side of JavaScript. The circles are kept in a list and when updated, the obstacle textures are redrawn in this list. This happens, for example, every time the mouse is moved on the screen, creating a moving obstacle. Textures provide shader-friendly access to geometry. Both manifestations serve two purposes.
When I started writing this part of the program, I envisioned shapes other than circles that could be placed. For example, a solid rectangle: normals look like this.
So far, these have not been implemented.
future ideas
I haven't tried it yet, but I wonder if particles can also interact by drawing themselves onto the obstacle texture. Two nearby particles bounce off each other. Maybe the whole liquid demo can run on GPU like this. If I guess correctly, the particles would increase in volume, and the bowl-forming obstacles would fill up instead of concentrating the particles to a single point.
I think this project still has some things to explore.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。