jMonkeyEngine VFX - Game Jam 01

Hi everyone,
I’d like to share some ideas with you on visual effects.

Here is the last video I posted:

@grizeldi → The first example of the video is a prototype I wrote to understand how to read the variable coordinates of the vertices in animated models. The result is cool, but probably inefficient. I got inspiration from this video.

The ‘YBot’ model I downloaded from the Mixamo site includes two geometries:

  1. Alpha_Joints (Geometry)
    Material[name=Alpha_Joints_MAT, def=PBR Lighting, tech=null]
    Triangles: 20840
    Vertices: 12473

  2. Alpha_Surface (Geometry)
    Material[name=Alpha_Body_MAT, def=PBR Lighting, tech=null]
    Triangles: 34480
    Vertices: 22978

The SkinnedVFXControl only acts on the Alpha_Surface mesh. The number of real vertices, discarding the repeated ones, is 17336. Then the control creates 17336 Quads and updates their position after a certain time interval. Despite all the optimizations, the number of quads is too high and the framerate becomes too low. As I said before, the idea is correct but the algorithm is inefficient.
I used a BloomFilter to make the particles brighter.

Here is the source code:

package com.test.main.particles;

import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.jme3.asset.AssetManager;
import com.jme3.asset.DesktopAssetManager;
import com.jme3.material.Material;
import com.jme3.material.RenderState.BlendMode;
import com.jme3.material.RenderState.FaceCullMode;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.control.AbstractControl;
import com.jme3.util.BufferUtils;
import com.test.engine.mesh.CenterQuad;

/**
 *
 * @author capdevon
 */
public class SkinnedVFXControl extends AbstractControl {

    private final AssetManager assetManager;
    private final List<Integer> indexList = new ArrayList<>();
    private final Vector3f origin = new Vector3f();
    
    private Mesh source;
    private Material debugMat;
    private float size;
    private float timer;
    
    public float refreshRate = 0.02f;
    public Spatial target;

    /**
     * Constructor.
     *
     * @param source
     * @param size
     */
    public SkinnedVFXControl(Mesh source, float size) {
        this.assetManager = new DesktopAssetManager(true);
        this.source = source;
        this.size = size;
        setupMaterial();
    }

    /**
     * Initialize debug material
     */
    private void setupMaterial() {
        debugMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        debugMat.setName("MAT_SkinnedMesh");
        debugMat.setColor("Color", ColorRGBA.White);
        debugMat.setColor("GlowColor", ColorRGBA.Cyan);
        debugMat.getAdditionalRenderState().setWireframe(true);
        debugMat.getAdditionalRenderState().setBlendMode(BlendMode.Additive);
        debugMat.getAdditionalRenderState().setFaceCullMode(FaceCullMode.Off);
    }

    @Override
    public void setSpatial(Spatial sp) {
        super.setSpatial(sp);
        if (spatial != null) {
            calculateVertices();
        }
    }

    private void calculateVertices() {
        CenterQuad quad = new CenterQuad(size, size);
        Map<Vector3f, Integer> mapPos = new HashMap<>(source.getVertexCount());

        // Get the geometry's vertex buffer
        FloatBuffer vertexBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
        for (int i = 0; i < vertexBuffer.limit() / 3; i++) {

            Vector3f position = new Vector3f();
            BufferUtils.populateFromBuffer(position, vertexBuffer, i);

            if (!mapPos.containsKey(position)) {
                mapPos.put(position, i);
                createVertexMesh(i, position, quad);
            }
        }

        indexList.addAll(mapPos.values());
        System.out.println("IndexList: " + indexList.size());
    }

    private void createVertexMesh(int vertIndex, Vector3f vertPos, Mesh vertMesh) {
        Geometry geo = new Geometry("QuadMesh_" + vertIndex, vertMesh);
        geo.setMaterial(debugMat);
        geo.setShadowMode(ShadowMode.Off);
        geo.setQueueBucket(Bucket.Transparent);
        geo.setLocalTranslation(vertPos);
        ((Node) spatial).attachChild(geo);
    }

    @Override
    protected void controlUpdate(float tpf) {
        timer += tpf;
        if (timer > refreshRate) {
            timer = 0;

            int j = 0;
            FloatBuffer vertexBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
            for (Integer index : indexList) {
                BufferUtils.populateFromBuffer(origin, vertexBuffer, index);
                ((Node) spatial).getChild(j).setLocalTranslation(origin);
                j++;
            }
        }

        if (target != null) {
            spatial.setLocalTransform(target.getWorldTransform());
        }
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        // TODO Auto-generated method stub
    }

}

I discarded this solution and started studying the source code of the particle effects.

I chose 3 libraries to run tests:

  1. the jme3-core module containing the official particle effects system.
  2. ParticleController by @zzuegg
  3. ParticleMonkey by @glh3586

With the jme3-core library I was unable to achieve the desired effect.
The EmitterMeshVertexShape only works with static models because it does not update the position of the vertices.

I have written a custom emitter for animated models which updates the position of the vertices with each loop. Even this solution seems not to work, the particles are emitted from the root node of the particles and not from the vertices. I don’t know if it’s a defect in the original code.

package com.test.main.particles;

import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.jme3.effect.shapes.EmitterShape;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import com.jme3.util.clone.Cloner;

/**
 *
 * @author capdevon
 */
public class EmitterMeshVertexVFX implements EmitterShape {

    private Mesh source;
    private List<Integer> indexList = new ArrayList<>();

    public EmitterMeshVertexVFX(Mesh source) {
        this.source = source;
        calculateVertices();
    }

    private void calculateVertices() {
        Map<Vector3f, Integer> mapPos = new HashMap<>(source.getVertexCount());

        // Get the geometry's vertex buffer
        FloatBuffer vertexBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
        for (int i = 0; i < vertexBuffer.limit() / 3; i++) {

            Vector3f position = new Vector3f();
            BufferUtils.populateFromBuffer(position, vertexBuffer, i);

            if (!mapPos.containsKey(position)) {
                mapPos.put(position, i);
            }
        }

        indexList.addAll(mapPos.values());
        System.out.println("IndexList: " + indexList.size());
    }

    @Override
    public void getRandomPoint(Vector3f store) {
        int vertIndex = FastMath.nextRandomInt(0, indexList.size() - 1);
        FloatBuffer vertexBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
        BufferUtils.populateFromBuffer(store, vertexBuffer, vertIndex);
    }

    @Override
    public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
        int vertIndex = FastMath.nextRandomInt(0, indexList.size() - 1);

        FloatBuffer vertBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
        BufferUtils.populateFromBuffer(store, vertBuffer, vertIndex);

        FloatBuffer normBuffer = source.getFloatBuffer(VertexBuffer.Type.Normal);
        BufferUtils.populateFromBuffer(normal, normBuffer, vertIndex);
    }

    @Override
    public EmitterShape deepClone() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        // TODO Auto-generated method stub
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        // TODO Auto-generated method stub
    }

    @Override
    public Object jmeClone() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void cloneFields(Cloner cloner, Object original) {
        // TODO Auto-generated method stub
    }

}

Usage:

    private ParticleEmitter testParticleMode(Geometry geo) {
        ParticleEmitter emit = new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 300);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
        mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
        emit.setGravity(0, 0, 0);
        emit.getParticleInfluencer().setVelocityVariation(1);
        emit.getParticleInfluencer().setInitialVelocity(new Vector3f(0, .5f, 0));
        emit.setLowLife(1);
        emit.setHighLife(1);
        emit.setImagesX(15);
        emit.setMaterial(mat);
        emit.setShape(new EmitterMeshVertexVFX(geo.getMesh()));
        return emit;
    }

The other two libraries already have an EmitterMesh so I didn’t have to write additional code and the particles are correctly generated on the vertices of the animated model. Unfortunately, the absence of an editor makes their use complicated.

I used this sample code for the ParticleController library: using-a-mesh-as-the-particle-source

Thanks to @glh3586 and @zzuegg for sharing their work on github. Their ideas could be useful for improving the particle effects system in version 3.6.0 of the engine.


Let me know what you think or tell me about your experience with particle effects. Any examples, videos, images or suggestions are welcome!

If you like the topic I can upload everything to github and we can do a game jam so everyone can practice and show their ideas.

7 Likes

Nice work.

I have to mention that i only forked the particle controller and don’t remember who the original author was

1 Like

It was Tim. I think he used to go by zarch around here all of those years ago (and is missed). Just for the record.

3 Likes

Hi everyone,
as promised I created a repository on github dedicated to this game jam. In the project you will find a basic model that I downloaded from the Mixamo site, to which I added some animations.
The DetailedProfilerState will help you monitor game performance. Press F6 to turn it off.
Edit the filters or add new particle effects and share the results with the community!

I hope this initiative is useful for learning new things.
Let me know if the SkinnedVFXControl can be optimized or made more interesting.

-

6 Likes

Hi everyone,
I would like to ask you for an opinion. The EmitterMeshVertexShape provided in the jme3-core module, only works with static models because it does not update the position of the vertices.

I have written a custom emitter EmitterMeshVertexVFX for animated models which updates the position of the vertices with each cycle. This solution seems not to work, the particles are emitted from the root node of the particle emitter and not from the vertices of the 3d model.

Am I doing something wrong or is it a bug? What do you think?

Here you can download and try the test case:

Regards

any suggestions please?

Where’s the code for EmitterMeshVertexVFX?

I wonder

  • whether EmitterMeshVertexVFX is using the correct position buffer and also
  • whether the model’s SkinningControl has hardware skinning enabled.

In order for software running on the CPU to access the actual vertex positions of an animated mesh, hardware skinning should be disabled and the Position buffer should be read (not BindPosePosition).

1 Like

Hi Stephen,
thank you for your time. You can find the project here:

Run the Test_JmeVFX class

Here you can find class EmitterMeshVertexVFX

Hardware Skinning is disabled.

I ran the code, but I couldn’t tell where the particles were coming from, so I set the velocities to zero:

        emit.getParticleInfluencer().setVelocityVariation(0);
        emit.getParticleInfluencer().setInitialVelocity(new Vector3f());

That showed that the initial positions all lay on a triangle connecting the hands to the legs.

Looking at EmitterMeshVertexVFX I see that the positions are being copied from the buffer during instantiation. I suspect that takes place before the first software update has happened. Perhaps the position buffer is garbage at that point?

In the EmitterMeshVertexVFX class

  • the calculateVertices() method stores the indices of the vertices (discarding duplicates) not the position.

  • The getRandomPoint(Vector3f) method, provided by the com.jme3.effect.shapes.EmitterShape interface, is invoked when a new particle is generated. It provides the dynamic position of a vertex chosen at random among the previously selected indices.
    With these instructions I expect to read the updated position of the vertices from the buffer.

    @Override
    public void getRandomPoint(Vector3f store) {
        int vertIndex = FastMath.nextRandomInt(0, indexList.size() - 1);
        FloatBuffer vertexBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
        BufferUtils.populateFromBuffer(store, vertexBuffer, vertIndex);
    }

Okay, thanks for that clarification.

nextRandomInt() is picking a random int between 0 and 17335 inclusive. However, the mesh contains 22967 vertices, so you’re only getting a subset of the available vertices.

Yes, because I noticed that some vertices are repeated. Probably because jme triangulates all polygons.
If I understand correctly, a similar logic is applied here:

1 Like

smoothNormals() loops over all the mesh indices using getIndicesAsList(), so it works for indexed meshes. EmitterMeshVertexVFX doesn’t do that.

I didn’t look at the detection code… but are you detecting that they are repeated by position? When the model is first loaded or after it has been posed?

After loading the model.

Edit:
In the meantime I have corrected a small error in the random selection of the indices in the EmitterMeshVertexVFX.getRandomPoint(Vector3f) method but the result does not change.

//before
    @Override
    public void getRandomPoint(Vector3f store) {
        int vertIndex = FastMath.nextRandomInt(0, indexList.size() - 1);
        FloatBuffer vertexBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
        BufferUtils.populateFromBuffer(store, vertexBuffer, vertIndex);
    }

//after
    @Override
    public void getRandomPoint(Vector3f store) {
        // selects the random index of a vertex from the list
        int i = FastMath.nextRandomInt(0, indexList.size() - 1);
        int vertIndex = indexList.get(i);
        FloatBuffer vertexBuffer = source.getFloatBuffer(VertexBuffer.Type.Position);
        BufferUtils.populateFromBuffer(store, vertexBuffer, vertIndex);
    }

Didn’t see an answer to this and I don’t have time at the moment to read “all your code” and understand it.

If you are doing this right after the model is loaded and not posed then it may collapse vertexes unnecessarily. I’ve even had models load with all vertexes at 0,0,0 until posed.

Anyway, I was just mentioning it. I generally have no problem querying the vertex position of an animated model once it has started animating as long as hardware skinning is off. (which looked like you already did)

yes I printed the positions of the vertices. Here is a snippet of the list. As you can see some positions are duplicated:


(0.16294156, 0.026637983, 0.13124737)
(0.15440038, 0.033306383, 0.13003819)
(0.15994239, 0.03024828, 0.1285499)
(0.16294472, 0.026630599, 0.12744713)
(0.15994239, 0.03024828, 0.1285499)
(0.15440038, 0.033306383, 0.13003819)
(0.11014057, 0.038131382, 0.16922876)
(0.12431566, 0.035562012, 0.16410422)
(0.13735424, 0.032632127, 0.15943918)
(0.10743454, 0.043108687, 0.15579253)
(0.122116946, 0.03961317, 0.15225668)
(0.14737079, 0.03301489, 0.14504518)
(0.14694479, 0.030439634, 0.15510984)
(0.13576558, 0.03595834, 0.14906298)
(0.09341645, 0.046479624, 0.15932828)
(0.096616544, 0.040442865, 0.17449823)
(0.089344405, 0.041885804, 0.17729364)
(0.08658497, 0.048266504, 0.1606812)
(0.0800434, 0.049713094, 0.16193521)
(0.08287843, 0.042704467, 0.17961158)
(0.08287843, 0.042704467, 0.17961158)

I see no obvious errors in the rest of the code.

@sgold you can verify that the vertex index selection algorithm works by uncommenting this line.

The SkinnedVFXControl class creates a CenterQuad for each vertex of the assigned Mesh (excluding duplicates) and correctly updates their position by reading it from the buffer during animations. This is why I suspect that there is something strange in the particle effects. What do you think?

Edit:
I’m looking at the ParticleEmitter class. This statement seems strange to me, but I could be wrong.

Why do you think that’s suspicious when lastPos is used to track the last position of the emitter in world space?

Ignore my guess for now, I’m not sure yet. Where do you think the problem could be?