MaterialDebugAppState not reloading materials

Hello guys! I’m trying to debug my materials using MaterialDebugAppState. However, whenever i press the key I’ve bind the materials are not getting updated until i restart my application.

I made a specific branch with a test case: GitHub - aegroto/TowerMonkeyExample at material_reload, most of the work is made in the class InGameAppState.

I create my app state this way:

    materialDebugAppState = new MaterialDebugAppState();
    getStateManager().attach(materialDebugAppState);

And then bind a key when the map is fully loaded, overriding OnEnable():

    materialDebugAppState.registerBinding(new KeyTrigger(KeyInput.KEY_R), mapGeom);

A message is shown in console, but the material is not reloaded when i modify this line in TowerMonkeyDefenseTerrain.j3m:

    TriplanarMaxBlending : 1.0 

The value I set doesn’t matter, the material is not refreshed anyway.

How are you refreshing the Material?
.setMaterial(assetManager, “Path”)?
Did you remember to clear the assetManager cache for that Material?

The MaterialDebugAppState should do that for me.

once i reported a bug, fixed and provided the solution for it. After that i began to enhance it for my purpose. Here is my contribution. My usecase was “reloading when i modify the file”.

/*
 * Copyright (c) 2009-2014 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package de.b00n.jme.util.file;

import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.UrlAssetInfo;
import com.jme3.input.InputManager;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.Trigger;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.post.Filter;
import com.jme3.post.Filter.Pass;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.RendererException;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.terrain.geomipmap.TerrainQuad;
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This appState is for debug purpose only, and was made to provide an easy way
 * to test shaders, with a live update capability.
 *
 * This class provides and easy way to reload a material and catches compilation
 * errors when needed and displays the error in the console.
 *
 * If no error occur on compilation, the material is reloaded in the scene.
 *
 * You can either trigger the reload when pressing a key (or whatever input is
 * supported by Triggers you can attach to the input manager), or trigger it
 * when a specific file (the shader source) has been changed on the hard drive.
 *
 * Usage :
 *
 * MaterialDebugAppState matDebug = new MaterialDebugAppState();
 * stateManager.attach(matDebug); matDebug.registerBinding(new
 * KeyTrigger(KeyInput.KEY_R), whateverGeometry);
 *
 * this will reload the material of whateverGeometry when pressing the R key.
 *
 * matDebug.registerBinding("Shaders/distort.frag", whateverGeometry);
 *
 * this will reload the material of whateverGeometry when the given file is
 * changed on the hard drive.
 *
 * you can also register bindings to the appState with a post process Filter
 *
 * @author Nehon
 */
public class MaterialDebugAppState extends AbstractAppState {

	private RenderManager renderManager;
	private AssetManager assetManager;
	private InputManager inputManager;
	private final List<Binding> bindings = new LinkedList<>();
	private final Map<Trigger, List<Binding>> fileTriggers = new HashMap<>();

	@Override
	public void initialize(AppStateManager stateManager, Application app) {
		renderManager = app.getRenderManager();
		assetManager = app.getAssetManager();
		inputManager = app.getInputManager();
		bindings.forEach(this::bind);
		super.initialize(stateManager, app);
	}

	/**
	 * Will reload the spatial's materials whenever the trigger is fired
	 *
	 * @param trigger the trigger
	 * @param callback the callback to call after reloading
	 * @param spat the spatial to reload
	 */
	public void registerBinding(Trigger trigger, Consumer<Material> callback, final Spatial spat) {
		registerBinding(trigger, callback, spat, spat);
	}

	private void registerBinding(Trigger trigger, Consumer<Material> callback, final Spatial spat, final Spatial association) {
		if (spat instanceof Geometry) {
			GeometryBinding binding = new GeometryBinding(trigger, callback, association, (Geometry) spat);
			bindings.add(binding);
			if (isInitialized()) {
				bind(binding);
			}
		} else if (spat instanceof Node) {
			for (Spatial child : ((Node) spat).getChildren()) {
				registerBinding(trigger, callback, association, child);
			}
		}
	}


	public void removeBinding(String shaderName, final Spatial spat) {
		if (spat instanceof Geometry) {
			Binding binding = getBinding(shaderName, spat);
			if (bindings != null) {
				bindings.remove(binding);
			}
		} else if (spat instanceof TerrainQuad) {
			Binding binding = getBinding(shaderName, (TerrainQuad) spat);
			if (bindings != null) {
				bindings.remove(binding);
			}
		} else if (spat instanceof Node) {
			for (Spatial child : ((Node) spat).getChildren()) {
				Binding binding = getBinding(shaderName, child);
				if (bindings != null) {
					bindings.remove(binding);
				}
			}
		}
	}
  /**
	 * Will reload the TerrainQuad's materials whenever the trigger is fired.
	 *
	 * @param trigger the trigger
	 * @param quad the TerrainQuad to reload
	 */
	public void registerBinding(Trigger trigger, final TerrainQuad quad) {
		TerrainQuadBinding binding = new TerrainQuadBinding(trigger, quad);
		bindings.add(binding);
		if (isInitialized()) {
			bind(binding);
		}
	}
	/**
	 * Will reload the filter's materials whenever the trigger is fired.
	 *
	 * @param trigger the trigger
	 * @param filter the filter to reload
	 */
	public void registerBinding(Trigger trigger, final Filter filter) {
		FilterBinding binding = new FilterBinding(trigger, filter);
		bindings.add(binding);
		if (isInitialized()) {
			bind(binding);
		}
	}

	/**
	 * Will reload the filter's materials whenever the shader file is changed on
	 * the hard drive
	 *
	 * @param shaderName the shader name (relative path to the asset folder or
	 * to a registered asset path)
	 * @param filter the filter to reload
	 */
	public void registerBinding(String shaderName, final Filter filter) {
		registerBinding(new FileChangedTrigger(shaderName), filter);
	}

	/**
	 * Will reload the TerrainQuad's materials whenever the shader file is changed on
	 * the hard drive
	 *
	 * @param shaderName the shader name (relative path to the asset folder or
	 * to a registered asset path)
	 * @param quad the TerrainQuad to reload
	 */
	public void registerBinding(String shaderName, final TerrainQuad quad) {
		registerBinding(new FileChangedTrigger(shaderName), quad);
	}

	/**
	 * Will reload the spatials's materials whenever the shader file is changed
	 * on the hard drive
	 *
	 * @param shaderName the shader name (relative path to the asset folder or
	 * to a registered asset path)
	 * @param callback the callback to call after reloading
	 * @param spat the spatial to reload
	 */
	public void registerBinding(String shaderName, Consumer<Material> callback, final Spatial spat) {
		registerBinding(new FileChangedTrigger(shaderName), callback, spat);
	}

	/**
	 * Will reload the spatial's materials whenever the trigger is fired
	 *
	 * @param shaderName the shader name (relative path to the asset folder or
	 * to a registered asset path)
	 * @param callback the callback to call after reloading
	 * @param assetName the MaterialDef to reload
	 */
	public void registerBinding(String shaderName, Consumer<Material> callback, final String assetName) {
		AssetNameBinding binding = new AssetNameBinding(new FileChangedTrigger(shaderName), assetName, callback);
		bindings.add(binding);
		if (isInitialized()) {
			bind(binding);
		}
	}

	/**
	 * Looks for any binding which is looking for the given shader
	 *
	 * @param shaderName
	 * @return
	 */
	public boolean hasBinding(String shaderName) {
		for (Binding b : bindings) {
			if (b instanceof FilterBinding) {
				FilterBinding binding = (FilterBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName)) {
						return true;
					}
				}
			} else if (b instanceof GeometryBinding) {
				GeometryBinding binding = (GeometryBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName)) {
						return true;
					}
				}
			} else if (b instanceof TerrainQuadBinding) {
				TerrainQuadBinding binding = (TerrainQuadBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName)) {
						return true;
					}
				}
			} else if (b instanceof AssetNameBinding) {
				AssetNameBinding binding = (AssetNameBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName)) {
						return true;
					}
				}
			}
		}
		return false;
	}

	public boolean hasBinding(String shaderName, final Filter filter) {
		for (Binding b : bindings) {
			if (b instanceof FilterBinding) {
				FilterBinding binding = (FilterBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName) && binding.filter == filter) {
						return true;
					}
				}
			}
		}
		return false;
	}

	private Binding getBinding(String shaderName, final TerrainQuad quad) {
		for (Binding b : bindings) {
			if (b instanceof TerrainQuadBinding) {
				TerrainQuadBinding binding = (TerrainQuadBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName) && binding.quad == quad) {
						return binding;
					}
				}
			}
		}
		return null;
	}

	private Binding getBinding(String shaderName, final Spatial spat) {
		for (Binding b : bindings) {
			if (b instanceof GeometryBinding) {
				GeometryBinding binding = (GeometryBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName) && binding.association == spat) {
						return binding;
					}
				}
			}
		}
		return null;
	}

	public boolean hasBinding(String shaderName, final Spatial spat) {
		return getBinding(shaderName, spat) != null;
	}

	public boolean hasBinding(String shaderName, String assetName) {
		for (Binding b : bindings) {
			if (b instanceof AssetNameBinding) {
				AssetNameBinding binding = (AssetNameBinding) b;
				if (binding.trigger instanceof FileChangedTrigger) {
					FileChangedTrigger trigger = (FileChangedTrigger) binding.trigger;
					if (trigger.fileName.equals(shaderName) && binding.assetName.equals(assetName)) {
						return true;
					}
				}
			}
		}
		return false;
	}

	private void bind(final Binding binding) {
		if (binding.getTrigger() instanceof FileChangedTrigger) {
			FileChangedTrigger t = (FileChangedTrigger) binding.getTrigger();
			List<Binding> b = fileTriggers.get(t);
			if (b == null) {
				t.init();
				b = new LinkedList<>();
				fileTriggers.put(t, b);
			}
			b.add(binding);
		} else {
			final String actionName = binding.getActionName();
			inputManager.addListener((ActionListener) (String name, boolean isPressed, float tpf) -> {
				if (actionName.equals(name) && isPressed) {
					//reloading the material
					binding.reload();
				}
			}, actionName);

			inputManager.addMapping(actionName, binding.getTrigger());
		}
	}

	public Material reloadMaterial(Material mat) {
		//clear the entire cache, there might be more clever things to do, like clearing only the matdef, and the associated shaders.
		assetManager.clearCache();

		//creating a dummy mat with the mat def of the mat to reload
		Material dummy = new Material(assetManager, mat.getMaterialDef().getAssetName());

		for (MatParam matParam : mat.getParams()) {
			dummy.setParam(matParam.getName(), matParam.getVarType(), matParam.getValue());
		}

		dummy.getAdditionalRenderState().set(mat.getAdditionalRenderState());

		//creating a dummy geom and assigning the dummy material to it
		Geometry dummyGeom = new Geometry("dummyGeom", new Box(1f, 1f, 1f));
		dummyGeom.setMaterial(dummy);

		try {
			//preloading the dummyGeom, this call will compile the shader again
			renderManager.preloadScene(dummyGeom);
		} catch (RendererException e) {
			//compilation error, the shader code will be output to the console
			//the following code will output the error
			//System.err.println(e.getMessage());
			Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, e.getMessage());
			return null;
		}

		Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.INFO, "Material succesfully reloaded");
		//System.out.println("Material succesfully reloaded");
		return dummy;
	}

	private Material reloadMaterial(String assetName) {
		//clear the entire cache, there might be more clever things to do, like clearing only the matdef, and the associated shaders.
		assetManager.clearCache();

		//creating a dummy mat with the mat def of the mat to reload
		Material dummy = new Material(assetManager, assetName);

		//creating a dummy geom and assigning the dummy material to it
		Geometry dummyGeom = new Geometry("dummyGeom", new Box(1f, 1f, 1f));
		dummyGeom.setMaterial(dummy);

		try {
			//preloading the dummyGeom, this call will compile the shader again
			renderManager.preloadScene(dummyGeom);
		} catch (RendererException e) {
			//compilation error, the shader code will be output to the console
			//the following code will output the error
			//System.err.println(e.getMessage());
			Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, e.getMessage());
			return null;
		}

		Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.INFO, "Material succesfully reloaded");
		//System.out.println("Material succesfully reloaded");
		return dummy;
	}

	@Override
	public void update(float tpf) {
		for (Trigger trigger : fileTriggers.keySet()) {
			if (trigger instanceof FileChangedTrigger) {
				FileChangedTrigger t = (FileChangedTrigger) trigger;
				if (t.shouldFire()) {
					List<Binding> b = fileTriggers.get(t);
					b.forEach(Binding::reload);
				}
			}
		}
	}

	private interface Binding {

		public String getActionName();

		public void reload();

		public Trigger getTrigger();
	}

	private class AssetNameBinding implements Binding {

		final Trigger trigger;
		final Consumer<Material> callback;
		private final String assetName;

		public AssetNameBinding(Trigger trigger, String assetName, Consumer<Material> callback) {
			this.trigger = trigger;
			this.callback = callback;
			this.assetName = assetName;
		}

		@Override
		public void reload() {
			Material reloadedMat = reloadMaterial(assetName);
			//if the reload is successful, we setup the material with its params and reassign it to the box
			if (reloadedMat != null) {
				// setupMaterial(reloadedMat);
				callback.accept(reloadedMat);
			}
		}

		@Override
		public String getActionName() {
			return assetName + "Reload";

		}

		@Override
		public Trigger getTrigger() {
			return trigger;
		}
	}

	private class TerrainQuadBinding implements Binding {

		final Trigger trigger;
		final TerrainQuad quad;

		public TerrainQuadBinding(Trigger trigger, TerrainQuad quad) {
			this.trigger = trigger;
			this.quad = quad;
		}

		@Override
		public void reload() {
			Material reloadedMat = reloadMaterial(quad.getMaterial());
			//if the reload is successful, we setup the material with its params and reassign it to the box
			if (reloadedMat != null) {
				// setupMaterial(reloadedMat);
				quad.setMaterial(reloadedMat);
			}
		}

		@Override
		public String getActionName() {
			return quad.getName() + "Reload";
		}

		@Override
		public Trigger getTrigger() {
			return trigger;
		}
	}

	private class GeometryBinding implements Binding {

		final Spatial association;
		final Trigger trigger;
		final Geometry geom;
		final Consumer<Material> callback;

		public GeometryBinding(Trigger trigger, Consumer<Material> callback, Spatial association, Geometry geom) {
			this.trigger = trigger;
			this.geom = geom;
			this.callback = callback;
			this.association = association;
		}

		@Override
		public void reload() {
			Material reloadedMat = reloadMaterial(geom.getMaterial());
			//if the reload is successful, we setup the material with its params and reassign it to the box
			if (reloadedMat != null) {
				// setupMaterial(reloadedMat);
				geom.setMaterial(reloadedMat);
				callback.accept(reloadedMat);
			}
		}

		@Override
		public String getActionName() {
			return geom.getName() + "Reload";

		}

		@Override
		public Trigger getTrigger() {
			return trigger;
		}
	}

	private class FilterBinding implements Binding {

		final Trigger trigger;
		final Filter filter;

		public FilterBinding(Trigger trigger, Filter filter) {
			this.trigger = trigger;
			this.filter = filter;
		}

		@Override
		public void reload() {
			Field[] fields1 = filter.getClass().getDeclaredFields();
			Field[] fields2 = filter.getClass().getSuperclass().getDeclaredFields();

			List<Field> fields = new LinkedList<>();
			fields.addAll(Arrays.asList(fields1));
			fields.addAll(Arrays.asList(fields2));
			Material m = new Material();
			Filter.Pass p = filter.new Pass();
			try {
				for (Field field : fields) {
					if (field.getType().isInstance(m)) {
						field.setAccessible(true);
						Material mat = reloadMaterial((Material) field.get(filter));
						if (mat == null) {
							return;
						}
						field.set(filter, mat);
					}
					if (field.getType().isInstance(p)) {
						field.setAccessible(true);
						p = (Filter.Pass) field.get(filter);
						if (p != null && p.getPassMaterial() != null) {
							Material mat = reloadMaterial(p.getPassMaterial());
							if (mat == null) {
								return;
							}
							p.setPassMaterial(mat);
						}
					}
					if (field.getName().equals("postRenderPasses")) {
						field.setAccessible(true);
						List<Pass> passes = (List<Pass>) field.get(filter);
						if (passes != null) {
							for (Pass pass : passes) {
								Material mat = reloadMaterial(pass.getPassMaterial());
								if (mat == null) {
									return;
								}
								pass.setPassMaterial(mat);
							}
						}
					}
				}
			} catch (IllegalArgumentException | IllegalAccessException ex) {
				Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
			}

		}

		@Override
		public String getActionName() {
			return filter.getName() + "Reload";
		}

		@Override
		public Trigger getTrigger() {
			return trigger;
		}
	}

	private class FileChangedTrigger implements Trigger {

		String fileName;
		File file;
		Long fileLastM;

		public FileChangedTrigger(String fileName) {
			this.fileName = fileName;
		}

		public void init() {
			AssetInfo info = assetManager.locateAsset(new AssetKey<>(fileName));
			if (info != null && info instanceof UrlAssetInfo) {
				try {
					Field f = info.getClass().getDeclaredField("url");
					f.setAccessible(true);
					URL url = (URL) f.get(info);
					file = new File(url.getFile().replace("%20", " "));
					fileLastM = file.lastModified();
					if (fileLastM == 0L) {
						Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, "The file does not exist or an I/O error occured: {0}", file.toString());
					}
				} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
					Logger.getLogger(MaterialDebugAppState.class.getName()).log(Level.SEVERE, null, ex);
				}
			}
		}

		public boolean shouldFire() {
			if (file.lastModified() != fileLastM) {
				fileLastM = file.lastModified();
				return true;
			}
			return false;
		}

		@Override
		public String getName() {
			return fileName;
		}

		@Override
		public int triggerHashCode() {
			return 0;
		}
	}
}

example for TerrainQuads:

add:

MaterialDebugAppState state = stateManager.getState(MaterialDebugAppState.class);
if (state != null) {
	TechniqueDef technique = material.getMaterialDef().getTechniqueDefs(TechniqueDef.DEFAULT_TECHNIQUE_NAME).get(0);

	state.registerBinding(technique.getVertexShaderName(), terrainQuad);
	state.registerBinding(technique.getFragmentShaderName(), terrainQuad);
	state.registerBinding("Common/ShaderLib/Effects.glsllib", terrainQuad);
}

remove

for (TerrainQuad t : terrain) {
	Material material = t.getMaterial();
	MaterialDebugAppState state = stateManager.getState(MaterialDebugAppState.class);
	if (state != null) {
		TechniqueDef technique = material.getMaterialDef().getTechniqueDefs(TechniqueDef.DEFAULT_TECHNIQUE_NAME).get(0);
		state.removeBinding(technique.getVertexShaderName(), t);
		state.removeBinding(technique.getFragmentShaderName(), t);
		state.removeBinding("Common/ShaderLib/Effects.glsllib", t);
	}
}

or when i debug the Waterfilter:

    if (!debugAppState.hasBinding("MatDefs/Post/Water.frag", waterFilter)) {
        debugAppState.registerBinding("MatDefs/Post/Water.frag", waterFilter);
    }
    if (!debugAppState.hasBinding("Common/ShaderLib/Water.glsllib", waterFilter) {
        debugAppState.registerBinding("Common/ShaderLib/Water.glsllib", waterFilter);
    }