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:
-
Alpha_Joints
(Geometry)
Material[name=Alpha_Joints_MAT, def=PBR Lighting, tech=null]
Triangles: 20840
Vertices: 12473 -
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:
- the jme3-core module containing the official particle effects system.
- ParticleController by @zzuegg
- 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.