org.bladecoder.bladeengine.model.Sprite3DRenderer.java Source code

Java tutorial

Introduction

Here is the source code for org.bladecoder.bladeengine.model.Sprite3DRenderer.java

Source

/*******************************************************************************
 * Copyright 2014 Rafael Garcia Moreno.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package org.bladecoder.bladeengine.model;

import java.nio.IntBuffer;
import java.util.HashMap;

import org.bladecoder.bladeengine.actions.ActionCallback;
import org.bladecoder.bladeengine.actions.ActionCallbackQueue;
import org.bladecoder.bladeengine.anim.AtlasFrameAnimation;
import org.bladecoder.bladeengine.anim.FrameAnimation;
import org.bladecoder.bladeengine.anim.Tween;
import org.bladecoder.bladeengine.assets.EngineAssetManager;
import org.bladecoder.bladeengine.util.ActionCallbackSerialization;
import org.bladecoder.bladeengine.util.EngineLogger;
import org.bladecoder.bladeengine.util.Utils3D;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight;
import com.badlogic.gdx.graphics.g3d.environment.PointLight;
import com.badlogic.gdx.graphics.g3d.model.Animation;
import com.badlogic.gdx.graphics.g3d.model.Node;
import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader.Config;
import com.badlogic.gdx.graphics.g3d.utils.AnimationController;
import com.badlogic.gdx.graphics.g3d.utils.AnimationController.AnimationDesc;
import com.badlogic.gdx.graphics.g3d.utils.AnimationController.AnimationListener;
import com.badlogic.gdx.graphics.g3d.utils.DefaultShaderProvider;
import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.BufferUtils;
import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonValue;

public class Sprite3DRenderer implements SpriteRenderer {

    private final static boolean USE_FBO = false;
    private final static int MAX_BONES = 40;
    private final static Format FRAMEBUFFER_FORMAT = Format.RGBA4444;

    private static final Rectangle VIEWPORT = new Rectangle();
    private final static IntBuffer VIEWPORT_RESULTS = BufferUtils.newIntBuffer(16);

    private HashMap<String, FrameAnimation> fanims = new HashMap<String, FrameAnimation>();

    /** Starts this anim the first time that the scene is loaded */
    private String initFrameAnimation;

    private FrameAnimation currentFrameAnimation;
    private int currentCount;
    private int currentAnimationType;

    private TextureRegion tex;

    private Environment environment;
    private Environment shadowEnvironment;

    private FrameBuffer fb = null;

    private int width = 200, height = 200;

    private Vector3 cameraPos;
    private Vector3 cameraRot;
    private String cameraName = "Camera";
    private float cameraFOV = 49.3f;

    // Rotation of the model in the Y axis
    private float modelRotation = 0;

    // CREATE STATIC BATCHS FOR EFICIENCY
    private static ModelBatch modelBatch;
    private static ModelBatch shadowBatch;
    private static ModelBatch floorBatch;

    // TODO Move shadowLight to static for memory eficiency.
    // This implies that the shadow must be calculated in the draw method and
    // not in the update
    private final DirectionalShadowLight shadowLight = (DirectionalShadowLight) new DirectionalShadowLight(1024,
            1024, 30f, 30f, 1f, 100f).set(1f, 1f, 1f, 0.01f, -1f, 0.01f);

    PointLight celLight;

    String celLightName = "Light";

    private ActionCallback animationCb = null;
    private String animationCbSer = null;

    private ModelCacheEntry currentModel;
    private HashMap<String, ModelCacheEntry> modelCache = new HashMap<String, ModelCacheEntry>();
    private float lastAnimationTime = 0;

    private boolean renderShadow = true;

    class ModelCacheEntry {
        int refCounter;
        ModelInstance modelInstance;
        AnimationController controller;
        PerspectiveCamera camera3d;
    }

    public Sprite3DRenderer() {

    }

    @Override
    public void addFrameAnimation(FrameAnimation fa) {
        if (initFrameAnimation == null)
            initFrameAnimation = fa.id;

        fanims.put(fa.id, fa);
    }

    @Override
    public HashMap<String, FrameAnimation> getFrameAnimations() {
        return fanims;
    }

    @Override
    public void setInitFrameAnimation(String fa) {
        initFrameAnimation = fa;
    }

    @Override
    public String getInitFrameAnimation() {
        return initFrameAnimation;
    }

    @Override
    public FrameAnimation getCurrentFrameAnimation() {
        return currentFrameAnimation;
    }

    @Override
    public String getCurrentFrameAnimationId() {
        return currentFrameAnimation.id;
    }

    @Override
    public String[] getInternalAnimations(String source) {
        retrieveSource(source);

        Array<Animation> animations = modelCache.get(source).modelInstance.animations;
        String[] result = new String[animations.size];

        for (int i = 0; i < animations.size; i++) {
            Animation a = animations.get(i);
            result[i] = a.id;
        }

        return result;
    }

    /**
     * Render the 3D model into the texture
     */
    private void renderTex() {
        updateViewport();

        fb.begin();

        Gdx.gl.glClearColor(0, 0, 0, 0);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

        drawModel();

        fb.end((int) VIEWPORT.x, (int) VIEWPORT.y, (int) VIEWPORT.width, (int) VIEWPORT.height);
    }

    /**
     * Generates the Shadow Map
     */
    private void genShadowMap() {
        updateViewport();

        shadowLight.begin(Vector3.Zero, currentModel.camera3d.direction);
        shadowBatch.begin(shadowLight.getCamera());
        shadowBatch.render(currentModel.modelInstance);
        shadowBatch.end();
        shadowLight.end();

        Gdx.graphics.getGL20().glViewport((int) VIEWPORT.x, (int) VIEWPORT.y, (int) VIEWPORT.width,
                (int) VIEWPORT.height);
    }

    private void drawModel() {
        if (currentModel != null) {

            // DRAW SHADOW
            if (renderShadow) {
                floorBatch.begin(currentModel.camera3d);
                floorBatch.render(Utils3D.getFloor(), shadowEnvironment);
                floorBatch.end();
            }

            // DRAW MODEL
            modelBatch.begin(currentModel.camera3d);

            if (EngineLogger.debugMode() && EngineLogger.debugLevel == EngineLogger.DEBUG1)
                modelBatch.render(Utils3D.getAxes(), environment);

            modelBatch.render(currentModel.modelInstance, environment);

            modelBatch.end();
        }
    }

    public void setCameraPos(float x, float y, float z) {
        if (cameraPos == null)
            cameraPos = new Vector3(x, y, z);
        else
            cameraPos.set(x, y, z);
    }

    public void setCameraRot(float x, float y, float z) {
        if (cameraRot == null)
            cameraRot = new Vector3(x, y, z);
        else
            cameraRot.set(x, y, z);
    }

    public void setCameraFOV(float fov) {
        this.cameraFOV = fov;
    }

    public void setCameraName(String name) {
        this.cameraName = name;
    }

    public void setCelLightName(String name) {
        this.celLightName = name;
    }

    private PerspectiveCamera getCamera(ModelInstance modelInstance) {
        PerspectiveCamera camera3d = new PerspectiveCamera(cameraFOV, width, height);

        if (cameraPos == null) {
            Node n = null;

            // SET CAMERA POS FROM MODEL IF EXISTS
            n = modelInstance.getNode(cameraName);

            if (n != null) {
                cameraPos = n.translation;
            } else {
                cameraPos = new Vector3(0, 0, 5);
            }
        }

        if (cameraRot == null) {
            Node n = null;

            // SET CAMERA POS FROM MODEL IF EXISTS
            n = modelInstance.getNode(cameraName);

            if (n != null) {
                float rx = (float) (MathUtils.radiansToDegrees
                        * Math.asin(2 * n.rotation.x * n.rotation.y + 2 * n.rotation.z * n.rotation.w));
                float ry = (float) (MathUtils.radiansToDegrees
                        * Math.atan2(2 * n.rotation.x * n.rotation.w - 2 * n.rotation.y * n.rotation.z,
                                1 - 2 * n.rotation.x * n.rotation.x - 2 * n.rotation.z * n.rotation.z));
                float rz = (float) (Math.atan2(2 * n.rotation.y * n.rotation.w - 2 * n.rotation.x * n.rotation.z,
                        1 - 2 * n.rotation.y * n.rotation.y - 2 * n.rotation.z * n.rotation.z));

                setCameraRot(rx, ry, rz);
            } else {
                cameraRot = new Vector3();
            }
        }

        camera3d.position.set(cameraPos);

        camera3d.rotate(cameraRot.x, 1, 0, 0);
        camera3d.rotate(cameraRot.y, 0, 1, 0);
        camera3d.rotate(cameraRot.z, 0, 0, 1);

        camera3d.near = 0.1f;
        camera3d.far = 30;
        camera3d.update();

        return camera3d;
    }

    private final AnimationListener animationListener = new AnimationListener() {

        @Override
        public void onLoop(AnimationDesc animation) {
        }

        @Override
        public void onEnd(AnimationDesc animation) {
            if (animationCb != null || animationCbSer != null) {
                if (animationCb == null) {
                    animationCb = ActionCallbackSerialization.find(animationCbSer);
                    animationCbSer = null;
                }

                ActionCallbackQueue.add(animationCb);
            }
        }
    };

    // public Array<Animation> getAnimations() {
    // return modelInstance.animations;
    // }

    @Override
    public void startFrameAnimation(String id, int repeatType, int count, ActionCallback cb) {
        FrameAnimation fa = fanims.get(id);

        if (fa == null) {
            EngineLogger.error("FrameAnimation not found: " + id);

            return;
        }

        if (currentFrameAnimation != null && currentFrameAnimation.disposeWhenPlayed)
            disposeSource(currentFrameAnimation.source);

        currentFrameAnimation = fa;
        currentModel = modelCache.get(fa.source);
        animationCb = cb;

        if (currentModel == null || currentModel.refCounter < 1) {
            // If the source is not loaded. Load it.
            loadSource(fa.source);
            EngineAssetManager.getInstance().finishLoading();

            retrieveSource(fa.source);

            currentModel = modelCache.get(fa.source);

            if (currentModel == null) {
                EngineLogger.error("Could not load FrameAnimation: " + id);
                currentFrameAnimation = null;

                return;
            }
        }

        if (repeatType == Tween.FROM_FA) {
            currentAnimationType = currentFrameAnimation.animationType;
            currentCount = currentFrameAnimation.count;
        } else {
            currentCount = count;
            currentAnimationType = repeatType;
        }

        lastAnimationTime = 0;
        float speed = currentFrameAnimation.duration;

        if (currentAnimationType == Tween.REVERSE || currentAnimationType == Tween.REVERSE_REPEAT)
            speed *= -1;

        if (currentModel.modelInstance.getAnimation(id) != null) {
            animationCb = cb;
            currentModel.controller.setAnimation(id, currentCount, speed, animationListener);
            return;
        }

        int idx = id.indexOf('.');

        if (idx != -1) {
            String s = id.substring(0, idx);
            String dir = id.substring(idx + 1);

            lookat(dir);

            if (currentModel.modelInstance.getAnimation(s) != null) {
                currentModel.controller.setAnimation(s, count, speed, animationListener);

                return;
            }
        }

        // ERROR CASE
        EngineLogger.error("Animation NOT FOUND: " + id);

        for (Animation a : currentModel.modelInstance.animations) {
            EngineLogger.debug(a.id);
        }

        if (cb != null) {
            ActionCallbackQueue.add(cb);
        }
    }

    @Override
    public void lookat(String dir) {
        EngineLogger.debug("LOOKAT DIRECTION - " + dir);

        if (dir.equals(FrameAnimation.BACK))
            lookat(180);
        else if (dir.equals(FrameAnimation.FRONT))
            lookat(0);
        else if (dir.equals(FrameAnimation.LEFT))
            lookat(270);
        else if (dir.equals(FrameAnimation.RIGHT))
            lookat(90);
        else if (dir.equals(FrameAnimation.BACKLEFT))
            lookat(225);
        else if (dir.equals(FrameAnimation.BACKRIGHT))
            lookat(135);
        else if (dir.equals(FrameAnimation.FRONTLEFT))
            lookat(-45);
        else if (dir.equals(FrameAnimation.FRONTRIGHT))
            lookat(45);
        else
            EngineLogger.error("LOOKAT: Direction not found - " + dir);

    }

    @Override
    public void lookat(float x, float y, Vector2 pf) {
        Vector2 tmp = new Vector2(pf);
        float angle = tmp.sub(x, y).angle() + 90;
        lookat(angle);
    }

    private void lookat(float angle) {
        currentModel.modelInstance.transform.setToRotation(Vector3.Y, angle);
        modelRotation = angle;
    }

    @Override
    public void stand() {
        startFrameAnimation(FrameAnimation.STAND_ANIM, Tween.NO_REPEAT, 1, null);
    }

    @Override
    public void startWalkFA(Vector2 p0, Vector2 pf) {
        lookat(p0.x, p0.y, pf);
        startFrameAnimation(FrameAnimation.WALK_ANIM, Tween.REPEAT, -1, null);
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer(super.toString());

        sb.append("\n  Anims:");

        for (Animation a : currentModel.modelInstance.animations) {
            sb.append(" ").append(a.id);
        }

        if (currentModel.controller.current != null)
            sb.append("\n  Current Anim: ").append(currentModel.controller.current.animation.id);

        sb.append("\n");

        return sb.toString();
    }

    public void setSpriteSize(Vector2 size) {
        this.width = (int) size.x;
        this.height = (int) size.y;
    }

    @Override
    public void update(float delta) {

        if (currentModel != null && currentModel.controller.current != null
                && currentModel.controller.current.loopCount != 0) {
            currentModel.controller.update(delta);
            lastAnimationTime += delta;

            // GENERATE SHADOW MAP
            if (renderShadow)
                genShadowMap();

            if (USE_FBO)
                renderTex();
        }
    }

    @Override
    public float getWidth() {
        return width;
    }

    @Override
    public float getHeight() {
        return height;
    }

    @Override
    public void draw(SpriteBatch batch, float x, float y, float scale) {

        x = x - getWidth() / 2 * scale;

        if (USE_FBO) {
            batch.draw(tex, x, y, 0, 0, width, height, scale, scale, 0);
        } else {
            float p0x, p0y, pfx, pfy;

            Vector3 tmp = new Vector3(); // TODO Make static for performance?
            updateViewport();

            // get screen coords for x and y
            tmp.set(x, y, 0);

            tmp.mul(batch.getTransformMatrix());
            tmp.prj(batch.getProjectionMatrix());
            p0x = VIEWPORT.width * (tmp.x + 1) / 2;
            p0y = VIEWPORT.height * (tmp.y + 1) / 2;

            tmp.set(x + width * scale, y + height * scale, 0);
            tmp.mul(batch.getTransformMatrix());
            tmp.prj(batch.getProjectionMatrix());
            pfx = VIEWPORT.width * (tmp.x + 1) / 2;
            pfy = VIEWPORT.height * (tmp.y + 1) / 2;

            batch.end();

            Gdx.gl20.glViewport((int) (p0x + VIEWPORT.x), (int) (p0y + VIEWPORT.y), (int) (pfx - p0x),
                    (int) (pfy - p0y));

            Gdx.gl.glClear(GL20.GL_DEPTH_BUFFER_BIT
                    | (Gdx.graphics.getBufferFormat().coverageSampling ? GL20.GL_COVERAGE_BUFFER_BIT_NV : 0));

            drawModel();

            Gdx.gl20.glViewport((int) VIEWPORT.x, (int) VIEWPORT.y, (int) VIEWPORT.width, (int) VIEWPORT.height);
            batch.begin();
        }
    }

    private void createEnvirontment() {
        environment = new Environment();

        // environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.8f,
        // 0.8f, 0.8f, 1f));

        // environment.add(new DirectionalLight().set(1f, 1f, 1f, 1f, -1f,
        // -1f));

        if (celLight == null) {
            Node n = null;

            if (currentModel != null)
                n = currentModel.modelInstance.getNode(celLightName);

            if (n != null) {
                celLight = new PointLight().set(1f, 1f, 1f, n.translation, 1f);
            } else {
                celLight = new PointLight().set(1f, 1f, 1f, 0.5f, 1f, 1f, 1f);
            }
        }

        environment.add(celLight);

        if (renderShadow) {
            shadowEnvironment = new Environment();
            shadowEnvironment.add(shadowLight);
            shadowEnvironment.shadowMap = shadowLight;
        }
    }

    private static void updateViewport() {
        // GET CURRENT VIEWPORT SIZE
        Gdx.gl20.glGetIntegerv(GL20.GL_VIEWPORT, VIEWPORT_RESULTS);
        VIEWPORT.x = VIEWPORT_RESULTS.get(0);
        VIEWPORT.y = VIEWPORT_RESULTS.get(1);
        VIEWPORT.width = VIEWPORT_RESULTS.get(2);
        VIEWPORT.height = VIEWPORT_RESULTS.get(3);
    }

    public static void createBatchs() {
        Config modelConfigShader = new Config(
                Gdx.files.classpath("org/bladecoder/engine/shading/cel.vertex.glsl").readString(),
                Gdx.files.classpath("org/bladecoder/engine/shading/cel.fragment.glsl").readString());

        modelConfigShader.numBones = MAX_BONES;
        modelConfigShader.numDirectionalLights = 0;
        modelConfigShader.numPointLights = 0;
        modelConfigShader.numSpotLights = 0;

        modelBatch = new ModelBatch(new DefaultShaderProvider(modelConfigShader));

        shadowBatch = new ModelBatch(new DepthShaderProvider());
        floorBatch = new ModelBatch(
                new DefaultShaderProvider(Gdx.files.classpath("org/bladecoder/engine/shading/cel.vertex.glsl"),
                        Gdx.files.classpath("org/bladecoder/engine/shading/floor.fragment.glsl")));
    }

    private void loadSource(String source) {
        ModelCacheEntry entry = modelCache.get(source);

        if (entry == null) {
            entry = new ModelCacheEntry();
            modelCache.put(source, entry);
        }

        if (entry.refCounter == 0) {
            EngineAssetManager.getInstance().loadModel3D(source);
        }

        entry.refCounter++;
    }

    private void retrieveSource(String source) {
        ModelCacheEntry entry = modelCache.get(source);

        if (entry == null || entry.refCounter < 1) {
            loadSource(source);
            EngineAssetManager.getInstance().finishLoading();
            entry = modelCache.get(source);
        }

        if (entry.modelInstance == null) {
            Model model3d = EngineAssetManager.getInstance().getModel3D(source);
            entry.modelInstance = new ModelInstance(model3d);
            entry.controller = new AnimationController(entry.modelInstance);
            entry.camera3d = getCamera(entry.modelInstance);
        }
    }

    private void disposeSource(String source) {
        ModelCacheEntry entry = modelCache.get(source);

        if (entry.refCounter == 1) {
            EngineAssetManager.getInstance().disposeModel3D(source);
            entry.modelInstance = null;
        }

        entry.refCounter--;
    }

    @Override
    public void loadAssets() {
        for (FrameAnimation fa : fanims.values()) {
            if (fa.preload)
                loadSource(fa.source);
        }

        if (currentFrameAnimation != null && !currentFrameAnimation.preload) {
            loadSource(currentFrameAnimation.source);
        } else if (currentFrameAnimation == null && initFrameAnimation != null) {
            FrameAnimation fa = fanims.get(initFrameAnimation);

            if (!fa.preload)
                loadSource(fa.source);
        }
    }

    @Override
    public void retrieveAssets() {
        for (String key : modelCache.keySet()) {
            if (modelCache.get(key).refCounter > 0)
                retrieveSource(key);
        }

        if (currentFrameAnimation != null) { // RESTORE FA
            ModelCacheEntry entry = modelCache.get(currentFrameAnimation.source);
            currentModel = entry;

            float speed = currentFrameAnimation.duration;

            if (currentAnimationType == Tween.REVERSE || currentAnimationType == Tween.REVERSE_REPEAT)
                speed *= -1;

            currentModel.controller.setAnimation(currentFrameAnimation.id, currentCount, speed, animationListener);

            update(lastAnimationTime);

        } else if (initFrameAnimation != null) {
            startFrameAnimation(initFrameAnimation, Tween.FROM_FA, 1, null);

            if (currentFrameAnimation != null)
                lookat(modelRotation);
        }

        // create STATIC BATCHS if not created yet
        if (modelBatch == null)
            createBatchs();

        createEnvirontment();

        if (currentModel != null && renderShadow)
            genShadowMap();

        if (USE_FBO) {
            fb = new FrameBuffer(FRAMEBUFFER_FORMAT, width, height, true) {
                @Override
                protected void setupTexture() {
                    colorTexture = new Texture(width, height, format);
                    colorTexture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
                    colorTexture.setWrap(TextureWrap.ClampToEdge, TextureWrap.ClampToEdge);
                }
            };

            tex = new TextureRegion(fb.getColorBufferTexture());
            tex.flip(false, true);

            renderTex();
        }
    }

    @Override
    public void dispose() {
        for (String key : modelCache.keySet()) {
            EngineAssetManager.getInstance().disposeModel3D(key);
        }

        modelCache.clear();
        currentModel = null;
        environment = null;
        shadowEnvironment = null;

        if (USE_FBO)
            fb.dispose();
    }

    public static void disposeBatchs() {
        modelBatch.dispose();
        shadowBatch.dispose();
        floorBatch.dispose();

        modelBatch = shadowBatch = floorBatch = null;
    }

    @Override
    public void write(Json json) {
        json.writeValue("fanims", fanims, HashMap.class, FrameAnimation.class);

        String currentFrameAnimationId = null;

        if (currentFrameAnimation != null)
            currentFrameAnimationId = currentFrameAnimation.id;

        json.writeValue("currentFrameAnimation", currentFrameAnimationId);

        json.writeValue("initFrameAnimation", initFrameAnimation);

        json.writeValue("width", width);
        json.writeValue("height", height);
        json.writeValue("cameraPos", cameraPos, cameraPos == null ? null : Vector3.class);
        json.writeValue("cameraRot", cameraRot, cameraRot == null ? null : Vector3.class);
        json.writeValue("cameraName", cameraName, cameraName == null ? null : String.class);
        json.writeValue("cameraFOV", cameraFOV);
        json.writeValue("modelRotation", modelRotation);

        if (animationCbSer != null)
            json.writeValue("cb", animationCbSer);
        else
            json.writeValue("animationCb", ActionCallbackSerialization.find(animationCb),
                    animationCb == null ? null : String.class);

        json.writeValue("currentCount", currentCount);
        json.writeValue("currentAnimationType", currentAnimationType);
        json.writeValue("renderShadow", renderShadow);
        json.writeValue("lastAnimationTime", lastAnimationTime);

        // TODO: SAVE AND RESTORE CURRENT DIRECTION
        // TODO: shadowlight, cel light
    }

    @SuppressWarnings("unchecked")
    @Override
    public void read(Json json, JsonValue jsonData) {
        fanims = json.readValue("fanims", HashMap.class, FrameAnimation.class, jsonData);

        String currentFrameAnimationId = json.readValue("currentFrameAnimation", String.class, jsonData);

        if (currentFrameAnimationId != null)
            currentFrameAnimation = (AtlasFrameAnimation) fanims.get(currentFrameAnimationId);

        initFrameAnimation = json.readValue("initFrameAnimation", String.class, jsonData);

        width = json.readValue("width", Integer.class, jsonData);
        height = json.readValue("height", Integer.class, jsonData);
        cameraPos = json.readValue("cameraPos", Vector3.class, jsonData);
        cameraRot = json.readValue("cameraRot", Vector3.class, jsonData);
        cameraName = json.readValue("cameraName", String.class, jsonData);
        cameraFOV = json.readValue("cameraFOV", Float.class, jsonData);
        modelRotation = json.readValue("modelRotation", Float.class, jsonData);
        animationCbSer = json.readValue("animationCb", String.class, jsonData);

        currentCount = json.readValue("currentCount", Integer.class, jsonData);
        currentAnimationType = json.readValue("currentAnimationType", Integer.class, jsonData);
        renderShadow = json.readValue("renderShadow", Boolean.class, jsonData);
        lastAnimationTime = json.readValue("lastAnimationTime", Float.class, jsonData);
    }
}