jMonkeyEngine / jmonkeyengine

A complete 3-D game development suite written in Java.
http://jmonkeyengine.org
BSD 3-Clause "New" or "Revised" License
3.75k stars 1.12k forks source link

Bug: Wrong particle position when using 'EmitterMeshVertexShape' or 'EmitterMeshFaceShape' and worldSpace flag equal to true. #1773

Closed capdevon closed 1 year ago

capdevon commented 2 years ago

Using the shape emitters (EmitterMeshVertexShape or EmitterMeshFaceShape with worldSpace = true), the particles are generated neither from the vertices nor from the faces of the 3d model, but in a position that seems to correspond to the position of the ParticleEmitter.

The bug could have been introduced with the following commit 6075e463.

bug

Here is the test case:

package mygame;

import java.util.Arrays;

import com.jme3.animation.LoopMode;
import com.jme3.app.SimpleApplication;
import com.jme3.cinematic.MotionPath;
import com.jme3.cinematic.events.MotionEvent;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh.Type;
import com.jme3.effect.shapes.EmitterMeshFaceShape;
import com.jme3.effect.shapes.EmitterMeshVertexShape;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.Materials;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.BloomFilter;
import com.jme3.post.filters.FXAAFilter;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.shape.CenterQuad;
import com.jme3.scene.shape.Torus;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.system.AppSettings;
import com.jme3.texture.Texture;

/**
 *
 * @author capdevon
 */
public class Test_ParticleIssue extends SimpleApplication implements ActionListener {

    /**
     *
     * @param args
     */
    public static void main(String[] args) {
        Test_ParticleIssue app = new Test_ParticleIssue();
        AppSettings settings = new AppSettings(true);
        settings.setResolution(1280, 720);
        settings.setRenderer(AppSettings.LWJGL_OPENGL32);
        app.setSettings(settings);
        app.setPauseOnLostFocus(false);
        app.setShowSettings(false);
        app.start();
    }

    private ParticleEmitter emit;
    private Node myModel;
    private BitmapText emitUI;
    private MotionEvent motionControl;
    private boolean playing;

    @Override
    public void simpleInitApp() {

        BitmapText hud = createTextUI(ColorRGBA.White, 20, 15);
        hud.setText("Play/Pause Motion: KEY_SPACE, InWorldSpace: KEY_I");

        emitUI = createTextUI(ColorRGBA.Blue, 20, 15 * 2);

        configCamera();
        setupLights();
        setupScene();
        setupCircle();
        createMotionControl();
        setupKeys();
    }

    private void setupCircle() {
        myModel = new Node("FieryCircle");

        Geometry torus = createTorus(1f);
        myModel.attachChild(torus);

        emit = createParticleEmitter(torus, true);
        myModel.attachChild(emit);

        rootNode.attachChild(myModel);
    }

    private Geometry createTorus(float radius) {
        float s = radius / 8f;
        Geometry geo = new Geometry("CircleXZ", new Torus(64, 4, s, radius));
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        mat.getAdditionalRenderState().setWireframe(true);
        geo.setMaterial(mat);
        return geo;
    }

    /**
     * emits the particles from the given shape's vertices. It doesn't work.
     */
    private ParticleEmitter createParticleEmitter(Geometry geo, boolean pointSprite) {
        Type type = pointSprite ? Type.Point : Type.Triangle;
        ParticleEmitter emitter = new ParticleEmitter("Emitter", type, 1000);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Particle.j3md");
        mat.setTexture("Texture", assetManager.loadTexture("Effects/Smoke/Smoke.png"));
        mat.setBoolean("PointSprite", pointSprite);
        emitter.setMaterial(mat);
        emitter.setLowLife(1);
        emitter.setHighLife(1);
        emitter.setImagesX(15);
        emitter.setStartSize(0.04f);
        emitter.setEndSize(0.02f);
        emitter.setStartColor(ColorRGBA.Orange);
        emitter.setEndColor(ColorRGBA.Red);
        emitter.setParticlesPerSec(900);
        emitter.setGravity(0, 0f, 0);
        //emitter.getParticleInfluencer().setVelocityVariation(1);
        //emitter.getParticleInfluencer().setInitialVelocity(new Vector3f(0, .5f, 0));
        emitter.setShape(new EmitterMeshVertexShape(Arrays.asList(geo.getMesh())));
        //emitter.setShape(new EmitterMeshFaceShape(Arrays.asList(geo.getMesh())));
        return emitter;
    }

    private void createMotionControl() {

        float radius = 5f;
        float height = 1.10f;

        MotionPath path = new MotionPath();
        path.setCycle(true);

        for (int i = 0; i < 8; i++) {
            float x = FastMath.sin(FastMath.QUARTER_PI * i) * radius;
            float z = FastMath.cos(FastMath.QUARTER_PI * i) * radius;
            path.addWayPoint(new Vector3f(x, height, z));
        }
        //path.enableDebugShape(assetManager, rootNode);

        motionControl = new MotionEvent(myModel, path);
        motionControl.setLoopMode(LoopMode.Loop);
        //motionControl.setInitialDuration(15f);
        //motionControl.setSpeed(2f);
        motionControl.setDirectionType(MotionEvent.Direction.Path);
    }

    private void setupKeys() {
        addMapping("ToggleMotionEvent", new KeyTrigger(KeyInput.KEY_SPACE));
        addMapping("InWorldSpace", new KeyTrigger(KeyInput.KEY_I));
    }

    private void addMapping(String mappingName, Trigger... triggers) {
        inputManager.addMapping(mappingName, triggers);
        inputManager.addListener(this, mappingName);
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (name.equals("InWorldSpace") && isPressed) {
            boolean worldSpace = emit.isInWorldSpace();
            emit.setInWorldSpace(!worldSpace);

        } else if (name.equals("ToggleMotionEvent") && isPressed) {
            if (playing) {
                playing = false;
                motionControl.pause();
            } else {
                playing = true;
                motionControl.play();
            }
        }
    }

    @Override
    public void simpleUpdate(float tpf) {
        emitUI.setText("InWorldSpace: " + emit.isInWorldSpace());
    }

    private void configCamera() {
        flyCam.setDragToRotate(true);
        flyCam.setMoveSpeed(10);

        cam.setLocation(new Vector3f(0, 6f, 9.2f));
        cam.lookAt(Vector3f.UNIT_Y, Vector3f.UNIT_Y);

        float aspect = (float) cam.getWidth() / cam.getHeight();
        cam.setFrustumPerspective(45, aspect, 0.1f, 1000f);
    }

    private void setupScene() {
        CenterQuad quad = new CenterQuad(12, 12);
        quad.scaleTextureCoordinates(new Vector2f(2, 2));
        Geometry floor = createMesh("Floor", quad);
        floor.rotate(-FastMath.HALF_PI, 0, 0);
        rootNode.attachChild(floor);
    }

    private Geometry createMesh(String name, Mesh mesh) {
        Geometry geo = new Geometry(name, mesh);
        Material mat = new Material(assetManager, Materials.LIGHTING);
        Texture tex = assetManager.loadTexture("Interface/Logo/Monkey.jpg");
        tex.setWrap(Texture.WrapMode.Repeat);
        mat.setTexture("DiffuseMap", tex);
        geo.setMaterial(mat);
        return geo;
    }

    private void setupLights() {
        viewPort.setBackgroundColor(ColorRGBA.DarkGray);
        rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);

        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White);
        //rootNode.addLight(ambient);

        DirectionalLight sun = new DirectionalLight();
        sun.setDirection((new Vector3f(-0.5f, -0.5f, -0.5f)).normalizeLocal());
        sun.setColor(ColorRGBA.White);
        rootNode.addLight(sun);

        DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 4096, 3);
        dlsf.setLight(sun);
        dlsf.setShadowIntensity(0.4f);
        dlsf.setShadowZExtend(256);

        FXAAFilter fxaa = new FXAAFilter();
        BloomFilter bloom = new BloomFilter(BloomFilter.GlowMode.Objects);

        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
        fpp.addFilter(bloom);
        fpp.addFilter(dlsf);
        fpp.addFilter(fxaa);
        viewPort.addProcessor(fpp);
    }

    private BitmapText createTextUI(ColorRGBA color, float xPos, float yPos) {
        BitmapFont font = assetManager.loadFont("Interface/Fonts/Console.fnt");
        BitmapText bmp = new BitmapText(font);
        bmp.setSize(font.getCharSet().getRenderedSize());
        bmp.setLocalTranslation(xPos, settings.getHeight() - yPos, 0);
        bmp.setColor(color);
        guiNode.attachChild(bmp);
        return bmp;
    }

}
stephengold commented 2 years ago

Thanks for documenting this issue.

FennelFetish commented 2 years ago

I too have noticed this long ago, but I forgot and just never used worldSpace=true again.

I think the problem is more general and not only applies to certain emitter shapes: worldSpace=true always introduces interpolation which messes with particle spawn positions. It's only visible when particles don't spawn at the origin.

I've posted a description of the issue and a shorter test case here: https://hub.jmonkeyengine.org/t/particle-gravity-doesnt-rotate-with-parent-node/39731/5

capdevon commented 2 years ago

If you agree, I propose to remove the code introduced with commit 6075e463. Tell me if I can create a new PR to do it.

@FennelFetish thank you for providing additional helpful information on the issue.

stephengold commented 2 years ago

Removing that code would re-introduce the problem that motivated PR #381. Let's not do that, if possible.

capdevon commented 2 years ago

ok I won't, what do you suggest we do?

stephengold commented 2 years ago

Let's fix this bug without getting rid of interpolation.

capdevon commented 2 years ago

I don't know how to do it, do you have any ideas?

stephengold commented 2 years ago

Yes, but I'm busy with other tasks.

Ali-RS commented 1 year ago

I could not figure out how to fix it for EmitterMeshVertexShape while keeping interpolation on. Maybe we should add a new boolean flag "particleInterpolationEnabled" with the appropriate setter/getter in ParticleEmmiter so users can turn it off when using EmitterMeshVertexShape or EmitterMeshFaceShape shapes.

What do you guys think?

stephengold commented 1 year ago

Adding a mode flag would break existing apps that don't assume the default mode. I still think this is fixable without breaking apps.

Ali-RS commented 1 year ago

I still think this is fixable without breaking apps

Found a new solution to fix this issue. Will submit a PR with the details soon.

Ali-RS commented 1 year ago

Submitted a fix in: https://github.com/jMonkeyEngine/jmonkeyengine/pull/1889

@stephengold can you please take a look at it?

stephengold commented 1 year ago

will do

Ali-RS commented 1 year ago

Fixed in https://github.com/jMonkeyEngine/jmonkeyengine/commit/36f9c895a28b9ff730bab55c252a4be5cdb2f2a2

@stephengold, thanks for reviewing!