A question about the lookAt() method of Quaternion

I’m having issues on creating a Billboard component for my game (yes, i’m using Zay-ES). The entities which have the Billboard component will always face the camera, but will be locked on the Y-axis. This way, I can create a “grass” quad that always faces the camera.

The problem is, the lookAt method of the Quaternion is different from the method from Spatial. And nn my current project structure, I only have access to a Transform component, which holds position, rotation and scaling,

The code:

// e == Entity
if(e.get(Billboard.class).needsUpdate()) {
    e.get(Billboard.class).updated();
    Transform t = e.get(Transform.class);
    t.getRotation().lookAt(app.getCamera().getLocation(), Vector3f.UNIT_Y);
    Transform newT = t.clone();
    e.set(newT);
}

Basically I’m trying to make the object to “face” the camera. I’m currently not locking the axis, so the model faces the camera in every axis.

However, it’s not the expected result (the grass in the one being rotated):

(And at certain angles, the grass disappears)

If I add some code to another AppState that control the models:

// e = Entity
// s = Spatial
if(e.get(Billboard.class) != null) {
    s.lookAt(app.getCamera().getLocation(), Vector3f.UNIT_Y);
}

The grass is now “looking” at the camera.

However, it would be better for me if I could have an AppState that could handle these Billboard objects and thus freeing the other from some spatial transforms.

The problem is, I have no idea how can I replicate the code to make them rotate correctly. I could just copy the code from the Spatial class, but of course that doesn’t feel right.

I also need to know how can I lock the rotation on the Y-axis, in a way that the object doesn’t rotate in the X and Z-axis.

Thanks,
Ev1lbl0w

Just take the billboardcontrol which is already built in.
It also allows to lock axis.

There used to be a Bug though, I made a topic about that back then but couldn’t fix it 100%.

Thanks @Darkchaos, but I’m using Zay-ES (Entity System), and it is supposed to make the structure of the game cleaner. I think Controls and ES shouldn’t be working together.

Have you looked inside it’s Code then?
It takes a mixture of parent.rotation.inverse and the cam’s rotation.
Aligning the rotation in the Y-Axis is something like setting the rotation in x and z to zero.

For the record, really grass should all be batched and billboarded in the shader.

As to the difference in your approaches, the one obvious thing would be to remember that spatial.lookAt() is in world space and Quaternion.lookAt() does not have enough info to do world space.

As for entities, billboarding through the ES is a very strange thing to do. It’s not a game state but just a visual state. Your game object has position. That visual position could be projected in half a dozen different ways… only one of which requires billboarding for that particular visual.

1 Like

Thanks @pspeed.

That visual position could be projected in half a dozen different ways… only one of which requires billboarding for that particular visual.

That’s why I intend to create a BillboardAppState, that only “rotates” those objects. The thing is, if I create it, I only have access to the Transform component. Storing each spatial on it will only take up more space, since it’s already being stored in VisualAppState and BillboardAppState. Maybe I could create a new variable in Transform to inform VisualAppState it’s a billboard, but it doesn’t feel right, and will only get worse if I add more info.

Yes, but the billboard transform is 100000% unnecessary as a component. It’s PURELY visual state. It is not game state. There is no game system that will be wondering what the current facing direction of grass is. Just the one visual state that cares… so let it be part of the spatial and get it out of the ES. To me it’s no different than the color of a particular pixel in a texture. And you definitely wouldn’t store that in an ES either.

You may have a misunderstanding of how Java works. One pointer is no different than any other as far as ‘space’ is concerned. But if you were talking about storing the spatial in a component, then yeah, that would be totally crazy.

But the thing is, all the spatials are being created, updated and removed in a VisualAppState. This has a set of Entities which contain the Model component and the Transform component. So, how should I tell the AppState that I want a certain model to act like a Billboard? Or shouldn’t I be doing the VisualAppState this way?

Also, I’m storing each Spatial on a Map with EntityId and Spatial. If I created another map on the BillboardAppState, would it take more space?

How does the spatial know which texture it has? How does it know what color it is?

Why would you have two maps when one app state can just look at the other?

…but again, there is no reason to even have a BillboardAppState since the right implementation has billboarding done in the shader and the grass put together in batches. At the very least you could be using BillboardControl on the spatials loaded from your visual state… but personally I wouldn’t even do grass that way.

Can your grass tufts be destroyed/moved/etc. as part of game logic? If not then maybe they don’t even deserve to be entities at all.

I also guess it is the model loader which could in this case add the billboard control. As @pspeed said if you have no game logic which affects your grass then an entity for grass is not even needed.

I use java reflection to implement some decorators around some models on load. If no corresponding model factory can be found I use default model loader. If interested in code I can place some here…

How does the spatial know which texture it has? How does it know what color it is?

The Model component has an enum from a ModelLoader class I have. The enum contains the model and texture path, hasAlpha, etc…

Can your grass tufts be destroyed/moved/etc. as part of game logic? If not then maybe they don’t even deserve to be entities at all.

At the moment no, it’s just a decorative model. If the game becomes more complex, it can maybe be cut or even give resources.

I use java reflection to implement some decorators around some models on load. If no corresponding model factory can be found I use default model loader. If interested in code I can place some here…

If there is no problem @ia97lies, I would like to see how you load models in-game. I’m still a bit new to this ES system…

And since we’re talking about this, when I implement Animations later in the game, how should I handle those? My idea is to store a Map<EntityId, AnimChannel>, and when every entity is created, I create an AnimChannel if needed, then play/change the anim.

The math for dummies tutorial explains how Quaternion.lookAt works (Page 55)

Here is the visual part which loads the models, looks pretty the same I have for my Invader example

/*
 * artificials.ch 2016
 * all rights reserved by christian liesch<liesch@gmx.ch>
 */
package ch.artificials.rose;

import ch.artificials.rose.components.Model;
import ch.artificials.rose.components.Position;
import ch.artificials.rose.components.Type;
import ch.artificials.rose.components.Visible;
import ch.artificials.rose.models.ModelFactory;
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.scene.Spatial;
import com.simsilica.es.Entity;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;
import com.simsilica.es.EntitySet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.slf4j.LoggerFactory;

public class VisualAppState extends AbstractAppState {

    private final org.slf4j.Logger logger;
    private final Map<EntityId, Spatial> models;

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet visibleEntities;
    private ModelFactory modelFactory;

    public VisualAppState() {
        this.logger = LoggerFactory.getLogger(VisualAppState.class);
        this.models = new HashMap<>();
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        logger.info("Initialize visual");

        super.initialize(stateManager, app);
        this.app = (SimpleApplication) app;

        ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
        visibleEntities = ed.getEntities(Visible.class, Model.class, Position.class);
        
        modelFactory = new ModelFactory(this.app);

    }

    @Override
    public void cleanup() {
        logger.debug("Cleanup");
    }

    @Override
    public void update(float tpf) {
        if (visibleEntities.applyChanges()) {
            removeVisibles(visibleEntities.getRemovedEntities());
            addVisibles(visibleEntities.getAddedEntities());
            updateVisibles(visibleEntities.getChangedEntities());
        }
    }

    private void removeVisibles(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            removeVisible(e);
        });
    }

    private void removeVisible(Entity e) {
        Spatial s = models.remove(e.getId());
        if (s == null) {
            logger.warn("Model not found for removed entity:{0}", e);
        } else {
            s.removeFromParent();
        }
    }

    private void addVisibles(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            addVisible(e);
        });
    }

    private void addVisible(Entity e) {
        Spatial s = models.get(e.getId());
        if (s != null) {
            logger.warn("Model already exists for added entity:{0}", e);
        } else {
            s = createVisual(e);
            this.app.getRootNode().attachChild(s);
            models.put(e.getId(), s);
            updateVisible(e, s);
        }
    }

    private void updateVisibles(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.get(e.getId());
            if (s == null) {
                logger.warn("Model not found for updated entity:{0}", e);
                continue;
            }
            updateVisible(e, s);
        }
    }

    private void updateVisible(Entity e, Spatial s) {
        Position position = e.get(Position.class);
        s.setLocalTranslation(position.getLocation());
        s.setLocalRotation(position.getRotation());
    }

    public Spatial createVisual(Entity e) {
        Model model = e.get(Model.class);
        Spatial spatial = modelFactory.create(model.getName());
        spatial.setName(e.getId().toString());
        Type type = ed.getComponent(e.getId(), Type.class);
        if (type != null && (type.getName().equals(Type.ITEM) || type.getName().equals(Type.DOOR))) {
            spatial.setUserData("EntityId", e.getId().getId());
        }
        return spatial;
    }
}

The main bit here is the createVisual (there are for sure better names :slight_smile:)
And my factory looks that way:

/*
 * artificials.ch 2016
 * all rights reserved by christian liesch<liesch@gmx.ch>
 */
package ch.artificials.rose.models;

import com.jme3.app.SimpleApplication;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.slf4j.LoggerFactory;

/**
 *
 * @author cli
 */
public class ModelFactory {

    private final SimpleApplication app;
    private final org.slf4j.Logger logger;

    public ModelFactory(SimpleApplication app) {
        this.app = app;
        this.logger = LoggerFactory.getLogger(ModelFactory.class);

    }

    public Spatial create(String name) {
        Node model;
        logger.debug("Create model " + name);

        try {
            logger.debug("Try \"ch.artificials.rose.models.\" + name");
            Class<?> modelClass = Class.forName("ch.artificials.rose.models." + name);
            logger.debug("Try to get create method");
            Method createMethod = modelClass.getDeclaredMethod("create", SimpleApplication.class);
            logger.debug("Try to call create method");
            model = (Node) createMethod.invoke(modelClass, app);
        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            logger.debug("Model " + name + "do not have a specific factory", ex);
            model = (Node) app.getAssetManager().loadModel("Models/" + name.replace(".", "/") + ".j3o");
            model.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        }

        model.setName(name);
        
        return model;
    }
}

It tries to load the corresponding class, if there is any it excutes the create method, if not it does a regular load with the asset manager.

A existing factory class could be

 /*
 * artificials.ch 2016
 * all rights reserved by christian liesch<liesch@gmx.ch>
 */
package ch.artificials.rose.models.basement;

import ch.artificials.rose.models.ModelFactory;
import com.jme3.app.SimpleApplication;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Node;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author cli
 */
public class WhiteDoor {

    private static final Logger logger = Logger.getLogger(ModelFactory.class.getName());

    static public Node create(SimpleApplication app) {
        logger.log(Level.FINE, "Load model \"WhiteDoor\" with specific factory");
        Node model = (Node) app.getAssetManager().loadModel("Models/basement/WhiteDoor.j3o");
        model.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);

        ((Node)model.getChild(0)).getChild(0).move(0.7f, 0, 0.2f);
        model.getChild(0).move(-0.7f, 0, -0.2f);
        
        return model;
    }
}

A door where I shift the inner node so I have a rotation point where I want it… That way I can decorate those stuff I want with what ever I want. In your case this would be the billboard control…

I guess there are better solutions, maybe with the scene composer, but for me this solution is tip top so far. What I do not like in the scene composer is if I change it in blender and re-import it to j3o all the stuff I did in the scene composer is gone.

Maybe you spoted the

spatial.setName(e.getId().toString());

above in the createVisual? I use this to find my model nodes with in my animation system. I have an Animation component where I store the action and if I should loop, not loop or cycle. Works also as wished. Maybe we can argue and say just have one System for Visuals and Animation, I don’t know I like to separate stuff if possible.

And in ES there is most often more than one way for a solution.

By the way, in my opinion this couples the visualization too closely to your game objects. The game objects generally shouldn’t have to know stuff like hasAlpha. What if you were rendeirng the view in 2D and none of that was needed?

It can be helpful to kind of constantly check “Would I need this in a different view?” If not then it’s not part of the game… it’s part of the particular visualization you picked today. I know it sounds like it will make your code more complicated but it’s just the opposite usually. 90% of my games have some kind of ModelInfo that’s effectively just a string. The particular visual system I’m using then uses that to decide what model to load, how to configure it for that environment, etc… and generally all of that is nicely in one place.

It’s more important than you might think because this one simple misdecision has now cut off all kinds of future options for you. Want to have save games that just save your entities? Too bad. Can’t do it now. Want to implement networking with the same components? Too bad. Can’t do it now.

Yes that I do exactly with my factory, at least that was the idea. A different visual system, let’s say the MapVisualSystem, would then have a different way to load the models (or even load only a 2D representational sprite like thing).

Mine is perfectly savable at least I do it :slight_smile:

Yep.

The problem comes when people accidentally mix visual state into their components. Then one day they decide the battleaxe model should be different and/or have a custom shader and suddenly all of those saved components with hasAlpha in them cause visual artifacts.

Better to just have ModelInfo(“battleaxe”) and let whatever visual system resolve that to what it needs. The nice thing is that the user could potentially even remap them through mods someday.

For the record: personally I go one step further and encode those strings as integers through the EntityData string index. But for ‘not very large’ games there is no strong reason to do that as it adds an extra step for any debug output.

Thanks @pspeed and @ia97lies for the help. I’m starting to understand ES way better now.

So, let me see if I got everything straight: Every time I create a model, I should create a factory class for it. For example, if I create a Tree, I would create a TreeFactory. If I create a Chair, I would create a ChairFactory. Then, in every class, I should load them the way I want. For example, give the Tree model alpha and the Chair model shadows.

And, if I name my spatials with the ID of the associated Entity, it is way easier to find then in the scenegraph.

Now, I have one more question. Let’s suppose I have my world filled of grass and tress. Of course, if I don’t batch then, I get terrible FPS and performance.

Now, what I think I must do, is to organize the scenegraph in the following way (ignore the names, it’s just to understand the hierarchy):

rootNode
/
batchedGrassNode > Grass 1323, Grass835, Grass5…

batchedTreeNode > Tree32432, Tree8938, Tree983…

Is this approach a good one?

I can not help with the batching as I do not yet understand that process. I do not know exactly when or how. sorry.

For the factories yes and no. I only have factories for that stuff I want to decorate my model with something. For the rest (in that catch clause above) I just us the AssetManager to load it with no special things. Ok the AssetManager is also kind of a factory :slight_smile:

To name the nodes with entity ids I do to have my Animation system separated from the Visual system. But maybe that is a stupid idea I just didnt found a better solution :slight_smile: But so far I don’t see a downside, maybe bad for performance? And maybe @pspeed do have a better idea.

1 Like

I sometimes name my Spatials with the entity ID but generally that’s only for debugging. If I want to store the entity ID on the Spatial then I use the user data.

model.setUserData(“entityId”, entyId.getId())

…then when I want to get a real EntityId from the spatial later:
new EntityId(model.getUserData(“entityId”));