Deciphering the Unthinkable:
This post delves into the fascinating process of computing results predominantly on the GPU using WebGL/Three.js - all achieved despite utilizing the integrated graphics of an Intel i7 4770k, which may seem less than ideal.
Overview:
The ingenious concept revolves around keeping all operations within the confines of the GPU: Each particle's state is represented by a single pixel color value in a texture. With one million particles, two 1024x1024 pixel textures are employed - one for storing current positions and another for velocities.
Surprisingly, nothing prohibits repurposing RGB color values in a texture to store entirely different data within the 0 to 255 range. Essentially, each texture pixel provides 32 bits (R + G + B + alpha) of space in GPU memory for any desired information. (Additional texture pixels can be utilized if more data per particle/object needs to be stored).
Their approach involves multiple shaders executed sequentially. A careful analysis of the source code reveals the distinct steps in their processing pipeline:
- Randomize particles (excluded from this explanation) ('randShader')
- Determining velocity for each particle based on its proximity to the mouse location ('velShader')
- Adjusting each particle's position according to its velocity ('posShader')
- Rendering to the screen ('dispShader')**
.
Step 2: Calculating Velocity per Particle:
A rendering process is initiated on a million points, generating output saved as a texture. Within the vertex shader, every fragment is assigned two additional varyings labeled "vUv", responsible for indicating the x and y pixel coordinates within the textures involved in the process.
The subsequent step takes place in the fragment shader, where output (as RGB values into the framebuffer, later converted into a texture buffer solely within GPU memory) occurs. Examining the id="velFrag"
fragment shader unveils an input variable named uniform vec3 targetPos;
. These uniforms are easily set by the CPU each frame since they are shared among all instances and do not entail extensive memory transfers. (pertaining to the mouse coordinates, likely within the -1.00f to +1.00f range - the mouse coordinates are updated intermittently, thereby reducing CPU usage).
What transpires here? This shader calculates the distance between the particle and the mouse coordinates, subsequently adjusting the particle's velocity based on this calculation - the velocity also encompasses details regarding the particle's flight trajectory. Nota bene: This velocity adjustment enables particles to gain momentum, persist in motion, and potentially overshoot the mouse position depending on the gray value.
.
Step 3: Updating Positions for Each Particle:
At this stage, every particle possesses a velocity and previous position. These values are processed to determine a new position, which is then recorded as a texture - specifically, the positionTexture. Until the entire frame is rendered (into the default framebuffer) and designated as the new texture, the prior positionTexture remains undisturbed, facilitating easy retrieval:
In the id="posFrag"
fragment shader, data is read from both textures (posTexture and velTexture), subsequently incorporated to derive a new position. The resulting x and y position coordinates are outputted as the colors of that texture (in the form of red and green values).
.
Step 4: Showcase Time (=output)
To showcase the outcomes, presumably another million points/vertices are processed with the positionTexture serving as input. Subsequently, the vertex shader assigns the position of each point, retrieving the RGB value from the texture at the specified x,y coordinates passed as vertex attributes.
// From <script type="x-shader/x-vertex" id="dispVert">
vec3 mvPosition = texture2D(posTex, vec2(x, y)).rgb;
gl_PointSize = 1.0;
gl_Position = projectionMatrix * modelViewMatrix * vec4(mvPosition,1.0);
Within the display fragment shader, merely setting a color suffices (note the minimal alpha level allowing up to 20 particles to collectively illuminate a pixel).
// From <script type="x-shader/x-fragment" id="dispFrag">
gl_FragColor = vec4(vec3(0.5, 1.0, 0.1), 0.05);
.
I trust this elucidates the inner workings of this intriguing demo :-) It is worth noting that I am not the creator of this demonstration. However, it is fascinating how this response evolved into a particularly detailed exploration - navigate through the key points swiftly for a concise overview.