diff --git a/commons/src/main/com/mbrlabs/mundus/commons/Scene.java b/commons/src/main/com/mbrlabs/mundus/commons/Scene.java index f057b9193..358f71002 100644 --- a/commons/src/main/com/mbrlabs/mundus/commons/Scene.java +++ b/commons/src/main/com/mbrlabs/mundus/commons/Scene.java @@ -34,6 +34,7 @@ import com.mbrlabs.mundus.commons.env.CameraSettings; import com.mbrlabs.mundus.commons.env.MundusEnvironment; import com.mbrlabs.mundus.commons.env.lights.DirectionalLight; +import com.mbrlabs.mundus.commons.scene3d.ModelCacheManager; import com.mbrlabs.mundus.commons.scene3d.SceneGraph; import com.mbrlabs.mundus.commons.shaders.DepthShader; import com.mbrlabs.mundus.commons.shaders.ShadowMapShader; @@ -66,6 +67,7 @@ public class Scene implements Disposable { public PerspectiveCamera cam; public ModelBatch batch; public ModelBatch depthBatch; + public ModelCacheManager modelCacheManager; protected FrameBuffer fboWaterReflection; protected FrameBuffer fboWaterRefraction; @@ -95,6 +97,7 @@ public Scene() { public Scene(boolean hasGLContext) { environment = new MundusEnvironment(); settings = new SceneSettings(); + modelCacheManager = new ModelCacheManager(this); cam = new PerspectiveCamera(CameraSettings.DEFAULT_FOV, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); cam.position.set(0, 1, -3); @@ -147,6 +150,8 @@ public void render(float delta) { initFrameBuffers((int) res.x, (int) res.y); } + modelCacheManager.update(delta); + if (sceneGraph.isContainsWater()) { captureDepth(delta); captureReflectionFBO(delta); @@ -163,6 +168,7 @@ private void renderObjects(float delta) { // Render objects batch.begin(cam); sceneGraph.render(delta, clippingPlaneDisable, 0); + batch.render(modelCacheManager.modelCache, environment); batch.end(); } @@ -196,6 +202,7 @@ private void renderShadowMap(float delta) { shadowMapper.begin(light.direction); depthBatch.begin(shadowMapper.getCam()); sceneGraph.renderDepth(delta, clippingPlaneDisable, 0, shadowMapShader); + depthBatch.render(modelCacheManager.modelCache, environment, shadowMapShader); depthBatch.end(); shadowMapper.end(); } @@ -225,6 +232,7 @@ private void captureReflectionFBO(float delta) { renderSkybox(); batch.begin(cam); sceneGraph.render(delta, clippingPlaneReflection, -settings.waterHeight + settings.distortionEdgeCorrection); + batch.render(modelCacheManager.modelCache, environment); batch.end(); fboWaterReflection.end(); @@ -240,6 +248,7 @@ private void captureDepth(float delta) { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT); depthBatch.begin(cam); sceneGraph.renderDepth(delta, clippingPlaneRefraction, settings.waterHeight + settings.distortionEdgeCorrection, depthShader); + depthBatch.render(modelCacheManager.modelCache, environment, depthShader); depthBatch.end(); fboDepthRefraction.end(); } @@ -259,6 +268,7 @@ private void captureRefractionFBO(float delta) { renderSkybox(); batch.begin(cam); sceneGraph.render(delta, clippingPlaneRefraction, settings.waterHeight + settings.distortionEdgeCorrection); + batch.render(modelCacheManager.modelCache, environment); batch.end(); fboWaterRefraction.end(); } @@ -357,5 +367,6 @@ public void dispose() { if (skybox != null) { skybox.dispose(); } + modelCacheManager.dispose(); } } diff --git a/commons/src/main/com/mbrlabs/mundus/commons/dto/ModelComponentDTO.java b/commons/src/main/com/mbrlabs/mundus/commons/dto/ModelComponentDTO.java index 3faa4012c..d8866b886 100644 --- a/commons/src/main/com/mbrlabs/mundus/commons/dto/ModelComponentDTO.java +++ b/commons/src/main/com/mbrlabs/mundus/commons/dto/ModelComponentDTO.java @@ -26,6 +26,7 @@ public class ModelComponentDTO { private String modelID; private HashMap materials; // g3db material id to material asset uuid + private boolean useModelCache; public ModelComponentDTO() { materials = new HashMap<>(); @@ -47,4 +48,11 @@ public void setModelID(String modelID) { this.modelID = modelID; } + public boolean isUseModelCache() { + return useModelCache; + } + + public void setUseModelCache(boolean useModelCache) { + this.useModelCache = useModelCache; + } } diff --git a/commons/src/main/com/mbrlabs/mundus/commons/scene3d/ModelCacheManager.java b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/ModelCacheManager.java new file mode 100644 index 000000000..7ea2c46cd --- /dev/null +++ b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/ModelCacheManager.java @@ -0,0 +1,119 @@ +package com.mbrlabs.mundus.commons.scene3d; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Mesh; +import com.badlogic.gdx.graphics.g3d.ModelCache; +import com.badlogic.gdx.graphics.g3d.ModelInstance; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Disposable; +import com.mbrlabs.mundus.commons.Scene; +import com.mbrlabs.mundus.commons.scene3d.components.Component; + +/** + * Manages a ModelCache and keeps it up to date based on requests for rebuilds and set intervals + * + * @author JamesTKhan + * @version August 02, 2022 + */ +public class ModelCacheManager implements Disposable { + private final Scene scene; + + public ModelCache modelCache; + protected float modelCacheUpdateInterval = 0.5f; + protected float lastModelCacheRebuild = modelCacheUpdateInterval; + protected boolean modelCacheRebuildRequested = true; + + public ModelCacheManager(Scene scene) { + modelCache = new ModelCache(); + this.scene = scene; + } + + public void update(float delta) { + if (modelCacheRebuildRequested) { + lastModelCacheRebuild += delta; + + if (lastModelCacheRebuild > modelCacheUpdateInterval) { + modelCacheRebuildRequested = false; + lastModelCacheRebuild = 0f; + rebuildModelCache(); + } + } + } + + /** + * Rebuilds model cache for the current scene. Potentially expensive + * depending on the size of the scene and should only be rebuilt when needed. + */ + public void rebuildModelCache() { + modelCache.begin(scene.cam); + addModelsToCache(scene.sceneGraph.getGameObjects()); + modelCache.end(); + } + + protected void addModelsToCache(Array gameObjects) { + for (GameObject go : gameObjects) { + + if (!go.active) continue; + + for (Component comp : go.getComponents()) { + if (comp instanceof ModelCacheable && ((ModelCacheable) comp).shouldCache()) { + ModelInstance modelInstance = ((ModelCacheable) comp).getModelInstance(); + + boolean skip = false; + for (Mesh mesh : modelInstance.model.meshes) { + if (mesh.getNumIndices() <= 0) { + Gdx.app.error(this.getClass().getSimpleName(), "Issues in mesh for " + go.name + " prevent it from being cacheable. Try cleaning mesh up in 3D modeling software."); + skip = true; + break; + } + } + if (skip) { + continue; + } + + modelCache.add(((ModelCacheable) comp).getModelInstance()); + } + } + + if (go.getChildren() != null) { + addModelsToCache(go.getChildren()); + } + } + } + + /** + * Request for the model cache to be rebuilt on the next interval + */ + public void requestModelCacheRebuild() { + modelCacheRebuildRequested = true; + } + + /** + * Change how often the model cache should be updated, in seconds. + * + * @param interval update interval + */ + public void setModelCacheUpdateInterval(float interval) { + modelCacheUpdateInterval = interval; + } + + /** + * Rebuild model cache if given GameObject has a cacheable component. + */ + public static void rebuildIfCached(GameObject go, boolean immediately) { + for (int i = 0; i < go.getComponents().size; i++) { + if (go.getComponents().get(i) instanceof ModelCacheable) { + if (immediately) + go.sceneGraph.scene.modelCacheManager.rebuildModelCache(); + else + go.sceneGraph.scene.modelCacheManager.requestModelCacheRebuild(); + break; + } + } + } + + @Override + public void dispose() { + modelCache.dispose(); + } +} diff --git a/commons/src/main/com/mbrlabs/mundus/commons/scene3d/ModelCacheable.java b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/ModelCacheable.java new file mode 100644 index 000000000..cccde0efb --- /dev/null +++ b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/ModelCacheable.java @@ -0,0 +1,27 @@ +package com.mbrlabs.mundus.commons.scene3d; + +import com.badlogic.gdx.graphics.g3d.ModelInstance; + +/** + * Indicates the object has a ModelInstance that can be cached. + * + * @author JamesTKhan + * @version August 02, 2022 + */ +public interface ModelCacheable { + + /** + * Should this be used in a model cache + */ + boolean shouldCache(); + + /** + * Sets if this should use a model cache or not + */ + void setUseModelCache(boolean value); + + /** + * Returns the model instance for model caching + */ + ModelInstance getModelInstance(); +} diff --git a/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/CullableComponent.java b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/CullableComponent.java index 17782ea99..a5daf776a 100644 --- a/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/CullableComponent.java +++ b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/CullableComponent.java @@ -23,6 +23,7 @@ import com.badlogic.gdx.math.collision.BoundingBox; import com.mbrlabs.mundus.commons.env.lights.DirectionalLight; import com.mbrlabs.mundus.commons.scene3d.GameObject; +import com.mbrlabs.mundus.commons.scene3d.ModelCacheable; import com.mbrlabs.mundus.commons.shadows.ShadowMapper; import com.mbrlabs.mundus.commons.utils.LightUtils; @@ -68,6 +69,13 @@ public void render(float delta) { return; } + // Cannot frustum cull model cache objects, no reason for perform calculations + if (this instanceof ModelCacheable) { + if (((ModelCacheable) this).shouldCache()) { + isCulled = false; + } + } + boolean visibleToPerspective; boolean visibleToShadowMap = false; diff --git a/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/ModelComponent.java b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/ModelComponent.java index 14fa70a9b..2e386c3dc 100644 --- a/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/ModelComponent.java +++ b/commons/src/main/com/mbrlabs/mundus/commons/scene3d/components/ModelComponent.java @@ -26,6 +26,7 @@ import com.mbrlabs.mundus.commons.assets.ModelAsset; import com.mbrlabs.mundus.commons.assets.TextureAsset; import com.mbrlabs.mundus.commons.scene3d.GameObject; +import com.mbrlabs.mundus.commons.scene3d.ModelCacheable; import com.mbrlabs.mundus.commons.shaders.ClippableShader; import com.mbrlabs.mundus.commons.shaders.ShadowMapShader; @@ -35,11 +36,12 @@ * @author Marcus Brummer * @version 17-01-2016 */ -public class ModelComponent extends CullableComponent implements AssetUsage, ClippableComponent { +public class ModelComponent extends CullableComponent implements AssetUsage, ClippableComponent, ModelCacheable { protected ModelAsset modelAsset; protected ModelInstance modelInstance; protected Shader shader; + protected boolean useModelCache = false; protected ObjectMap materials; // g3db material id to material asset uuid @@ -97,6 +99,17 @@ public void applyMaterials() { } } + @Override + public boolean shouldCache() { + return useModelCache; + } + + @Override + public void setUseModelCache(boolean value) { + useModelCache = value; + } + + @Override public ModelInstance getModelInstance() { return modelInstance; } @@ -108,7 +121,7 @@ public void render(float delta) { super.render(delta); - if (isCulled) return; + if (isCulled || useModelCache) return; if (shader != null) { gameObject.sceneGraph.scene.batch.render(modelInstance, gameObject.sceneGraph.scene.environment, shader); @@ -119,7 +132,7 @@ public void render(float delta) { @Override public void renderDepth(float delta, Vector3 clippingPlane, float clipHeight, Shader depthShader) { - if (isCulled) return; + if (isCulled || useModelCache) return; if (depthShader instanceof ClippableShader) { ((ClippableShader) depthShader).setClippingPlane(clippingPlane); @@ -157,6 +170,8 @@ public Component clone(GameObject go) { mc.shader = this.shader; mc.materials = this.materials; mc.setDimensions(mc.modelInstance); + mc.setUseModelCache(useModelCache); + gameObject.sceneGraph.scene.modelCacheManager.requestModelCacheRebuild(); return mc; } diff --git a/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt b/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt index 2e3d37dfd..c36565ecf 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt @@ -33,6 +33,7 @@ import com.mbrlabs.mundus.editor.core.project.ProjectManager import com.mbrlabs.mundus.editor.core.registry.Registry import com.mbrlabs.mundus.editor.events.FilesDroppedEvent import com.mbrlabs.mundus.editor.events.FullScreenEvent +import com.mbrlabs.mundus.editor.events.GameObjectModifiedEvent import com.mbrlabs.mundus.editor.events.ProjectChangedEvent import com.mbrlabs.mundus.editor.events.SceneChangedEvent import com.mbrlabs.mundus.editor.input.FreeCamController @@ -59,7 +60,8 @@ import org.lwjgl.opengl.GL11 class Editor : Lwjgl3WindowAdapter(), ApplicationListener, ProjectChangedEvent.ProjectChangedListener, SceneChangedEvent.SceneChangedListener, - FullScreenEvent.FullScreenEventListener { + FullScreenEvent.FullScreenEventListener, + GameObjectModifiedEvent.GameObjectModifiedListener { private lateinit var axesInstance: ModelInstance private lateinit var compass: Compass @@ -251,6 +253,11 @@ class Editor : Lwjgl3WindowAdapter(), ApplicationListener, Mundus.postEvent(FilesDroppedEvent(files)) } + override fun onGameObjectModified(event: GameObjectModifiedEvent) { + if (event.gameObject == null) return + projectManager.current().currScene.modelCacheManager.requestModelCacheRebuild() + } + override fun dispose() { debugRenderer.dispose() Mundus.dispose() diff --git a/editor/src/main/com/mbrlabs/mundus/editor/core/converter/ModelComponentConverter.java b/editor/src/main/com/mbrlabs/mundus/editor/core/converter/ModelComponentConverter.java index c63e531e7..b2bd1427d 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/core/converter/ModelComponentConverter.java +++ b/editor/src/main/com/mbrlabs/mundus/editor/core/converter/ModelComponentConverter.java @@ -22,7 +22,6 @@ import com.mbrlabs.mundus.commons.dto.ModelComponentDTO; import com.mbrlabs.mundus.commons.scene3d.GameObject; import com.mbrlabs.mundus.editor.scene3d.components.PickableModelComponent; -import com.mbrlabs.mundus.editor.shader.Shaders; import com.mbrlabs.mundus.editor.utils.Log; import java.util.Map; @@ -48,6 +47,7 @@ public static PickableModelComponent convert(ModelComponentDTO dto, GameObject g PickableModelComponent component = new PickableModelComponent(go); component.setModel(model, false); + component.setUseModelCache(dto.isUseModelCache()); for (String g3dbMatID : dto.getMaterials().keySet()) { String uuid = dto.getMaterials().get(g3dbMatID); @@ -64,6 +64,7 @@ public static PickableModelComponent convert(ModelComponentDTO dto, GameObject g public static ModelComponentDTO convert(PickableModelComponent modelComponent) { ModelComponentDTO dto = new ModelComponentDTO(); dto.setModelID(modelComponent.getModelAsset().getID()); + dto.setUseModelCache(modelComponent.shouldCache()); // materials for (String g3dbMatID : modelComponent.getMaterials().keys()) { diff --git a/editor/src/main/com/mbrlabs/mundus/editor/history/commands/DeleteCommand.kt b/editor/src/main/com/mbrlabs/mundus/editor/history/commands/DeleteCommand.kt index 1b05637bd..bd236d603 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/history/commands/DeleteCommand.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/history/commands/DeleteCommand.kt @@ -17,6 +17,7 @@ package com.mbrlabs.mundus.editor.history.commands import com.badlogic.gdx.scenes.scene2d.ui.Tree import com.mbrlabs.mundus.commons.scene3d.GameObject +import com.mbrlabs.mundus.commons.scene3d.ModelCacheManager import com.mbrlabs.mundus.commons.scene3d.components.Component import com.mbrlabs.mundus.editor.Mundus import com.mbrlabs.mundus.editor.events.ComponentAddedEvent @@ -54,6 +55,7 @@ class DeleteCommand(private var go: GameObject?, private var node: Outline.Outli // remove from outline tree tree!!.remove(node) Mundus.postEvent(SceneGraphChangedEvent()) + ModelCacheManager.rebuildIfCached(go, true) } override fun undo() { @@ -67,6 +69,7 @@ class DeleteCommand(private var go: GameObject?, private var node: Outline.Outli parentNode!!.add(node) node.expandTo() Mundus.postEvent(SceneGraphChangedEvent()) + ModelCacheManager.rebuildIfCached(go, true) // For components that utilize gizmos we should send a ComponentAddedEvent // so that GizmoManager can update as needed. diff --git a/editor/src/main/com/mbrlabs/mundus/editor/scene3d/components/PickableModelComponent.java b/editor/src/main/com/mbrlabs/mundus/editor/scene3d/components/PickableModelComponent.java index 04642933e..2ca2a28cc 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/scene3d/components/PickableModelComponent.java +++ b/editor/src/main/com/mbrlabs/mundus/editor/scene3d/components/PickableModelComponent.java @@ -58,7 +58,9 @@ public Component clone(GameObject go) { mc.shader = this.shader; mc.materials = this.materials; mc.setDimensions(mc.modelInstance); + mc.setUseModelCache(useModelCache); mc.encodeRaypickColorId(); + gameObject.sceneGraph.scene.modelCacheManager.requestModelCacheRebuild(); return mc; } } diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/ModelComponentWidget.kt b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/ModelComponentWidget.kt index 2eb7b48d8..7968aa37e 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/ModelComponentWidget.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/inspector/components/ModelComponentWidget.kt @@ -16,6 +16,9 @@ package com.mbrlabs.mundus.editor.ui.modules.inspector.components +import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener +import com.kotcrab.vis.ui.widget.VisCheckBox import com.kotcrab.vis.ui.widget.VisLabel import com.kotcrab.vis.ui.widget.VisTable import com.mbrlabs.mundus.commons.assets.MaterialAsset @@ -23,6 +26,7 @@ import com.mbrlabs.mundus.commons.scene3d.GameObject import com.mbrlabs.mundus.commons.scene3d.components.Component import com.mbrlabs.mundus.commons.scene3d.components.ModelComponent import com.mbrlabs.mundus.editor.ui.widgets.MaterialWidget +import com.mbrlabs.mundus.editor.ui.widgets.ToolTipLabel /** * @author Marcus Brummer @@ -31,6 +35,7 @@ import com.mbrlabs.mundus.editor.ui.widgets.MaterialWidget class ModelComponentWidget(modelComponent: ModelComponent) : ComponentWidget("Model Component", modelComponent) { private val materialContainer = VisTable() + private val useModelCache = VisCheckBox(null) init { this.component = modelComponent @@ -44,6 +49,11 @@ class ModelComponentWidget(modelComponent: ModelComponent) : ComponentWidget entry : modelComponentDTO.getMaterials().entrySet()) { mc.getMaterials().put(entry.getKey(), (MaterialAsset) assetManager.findAssetByID(entry.getValue()));