jMonkeyEngine / jMonkeyEngine/jmonkeyengine

NullPointer on Geometry.setMorphState(String, float)

Open Beginner friendly
#2,919 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
4.3k
Forks
1.2k
Avg merge
4d 7h
Merged PRs (30d)
14

Description

(Created with help from AI)

Geometry.setMorphState(String, float) throws NPE on a geometry whose morph state was never set

Summary

Geometry allocates its morphState array lazily. setMorphState(float[]) and
getMorphState() both allocate it on demand, but the two name-based overloads
setMorphState(String, float) and getMorphState(String) index the field
directly. Calling either of them before one of the array-based methods throws:

java.lang.NullPointerException: Cannot store to float array because "this.morphState" is null
        at com.jme3.scene.Geometry.setMorphState(Geometry.java:632)

Version

Reproduced on 3.8.1-stable, 3.9.0-stable and 3.10.0-beta1 (the newest
release on Maven Central), and the code is unchanged on current master
(65e413232).

Reproducer

No Application or render loop needed this is pure scene-graph state:

import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.mesh.MorphTarget;

public class Repro {
    public static void main(String[] args) {
        Mesh mesh = new Mesh();
        mesh.addMorphTarget(new MorphTarget("morph"));
        Geometry geometry = new Geometry("body", mesh);

        System.out.println("morph index = " + mesh.getMorphIndex("morph")); // 0
        geometry.setMorphState("morph", 1f);                                // NPE
    }
}

getMorphState(String) fails the same way, at return morphState[index];.

Cause

In jme3-core/src/main/java/com/jme3/scene/Geometry.java, the array-based setter
allocates:

public void setMorphState(float[] state) {
    if (mesh == null || mesh.getMorphTargets().length == 0) {
        return;
    }
    int nbMorphTargets = mesh.getMorphTargets().length;
    if (morphState == null) {
        morphState = new float[nbMorphTargets];   // <-- allocated here
    }
    System.arraycopy(state, 0, morphState, 0, morphState.length);
    this.dirtyMorph = true;
}

…while the name-based one immediately below it does not:

public void setMorphState(String morphTarget, float state) {
    int index = mesh.getMorphIndex(morphTarget);
    if (index >= 0) {
        morphState[index] = state;   // <-- NPE if nothing allocated it yet
        this.dirtyMorph = true;
    }
}

getMorphState(String) has the same omission, even though the no-arg
getMorphState() right beside it allocates correctly.

Why it is easy to hit

Nothing else populates the array. MorphControl only ever reads it, and
GltfLoader attaches a MorphControl at all only when the asset carries an
animation channel targeting weights (its hasMorphTrack flag).

So an application that drives morph targets from its own code — expression
blending, a UI slider, any procedural deformation — has an unallocated array
exactly when it reaches for the name-based setter. And the name-based setter is
the natural choice there, because it is the only API that resolves a morph by
name; the float[] overload requires the caller to know target indices.

Working around it means calling setMorphState(new float[mesh.getMorphTargets().length])
once purely to force the allocation, which is not discoverable from the signature
or the javadoc.

Suggested fix

Route both name-based overloads through getMorphState(), which already
allocates on demand. The lookup guard is untouched, so an unknown name still
writes nothing and reads -1:

public void setMorphState(String morphTarget, float state) {
    int index = mesh.getMorphIndex(morphTarget);
    if (index >= 0) {
        getMorphState()[index] = state;
        this.dirtyMorph = true;
    }
}

public float getMorphState(String morphTarget) {
    int index = mesh.getMorphIndex(morphTarget);
    if (index < 0) {
        return -1;
    } else {
        return getMorphState()[index];
    }
}

A PR follows with this change plus GeometryMorphStateTest, covering the setter,
the getter, and the unknown-name path. The tests fail on master and pass with
the fix.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in jme3-core/src/main/java/com/jme3/scene/Geometry.java and inspect the name-based morph-state accessors alongside the array-based methods. Run GeometryMorphStateTest, which covers the setter, getter, and unknown-name path. Done means name-based access works before array initialization, unknown names retain their existing behavior, and the tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
computer-graphics, game-dev
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
82/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.