Behind the Scenes: Flamethrower | jMonkeyEngine VFX

Let’s continue the discussion here :wink:

I will analyze your feedback one at a time, otherwise it would take too long to write a single answer to all the questions. :sweat_smile:

1 Like

@Ali_RS
No. Could you please explain to me how Soft Particles work and what benefits they bring compared to normal ones? I ran the test case you suggested, but I don’t quite understand the difference. I could not find information on the documentation Particle Emitter Settings :: jMonkeyEngine Docs

@kevinba99

Great glance! I hadn’t noticed it. Maybe it could be solved in 3 ways:

  • reduce the maximum collision distance of the flamethrower (5 wu instead of 10).
  • increase the initial speed of the flamethrower particles.
  • progressively increase the maximum collision distance of the flamethrower from a minimum starting value to the maximum final value (for example from 1 to 5).

I had in mind to add transient animation while the enemy is engulfed in flames, instead of a calm, unperturbed pose like in the video. :smile:

This is an interesting aspect, but it can be a problem. In fact, the flames would go beyond the walls in case of obstacles. How can I reduce the distance of the particle effect when I hit an obstacle? Do you have any ideas in mind?

This is my setup.

public static ParticleEmitter createFlamethrower(AssetManager assetManager, boolean pointSprite) {
    Type type = pointSprite ? Type.Point : Type.Triangle;
    ParticleEmitter flame = new ParticleEmitter("Flamethrower.Emitter", type, 900);
    Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
    mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
    mat.setBoolean("PointSprite", pointSprite);
    flame.setMaterial(mat);
    flame.setLowLife(0.2f);
    flame.setHighLife(1f);
    flame.setImagesX(15);
    flame.setStartSize(0.5f);
    flame.setEndSize(10f);
    flame.setStartColor(ColorRGBA.Orange);
    flame.setEndColor(ColorRGBA.Red);
    flame.setGravity(0, 0, 0);
    flame.setFaceNormal(Vector3f.ZERO);
    flame.getParticleInfluencer().setVelocityVariation(0.1f);
    flame.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 0, 5));
    flame.setParticlesPerSec(0); //Particle Effect Disable
    return flame;
}
1 Like

@oxplay2

  • I like the idea of ​​being able to roast multiple zombies at the same time. Consider it done. You will see it in the next video. :wink:

  • Consider it done. You will tell me what you think in the next video.

  • I tried to reproduce the effect you see in the video by superimposing two ParticleEmitter: the larger smoke and the smaller flames but the dark gray and black tones appear white (I don’t know why). I will show you an image to make you understand better.

1 Like

When particles cut through other geometries (ground, obstacles,…) you might see a hard edge on particles. From what I understand Soft particles smooth those hard visible edges by applying transparency to particles as they get closer to a geometry.

This video will help you see those visual effects:

After you run the test, use Space key to enable/disable the effect and you will clearly notice the difference.

2 Likes

I think a ray tracing or a collision detection (if you apply physics) could do it, a particle emitter could have the same properties like other spatials, you could then apply gravity in different directionality when collisioned.

ehmm… my first idea was to try enabling soft particles and see if it will help in this case!? :thinking: as particles will be dissolved when they get close to persons.

It doesn’t seem to work … I could write a test case to share so we can all try our ideas, but it will take me some time.

I use physics for the raycast. Suppose I have the following code:

ParticleEmitter flame = ...;
flame.setLowLife(0.2f);
flame.setHighLife(1f);
flame.setImagesX(15);
flame.setStartSize(0.5f);
flame.setEndSize(10f);
flame.setStartColor(ColorRGBA.Orange);
flame.setEndColor(ColorRGBA.Red);
flame.setGravity(0, 0, 0);
flame.setFaceNormal(Vector3f.ZERO);
flame.getParticleInfluencer().setVelocityVariation(0.1f);
flame.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 0, 5));

Vector3f beginVec = ...;
Vector3f finalVec = ...;

List<PhysicsRayTestResult> results = PhysicsSpace.getPhysicsSpace().rayTest(beginVec, finalVec);

if (!results.isEmpty()) {
	// I take the first item on the list which is closest
	PhysicsRayTestResult ray = results.get(0);
	Vector3f hitPoint = new Vector3f();
	hitPoint.interpolateLocal(beginVec, finalVec, ray.getHitFraction());
	
	// What emitter value should I change to get the desired effect?
	...
}
1 Like

Hmm, from a quick look at “TranslucentBucketFilter” it seems when it gets enabled it looks for the particle emitters that are attached to the scene and makes them soft particle. So if a particle emitter is added to the scene after the filter is enabled it wont be affected!

You can copy this code from TranslucentBucketFilter and apply it to the emitter material when you create the emitter in your code:

Edit:
Also make sure you have added TranslucentBucketFilter(true) to the FilterPostProcessor:

TranslucentBucketFilter tbf = new TranslucentBucketFilter(true);
fpp.addFilter(tbf);

otherwise the DepthTexture returned by processor.getDepthTexture() might be null.

1 Like

I’ve used collision testing on the particle and make its life 0 when a detection is hit. I use this so it doesn’t rain inside areas in the my city that house roofs.

2 Likes

I wrote a test case, but I can’t tell if it’s working or not, or if I’m doing something wrong … :thinking:

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh.Type;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.TranslucentBucketFilter;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.Grid;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Line;
import com.jme3.system.AppSettings;
import com.jme3.util.SkyFactory;

public class TestSoftParticles extends SimpleApplication implements ActionListener {

	/**
	 * Start the jMonkeyEngine application
	 * @param args
	 */
	public static void main(String[] args) {
		TestSoftParticles app = new TestSoftParticles();
		AppSettings settings = new AppSettings(true);
        settings.setResolution(1280, 720);
        settings.setSamples(4);
        
        app.setSettings(settings);
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.start();
	}
	
	private boolean softParticles = true;
	private boolean pointSprite = true;
	private FilterPostProcessor fpp;
	private TranslucentBucketFilter tbf;
	private Node particleNode;
	
	@Override
	public void simpleInitApp() {

		cam.setLocation(new Vector3f(-7.2221026f, 4.1183004f, 7.759811f));
		cam.setRotation(new Quaternion(0.06152846f, 0.91236454f, -0.1492115f, 0.37621948f));
		flyCam.setMoveSpeed(10);
		
		createSky();
		createGrid();
		configInputs();
		createScene();
	}
	
	private void configInputs() {
    	inputManager.addMapping("toggle", new KeyTrigger(KeyInput.KEY_SPACE));
		inputManager.addMapping("refire", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
		inputManager.addListener(this, "toggle", "refire");
	}

	@Override
	public void onAction(String name, boolean isPressed, float tpf) {
		if (name.equals("refire") && isPressed) {
			// emit again
			// fpp.removeFilter(tbf); // <-- add back in to fix
			particleNode.detachAllChildren();
			createParticles();
			// fpp.addFilter(tbf);

		} else if (name.equals("toggle") && isPressed) {
			//tbf.setEnabled(!tbf.isEnabled());
			softParticles = !softParticles;
			if (softParticles) {
				viewPort.addProcessor(fpp);
				System.out.println("Added TranslucentBucketFilter");
			} else {
				viewPort.removeProcessor(fpp);
				System.out.println("Removed TranslucentBucketFilter");
			}
		}
	}
	
    private void createScene() {
		// TODO Auto-generated method stub
    	Geometry geom = createCube(.5f, ColorRGBA.Green, "Interface/Logo/Monkey.jpg");
		rootNode.attachChild(geom);

		Geometry geom2 = createCube(.5f, ColorRGBA.Blue);
		geom.setLocalTranslation(2, 0, 0);
		rootNode.attachChild(geom2);

		fpp = new FilterPostProcessor(assetManager);
		tbf = new TranslucentBucketFilter(true);
		fpp.addFilter(tbf);
		
		int samples = context.getSettings().getSamples();
		if (samples > 0) {
			fpp.setNumSamples(samples);
		}
		viewPort.addProcessor(fpp);

		particleNode = new Node("particleNode");
		rootNode.attachChild(particleNode);
		createParticles();
	}

	private void createSky() {
        Spatial sky = SkyFactory.createSky(assetManager, "Scenes/Beach/FullskiesSunset0068.dds", SkyFactory.EnvMapType.CubeMap);
        sky.setShadowMode(RenderQueue.ShadowMode.Off);
        rootNode.attachChild(sky);
    }
    
    private void createGrid() {
        Geometry grid = new Geometry("DebugGrid", new Grid(21, 21, 1));
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Gray);
        grid.setMaterial(mat);
        grid.center().move(0, 0, 0);
        grid.setShadowMode(ShadowMode.Off);
        rootNode.attachChild(grid);
        
        createArrow("X", new Vector3f(10, 0, 0), ColorRGBA.Red);
        createArrow("Z", new Vector3f(0, 0, 10), ColorRGBA.Blue);
    }
    
	private void createArrow(String name, Vector3f axis, ColorRGBA color) {
		Geometry geom = new Geometry("DebugAxis_" + name, new Line(axis.negate(), axis));
		Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
		mat.setColor("Color", color);
		mat.getAdditionalRenderState().setLineWidth(2f);
		mat.getAdditionalRenderState().setWireframe(true);
		geom.setMaterial(mat);
		geom.setShadowMode(ShadowMode.Off);

		rootNode.attachChild(geom);
	}
	
	private Geometry createCube(float size, ColorRGBA color) {
		Box box = new Box(size, size, size);
		Geometry geom = new Geometry("Box", box);
		Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
		mat.setColor("Color", color);
		geom.setMaterial(mat);
		return geom;
	}
	
	private Geometry createCube(float size, ColorRGBA color, String texture) {
		Box box = new Box(size, size, size);
		Geometry geom = new Geometry("Box", box);
		Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
		mat.setTexture("ColorMap", assetManager.loadTexture(texture));
		mat.setColor("Color", color);
		geom.setMaterial(mat);
		return geom;
	}

	private void createParticles() {

		Type type = pointSprite ? Type.Point : Type.Triangle;
		
		Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
		mat.setTexture("Texture", assetManager.loadTexture("Effects/Explosion/flame.png"));
		mat.setBoolean("PointSprite", pointSprite);
		mat.setFloat("Softness", 3f); //

		ParticleEmitter fire = new ParticleEmitter("Fire", type, 30);
		fire.setMaterial(mat);
//		fire.setShape(new EmitterSphereShape(Vector3f.ZERO, 0.1f));
		fire.setImagesX(2);
		fire.setImagesY(2); // 2x2 texture animation
		fire.setLowLife(0.2f);
		fire.setHighLife(1f);
		fire.setStartSize(0.5f);
		fire.setEndSize(2f);
		fire.setStartColor(ColorRGBA.Orange);
		fire.setEndColor(ColorRGBA.Red);
		fire.setGravity(0, 0, 0);
		fire.setFaceNormal(Vector3f.ZERO);
		fire.setFacingVelocity(true);
		fire.setSelectRandomImage(true);
		fire.getParticleInfluencer().setVelocityVariation(0.1f);
		fire.getParticleInfluencer().setInitialVelocity(new Vector3f(5, 0, 0));
		fire.setLocalTranslation(-2, 0, 0);
		particleNode.attachChild(fire);
	}
	
}

Could you show us a working example? Maybe it can be useful for improving the current system of particle effects.

Change the gravity or force direction and magnitude (if you are playing with forces initially) based on the value of the interpolation, this can alter the total magnitude of the flame thrower or redirect it to the original target, i can see this effect along the lifespan one mentioned by Kevin are the most used in modern games.

Change the above code to:

settings.setSamples(1);

it will work.

I do not know why it does not work when it is greater than 1.

I made the change you suggested.
The flames spread from the black diamond towards the positive x axis. Sorry, I didn’t quite understand what should happen with soft particles. Should the blue cube block them or not? They reach up to the green cube.

Yep, sorry about it. Turns out my idea did not work as I expected. :slightly_frowning_face:

Hope other solutions help.

1 Like

Don’t worry @Ali_RS, I appreciate the idea you got :wink:

1 Like

@Pavl_G thank you for your contribution but I have already explored the path of gravity and it does not work.

1 Like

well i also thought about visual changer like "Heat haze/distortion effect"

i would really like have something like this ready in JME(maybe there is but i didnt seen). Since i would like use it later myself. In flamethrower it should be affecting much near weapon ending, and for flame a little

I would love so, but it is so refined to my game that it would not work for general purposes.

I have rain particles, on heavy rain, it drops about 20k particles, so instead of check every frame up for collision, I know my map and it limits. It is grid based.

So I have many checks that prevent the collision check because I know it will not be close to hitting an object, so I skip it.

So basically, things like outside the grid (particle emitter is a box and the random position can be outside the grid, to give a look like there is distance). No need to check for collision.
I check out, because I know my highest building, so I skip checking until it gets below a certain position. I also, because of the grid, I know if that grid space has a ceiling, and if not skip the check.

So my use case is way to specific to my game.

Edited:
I use JME particle System, I tried ParticleMonkey and overall I saw that is was more expansive but comes with a performance hit. But JME Particle System is “EXTREMELY” limiting… To the point you can’t even “Extend” the class. The way it is coded, it prevents extending the class. It forced me to copy the class to extend it for a few things I needed.

I mainly added

    /**
     * Set the {@link ParticleInfluencer} to influence this particle emitter.
     *
     * @param particleInfluencer the {@link ParticleInfluencer} to influence
     * this particle emitter.
     *
     * @see ParticleInfluencer
     */
    public void addParticleInfluencer(ParticleInfluencer particleInfluencer) {
    	this.particleInfluencer.put(particleInfluencer.getClass().getName(), particleInfluencer);
    }

//my code
		RainParticleInfluencer influencer = new RainParticleInfluencer(weather);
		influencer.setHorizontal(false);
		influencer.setOrigin(points.getLocalTranslation());
		influencer.setRadialVelocity(0f);
		influencer.setInitialVelocity(new Vector3f(0.0f, -05f, 0.0f));
		influencer.setVelocityVariation(-0.3f);
		
		points.addParticleInfluencer(influencer);
		points.addParticleInfluencer(new BasicPhysicsInfluencer(GuiGlobals.rootNode.getChild("map")));



I wanted multiple particle influencers like Particle Monkey, 1st is just alter the direction of the rain based on weather conditions speed and direction. If the wind is North direction, the rain falls that way. The rain falls at a speed related to weather conditions. Very stormy rain it falls fast.
Then the basicPhysics is what I used for game generic particle collision system.

1 Like