Mesh particles

<<< PREV TO BE CONTINUED

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 many 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 first paved the road 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). Thus, the fields possessed by particle data entities for the respective category (sprite / mesh) will reflect this difference. Fortunately, these two kinds of particles have a set of common base properties that behave identically in either case.

This slightly shorter 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 emphasizing their distinguishing features. Mesh particles are a very interesting addition, which (hopefully) will broaden the horizons of what TRLE builders can come to expect when thinking of particle effects.

Sections

        1. Motivation for mesh particles
        2. Similarities between sprite particles
        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 types and color tint
        9. Mesh particle functions
        10. Limitations of mesh particles


Motivation for mesh particles

A form of mesh-based “particle” effects already existed in the original Tomb4 engine. For example, projectiles fired by SETHA, DEMIGOD, HARPY, mesh chunks flung around when enemies are blasted apart by explosive ammo, locusts and little beetles being spawned from their emitter nullmeshes, are all examples of mesh particles in the original TRLE engine. However, there was never a way to control what these mesh particles do, since their behavior was hardcoded into the engine, in order to fulfill a specific purpose.

The scriptable mesh particles brought forth by the plugin aim to fill this exact niche. They are under your full Lua scripting control. You can determine how they look, how they spawn and what they do after spawning. Mesh particles can take the form of an arbitrary mesh from any moveable slot present in the level. You are able to set their position, velocity, acceleration, lifetime (just like with sprite particles)!

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 seems 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 data entity that is used in either case, created with createGroup(). In theory, this means that you can have both kinds of particles belonging to the same group. However, this practice is discouraged, unless you have a very good reason for it. Prefer making separate, designated groups for mesh particles and sprite particles, so each group has only one kind of particle associated with it. Sprite and mesh particles sharing one group is far more error-prone and confusing down the line, trust me on this.

If you need specific examples of where mesh particles can be useful, think of crumbling debris falling from the ceiling to the ground, an avalanche of rocks or snow making its way down a mountain side, simulating ambient creatures like insects, fish or birds, crafting throwable weapons for Lara, e.g. hand grenades or molotov cocktails, or perhaps scripting new projectile / ranged attacks for enemy slots that never had them before, such as BADDY_2, MUMMY or AHMET. Are you sold on mesh particles already? I’m sure you are, so let’s get started!

Similarities between sprite and mesh particless

Mesh particles and sprite particles are treated as separate types of entities in the plugin, on account of having different sets of properties. Nonetheless, there is an overlap of some properties/fields, common to both kinds of particles. This is to be expected, since 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 with no new information to provide on the matter. Below are listed all the properties / fields, present both in mesh particles and sprite particles, which were already explained in earlier chapters for sprite particles, and work identically for mesh particles:

Common fields of sprite parts and mesh parts
Field name Value type Explained in
lifeSpan integer (0 - 32767) Chapter 1 – Particle lifetime
lifeCounter integer (0 - 32767) Chapter 1 – Particle lifetime
pos Vector Chapter 1 – Particle position
vel Vector Chapter 1 – Particle velocity
accel Vector Chapter 1 – Particle acceleration
emitterIndex tomb4 moveable indices Chapter 1 – Particles emitted from objects
emitterNode mesh indices Chapter 1 – Particle emitter nodes
t (t-parameter) read-only number Chapter 3 – Particle t-parameter
roomIndex room indices Chapter 3 – Colliding particles
(Why particles have a 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 that some of the sprite particle fields are also shared by mesh particles. However, there are certain 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 (a 3D mesh versus a flat 2D polygon for sprites), 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.

The source of sprites for sprite particles are the sprite texture slots, i.e. DEFAULT_SPRITES, MISC_SPRITES and CUSTOM_SPRITES. The spriteIndex gave the index number for one texture inside of these sprite slots, telling the renderer which sprite to use.

However, with mesh particles, specifying the used mesh works a bit differently.

Object field and meshIndex 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.

Setting the object slot

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 all these numbers, you have to resort to looking them up in some listing, such as 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.
part.object = SLOT_SKELETON -- take meshes from the SKELETON moveable slot (35)
-- this is equivalent to

part.object = 35

The default for object is 0, i.e. SLOT_LARA. The plugin will fall back on this mandatory slot in case the requested object slot is not present in the level WAD (LARA must always be present and placed somewhere in a level map, otherwise the level will crash the game). After specifying an object slot, we also need to choose a specific mesh for the particle.

Setting the mesh index

Specifying the object is step one, specifying which mesh we want from the object is step two. With the part.meshIndex field, we choose which mesh the particle takes the form of. The theoretical maximum 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 the (vanilla) BADDY_2 enemy.

part.object = SLOT_SKELETON -- take meshes from the SKELETON moveable slot (35)
part.meshIndex = 9 -- take mesh index 9 (the head)
Because of the index clamping dependent on the object slot, part.object and part.meshIndex is another example of fields which must be set in a specific order (part.object first, part.meshIndex second).

You can also store custom meshes for particles in other slots, such 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 randomInt().
part.meshIndex = randomInt(0, 10)

Another fun idea to try is animating the mesh index in the update function. As a matter of fact, the partAnimate() function, when applied to a mesh particle, precisely does this (by analogy to animated sprite particles), no need to reinvent the wheel! You just specify the starting mesh, ending mesh, the framerate, the plugin takes care of 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 several 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 this rotation can occur. We say that there is only one degree of freedom in 2D rotation. 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 (X, Y, Z) 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. As a consequence, a single angle value for rotation and another for angular velocity would not be sufficient to describe 3D rotations.

With mesh particles, we also have identically named part.rot and part.rotVel fields, however they are no longer single numeric values, but rotation vectors. Rotation vectors are special kind of vector. They are similarly structured to coordinate vectors (e.g. like part.pos), in the sense that they have 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 very different things and thus are not interchangeable with each other. Trying to use a coordinate vector in place of a rotation vector will not work, and vice-versa.

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

part.rotVel.x -- [0, 2π]
part.rotVel.y -- [0, 2π]
part.rotVel.z -- [0, 2π]
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 = randomFloat(0, PI_TWO)
part.rot.y = randomFloat(0, PI_TWO)
part.rot.z = randomFloat(0, PI_TWO)
will give each mesh particle a randomized rotation on all 3 axes, whereas something like:
part.rotVel.y = PI * 0.05
will cause the mesh to rotate by increments of PI * 0.05 about the vertical Y axis. Note that randomNegate() is also applicable here, if you want a chance for the mesh to rotate in the opposite direction.

Scale vector

Continuing with the differences between 2D sprites and 3D meshes, we have another difference in 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 (the notion of the particle square / rectangle for the sprites). 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. It is also part of the import settings, when importing a mesh from an external 3D modeling program. 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.

Indeed for that reason, 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 just a single number value, that determined the scale as a whole. But what if you could independently set scale on the three axes: X, Y, Z?

Good news, you are able to 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.0
part.scale.y = 1.0
part.scale.z = 1.0
A value of 1.0 for a given axis means 100% of the default mesh scale for that axis. Therefore, each component allows to specify a different proportion 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 here). If all components of part.scale are 0, the mesh will be infinitely shrunk, becoming invisible.

To give the particle a uniform scale factor on XYZ, you set each component to the same value, for example:
local scale = randomFloat(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 manipulated in update functions, allowing mesh particles to grow or shrink in a dynamic way, even indepentently on each axis.

Transparency field

Do you know how enemies in TR4 and TR5, after being killed, gradually vanish, becoming more and more transparent? This was a change from what players were used to in previous TR games, where the bodies of dead enemies remained in the level map. Presumably, this was done to improve performance of the latter games, as they got more complex and detailed, while PSX hardware remained the same (by vanishing the dead bodies, the renderer had less polygons to work on).

Anyway, this transparency effect can now be utilized with mesh particles. The part.transparency field allows you to control the (alpha) transparency level of the given mesh particle. It accepts integer values between 0 (fully opaque) and 255 (fully transparent – 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 * 255 in the update function:
local function vanishUpdate(part)
    part.transparency = part.t * 255
end
You can also transform part.t with a math function like cos() (remapped to fit into the expected 0 to 255 range) to make the mesh blink in and out of existence, for example. Or you can perhaps make the particle materialize and become more fully opaque, the closer Lara comes towards it. So many possibilities!

Lighting types and color tint

Mesh particles, depending on the lighting type applied on mesh 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 (like from fires, flares or gunshots):

Differently, if the static lighting mode is applied to the host mesh, the mesh particles do not respond to room ambience nor dynamic lights. In exchange though, this permits the statically lit mesh particles to have a customizable color tint (as with statics placed in the level map). The color tint is accessed via the part.tint field. This is a ColorRGB property, as were colStart, colEnd and colCust in the case of sprite particles.

part.tint.r -- tint red component
part.tint.g -- tint green component
part.tint.b -- tint blue component
This field allows to set the RGB tint on the statically lit mesh. Again, the tint property does not work if the meshes’ lighting mode is set to dynamic, only statically lit meshes have an enabled tint setting on the particles.

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 partAnimate(), which animates over the part.meshIndex 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 in the log.

These functions are listed below.

Function meshShatter()

The meshShatter() function is a specialized version of the more general partKill() function. It takes a single argument, the mesh particle. Just like partKill(), 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 partCollideWalls(part, 1.0) then
    meshShatter(part) -- kills the particle and shatters the mesh
end

Function meshAlignVelocity()

The meshAlignVelocity() 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:
meshAlignVelocity(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 TR4 game). The alignFactor 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.

Function meshLookAtTarget()

The meshLookAtTarget() 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:
meshLookAtTarget(part, target, alignFactor, invert)
The arguments are as follows: Differently to meshAlignVelocity(), which orients the mesh to face the velocity vector of the particle, meshLookAtTarget() allows you to specify an arbitrary position vector target, to which the particle will turn to.

There is a built-in function, that allows us to obtain the global XYZ coordinates of some mesh of a moveable, returning it as a vector. The function is called getItemJointPosition() and it looks like this:
getItemJointPosition(index, mesh, offsetX, offsetY, offsetZ)
Its arguments are: As stated, this function returns a vector containing the global position of the specified mesh. The offsetX, offsetY and offsetZ will offset the position by the specified amount from the joint. This respects the rotation of the mesh in various animations, using a similar principle as the part.emitterNode in offsetting the particle position!

Anyway, we can use this function to get the location of Lara’s head, then use that position as the target to which the mesh particles can look. Here’s an example:
local laraid = getLaraIndex()
local mesh = 14
local target = getItemJointPosition(laraid, mesh, 0, -64, 0) -- get the position of Lara's head mesh 14

meshLookAtTarget(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 do not have the same properties as full-fledged moveable items. It is impossible for them to consists of multi-mesh 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” such particles is to jump through the meshes of the object, as a sequence of still-frames (e.g. via partAnimate()). 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. You should use mesh particles more sparingly, especially if the mesh taken for it is extremely detailed, with many polygons.

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.

<<< PREV TO BE CONTINUED