Asset - Graphics sPart 3D particle system - New update sept 2019! Now entirely free

TheSnidr

Heavy metal viking dentist
GMC Elder

Thank you for showing interest in the sPart system!

What is it?
The sPart system is a 3D particle system for Gamemaker Studio 2 that provides you with a fast and easy way to create beautiful particle effects in 3D. It aims to be similar in use to the built-in 2D particle system in Gamemaker, and the documentation will assume the user has a basic familiarity with the built-in system.

Where do I find it?
The sPart system is available on the Marketplace:
Get it for free on the Marketplace

Download executable demo
Download documentation

How does it do it? Some technical info!
The particles are simulated entirely on the GPU via a vertex shader using the equations of motion. This is much, much faster than moving and drawing particles individually as sprites, and allows for drawing tens of thousands, or even hundreds of thousands of particles at a time.
To be able to draw particles, we need to send a vertex buffer to the shader. The vertex buffer used by the sPart system is a bare-bones buffer that contains the following data per vertex:
  1. The index of the particle, stored in three bytes. This allows for a maximum of 255x255x255 unique particles to be drawn per batch. This index is used to generate a unique seed for the noise function in the shader, giving each particle a unique trajectory.
  2. The corner ID of the vertex, stored in the last byte. This can be 0, 1, 2, or 3. This is used to construct the local vertex position and texture coordinate.
The vertex buffer uses four bytes per vertex, and six vertices per particle. Multiple vertex buffers with differing numbers of particles can be created, so that the most fitting one can be used. This will be described later, in the function called spart_system_create. If the number of particles exceeds the maximum number of the vertex buffer, they will have to be split up into smaller batches.
We need to send some info about where to spawn particles to the shader. This is where the emitters come in handy. You can create an emitter and tell it how, where and how many particles it should emit per unit of time, and the sPart system sends this info to the vertex shader. Emitters are the most efficient when static, but they can be dynamically moved as well. Each batch can draw up to 32 separate static emitters, or 32 steps of a moving emitter at a time, after which the particle system has to be split up into multiple batches.
Particles can spawn other particles. This only works for one generation, particles spawned from other particles cannot spawn particles of their own. Particles can spawn other particles either each step or upon death. This uses a slightly different shader than the regular particles.
Particles are by default billboarded images, but meshes can also be used to achieve fully 3D particles. The format for 3D mesh particles contains the following data per vertex:
  1. Vertex position, stored in three bytes (one byte per dimension)
  2. Particle index, stored in one byte (and so is capped to 255)
  3. Texture coords stored in two bytes (one byte per dimension)
  4. Vertex normals stored in two bytes (polar coordinate angles)
The mesh vertex buffer contains eight bytes per vertex. The number of vertices is uncapped, but for the sake of speed and memory usage, it is recommended to keep the vertex count low.

Particle system functions overview
Following is an overview of the various functions included in the sPart system, as well as a description of each.
///////////////////////////////////////////////////////////////////////////////////////////////
Particle system functions overview
Following is an overview of the various functions included in the sPart system, as well as a
description of each.

spart_init()
Returns: N/A
Initializes the sPart system.
Should be run once at the start of the game. It creates a vertex format, and initializes
various global variables, macros and enums used by the sPart system.

spart_system_create(batchSizeArray)
Returns: Index of new particle system
Creates a new particle system, which is a container for the emitters created within the
system. You must define the particle batch sizes in an array of ascending values, and the
sPart system will at any time pick the batch size that fits best for your particles. For a
dynamic particle system it’s often useful to define multiple vertex batch sizes. Say there are
only fifty particles on screen; if the minimum vertex batch is ten thousand, that is a lot of
wasted vertices. On the opposite extreme, say there are ten thousand particles, but the
maximum batch size is fifty – this would require a whole lot of draw calls to draw all
particles, and would slow down your game!
Usage example:
spart_system_create([50, 150, 500, 1500, 5000]); //<- this would let you draw a wide range
of particles with as few draw calls as possible

spart_system_update(partSystem, timeIncrement)
Returns: N/A
Updates the particle system, keeping track of time and emitter deaths. Must be called every step!
This script lets you define the time increment per step. Set the timeIncrement to for example 1
to increment time by one unit per step, or to for example delta_time / 10000000 to increment it
by one unit per second irrespective of the framerate.
Usage example:
spart_system_update(partSystem, delta_time / 10000000);

spart_system_draw(partSystem)
Returns: N/A
Draws your particle system. Must be called in draw event. You can set the world matrix with
matrix_set(matrix_world, etc) to alter the position, orientation and scale of the entire particle
system as a whole.

spart_system_destroy(partSystem)
Returns: N/A
Destroys the particle system and all emitters, particle types and vertex buffers it contains.

///////////////////////////////////////////////////////////////////////////////////////////////
Particle type functions overview
Following is a list of all particle type functions, as well as a description of each.
spart_type_create(partSystem)
Returns: Index of new particle type
Creates a new blank particle type. You can define the rules for this particle type with the
following functions.

spart_type_destroy(partTypeInd)
Returns: N/A
Destroys a particle type. If this function isn’t called, the particle type will live on for the
entire duration of the game.

spart_type_life(partType, life_min, life_max)
Returns: N/A
Define the possible life span of each particle. A life span is randomly selected between the
minimum and maximum.

spart_type_sprite(partType, sprite, image_speed, random)
Returns: N/A
Defines the sprite of a particle. The sprite can be animated, and you can set the animation speed.
Set image_speed to -1 to stretch the animation to the particle’s life span. This function will
duplicate the sprite and transform it into a format the particle shader can use. Sprites whose
dimensions are not powers of two will be resized to the nearest power of two. Animated sprites will
be arranged one after another horizontally on the new sprite sheet. This new sprite is deleted when
the particle type is destroyed. If the particle is a mesh particle, the sprite will be used as a
texture. The texture may be animated as well.

spart_type_mesh(partType, model, numPerBatch)
Returns: N/A
Makes the particle draw a 3D mesh instead of a billboard. The meshes will be simulated in the same
way as the billboarded particles, but will be drawn in full 3D and not in view-space. «Model» can
be an index of a vertex buffer, a path to an .obj model or a path to a saved buffer. If a vertex
buffer is supplied, it must be formatted in the following way:
1. 3D position 3x4 bytes
2. Normal 3x4 bytes
3. Texture coordinates 2x4 bytes
4. Colour 4x1 bytes
NumPerBatch cannot exceed 255

spart_type_mesh_rotation_axis(partType, x, y, z, axisDeviation)
Returns: N/A
A 3D mesh can rotate about an axis, and this axis is defined in this function. Give the axis to
rotate around, as well as how far (in degrees) from this axis the rotation axis of the individual
particles may deviate. Setting axisDeviation to 360 gives totally random rotation axes. The final
rotation axis is also affected by the «radial» setting in spart_type_direction, which makes each
particle face in its moving direction.

spart_type_mesh_lighting(partType, ambientCol, lightCol, lightDirX, lightDirY, lightDirZ)
Returns: N/A
Enables a directional light onto mesh particles. Only one directional light may be used with the
built-in shaders, for more advanced lighting you will have to manually edit the shaders.

spart_type_direction(partType, xdir, ydir, zdir, dir_vary, radial)
Returns: N/A
Set the starting direction of the particle. Also lets you define the angle (in degrees) from the given
direction the particle may randomly deviate. Setting relative to true modifies the starting direction
to be relative to its spawning pos within the emitter. Particles that have been spawned by other
particles will ignore their own direction, and instead keep moving in the direction of its creator,
plus the direction variation.

spart_type_scale(partType, scale_min, scale_max, scale_incr)
Returns: N/A
Sets the scale of the particle. A starting scale will be randomly selected between scale_min and
scale_max, and the scale will be incremented by scale_incr for each unit of time.

spart_type_orientation(partType, ang_min, ang_max, ang_incr, ang_relative)
Returns: N/A
Defines the angle and angular momentum (in degrees per unit of time) of the particle in screen-space.
Astarting angle will be randomly selected between ang_min and ang_max, and the angle will be
incremented by ang_incr for each unit of time. Setting ang_relative to true will make the particle
face in its screen-space moving direction, plus the given angle.

spart_type_speed(partType, speed_min, speed_max, speed_acc, speed_jerk)
Returns: N/A
Sets the speed of the particle. A starting speed will be randomly selected between speed_min and
speed_max, and the speed will be incremented by speed_acc for each unit of time. The speed will also
be incremented by speed_jerk for each unit of time squared, enabling more advanced movement patterns.

spart_type_gravity(partType, grav_amount, xdir, ydir, zdir)
Returns: N/A
Define the gravity vector and amount.

spart_type_colour_mix(partType, colour1, alpha1, colour2, alpha2)
Returns: N/A
Sets the colour of the particle to a random mix between the two given colours

spart_type_colour1/2/3/4(partType, colour1, alpha1, colour2, alpha2…etc)
Returns: N/A
Sets the colour of the particle to a linear blend of the given colours, stretched over the course of
the particle’s life.

spart_type_blend(partType, additive)
Returns: N/A
Lets you toggle additive blend mode. Turning additive blend mode on will also disable zwriting. This
is useful for, for example, fire effects.

spart_type_blend_ext(partType, src, dest, zwrite)
Returns: N/A
Lets you define source and destination blend modes, as well as give you control over the zwriting,
for the particle.

spart_type_step(partType, num, childType)
Returns: N/A
Make the particle emit a given number of particles for each unit of time. Note that particles emitted
this way cannot emit particles of their own.

spart_type_death(partType, num, childType)
Returns: N/A
Make the particle emit a given number of particles upon death. Note that particles emitted this way
cannot emit particles of their own.

spart_type_save(partType, fname)
Returns: N/A
Saves your particle type to a file. This also saves any secondary particles.

spart_type_load(partSystem, fname)
Returns: Index of new particle type
Loads a particle type from file.

///////////////////////////////////////////////////////////////////////////////////////////////
Particle emitter function overview
Following is a list of all particle emitter functions, as well as a description of each.
spart_emitter_create(partSystem)
Returns: Index of new emitter
Create a new inactive emitter. To activate the emitter, use spart_emitter_stream.

spart_emitter_stream(emitterInd, parttype, particleNumPerTime, lifeTime, dynamic)
Returns: N/A
Make the emitter emit the given number of particles per unit of time. You can define the emitters life
time, or set it to -1 to use the default life span. Setting dynamic to true will let you change the
emitter’s orientation without altering the particles that have already been created. This effectively
keeps a «log» of all previous emitter positions/orientations, so that existing particles are not
retroactively altered.

spart_emitter_region(ind, centerPos[3], scale[3], eulerAngles[3], shape, distribution, dynamic)
Returns: N/A
Move the emitter. The new position, scale and euler angles should all be supplied as three-part vectors.
The angles are defined in degrees. Shape can be one of the following constants:
0. spart_shape_cube
1. spart_shape_circle
2. spart_shape_cylinder
3. spart_shape_sphere
Distribution can be one of the following constants:
0. spart_distr_linear (or ps_distr_linear) //Generate with equal propbability anywhere in space
1. spart_distr_gaussian (or ps_distr_gaussian) //Generate more at the center of the shape
2. spart_distr_invgaussian (or ps_distr_invgaussian) //Generate more at the edges of the shape
Setting dynamic to true will let you change the emitter’s orientation without altering the particles that
have already been created. This effectively keeps a «log» of all previous emitter positions/orientations,
so that existing particles are not retroactively altered.
Usage example:
spart_emitter_region(emitterInd, [x, y, z], [32, 32, 32], [0, 0, direction], spart_shape_cube, spart_distr_gaussian, true);

spart_emitter_mature(emitterInd)
Returns: N/A
This function will mature an emitter. This means that it will look like it has been emitting for quite a
while, even though it just started. This function must be called after you’re done defining the emitter, as
all it really does is subtract the maximum life span of its particle types from the emitter’s creation time.

spart_emitter_retire(ind)
Returns: N/A
Deletes the selected emitter from the active emitters list, and makes a new, special emitter that will only
finish the old emitter’s spawned particles without spawning new ones for its own. The new emitter will
indicate the given emitter as its parent.

spart_emitter_destroy(ind)
Returns: N/A
Destroys the emitter, as well as any child emitters. This function will be unnecessary if you also delete the
particle system with spart_system_destroy, as that function also deletes all emitters.

///////////////////////////////////////////////////////////////////////////////////////////////
Helper function overview
Here’s an overview of the helper scripts that are not meant for used by themselves, but are used in the other functions.

spart__get_uniform_index(shader)
Returns: Uniform index of the given shader
Uniforms are indexed in a grid so that the system only has to index them once.
If the shader has not been indexed previously, get the relevant shader uniforms.

spart__emitter_set_uniforms(uniformIndex, emitterIndex)
Returns: N/A
Sets the shader uniforms for the given emitter

spart__set_type_uniforms(uniformIndex, partType)
Returns: N/A
Sets the shader uniforms for the given particle type.

spart__type_set_gpu_settings(partType);
Returns: N/A
Sets the GPU settings for the given particle type

spart__type_set_parent_uniforms(uniformIndex, partType, partNum)
Returns: N/A
Sets the shader uniforms for the given parent particle type. This is used for particles that are spawned each
step or upon death of other particles.

spart__submit_vbuffs(partSystem, partType, partNum, uniformIndex)
Returns: N/A
Splits the particles up into batches and submits them to the GPU

spart__update_vbuffers(partSystem)
Returns: N/A
Makes sure the necessary vertex buffers exist.

spart__load_obj(fname)
Returns: Buffer index
Loads a .obj file into a buffer that is formatted in a way the sPart system can use. This is used for loading
meshes in the spart_type_mesh function.

spart__read_obj_line(str)
Returns: Array containing the info in the string that's been separated by blank spaces

spart__read_obj_vertstring(str)
Returns: The vertex, normal and texture index of the vertex

spart__read_obj_face(faceList, str)
Returns: N/A
Adds the triangles in str to the facelist

spart__write_type_to_buffer(buffer, partType, depth)
Returns: N/A
Writes a particle type to the given buffer. Also writes any secondary particles.

spart__read_type_from_buffer(buffer, partSystem)
Returns: Index of new particle type
Loads a particle type from the given buffer.

spart_type_step(partType, num, childType)
Returns: N/A
Make the particle emit a given number of particles for each unit of time. Note that particles emitted this way cannot emit particles of their own.

spart_type_death(partType, num, childType)
Returns: N/A
Make the particle emit a given number of particles upon death. Note that particles emitted this way cannot emit particles of their own.



Particle emitter function overview
Following is a list of all particle emitter functions, as well as a description of each.

spart_emitter_create(partSystem)
Returns: Index of new emitter
Create a new inactive emitter. To active the emitter, use spart_emitter_stream.

spart_emitter_stream(emitterInd, parttype, particleNumPerTime, lifeTime, dynamic)
Returns: N/A
Make the emitter emit the given number of particles per unit of time. You can define the emitters life time, or set it to -1 to use the default life span.
Setting dynamic to true will let you change the emitter’s orientation without altering the particles that have already been created. This effectively keeps a «log» of all previous emitter positions/orientations, so that existing particles are not retroactively altered.

spart_emitter_region(ind, centerPos[3], scale[3], eulerAngles[3], shape, distribution, dynamic)
Returns: N/A
Move the emitter. The new position, scale and euler angles should all be supplied as three-part vectors. The angles are defined in degrees.
Shape can be one of the following constants:
0. spart_shape_cube
1. spart_shape_circle
2. spart_shape_cylinder
3. spart_shape_sphere
Distribution can be one of the following constants:
0. spart_distr_linear (or ps_distr_linear) //Generate with equal propbability anywhere in space
1. spart_distr_gaussian (or ps_distr_gaussian) //Generate more at the center of the shape
2. spart_distr_invgaussian (or ps_distr_invgaussian) //Generate more at the edges of the shape
Setting dynamic to true will let you change the emitter’s orientation without altering the particles that have already been created. This effectively keeps a «log» of all previous emitter positions/orientations, so that existing particles are not retroactively altered.
Usage example:
spart_emitter_region(emitterInd, [x, y, z], [32, 32, 32], [0, 0, direction], spart_shape_cube, spart_distr_gaussian, true);

spart_emitter_mature(emitterInd)
Returns: N/A
This function will mature an emitter. This means that it will look like it has been emitting for quite a while, even though it just started. This function must be called after you’re done defining the emitter, as all it really does is subtract the maximum life span of its particle types from the emitter’s creation time.

spart_emitter_retire(ind)
Returns: N/A
Deletes the selected emitter from the active emitters list, and makes a new, special emitter that will only finish the old emitter’s spawned particles without spawning new ones for its own. The new emitter will indicate the given emitter as its parent.

spart_emitter_destroy(ind)
Returns: N/A
Destroys the emitter, as well as any child emitters. This function will be unnecessary if you also delete the particle system with spart_system_destroy, as that function also deletes all emitters.



Helper function overview
Here’s an overview of the helper scripts that are not meant for used by themselves, but are used in the other functions.

spart__type_set_uniforms(shader, partType);
Returns: N/A
Sets the shader uniforms for the given particle type.

spart__type_set_parent_uniforms(shader, partType, effectType);
Returns: N/A
Sets the shader uniforms for the given parent particle type. This is used for particles that are spawned each step or upon death of other particles.

spart__get_vbuff_settings(partSystem, partNum)
Returns: Array containing [vertex buffer, number of batches, number of particles per batch]
Looks up the batch sizes in this particle system, and finds a fitting one for the desired number of particles.

spart__update_vbuffers(partSystem)
Returns: N/A
Makes sure the necessary vertex buffers exist. When a particle system is destroyed, its vertex buffers are deleted. However, other particle systems may still use the same vertex buffers, and they will have to be created again. This is basically a cheap workaround.

spart__load_obj(fname)
Returns: Buffer index
Loads a .obj file into a buffer that is formatted in a way the sPart system can use. This is used for loading meshes in the spart_type_mesh function.

Gallery






 
Last edited:

TheSnidr

Heavy metal viking dentist
GMC Elder
New version is up with a couple'a fixes:
- Added the possibility to set a custom cull mode for your particle type. This particularly applies to mesh particles. The script is called spart_type_mesh_cullmode
- Fixed a problem with dynamically moving emitters, billboarded particles would flicker once emitters passed their life span. This was due to a problem when generating the vertex buffers. If you have incorporated the particle system into your project, I recommend to at the very least update the helper script called spart__update_vbuffers.

Feedback is greatly appreciated!
Cheers!
 
R

Rukola

Guest
It seems like a good system @TheSnidr. Some suggestions :D
  • Post a link in your open source(ish?) project. Like an add-on to the project :)
  • Use more populair effects. Like the zelda explosion
  • Use a higher texture grass. Pixels might work.
  • Instead of low quality gifs, upload it to youtube using recorder: https://obsproject.com/
Anyway, it seems great. Good job!
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Thank you, I will definitely have to market it more! While working on an explosion effect, however, I found a bug that I haven't managed to fix yet:
* When particles emit particles per step, retiring the emitter makes the emitter think it has to draw the maximum possible number of particles that could have existed at the same time if its life span was longer. For short burst effects like explosions, it could end up drawing hundreds of thousands of particles when it only needs a couple of thousand. This will be fixed as soon as I have the time.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
I'm in the process of fixing up my new apartment, sorry for being slow with updates!
I fixed the bug with the burst emitters! Secondary particles should now work correctly even for burst effects and dynamically moving emitters. There are also a couple of other changes, so here's a full change log:
  • Fixed an error with the step particle effect, which made the system think it had to render all possible particles even for short burst effects. The bug resulted in possibly rendering ten thousand times more particles than necessary when dynamically moving emitters with step effects.
  • Added a new demo: Simple explosion. Press 9 in the demo project to view this effect.
  • Added acceleration to the scale and angle settings. This allows you to change the rate at which the scale and angle increases. This could be used to for example create a particle that first increases in size, and then gradually slows down, and in the end decreases in size again.
  • Generally cleaner code, particularly in the shaders


Download at the Marketplace, link in first post!
If there is a demand for it, I'll make a 1.4 version as well. Gimme a holler.
 
Last edited:

sylvain_l

Member
You forget the first rule of business: Supply and demand.
you should supply a request in that case :D

hi @TheSnidr just to be sure; we can constrain the trajectories at will?

for example, instead of plastic ducks, have basic car looking things, and have them go straight line on a road , or even more complex road shape (as long as we can feed the system with telling him what the trajectory)
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Not with the standard sPart shader, no. You specify the starting direction, starting speed, gravity and the like, and the system generates particles randomly within given parameters.

It is, however, possible to modify the shader in many ways! I plan to make a dynamic clutter engine, that places grass and flowers and stuff randomly near the camera. Your idea seems like an interesting one that I'd like to explore.
 
M

Misty

Guest
you should supply a request in that case :D

hi @TheSnidr just to be sure; we can constrain the trajectories at will?

for example, instead of plastic ducks, have basic car looking things, and have them go straight line on a road , or even more complex road shape (as long as we can feed the system with telling him what the trajectory)
Sniders EXACT words were:

"If there is a demand for it, I'll make a 1.4 version as well. Gimme a holler."

He said two keywords "demand". "Holler". Not soft, not sensitive.

So I say what my demands are, and then society comes at me, society is getting so sensitive and soft, I have to go through my neurons and always pick the softess, most gentle, sensitive words at all times or else I KNOW some guy will come here giving my flak about it.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
I've been swamped with IRL stuff lately, but I will make a 1.4 version as soon as possible.
Just popping in to say I've put the asset on sale for $2.99 for a short while to hopefully get more reviews on the Marketplace page! So if you buy, a review would be greatly appreciated! Even a super short one ^.^
 

Mert

Member
I've been swamped with IRL stuff lately, but I will make a 1.4 version as soon as possible.
Just popping in to say I've put the asset on sale for $2.99 for a short while to hopefully get more reviews on the Marketplace page! So if you buy, a review would be greatly appreciated! Even a super short one ^.^
Just purchased it. Thanks for the discount :)
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Oh absolutely, neither affects the other, so that should be no problem! I plan to incorporate a particle editor into the SMF model tool as well, specifically for the sPart system. You'll still need the sPart system as well though ;)
 

TheSnidr

Heavy metal viking dentist
GMC Elder
I've added a new Marketplace link, for a free version of the sPart system! My goal is for this to generate more feedback so that the system as a whole can improve.
To make sure the paid version still is worth it, the following functionality is only in the paid version:
- Secondary particles, ie. particles spawning more particles. Particles can spawn more particles either each step or upon death (or both)
- Mesh particles, ie. using 3D models for particles. The free version only supports billboarded particles
- Certain movement options, like jerk and scale acceleration
I was also considering stripping the possibility to move shaders, but I decided against it.

If you tried the free version and liked it, please leave a review - or even better, buy the full version :D

Download link is in the first post.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
Both the paid and the free versions have been updated! Big improvements on how the particle system is handled CPU side. Here's a rough changelog:
  • I've combined the step and death shaders into one, and I've removed the secondary mesh particle shaders. The latter is not a big sacrifice, since secondary mesh particles would have little to no practical use anyway.
  • The system no longer requires that all shaders are present. If you only need the regular particle shader, you can delete the rest without having to edit any code.
  • As such, the only difference between the free and the paid system is that the free system does not include the secondary and mesh particle shaders.
  • spart_system_set_shaders lets you define the shaders that should be used by the particle system. This lets you easily plug in your own custom versions of the sPart shaders, where you can play with the movement function to make particles move in any way you like.
  • The system only gets uniform handles the first time it needs them, and then keeps them around in a ds_grid. This has a surprisingly large impact on framerate.
  • Emitters are no longer batched together, instead, each emitter requires its own draw call (or multiple draw calls if there are too many particles for one batch). This may seem like a negative thing, but I consider it exclusively positive. This allows me to get rid of the only for-loop in the shader, and it also allows the next point on the list:
  • I've finally found a great way of handling moving emitters: When dynamically moving an emitter, the system only actually adds a new emitter to the log every couple of frames. You can define this interval yourself with the function called spart_system_set_dynamic_interval. Emitter positions are interpolated between the logged positions, resulting in what appears to be smooth motion.
  • Particle types can be saved and loaded. You can do this either by using spart_type_save/spart_type_load, or you can write them to a custom buffer with spart__type_write_to_buffer/spart__type_read_from_buffer. The latter allows you to utilize async loading.
The first post has been updated.
 
Last edited:

TheSnidr

Heavy metal viking dentist
GMC Elder
Thank you! The SMF update is creeping its way to completion as well, but not just yet ;)

I've sold in total 25 copies of the paid sPart system, and only a handful of those were while it was at full price. I really really appreciate the support from the people who have bought the asset, you rock!
But my primary goal with all I create is for the code to be used. I want to help the community make awesome games, and I don't really need this as a source of income.

So I've made the full asset completely free, permanently. Have fun, and make awesome 3D games with it! With the latest update, I'm confident this is the best possible way to simulate 3D particles in GMS2, and I hope you'll think the same.
 

TheSnidr

Heavy metal viking dentist
GMC Elder
New update is live! Download it for free from the Marketplace, download link is in first post!
I messed around with the mesh emitters a bit more, and realized making the man model black made it waaay cooler:

Also, a fire dragon:
 
Top