Mesh particles

<<< PREV NEXT >>>

We have seen several particle effects achievable in this plugin so far. However, up until now, we exclusively used sprite particles for them.

I decided to focus on sprite particles in the beginning of these tutorials, because they seem more familiar and approachable. Many TRLE builders have used FLEP’s Custom Smoke Emitters OCBs before, or are at least aware of their existence and have a general idea of what can be done with them. This allowed for a nice introduction to particle scripting in Chapter 1, where most aspects of particle effects were explained in relation to FLEP smoke emitters.

Nevertheless, as was advertised in the introduction, sprite particles are not the only variant of particle available in the plugin. The plugin also gives mesh particles to our disposal. They may seem quite far removed from the idea of sprite-based particles at first sight, but they have a lot more in common than you might think. Gaining experience with sprite particles paved the path towards working with mesh particles. Remember, the main distinction between these two categories of particles is what computer graphics entity gets rendered at a particle’s position (either a 2D sprite or a 3D mesh), so the fields possesed by the particle instances for the respective category (sprite / mesh) reflect this difference. Fortunately, all particles have a set of common base properties that behave indentically in both categories of particles.

This succinct chapter delves into everything you need to know about mesh particles, starting with a section comparing both kinds of particles, where the common fields are factored out, then moving onto emphasizing their distinguishing features. Mesh particles are a very interesting addition, which (hopefully) will broaden the horizons of what builders picture of when thinking of particle effects.

Sections

        1. Motivation for mesh particles
        2. Similarities between sprite parts and mesh parts
        3. How mesh parts differ from sprite parts
        4. Object field and mesh field
        5. Rotation vector and angular velocity vector
        6. Scale vector
        7. Transparency field
        8. Lighting type and color tint
        9. Mesh particle functions
        10. Limitations of mesh particles


Motivation for mesh particles

A form of mesh-based “particles” already existed in the Tomb4 engine. For example, projectiles like the ones fired by SETHA, DEMIGOD3, HARPY or mesh chunks flung around when enemies die due to explosive ammo, are examples of mesh particles in the original engine. However, there was never a way to control what these mesh particles do, since their behavior was hardcoded. You couldn’t even spawn them with triggers.

The mesh particles brought forth by the plugin aim to fill this niche. They are under your full scripting control. You can access their fields, determine how they spawn and what they do after spawning. It is possible to make a mesh particle take the form of any mesh from any moveable slot present in the level, on top of being able to change its position, velocity, acceleration, lifetime and manipulate it with particle functions in update functions.

Mesh particle instances are created with the built-in function createMeshPart(). Like its counterpart, createSpritePart(), it expects to receive a particle group as its single argument, and returns a blank-slate mesh particle that must be initialized by the init function. Everything so far is similar to working with sprite particles, right? Note that there is no distinction between “sprite” particle groups and “mesh” particle groups, there is only one kind of particle group that is used in either case. In theory, this means that you can have both kinds of particles belonging to the same group, but this is not recommendend. Prefer making separate groups for mesh particles and sprite particles, so each group has only one “type” of particle associated with it.

If you need specific examples of where mesh particles can be useful, think of debris falling to the ground, an avalanche of rocks or snow making its way down a mountain side, throwable weapons for Lara, like hand grenades or molotov cocktails, or perhaps projectile attacks fired by enemy slots that never had them before, such as BADDY_2, MUMMY or AHMET. Did you get sold on mesh particles already? If so, let’s carry onward!


Similarities between sprite parts and mesh parts

Mesh particles and sprite particles are treated as separate entities in the plugin. Nonetheless, there is an overlap of some properties/fields, common to both kinds of particles. I think this makes sense, as concepts of position, velocity and acceleration, as well as life span, are universal to particles in general.

Since these overlapping fields work identically across all particles, I feel there is no point in explaining how they work again, they have already been covered in-depth in context of sprite particles and I would only end up repeating myself. Below are listed all the properties / fields, present both in mesh particles and sprite particles:

Common fields of sprite parts and mesh parts
Field name Expected value Explained in
lifeSpan integer (0 - 32767) Particle lifetime
lifeCounter integer (0 - 32767) Particle lifetime
pos vector3D Particle position
vel vector3D Particle velocity
accel vector3D Particle acceleration
emitterIndex tomb4 moveable indices Particles emitted from objects
emitterNode mesh indices Particle emitter nodes
roomIndex room indices Simple colliding particles (room index)


Now that we have these common, overlapping fields out of the way, we’ll focus on the unique fields of mesh particles, which set them apart from sprite particles.

How mesh parts differ from sprite parts

In the previous section, we saw which of the sprite particle fields are the same for mesh particles. However, there are also other fields in mesh particles, which don’t have their exact counterpart in sprite particles.

Because the rendered object in the case of mesh particles is fundamentally different, mesh particles must host a different set of parameters to describe the rendered mesh. For example, it would make no sense for the mesh particle to have a spriteIndex field, considering a sprite won’t even be rendered for it.

The source of sprites for sprite particles are the sprite texture slots, such as DEFAULT_SPRITES, MISC_SPRITES and CUSTOM_SPRITES. The spriteIndex gives the index number to one of these sprite slots and this tells the sprite particle which sprite to use.

However, with mesh particles, it works quite differently.

Object field and mesh field

Meshes in the Tomb4 engine belong to specific moveable slots (though there are also meshes of static objects, but those can’t be used with mesh particles). Each moveable slot can have at most 32 meshes, indexed 0-31. Because there is no agglomerate DEFAULT_MESHES slot like we have for sprites, mesh particles must take these meshes from specific moveable slots present in the level WAD.

Object field

First of all, how do we specify what moveable slot we want the particle to take meshes from? That’s the responsibility of the part.object field of mesh particles. This is the numeric index of the moveable slot. These numeric indices are present in several places. In the Tomb Editor toolchain, this is the number in round parentheses (), before the name of a moveable slot in ALL_CAPS. You can also see this number in NG Center, in the SLOT MOVEABLE indices list on the Reference tab.

The point I’m trying to get across is that this numeric slot index is the number that the part.object field is expecting to receive. However, it would be quite difficult to memorize these numbers, you have to resort to looking them up in a listing, like in the aforementioned Reference tab of NG Center. For convenience, SLOT_ constants were implemented, that when used, get translated to the numerical slot indices in the Lua scripts. For example, instead of typing 0 for the LARA slot, you type SLOT_LARA and it gets replaced by the numeric index 0. Similary, if you type SLOT_SKELETON, it gets replaced by 35, and SLOT_ANIMATING2 gets replaced by 429. The default for object is 0, i.e. SLOT_LARA.
part.object = SLOT_SKELETON -- take meshes from the SKELETON moveable slot (35)
-- this is equivalent to

part.object = 35

After specifying an object slot, we also need to choose a specific mesh for the particle.

Mesh field

Specifying the object is step one, specifying which mesh we want from the object is step two. With the part.mesh field, we choose which mesh the particle takes the form of. The theoretical range for this field is 0-31. However, in practice, the maximum end of the range gets clamped to the highest index of mesh in the given slot, be it 14 for LARA, or 26 for BADDY_2.

You can also store custom meshes for particles in other slots, like ANIMATINGX or NEW_SLOTX. A neat thing to do in the init function, is to have a few different mesh variations in an object and select them at random with randint().
part.mesh = randint(0, 10)

Another fun idea to try is animating the mesh index in the update function. As a matter of fact, the particleAnimate() function, when applied to a mesh particle, already does this (by analogy to animated sprite particles), there is no need to reinvent the wheel. You just specify the starting mesh, ending mesh, the framerate and the plugin does the rest.

The above two fields are the most essential to make use of mesh particles, but there are also other important fields to be aware of.

Rotation vector and angular velocity vector

While a sprite is a 2D entity, a mesh is a 3D entity. This has concrete implications for how some properties of mesh particles will be different in comparison to sprite particles. One such property is that of rotation.

In 2D space, rotation is pretty simple. You can rotate an object clockwise or counter-clockwise. But disregarding the direction in which we choose to rotate, we see there is only one way to rotate in 2D space, because there is only one 2D plane in which the rotation can occur. We say that there is only one degree of freedom in 2D rotation. Because of this, in the case of sprite parts, it is enough to have one angle value, part.rot and one angular velocity value, part.rotVel.

In 3D space, it gets a bit more complicated. Notice there are 3 axes on which you can rotate a 3D object. For each way to rotate, we have three separate and independent angle values, rotating about the X, Y and Z axes. This means that there are three degrees of freedom for 3D rotation. Unlike for 2D sprites, a single value each for rotation and angular velocity would not be enough.

With mesh particles, we also have part.rot and part.rotVel, however they are no longer single numeric values, but rotation vectors. Rotation vectors are special subtype of vector. It is similar to a coordinate vector like part.pos, in that it has x, y and z components. But that’s where the similarities end. Rotation vectors cannot be used for representing XYZ coordinates, nor can coordinate vectors be used for representing 3D rotations. Therefore, even though both types of vectors seem to have the same components, they are actually two very different things and thus are not interchangeable with each other.

The range of values for each of the x, y, z components of a rotation vector goes from 0 to 2π radians, which, if you remember, corresponds to a 0 to 360 range in degrees. Naturally, any value above 2π radians loops back around to 0.
part.rot.x -- 0 to 2*pi
part.rot.y -- 0 to 2*pi
part.rot.z -- 0 to 2*pi

part.rotVel.x -- 0 to 2*pi
part.rotVel.y -- 0 to 2*pi
part.rotVel.z -- 0 to 2*pi
In analogy to rot and rotVel for sprites, the 3D rot vector represents the current rotation of the mesh about the XYZ axis, while rotVel specifies the amount by which the mesh is rotated on each frame, for each respective axis. For example, in the init function:
part.rot.x = randfloat(0, PI_TWO)
part.rot.y = randfloat(0, PI_TWO)
part.rot.z = randfloat(0, PI_TWO)
will give each mesh particle a random, unique rotation, whereas:
part.rotVel.y = PI * 0.05
will cause the mesh to rotate by increments of PI * 0.05 about the vertical Y axis. Reminder that randomNegate() is also applicable here, in case you want an even chance for the mesh to rotate in the opposite direction.

Scale vector

Continuing with the differences between 2D sprites and 3D meshes, we have the concept of size. In 2D sprite particles, we had part.sizeStart, part.sizeEnd and part.sizeCust, each addressing the sprite size property in an absolute manner (remember the notion of the particle square?). However, for mesh particles, a concept of relative size applies instead, due to how meshes work.

Object meshes have an innate size to them, which we see when we examine the skeleton (mesh tree) of the object in question. This is the default size of the mesh, i.e. the way it will appear when the engine attempts to render it with no alterations. It would be a shame though, if this was the size we were forced to always use. Say that you have some mesh particles simulating pieces of debris. Naturally, some of those pieces will be larger and others will be smaller. It would be a bother to have to manually create variants of different size just to give them size variations.

Instead, mesh particles host a part.scale property. As you may be able to guess, it is responsible for setting the scale of the mesh, in reference to the default size. It would perhaps be entirely reasonable if this property was a single value that determined the scale in general. But what if you could independently scale the mesh on the three axes: X, Y, Z?

Good news, you can do that, because this property is a scale vector. It is yet another specialized kind of vector, different to rotation vectors and coordinate vectors. You cannot swap out scale vectors with the other two types of vectors. They are their own kind of beast, responsible for setting the scale of a mesh particle, not rotation nor position.

Anyway, it was mentioned that the scale is in relation to the default size of a mesh. Therefore, by default, the scale vector has the values:
part.scale.x = 1
part.scale.y = 1
part.scale.z = 1
A value of 1 for a given axis means 100% of the default scale for that axis. Therefore, each component allows to specify a fraction of the default scale for a given axis:
part.scale.x = 0.5 -- 50% scale on X axis
part.scale.y = 1.5 -- 150% scale on Y axis
part.scale.z = 2 -- 200% scale on Z axis
The above will mean that the mesh will be downscaled by half on its X axis, upscaled 1.5 times on the Y axis and 2 times on the Z axis. Only 0 and positive values can be used for the scale vector components (since negative scale does not really make much sense). If all components of part.scale are 0, the mesh will be infinitely shrunk, thus not rendered by the engine.

To give the particle a uniform scale factor on XYZ, you set each component to the same value, for example:
local scale = randfloat(0.5, 2) -- random float between 0.5 and 2
part.scale.x = scale
part.scale.y = scale
part.scale.z = scale
Mesh particle scale can also be animated in update functions, allowing mesh particles to grow or shrink in real time.

Transparency field

Do you know how enemies in TR4 and TR5, after being killed, gradually disappear, becoming more and more transparent? This was a change of the behavior players were used to from TR1-3, where the bodies of dead enemies remained in the level map. Presumably, it was done to improve performance of the latter games (by vanishing the dead bodies, the renderer had less polygons to do deal with).

Anyway, this vanishing effect is applicable to mesh particles. The part.transparency field allows you to control the transparency level. It accepts float values between 0.0 (fully opaque) and 1.0 (fully transparent – i.e. invisible). Therefore, making a particle vanish slowly over its lifespan is a simple matter of setting the part.transparency to the value of part.t in the update function:
local function vanishUpdate(part)
    part.transparency = part.t
end
You can also transform part.t with a function like sin() (remapped to fit into the expected 0 to 1 range) to make the mesh blink in and out of existence. Or you can perhaps make the particle to materialize and become more opaque, the closer Lara comes towards it. So many possibilities!

Lighting type and color tint

Mesh particles, depending on the lighting type applied on the slot that hosts the meshes it comes from (dynamic or static), allow for a dynamic or static lighting type.

Dynamically lit mesh particles respond to ambient room lighting as well as dynamic light sources such as fires, flares or gunshots:

Differently, if the static lighting is applied, the mesh particles do not respond to room ambience nor dynamic lights. In exchange, this permits the statically lit mesh particles to have a color tint. The color tint is accessed via the part.tint field. This is a ColorRGB property, as were colStart, colEnd and colCust in sprite particles.

part.tint.r -- tint red component
part.tint.g -- tint green component
part.tint.b -- tint blue component
This field corresponds to the RGB tint on the statically lit mesh. Note that the tint does not work if the mesh lighting mode is set to dynamic.

Mesh particle functions

Mesh particles can use all of the particle functions available for sprite particles, with no exceptions. In some cases, there are minor changes in how they work when applied to mesh particles, an example being ParticleAnimate(), which animates over the part.mesh field rather than the part.spriteIndex field.

However, there are also a select few particle functions that are unique and specific to mesh particles. They cannot be used with sprite particles, attempting to do so will cause the particle system to throw an error.

These functions are listed below.

MeshShatter

The meshShatter() function is a specialized version of the particleKill() function. It takes a single argument, the mesh particle. Just like particleKill(), it kills the underlying particle instantly, but on top of that, creates a “shattering” effect for the mesh. This can be useful for projectiles which shatter on impact, like the ones using meshes from the BUBBLES slot. In such cases, you should check that the particle made a collision with room geometry or the target moveable before engaging the shatter function.
if particleCollideWithWalls(part, 1.0) then
    meshShatter(part) -- kills the particle and shatters the mesh
end

MeshAlignToVel()

The meshAlignToVel() function aligns the facing (X and Y rotation) of the mesh particle to its vel vector, i.e. make it face the direction in which it is moving. The function takes 3 arguments:
meshAlignToVel(part, alignFactor, invert)
The arguments are as follows: Again, this function is applicable to projectiles, specifically homing ones, which curve to face the target up front (SETHA, DEMIGODS shoot these homing projectiles in the original Tomb4 engine). The alignSpeed factor controls the “delay” of how quickly the meshes adapt their facing to changes in direction. The maximum of 1.0 can sometimes result in jerky motion, lower values like 0.3 to 0.1 smoothen it out. Since only the rotation on the X and Y axis is affected, if the particles travel straight up or straight down, this can result in very abrupt “twists” when the facing is corrected. There is not much that can be done to fix this behavior.

MeshAlignToTarget()

The meshAlignToTarget() function aligns the facing (X and Y rotation) of the mesh particle to the target 3D vector given as one of the arguments. The function takes 4 arguments in total:
meshAlignToVel(part, target, alignFactor, invert)
The arguments are as follows: Differently to meshAlignToVel(), which orients the mesh towards the velocity vector of the particle, meshAlignToTarget() allows you to specify an arbitrary position vector to which the particle will turn to. An example could be something like:
local laraid = getLaraIndex()
local mesh = 14
local target = getItemJointPosition(laraid, mesh, 0, 0, 0) -- get the position of Lara's head mesh 14

meshAlignToTarget(part, target, 0.2, false) -- make the mesh particle "look at" Lara's head

Limitations of mesh particles

Mesh particles are a great extension to the concept of particles, opening up new potential for new visual effects and unique gameplay ideas. Nonetheless, there are still certain limitations and restrictions that are associated with them. For one, mesh particles cannot have the same properties as moveable items. It is impossible for them to consists of skeletons as moveable objects do. You can only take individual meshes from a moveable object slot. Related to this, it is not possible for the mesh particles to be animated with conventional keyframe animations. The only way to “animate” the particles is to jump through the meshes of the object, as a sequence of still-frames (e.g. via particleAnimate()). Also, using static meshes (belonging to static slots such as PLANT0-9, ARCHITECTURE0-9, etc.) is also not currently possible.

As for the “particle” aspect itself, there is a smaller pool of mesh particles at your disposal (1024) than sprite particles (4096). This is because meshes tend to consist of several polygons with numerous vertices, whereas a sprite consists of only a single polygon (a quad, with 4 vertices), so it’s less complicated to render and, in theory, will not overflow the vertex buffer (the number of vertices that can be simultaneously drawn on screen) as quickly.

We have finally covered the second main variant of particles in the plugin, mesh particles. They have some specific differences compared to sprite particles, but hopefully, after reading to the end of this chapter, they have no more secrets to hide from you.

In the next chapter, we will talk more in-depth about particle groups themselves, which bring even more possibilities to the table, as you will see for yourself.