Creating Engaging 3D Particles in WebGL: Insights and Updates
Written on
Chapter 1: Introduction to 3D Particle Systems
Particle systems are captivating to observe. The sight of countless particles dispersing in various directions is not only enjoyable but also highly functional. Many visual effects in video games and animations, such as fire, smoke, dust, rain, snow, and spell effects, rely on particle systems. This week, I integrated 3D particle systems into my Sparrow WebGL engine.
3D Particles vs. 2D Particles
Several months ago, I introduced 2D particles that share many similarities with their 3D counterparts. The distinctions primarily lie in the addition of a third dimension affecting parameters like position, velocity, and acceleration, while attributes such as size, age, color, and texture remain consistent.
The most significant variation from 2D to 3D is that 3D particles must always orient towards the camera, a technique known as billboarding. I've previously discussed this in another devlog. Given my experience with implementing 3D particles in C++ and OpenGL, the transition to my WebGL engine was relatively straightforward.
In WebGL, geometry shaders aren't available, which differs from OpenGL. Instead, I utilized the same instanced rendering method used for 2D particles, where the geometry of the particle quad remains constant while only its position and age are updated each frame. This approach reduces the data sent to the GPU, requiring just a single vec4 (3 floats for position and 1 float for age). To allow for varied sizes and colors from a single emitter, I included an additional instanced buffer that holds per-particle size and color data.
Emitter Functionality
Emitters can be configured in three ways: as a point where all particles originate at a single location, or as a box or sphere from which particles spawn at random positions within the defined volume. The emitter’s settings dictate how the particles behave, which can be specified during the emitter's construction.
To simplify the particle management process, I streamlined the particle manager compared to my previous 2D implementation and C++/OpenGL version. In those versions, a particle manager or controller needed to be created before adding emitters. While this allowed for multiple controllers, I found it unnecessary at this stage. Now, a single particle manager is automatically created within the engine, enabling instant addition of emitters. The engine also takes care of updating and rendering, relieving the user of these responsibilities.
Depth Sorting Challenges
When rendering semi-transparent objects in 3D, it’s essential to render triangles from back to front to accurately blend colors. This presents challenges for particles due to their random positions and movements. Sorting them by distance from the camera can be computationally intensive, especially with thousands of particles. Some effective heuristics include sorting emitters by depth or rendering based on particle age, but I haven't incorporated these techniques yet.
Surprisingly, you can achieve decent results without semi-transparent particles, especially in non-realistic styles. Textures can be created with fully transparent pixels, as the fragment shader will discard fragments with an alpha value below 0.5. For instance, the simple fire texture I used only contains fully transparent or fully opaque pixels, similar to how particles are handled in games like Minecraft.
Chapter 2: UI Enhancements and Emitter Control
The most labor-intensive aspect of adding particles was developing their options menu. After implementing the particle system, I realized the need for a more efficient method to adjust particle settings in real-time. Initially, changing parameters required refreshing the page to view updates, which was not ideal for creating diverse particle effects. Thus, I decided to design an in-engine menu for real-time adjustments and concurrently redesigned the UI elements.
I began with a fresh spin box design, inspired by Blender, featuring < and > buttons to modify values. However, placing these buttons adjacent to the text input created visual issues. Instead, I introduced a new advancedBorderWidth feature, allowing for customizable border widths on each side.
In addition to the spin boxes, I developed new drop-down menus and toggle buttons. The toggle button was simple to create, resembling an oversized checkbox with text. The drop-down menu required more effort, as it included a popup that appears upon clicking.
To enhance texture generation for UI elements, I consolidated the overlapping functionalities into a new class, facilitating the creation of consistent textures across various UI components.
With all these UI improvements and the numerous tweaks to the particle options menu, the implementation took several days, but I am pleased with the outcome. However, this means I will need to redesign other engine menus to match this new style.
Emitter Management and Future Plans
Beyond the emitter options menu, I also developed a feature that displays a list of all emitters. Users can create, rename, and delete emitters, marking the first instance in the engine where such control is possible. This foreshadows a future where users can create and manage various elements within the engine. Currently, the engine state is not persistent, necessitating users to copy options back to the JavaScript source code after creating an emitter.
I am quite satisfied with how the 3D particle system has evolved, particularly with the in-engine creation and editing workflow. The options menu simplifies adjustments and provides immediate visual feedback. While there are countless improvements to consider, the system is robust enough for the time being.