/******************************************************************************* * 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 com.bladecoder.engine.model; import java.nio.IntBuffer; import java.util.HashMap; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.PerspectiveCamera; import com.badlogic.gdx.graphics.Pixmap.Format; 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.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; import com.bladecoder.engine.actions.ActionCallback; import com.bladecoder.engine.actions.ActionCallbackQueue; import com.bladecoder.engine.anim.AnimationDesc; import com.bladecoder.engine.anim.Tween; import com.bladecoder.engine.assets.EngineAssetManager; import com.bladecoder.engine.util.ActionCallbackSerialization; import com.bladecoder.engine.util.EngineLogger; import com.bladecoder.engine.util.SerializationHelper; import com.bladecoder.engine.util.SerializationHelper.Mode; import com.bladecoder.engine.util.Utils3D; @SuppressWarnings("deprecation") public class Sprite3DRenderer extends AnimationRenderer { private static final String FRAGMENT_SHADER = "com/bladecoder/engine/shading/cel.fragment.glsl"; private static final String FLOOR_FRAGMENT_SHADER = "com/bladecoder/engine/shading/floor.fragment.glsl"; private static final String VERTEX_SHADER = "com/bladecoder/engine/shading/cel.vertex.glsl"; 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 int currentCount; private Tween.Type 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 float lastAnimationTime = 0; private boolean renderShadow = true; class ModelCacheEntry extends CacheEntry { ModelInstance modelInstance; AnimationController controller; PerspectiveCamera camera3d; } public Sprite3DRenderer() { } @Override public String getCurrentAnimationId() { return currentAnimation.id; } @Override public String[] getInternalAnimations(AnimationDesc anim) { retrieveSource(anim.source); Array<Animation> animations = ((ModelCacheEntry)sourceCache.get(anim.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(); ModelCacheEntry cs = (ModelCacheEntry)currentSource; shadowLight.begin(Vector3.Zero, cs.camera3d.direction); shadowBatch.begin(shadowLight.getCamera()); shadowBatch.render(cs.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 (currentSource != null) { ModelCacheEntry cs = (ModelCacheEntry)currentSource; // DRAW SHADOW if (renderShadow) { floorBatch.begin(cs.camera3d); floorBatch.render(Utils3D.getFloor(), shadowEnvironment); floorBatch.end(); } // DRAW MODEL modelBatch.begin(cs.camera3d); if (EngineLogger.debugMode() && EngineLogger.debugLevel == EngineLogger.DEBUG1) modelBatch.render(Utils3D.getAxes(), environment); modelBatch.render(cs.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; } public float getCameraFOV() { return cameraFOV; } public String getCameraName() { return cameraName; } public Vector2 getSpriteSize() { return new Vector2(width, height); } 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(com.badlogic.gdx.graphics.g3d.utils.AnimationController.AnimationDesc animation) { } @Override public void onEnd(com.badlogic.gdx.graphics.g3d.utils.AnimationController.AnimationDesc animation) { if (animationCb != null) { ActionCallbackQueue.add(animationCb); } } }; // public Array<Animation> getAnimations() { // return modelInstance.animations; // } @Override public void startAnimation(String id, Tween.Type repeatType, int count, ActionCallback cb) { AnimationDesc fa = fanims.get(id); if (fa == null) { EngineLogger.error("AnimationDesc not found: " + id); return; } if (currentAnimation != null && currentAnimation.disposeWhenPlayed) disposeSource(currentAnimation.source); currentAnimation = fa; currentSource = sourceCache.get(fa.source); animationCb = cb; if (currentSource == null || currentSource.refCounter < 1) { // If the source is not loaded. Load it. loadSource(fa.source); EngineAssetManager.getInstance().finishLoading(); retrieveSource(fa.source); currentSource = sourceCache.get(fa.source); if (currentSource == null) { EngineLogger.error("Could not load AnimationDesc: " + id); currentAnimation = null; return; } } if (repeatType == Tween.Type.SPRITE_DEFINED) { currentAnimationType = currentAnimation.animationType; currentCount = currentAnimation.count; } else { currentCount = count; currentAnimationType = repeatType; } lastAnimationTime = 0; float speed = currentAnimation.duration; if (currentAnimationType == Tween.Type.REVERSE || currentAnimationType == Tween.Type.REVERSE_REPEAT) speed *= -1; ModelCacheEntry cs = (ModelCacheEntry)currentSource; if (cs.modelInstance.getAnimation(id) != null) { animationCb = cb; cs.controller.setAnimation(id, currentCount, speed, animationListener); computeBbox(); return; } int idx = id.indexOf('.'); if (idx != -1) { String s = id.substring(0, idx); String dir = id.substring(idx + 1); lookat(dir); if (cs.modelInstance.getAnimation(s) != null) { cs.controller.setAnimation(s, count, speed, animationListener); computeBbox(); return; } } // ERROR CASE EngineLogger.error("Animation NOT FOUND: " + id); for (Animation a : cs.modelInstance.animations) { EngineLogger.debug(a.id); } if (cb != null) { ActionCallbackQueue.add(cb); } computeBbox(); } @Override public void startAnimation(String id, Tween.Type repeatType, int count, ActionCallback cb, String direction) { startAnimation(id, repeatType, count, null); if (direction != null && currentAnimation != null) lookat(direction); } @Override public void startAnimation(String id, Tween.Type repeatType, int count, ActionCallback cb, Vector2 p0, Vector2 pf) { startAnimation(id, repeatType, count, null); if (currentAnimation != null) { Vector2 tmp = new Vector2(pf); float angle = tmp.sub(p0).angle() + 90; lookat(angle); } } private void lookat(String dir) { EngineLogger.debug("LOOKAT DIRECTION - " + dir); if (dir.equals(AnimationDesc.BACK)) lookat(180); else if (dir.equals(AnimationDesc.FRONT)) lookat(0); else if (dir.equals(AnimationDesc.LEFT)) lookat(270); else if (dir.equals(AnimationDesc.RIGHT)) lookat(90); else if (dir.equals(AnimationDesc.BACKLEFT)) lookat(225); else if (dir.equals(AnimationDesc.BACKRIGHT)) lookat(135); else if (dir.equals(AnimationDesc.FRONTLEFT)) lookat(-45); else if (dir.equals(AnimationDesc.FRONTRIGHT)) lookat(45); else EngineLogger.error("LOOKAT: Direction not found - " + dir); } private void lookat(float angle) { ((ModelCacheEntry)currentSource).modelInstance.transform.setToRotation(Vector3.Y, angle); modelRotation = angle; } public void setSpriteSize(Vector2 size) { this.width = (int) size.x; this.height = (int) size.y; } @Override public void update(float delta) { ModelCacheEntry cs = (ModelCacheEntry)currentSource; if (cs != null && cs.controller.current != null && cs.controller.current.loopCount != 0) { cs.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; } private static final Vector3 tmp = new Vector3(); @Override public void draw(SpriteBatch batch, float x, float y, float scale, float rotation, Color tint) { x = x - getWidth() / 2 * scale; if (USE_FBO) { if(tint != null) batch.setColor(tint); batch.draw(tex, x, y, 0, 0, width, height, scale, scale, 0); if(tint != null) batch.setColor(Color.WHITE); } else { float p0x, p0y, pfx, pfy; 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 (currentSource != null) n = ((ModelCacheEntry)currentSource).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(VERTEX_SHADER).readString(), Gdx.files.classpath(FRAGMENT_SHADER).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(VERTEX_SHADER), Gdx.files.classpath(FLOOR_FRAGMENT_SHADER))); } private void loadSource(String source) { ModelCacheEntry entry = (ModelCacheEntry)sourceCache.get(source); if (entry == null) { entry = new ModelCacheEntry(); sourceCache.put(source, entry); } if (entry.refCounter == 0) { EngineAssetManager.getInstance().loadModel3D(source); } entry.refCounter++; } private void retrieveSource(String source) { ModelCacheEntry entry = (ModelCacheEntry)sourceCache.get(source); if (entry == null || entry.refCounter < 1) { loadSource(source); EngineAssetManager.getInstance().finishLoading(); entry = (ModelCacheEntry)sourceCache.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 = (ModelCacheEntry)sourceCache.get(source); if (entry.refCounter == 1) { EngineAssetManager.getInstance().disposeModel3D(source); entry.modelInstance = null; } entry.refCounter--; } @Override public void loadAssets() { for (AnimationDesc fa : fanims.values()) { if (fa.preload) loadSource(fa.source); } if (currentAnimation != null && !currentAnimation.preload) { loadSource(currentAnimation.source); } else if (currentAnimation == null && initAnimation != null) { AnimationDesc fa = fanims.get(initAnimation); if (!fa.preload) loadSource(fa.source); } } @Override public void retrieveAssets() { // create STATIC BATCHS if not created yet if (modelBatch == null) createBatchs(); createEnvirontment(); for (String key : sourceCache.keySet()) { if (sourceCache.get(key).refCounter > 0) retrieveSource(key); } if (currentAnimation != null) { // RESTORE FA ModelCacheEntry entry = (ModelCacheEntry)sourceCache.get(currentAnimation.source); currentSource = entry; float speed = currentAnimation.duration; if (currentAnimationType == Tween.Type.REVERSE || currentAnimationType == Tween.Type.REVERSE_REPEAT) speed *= -1; ((ModelCacheEntry)currentSource).controller.setAnimation(currentAnimation.id, currentCount, speed, animationListener); update(lastAnimationTime); } else if (initAnimation != null) { startAnimation(initAnimation, Tween.Type.SPRITE_DEFINED, 1, null); if (currentAnimation != null) lookat(modelRotation); } if (currentSource != null && renderShadow) genShadowMap(); if (USE_FBO) { fb = new FrameBuffer(FRAMEBUFFER_FORMAT, width, height, true); tex = new TextureRegion(fb.getColorBufferTexture()); tex.flip(false, true); renderTex(); } computeBbox(); } @Override public void dispose() { for (String key : sourceCache.keySet()) { if(sourceCache.get(key).refCounter > 0) EngineAssetManager.getInstance().disposeModel3D(key); } sourceCache.clear(); currentSource = null; environment = null; shadowEnvironment = null; if (USE_FBO) fb.dispose(); } public static void disposeBatchs() { if (modelBatch == null) return; modelBatch.dispose(); shadowBatch.dispose(); floorBatch.dispose(); modelBatch = shadowBatch = floorBatch = null; } @Override public void write(Json json) { super.write(json); if (SerializationHelper.getInstance().getMode() == Mode.MODEL) { float worldScale = EngineAssetManager.getInstance().getScale(); json.writeValue("width", width / worldScale); json.writeValue("height", height / worldScale); 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("renderShadow", renderShadow); } else { json.writeValue("animationCb", ActionCallbackSerialization.find(animationCb)); json.writeValue("currentCount", currentCount); json.writeValue("currentAnimationType", currentAnimationType); json.writeValue("lastAnimationTime", lastAnimationTime); // TODO: SAVE AND RESTORE CURRENT DIRECTION // TODO: shadowlight, cel light } json.writeValue("modelRotation", modelRotation); } @SuppressWarnings("unchecked") @Override public void read(Json json, JsonValue jsonData) { super.read(json, jsonData); if (SerializationHelper.getInstance().getMode() == Mode.MODEL) { fanims = json.readValue("fanims", HashMap.class, AnimationDesc.class, jsonData); float worldScale = EngineAssetManager.getInstance().getScale(); width = (int) (json.readValue("width", Integer.class, jsonData) * worldScale); height = (int) (json.readValue("height", Integer.class, jsonData) * worldScale); 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); renderShadow = json.readValue("renderShadow", Boolean.class, jsonData); } else { animationCb = ActionCallbackSerialization.find(json.readValue("animationCb", String.class, jsonData)); currentCount = json.readValue("currentCount", Integer.class, jsonData); currentAnimationType = json.readValue("currentAnimationType", Tween.Type.class, jsonData); lastAnimationTime = json.readValue("lastAnimationTime", Float.class, jsonData); } modelRotation = json.readValue("modelRotation", Float.class, jsonData); } }