JME3 discussion: Approaches for infinitely big space scenes

Hello togehter,

I would like to start some discussion about how to handle really big scenes in JME3.
Currently I am writing a Space exploration game which shall come along with realistically scaled universe using a Newtonain Flight model capable of displaying high velocities.

As you can see the following screen shots, my space simulation game requires pretty high accuracy both for position and velocity.

Distances:

High velocity:

Low velocity:

My current approach is: Every direct child of the RootNode stores its absolute coordinates in Decimal128 (BigDecimal[3]) format (for velocity representation I use double[3]). Scene graph normalizes the position around the player thus object location responsibility is shifted from scene graph to external logic.Because of this I do not really like the approach because it seems a little bit clumsy and error prone to me storing position twice and synchronizing it every cycle.

For displaying high velocities I tried with Bullet but I quickly realized that single precision floating point is not sufficient of displaying high velocities (example: at 8000 km/s the accuracy is about 1 m/s making physics calculation impossible)

Does anybody know a better or more elegant way to solve this problem?

Regards
Harry

You are on the right track,

Scenegraph is pure for visualisation, your so it might contain similar/duplicate data to your actual game model.
I would actually fake the hyperspace travel drive whatever physics

Use a greatly scaled down bullet space for the collision calcualtions on spaceship level, and another one for sub spaceship details.

1 Like

Yes, the critical part will be to never ever treat spatials as game objects.

You will have separate game objects and they will use double for positioning.

Spatials are just a temporary visualization element you create for local space. (Or even split into separate local, mid, far space depending on your scenes.)

Fortunately, in real space, objects are never very near each other. You may only ever have one celestial body in ‘near’ space at any given time… depending on what scale you want your camera to be dealing with.

Hello together,

thanks for the quick reply.

thank you. This is a very important information for me because I understand now that I do not do something which is fundamentally wrong.

I think exactly this is the problem here. Because i made it running somehow on application level but my goal is a bullet proof software concept retaining as much of the JME3 concepts and use cases as possible. For this I have defined preliminary requirements:

  1. the center of the scene coordinates is always the players vehicle. player.getWorldLocation() → new Vector3f(0,0,0). The origin of space coordinate system center has BigDecimal{0,0,0} position and double{0,0,0} velocity. space coordinates and velocities are stored inside players vehicle spatial.

  2. All vehicles have their own absolute space coordinates x3 and velocity v3.

  3. At any time space coordinates and scene coordinates and velocities shall be transformable into each other. ==> Solved: a static BigDecimal vector storing the absolute position of the spatial

  4. Definitions of hierarchical structures of celestial bodies in space: e.g… solar system: SolarSystem[x3,v3] → Stars[x3rel,v3rel] → planets{x3rel2,v3rel2] → moons[x3rel3,v3rel3]–> space stations. Arbitrary transformations (especially rotational position) has to be considered. Rotating solar system node affects star and planets and moons.

  5. Native JME3 function like rootNode.attachChild(spatial) shall work and provide default functionality and scene placement. x3 and v3 are then set with respect to the players vessel.

Am i still on the right track with this? :slight_smile:

Regards,
Harry

After some nights of coding here my (now working) intermediate solution of the requirements described above. The problem is that this app state only provides ultra accurate positioning and speeding for rootNode children and doesn’t allow recursion (for example the star–>planet–>moon problem described above).

package spacecraft.scene;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetManager;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.math.BigDecimal;
import java.util.Objects;
import java.util.Optional;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import java.util.function.BiFunction;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import spacecraft.character.PlayerState;
import spacecraft.objects.SceneControl;
import static spacecraft.scene.StateVelocityUtilities.doForwardEuler;

/**
 * This class fullfills two tasks associated with the placement of the scene.
 * <p>
 *
 * Requirements: 
 * <p>
 * This appstate provides functionality for displaying every possible position in space and
 * every velocities which range from millimeters per second to multiples the
 * speed of light 
 * (Yes I know Einstein but this is for SoftSciFi warp speed or time compression).
 * <p>
 * Req 1. the center of the scene coordinates is always the players vehicle.
 * player.getWorldLocation() --> new Vector3f(0,0,0). The origin of space
 * coordinate system center has BigDecimal[]{0,0,0} position and double[]{0,0,0}
 * velocity. space coordinates and velocities are stored inside players vehicle
 * spatial.
 *
 * Req 2. All vehicles have their own absolute space coordinates x3 and velocity
 * v3.
 *    
 * Req 3. At any time space coordinates and scene coordinates and velocities
 * shall be transformable into each other. ==> Solved: a static BigDecimal[]
 * vector storing the absolute position of the spatial
 *
 * (Req 4.) Definitions of hierarchical structures of celestial bodies in space:
 * e.g.. solar system: SolarSystem[x3,v3] --> Stars[x3rel,v3rel] -->
 * planets{x3rel2,v3rel2] --> moons[x3rel3,v3rel3]--> space stations. Arbitrary
 * transformations (especially rotational position) has to be considered.
 * Rotating solar system node affects star and planets and moons.
 *
 * Req 5. Native JME3 function like rootNode.attachChild(spatial) shall work and
 * provide default functionality and scene placement. x3 and v3 are then set
 * with respect to the players vessel.
 *
 * Req 6. Usually spatial controls do not have direct access to the assetManager
 * or rootNode they do not/shall not have the capability to generate/remove
 * scene content directly. This apostate shall collect command objects (generated by     
 * SceneControl objects) for adding/removing content (GoF command pattern).
 *
 *
 * @author harryschwenk
 */
public final class BigSceneState extends AbstractAppState {

private AssetManager assetManagerOpt = null;
private Camera cameraOpt = null;
private Node rootNodeOpt = null;
// private float tpf;
private AppStateManager stateManager = null;

/**
 * asset manager getter
 *
 * @return asset manager
 */
public AssetManager getAssetManager() {
    return Objects.requireNonNull(assetManagerOpt);
}

/**
 * camera getter
 *
 * @return asset manager
 */
public Camera getCameraOpt() {
    return Objects.requireNonNull(cameraOpt);
}

/**
 * getter for all children of the root node (TopLevel children)
 *
 * @return asset manager
 */
public Stream<Spatial> getChildren() {
    return getRootNode().getChildren().stream();
}

/**
 * root node getter function
 *
 * @return rootNode
 */
public Node getRootNode() {
    return Objects.requireNonNull(rootNodeOpt);
}

/**
 * initialization of app state. rootNode, assetManager and camera references
 * are stored for later use.
 *
 * @param stateManager der StateManager
 * @param app das Applikaitonsobjekt.
 * @throws IllegalStateException if initialize is called twice.
 */
@Override
public void initialize(AppStateManager stateManager, Application app) {
    this.stateManager = Objects.requireNonNull(stateManager);

    // Nur ausführen, wenn nicht initialsisiert!
    if (isInitialized()) {
        throw new IllegalStateException("Already initialized");
    }

    /*
     * Auslesen der RootNode
     */
    final Node rootNodeTmp = ((SimpleApplication) app).getRootNode();
    setRootNode(Objects.requireNonNull(rootNodeTmp));

    /*
     * Auslesen des AssetManagers
     */
    final AssetManager assetManagerTmp = app.getAssetManager();
    setAssetManager(Objects.requireNonNull(assetManagerTmp));

    /*
     * Auslesen der Camera
     */
    final Camera cameraTmp = app.getCamera();
    setCamera(Objects.requireNonNull(cameraTmp));

    // Hier wird das initialized flag auf true gesetzt.
    super.initialize(stateManager, app); //To change body of generated methods, choose Tools | Templates.
}

/**
 *
 * @return
 */
@Override
public String toString() {
    return "SceneState{" + "assetManager=" + assetManagerOpt
            //+ ", bulletAppState=" + bulletAppState
            + ", camera=" + cameraOpt + ", rootNode=" + rootNodeOpt + '}';
}

/**
 * Update cycle.
 *
 * <p>
 * 1. (Req 2) updateEachTopLevelChild performs additional updates for every
 * direct root node child. An Euler-Integration step is performed on every
 * single top-Level-child for updating ultra accurte position and velocity
 * vector.
 * <p>
 * 2. (Req 6) every child is checked for scene modification commands
 * objects.
 * <p>
 * 3. Req 1. the center of the scene coordinates is always the players
 * vehicle. player.getWorldLocation() --> Vector3f(0,0,0). The origin of
 * space coordinate system center has BigDecimal[]{0,0,0} position and
 * double[]{0,0,0} velocity. space coordinates and velocities are stored
 * inside players vehicle spatial.
 *
 * @param tpf
 */
@Override
public void update(float tpf) {
    super.update(tpf); //To change body of generated methods, choose Tools | Templates.

    getRootNode().getChildren().stream().forEach(topLevelChild -> doForwardEuler(topLevelChild, tpf));
    updateReferenceLocation();

    getRootNode().getChildren().stream().forEach(this::updateEachTopLevelChild);
    getRootNode().breadthFirstTraversal(this::loadOrRemoveSceneObjects);
}

/**
 * @return the cameraState
 */
public BigDecimal[] getReferenceLocation() {
    return StateVelocityUtilities.centerPositionOfLocalScene.clone();
}

/**
 * @param referenceStateArray the cameraState to set
 */
public void setReferenceLocation(BigDecimal[] referenceStateArray) {
    StateVelocityUtilities.centerPositionOfLocalScene = referenceStateArray.clone();
}

/**
 *
 * @param xx
 * @param xy
 * @param xz
 * @param vx
 * @param vy
 * @param vz
 */
public void setCameraStateVector(BigDecimal xx, BigDecimal xy, BigDecimal xz, BigDecimal vx, BigDecimal vy, BigDecimal vz) {
    StateVelocityUtilities.centerPositionOfLocalScene = new BigDecimal[]{xx, xy, xz, vx, vy, vz};
}

/**
 * (Req 1) the reference location is updated here. This is the ultra
 * accurate BigDecimal[] vector which defines the space location of the
 * rootNode origin.
 */
private void updateReferenceLocation() {
    ofNullable(getStateManager().getState(PlayerState.class))
            .flatMap(PlayerState::getPlayerVehicle)
            .ifPresent(sp -> this.setReferenceLocation(StateVelocityUtilities.getAbsoluteLocation(sp)));
}

/**
 * (Req 6) every child is checked for scene modification commands objects.
 *
 * @param sc
 */
private void loadOrRemoveSceneObjects(Spatial oneOfAllSceneElements) {
    SceneControl sc = oneOfAllSceneElements.getControl(SceneControl.class);
    if (sc == null) {
        return;
    }

    // this is necessary at least one time per scene control beacuse
    // sceneState reference is necessary for command object creation.
    // Probably redundant but there is virtually no runtime overhead here.
    sc.setSceneState(of(this));

    /*
    * retrieving commands for execution. The commands are cleared
    * automatically by the control.
     */
    sc.pollCommands().stream().forEach((
            BiFunction<AssetManager, Node, Spatial> cmd) -> {
        Spatial newSpatial = cmd.apply(getAssetManager(),
                getRootNode());

        // Only to be sure that every thing is running fine the first cycle.
        newSpatial.breadthFirstTraversal((Spatial spNew)
                -> ofNullable(spNew.getControl(SceneControl.class)).ifPresent(
                (SceneControl spNewSc) -> spNewSc.setSceneState(
                        of(this))));
    });

    /*
     * Perform destruction of spatial if condition is true.
     */
    Spatial scSp = sc.getSpatial();
    if (sc.isDestroyed() && scSp != null) {
        Node scSpParent = scSp.getParent();
        if (scSpParent != null) {
            scSpParent.detachChild(scSp);
        }

        // Unlink scene state: the object is out of scene and cannot do anyting.
        sc.setSceneState(Optional.empty());
    }
}

/**
 *
 * @param assetManager
 */
private void setAssetManager(AssetManager assetManager) {
    // Fail fast
    this.assetManagerOpt = Objects.requireNonNull(assetManager);
}

/**
 *
 * @param camera
 */
private void setCamera(Camera camera) {
    // Fail fast
    this.cameraOpt = Objects.requireNonNull(camera);
}

/**
 *
 * @param rootNode
 */
private void setRootNode(Node rootNode) {
    // Fail fast
    this.rootNodeOpt = Objects.requireNonNull(rootNode);
}

/**
 *
 * Req 1. the center of the scene coordinates is always the players vehicle.
 * player.getWorldLocation() --> new Vector3f(0,0,0). The origin of space
 * coordinate system center has BigDecimal[]{0,0,0} position and
 * double[]{0,0,0} velocity. space coordinates and velocities are stored
 * inside players vehicle spatial.
 *
 * @param topLevelChild child of rootNode which is placed by ujltra accurate
 * coordinates.
 */
private void updateEachTopLevelChild(Spatial topLevelChild) {

    // Transformation to scene coordinates is necessary here because the reference
    // vector has been updated here requiring a complete retransformatino of the scene.
    BigDecimal[] absoluteLocation = StateVelocityUtilities.getAbsoluteLocation(topLevelChild);
    BigDecimal[] sceneLocation = IntStream.range(0, 3).mapToObj(i -> absoluteLocation[i].subtract(this.getReferenceLocation()[i])).toArray(i -> new BigDecimal[i]);
    Vector3f vector = new Vector3f(sceneLocation[0].floatValue(), sceneLocation[1].floatValue(), sceneLocation[2].floatValue());
    topLevelChild.setLocalTranslation(vector);
    if (topLevelChild instanceof Node) {

        // We remove position data from sub-children in order to clear
        // non updated / false information. This covers the case if a topLevel child
        // is attached to another spatial and then reattached to the rootNode.
        ((Node) topLevelChild)
                .getChildren()
                .stream()
                .forEach(sp -> sp.breadthFirstTraversal(subchild -> StateVelocityUtilities.clearLocationData(subchild)));
    }
}

/**
 *
 * @return
 */
private AppStateManager getStateManager() {
    // Fail fast
    return Objects.requireNonNull(stateManager);
}
}

Critics and feedback are always welcome.

Thank you very much.

Regards,
Harry

P.S.:

this is a very interesting point. After the coding night and reading twice this statement is now far clearer to me :slight_smile: