Re-Compile Shaders During Runtime

Hello,
Sorry if this is a stupid question, but is there a way to recompile or reload a shader or material? I couldn’t really find a way to do it and it seems like it would make general development move a long at a much quicker pace…
Thanks!

@okelly4408 said: Hello, Sorry if this is a stupid question, but is there a way to recompile or reload a shader or material? I couldn't really find a way to do it and it seems like it would make general development move a long at a much quicker pace... Thanks!

I know that shaders will get recompiled if a uniform that effects a pre-processor directive #define is changed.

I guess you could try swapping out the shaders in assets jar while the app is running and then set up a trigger to modify one of these uniforms to try and force a recompile of the shader.

No clue if it will actual work… but it might /shrug

EDIT: Unfortunately, I don’t think there is a way of doing this with the Material itself though. So this would be limited to shader updates only.

Probably it would need to be removed from the cache also. Even though the shader gets recompiled, I’m 99% sure that it’s code is cached.

@pspeed said: Probably it would need to be removed from the cache also. Even though the shader gets recompiled, I'm 99% sure that it's code is cached.

Ah… bummer. Still, not sure how useful this would have been if it worked anyways. More than half the time, the update would require some change to the Material Def anyways =(

@okelly4408 I tricked jME to this some time go…

It was very dirty but it kind of worked for development.

Warning! Do not use this technique unless you want something to break…

  1. Create you own material class (extend Material) and override the render(Geometry geom, RenderManager rm) method and make sure it catches any render exceptions. You can delegate rendering to a “fallback” material here if you got an exception.

  1. ((DesktopAssetManager)assetManager).clearCache(); to clear assets from the cache.
  2. Create a new instance of your material and apply.

Do step 2-3 every time you wanna reload the material. I would be very happy if someone invented something smarter than this…

@kwando said: @okelly4408 I tricked jME to this some time go..

It was very dirty but it kind of worked for development.

Warning! Do not use this technique unless you want something to break…

  1. Create you own material class (extend Material) and override the render(Geometry geom, RenderManager rm) method and make sure it catches any render exceptions. You can delegate rendering to a “fallback” material here if you got an exception.

  1. ((DesktopAssetManager)assetManager).clearCache(); to clear assets from the cache.
  2. Create a new instance of your material and apply.

Do step 2-3 every time you wanna reload the material. I would be very happy if someone invented something smarter than this…


I did something dirty like that too, when I was trying out shaders :P. And even though it was a dirty trick, it saved me loads of time as fine tuning / finding right colors etc. are soooo much faster. I probably still have the code, so when I’m back home later this evening I’ll see if I can get a working snippet of this online!

Actually, one could use renderManager.preloadScene(Spatial)

Each time you want to reload the material, assign it to a dummy spatial and preload it. If there are compilation error it will throw a RendererException.
if it pass then assign it in your scene and go on.

We use that for hardware skinning in the skeleton control. The shader is preloaded with hardware skinning enabled, if it fails (due to hardware not supporting it) we revert back to software skinning.
Not sure it will work out of the box (maybe you’ll have to use a dummy material and ping pong) but it would be less hacky.

EDIT : actually I’m gonna try that because that would be a huge enhancement for the ShaderNode editor too.

Here we go, it works pretty well…I wish I did that 4 years ago …meh…

Maybe I’ll make some utility class in the engine for this.

[java]

import com.jme3.app.ChaseCameraAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.DesktopAssetManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.material.Material;
import com.jme3.renderer.RendererException;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;

public class TestMaterialHotReload extends SimpleApplication {

public static void main(String[] args) {
    TestMaterialHotReload app = new TestMaterialHotReload();

    //ignore this, it's just for convenience
    AppSettings settings = new AppSettings(true);
    settings.setFrameRate(30);
    settings.setResolution(1280, 720);
    app.setSettings(settings);
    app.setShowSettings(false);

    app.start();
}

@Override
public void simpleInitApp() {

    //a box geom that will have the material we want to test 
    final Geometry boxGeo = new Geometry("Box", new Box(1f, 1f, 1f));
    rootNode.attachChild(boxGeo);
    //create the material
    final Material mat = createMaterial();
    boxGeo.setMaterial(mat);

    //ignore this, it's just for convenience
    flyCam.setEnabled(false);
    ChaseCameraAppState chaseCam = new ChaseCameraAppState();
    chaseCam.setTarget(boxGeo);
    stateManager.attach(chaseCam);

    inputManager.addListener(new ActionListener() {
        public void onAction(String name, boolean isPressed, float tpf) {
            if ("reload".equals(name) && isPressed) {
                //reloading the material 
                Material reloadedMat = reloadMaterial(mat);
                //if the reload is successful, we re setupt the material with its params and reassign it to the box
                if (reloadedMat != null) {
                    setupMaterial(reloadedMat);
                    boxGeo.setMaterial(reloadedMat);
                }
            }
        }
    }, "reload");

    //hit R to realod the material
    inputManager.addMapping("reload", new KeyTrigger(KeyInput.KEY_R));
}

private 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.
    ((DesktopAssetManager) assetManager).clearCache();

    //creating a dummy mat with the mat def of the mat to reload
    Material dummy = new Material(mat.getMaterialDef());
    //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());
        return null;
    }

    System.out.println("Material succesfully reloaded");
    return dummy;
}

/**
 * Creates the material, use whatever material that uses the shader you are
 * coding
 *
 * @return
 */
protected Material createMaterial() {
    Material mat = new Material(assetManager, "MatDefs/distort.j3md");
    setupMaterial(mat);
    return mat;
}

/**
 * sets the parameters to your material
 *
 * @param mat
 */
protected void setupMaterial(Material mat) {
    mat.setTexture("ColorMap", assetManager.loadTexture("com/jme3/app/Monkey.png"));
    mat.setBoolean("UseDistort", true);
}

}

[/java]

Note that for a Filter, you may have to remove the filter from the fpp, create a new one and re-add it to the fpp after the material has been successfully reloaded.

4 Likes

@nehon, really cool.

@nehon Right, I forgot about that preload method. Your suggestion is cleaner! =)
I was experimenting with a smarter way of expiring the cache, I was using a java7 to watch the filesystem for changes and then expire the right asset from cache… but that was kind of complicated and for simple usage it is was fast enough to just blow the cache away…
Maybe provide a way disable asset caching? (I do not know hard that would be though or how much of jME relies on the caching for performance).

I would like to have a fallback on materials, to me it is nicer if game does not crash and burn on a bad shader or missing texture.

@nehon said: Here we go, it works pretty well....I wish I did that 4 years ago ...meh...

Maybe I’ll make some utility class in the engine for this.

[java]

import com.jme3.app.ChaseCameraAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.DesktopAssetManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.material.Material;
import com.jme3.renderer.RendererException;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;

public class TestMaterialHotReload extends SimpleApplication {

public static void main(String[] args) {
    TestMaterialHotReload app = new TestMaterialHotReload();

    //ignore this, it's just for convenience
    AppSettings settings = new AppSettings(true);
    settings.setFrameRate(30);
    settings.setResolution(1280, 720);
    app.setSettings(settings);
    app.setShowSettings(false);

    app.start();
}

@Override
public void simpleInitApp() {

    //a box geom that will have the material we want to test 
    final Geometry boxGeo = new Geometry("Box", new Box(1f, 1f, 1f));
    rootNode.attachChild(boxGeo);
    //create the material
    final Material mat = createMaterial();
    boxGeo.setMaterial(mat);

    //ignore this, it's just for convenience
    flyCam.setEnabled(false);
    ChaseCameraAppState chaseCam = new ChaseCameraAppState();
    chaseCam.setTarget(boxGeo);
    stateManager.attach(chaseCam);

    inputManager.addListener(new ActionListener() {
        public void onAction(String name, boolean isPressed, float tpf) {
            if ("reload".equals(name) && isPressed) {
                //reloading the material 
                Material reloadedMat = reloadMaterial(mat);
                //if the reload is successful, we re setupt the material with its params and reassign it to the box
                if (reloadedMat != null) {
                    setupMaterial(reloadedMat);
                    boxGeo.setMaterial(reloadedMat);
                }
            }
        }
    }, "reload");

    //hit R to realod the material
    inputManager.addMapping("reload", new KeyTrigger(KeyInput.KEY_R));
}

private 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.
    ((DesktopAssetManager) assetManager).clearCache();

    //creating a dummy mat with the mat def of the mat to reload
    Material dummy = new Material(mat.getMaterialDef());
    //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());
        return null;
    }

    System.out.println("Material succesfully reloaded");
    return dummy;
}

/**
 * Creates the material, use whatever material that uses the shader you are
 * coding
 *
 * @return
 */
protected Material createMaterial() {
    Material mat = new Material(assetManager, "MatDefs/distort.j3md");
    setupMaterial(mat);
    return mat;
}

/**
 * sets the parameters to your material
 *
 * @param mat
 */
protected void setupMaterial(Material mat) {
    mat.setTexture("ColorMap", assetManager.loadTexture("com/jme3/app/Monkey.png"));
    mat.setBoolean("UseDistort", true);
}

}

[/java]

Note that for a Filter, you may have to remove the filter from the fpp, create a new one and re-add it to the fpp after the material has been successfully reloaded.

Wow!! Thanks a lot, this is perfect. You are the man.

There is a small flaw in this code though.
you should insert
setupMaterial(dummy);
on line 68 and remove setupMaterial(reloadedMat); on line 51

The params have to be set to the mat before preloading the scene. Some params may set some defines and the shader source won’t be exactly the same when preloaded.

For example if you have an error inside a #ifdef the previous code won’t raise the error when reloading, but will crash afterward.
Setting the params before reloading fixes the issue