Debugging shaders in cases where there’s no errors like this is always tricky, especially when I’m not able to reproduce the issue on my device.
Usually I’d place a bunch of discard() or gl.fragColor assignments to determine where the shader may be crashing or to see if it is returning an incorrect or empty value for a variable that goes into the lighting equation… but unlike debugging java code, the debug output from a shader is not in text/numbers , and the best you can do is output a variable to each pixel and see what it looks like when it gets rendered onto the model. So its hard to describe to someone else what code to change and what visual indicators to look for to determine if something is working corectly or not.
But aside from that there is only one more thing I can think of that you could easily check for me.
I code all my shaders to have an Int param called DebugValuesMode, and that renders the model with a a single un-lit color (either the final albedo, normal, roughness, metallic, ao, or emissive color) and that can let me know if it is an issue with one of those channels, or if the shader possibly is bugging out and silently crashing between the calculation of 2 layers.
Here’s an updated test case where you can press the O key to toggle which value gets output for debug purposes:
package com.github.shader.pack;
import BlendLayerEffect.BlendLayerEffect;
import java.util.LinkedList;
import java.util.Queue;
import com.github.mat.editor.MatPropertyPanelBuilder;
import com.github.tools.GameObject;
import com.github.tools.editor.AbstractEditor;
import com.github.tools.editor.ReflectedEditorBuilder;
import com.github.tools.editor.ReflectedEditorBuilder.StringFilterMode;
import com.jme3.anim.AnimComposer;
import com.jme3.anim.SkinningControl;
import com.jme3.app.Application;
import com.jme3.app.ChaseCameraAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.light.LightProbe;
import com.jme3.material.MatParam;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.FXAAFilter;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitorAdapter;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.custom.ArmatureDebugAppState;
import com.jme3.shader.VarType;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.shadow.EdgeFilteringMode;
import com.jme3.system.AppSettings;
import com.jme3.util.SkyFactory;
import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
import com.simsilica.lemur.Axis;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.DefaultRangedValueModel;
import com.simsilica.lemur.FillMode;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.Panel;
import com.simsilica.lemur.RollupPanel;
import com.simsilica.lemur.Slider;
import com.simsilica.lemur.component.SpringGridLayout;
import com.simsilica.lemur.core.VersionedReference;
import com.simsilica.lemur.props.PropertyPanel;
import com.simsilica.lemur.style.BaseStyles;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.beans.IntrospectionException;
import java.util.ArrayList;
/**
*
* @author capdevon
*/
public class Test_CharacterEffects1 extends SimpleApplication {
private static int width, height;
public BlendLayerEffect iceEffect, stoneEffect, shieldEffect;
private ArrayList<BlendLayerEffect> activeEffectsInOrder = new ArrayList<>();
private ArrayList<Slider> blendValSliders = new ArrayList<>();
private ArrayList<VersionedReference> sliderVersionedReferences = new ArrayList<>();
private int shaderDebugMode = -1;
/**
*
* @param args
*/
public static void main(String[] args) {
Test_CharacterEffects1 app = new Test_CharacterEffects1();
AppSettings settings = new AppSettings(true);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
width = (int) (screenSize.getWidth() * .99f);
height = (int) (screenSize.getHeight() * .93f);
settings.put("Width", width);
settings.put("Height", height);
app.setSettings(settings);
app.setShowSettings(false);
app.setPauseOnLostFocus(false);
app.start();
}
private static final String MODEL_ASSET_PATH = "Models/Erika/Erika.j3o"; //"Models/YBot/YBot.j3o";
private BitmapText hud;
private BitmapText shaderDebugModeText;
private Spatial model;
private AnimComposer animComposer;
private SkinningControl skinningControl;
private ArmatureDebugAppState armatureDebugState;
private final Queue<String> animsQueue = new LinkedList<>();
@Override
public void simpleInitApp() {
armatureDebugState = new ArmatureDebugAppState();
stateManager.attach(armatureDebugState);
stateManager.attach(new DebugGridState());
hud = makeLabelUI("", ColorRGBA.Blue, guiNode);
hud.setLocalTranslation(10f, settings.getHeight() - 10f, 0);
shaderDebugModeText = makeLabelUI("Debug Mode: -1", ColorRGBA.White, guiNode);
shaderDebugModeText.setLocalTranslation(10f, settings.getHeight() - 30f, 0);
rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
initLemur(this);
configureCamera();
setupCamera();
setupSky();
initFilters();
loadModel();
initEffects();
initMaterialEditor();
initKeys();
}
private void initEffects(){
iceEffect = new BlendLayerEffect("Freeze", 0, model);
// iceEffect.addMaterialsFromSpatial(erikaSpatial);
iceEffect.setBaseColorMap(assetManager.loadTexture("Models/Cracked_Ice/DefaultMaterial_baseColor.png"));
iceEffect.setNormalMap(assetManager.loadTexture("Models/Cracked_Ice/DefaultMaterial_normal.png"));
iceEffect.setMetallicRoughnessAoMap(assetManager.loadTexture("Models/Cracked_Ice/DefaultMaterial_occlusionRoughnessMetallic.png"));
stoneEffect = new BlendLayerEffect("Petrify", 1, model);
// stoneEffect.addMaterialsFromSpatial(yBotSpatial);
stoneEffect.setBaseColorMap(assetManager.loadTexture("Models/Cracked_Stone/DefaultMaterial_baseColor.png"));
stoneEffect.setNormalMap(assetManager.loadTexture("Models/Cracked_Stone/DefaultMaterial_normal.png"));
stoneEffect.setMetallicRoughnessAoMap(assetManager.loadTexture("Models/Cracked_Stone/DefaultMaterial_occlusionRoughnessMetallic.png"));
shieldEffect = new BlendLayerEffect("Shield", 2, model);
// shieldEffect.addMaterialsFromSpatial(yBotSpatial);
shieldEffect.setBaseColorMap(assetManager.loadTexture("Models/Shield_Armor/DefaultMaterial_baseColor.png"));
shieldEffect.setNormalMap(assetManager.loadTexture("Models/Shield_Armor/DefaultMaterial_normal.png"));
shieldEffect.setMetallicRoughnessAoMap(assetManager.loadTexture("Models/Shield_Armor/DefaultMaterial_occlusionRoughnessMetallic.png"));
shieldEffect.setEmissiveMap(assetManager.loadTexture("Models/Shield_Armor/DefaultMaterial_emissive.png"));
shieldEffect.setBlendAlpha(true);
registerBlendEffectToSlider(iceEffect);
registerBlendEffectToSlider(stoneEffect);
registerBlendEffectToSlider(shieldEffect);
}
@Override
public void simpleUpdate(float tpf) {
super.simpleUpdate(tpf);
// float frameScaleAmt = tpf * 0.05f;
// model.setLocalScale(model.getLocalScale().add(frameScaleAmt, frameScaleAmt, frameScaleAmt));
for(int i = 0; i < sliderVersionedReferences.size(); i++){
VersionedReference sliderVersionedReference = sliderVersionedReferences.get(i);
Slider slider = blendValSliders.get(i);
if(sliderVersionedReference.update()){
float val = (float) slider.getModel().getValue();
BlendLayerEffect blendEffect = activeEffectsInOrder.get(i);
blendEffect.setBlendValue(val);
}
}
}
private void registerBlendEffectToSlider(BlendLayerEffect blendLayerEffect){
activeEffectsInOrder.add(blendLayerEffect);
//make container for tweaking this blend effect (currently only has a slider for blendVal)
Container blendLayerContainer = new Container(new SpringGridLayout(Axis.Y, Axis.X, FillMode.None, FillMode.Even));
Label label = new Label(blendLayerEffect.getName());
Slider slider = new Slider();
DefaultRangedValueModel sizeSliderModel = new DefaultRangedValueModel(0, 1.0, 0);
slider.setModel(sizeSliderModel);
blendValSliders.add(slider);
sliderVersionedReferences.add(sizeSliderModel.createReference());
float boxWidth = width * 0.15f;
float xOffset = (boxWidth * 0.1f) + (blendValSliders.size() * boxWidth);
blendLayerContainer.setLocalTranslation(xOffset,height * 0.94f, 1);
// blendLayerContainer.setPreferredSize(new Vector3f(boxWidth,50,1));
slider.setPreferredSize(new Vector3f(boxWidth,25,1));
blendLayerContainer.addChild(label);
blendLayerContainer.addChild(slider);
MatPropertyPanelBuilder matPropertyPanelForLayer = new MatPropertyPanelBuilder(StringFilterMode.IncludeStartingWith, blendLayerEffect.getParamStringPrefix());
Container matPropertyContainer = matPropertyPanelForLayer.buildPanel(model);
ReflectedEditorBuilder builder = new ReflectedEditorBuilder();
Container layerParamsContainer = builder.buildPanel(blendLayerEffect);
blendLayerContainer.addChild(layerParamsContainer);
// layerParamsContainer.setLocalTranslation(xOffset,height * 0.94f, 1);
guiNode.attachChild(blendLayerContainer);
}
private void setCorrectShader(Spatial spatial){
spatial.breadthFirstTraversal(new SceneGraphVisitorAdapter() {
@Override
public void visit(Geometry geom) {
Material oldMat = geom.getMaterial();
Material characterMat = new Material(assetManager, "MatDefs/PBRCharacters.j3md");
ArrayList<MatParam> matParams = new ArrayList<>(oldMat.getParams());
for(int p = 0; p < matParams.size(); p++){
MatParam matParam = matParams.get(p);
if(matParam.getValue() != null){
Object val = matParam.getValue();
characterMat.setParam(matParam.getName(), matParam.getVarType(), val);
}
}
geom.setMaterial(characterMat);
}
});
}
private void configureCamera() {
float aspect = (float) cam.getWidth() / cam.getHeight();
cam.setFrustumPerspective(45, aspect, 0.01f, 1000f);
// flyCam.setDragToRotate(true);
// flyCam.setMoveSpeed(25);
}
private void setupCamera() {
// disable the default 1st-person flyCam!
flyCam.setEnabled(false);
Node target = new Node("CamTarget");
target.move(0, 1, 0);
ChaseCameraAppState chaseCam = new ChaseCameraAppState();
chaseCam.setTarget(target);
stateManager.attach(chaseCam);
chaseCam.setInvertHorizontalAxis(true);
chaseCam.setInvertVerticalAxis(true);
chaseCam.setZoomSpeed(0.5f);
chaseCam.setMinDistance(1);
chaseCam.setMaxDistance(10);
chaseCam.setDefaultDistance(3);
chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
chaseCam.setRotationSpeed(3);
chaseCam.setDefaultVerticalRotation(0.3f);
}
/**
* a sky as background
*/
private void setupSky() {
Spatial sky = SkyFactory.createSky(assetManager, "Scenes/Beach/FullskiesSunset0068.dds", SkyFactory.EnvMapType.CubeMap);
sky.setShadowMode(RenderQueue.ShadowMode.Off);
rootNode.attachChild(sky);
}
private void initFilters() {
DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-0.2f, -1, -0.3f).normalizeLocal());
sun.setDirection(new Vector3f(0.0f, -1.0f, 0.0f).normalizeLocal());
rootNode.addLight(sun);
AmbientLight ambient = new AmbientLight();
ambient.setColor(new ColorRGBA(0.25f, 0.25f, 0.25f, 1));
// rootNode.addLight(ambient);
// add a PBR probe.
Spatial probeModel = assetManager.loadModel("Scenes/defaultProbe.j3o");
LightProbe lightProbe = (LightProbe) probeModel.getLocalLightList().get(0);
lightProbe.getArea().setRadius(100);
rootNode.addLight(lightProbe);
FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
viewPort.addProcessor(fpp);
DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 4096, 3);
dlsf.setLight(sun);
dlsf.setShadowIntensity(0.4f);
dlsf.setShadowZExtend(256);
dlsf.setEdgeFilteringMode(EdgeFilteringMode.PCFPOISSON);
fpp.addFilter(dlsf);
FXAAFilter fxaa = new FXAAFilter();
fpp.addFilter(fxaa);
}
private void loadModel() {
model = assetManager.loadModel(MODEL_ASSET_PATH);
setCorrectShader(model);
rootNode.attachChild(model);
animComposer = GameObject.getComponentInChildren(model, AnimComposer.class);
animsQueue.addAll(animComposer.getAnimClipsNames());
animsQueue.forEach(System.out::println);
skinningControl = GameObject.getComponentInChildren(model, SkinningControl.class);
armatureDebugState.addArmatureFrom(skinningControl);
// skinningControl.setHardwareSkinningPreferred(false);
}
private BitmapText makeLabelUI(String text, ColorRGBA color, Node parent) {
BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt");
BitmapText bmp = new BitmapText(font);
bmp.setName("MyLabel");
bmp.setText(text);
bmp.setColor(color);
bmp.setSize(font.getCharSet().getRenderedSize());
parent.attachChild(bmp);
return bmp;
}
private void initLemur(Application app) {
// initialize lemur
GuiGlobals.initialize(app);
BaseStyles.loadGlassStyle();
GuiGlobals.getInstance().getStyles().setDefaultStyle("glass");
}
private void initMaterialEditor() {
MatPropertyPanelBuilder builder = new MatPropertyPanelBuilder(StringFilterMode.IgnoreStartingWith, "BlendLayer");
Container container = builder.buildPanel(model);
container.setLocalTranslation(settings.getWidth() - settings.getWidth() / 4f, settings.getHeight() - 10f, 1);
guiNode.attachChild(container);
}
/**
* Mapping a named action to a key input.
*/
private void initKeys() {
addMapping("Speed+", new KeyTrigger(KeyInput.KEY_U));
addMapping("Speed-", new KeyTrigger(KeyInput.KEY_I));
addMapping("Next", new KeyTrigger(KeyInput.KEY_N));
addMapping("ToggleArmature", new KeyTrigger(KeyInput.KEY_M));
addMapping("ToggleShaderDebugMode", new KeyTrigger(KeyInput.KEY_O));
addMapping("ToggleShaderDebugMode_Normals", new KeyTrigger(KeyInput.KEY_L));
}
private void addMapping(String mappingName, Trigger... triggers) {
inputManager.addMapping(mappingName, triggers);
inputManager.addListener(actionListener, mappingName);
}
/**
* Defining the named action that can be triggered by key inputs.
*/
private final ActionListener actionListener = new ActionListener() {
@Override
public void onAction(String name, boolean keyPressed, float tpf) {
if (!keyPressed) {
return;
}
if (name.equals("Next")) {
String anim = animsQueue.poll();
animsQueue.add(anim);
animComposer.setCurrentAction(anim);
hud.setText(anim);
} else if (name.equals("Speed+")) {
float glSpeed = animComposer.getGlobalSpeed();
animComposer.setGlobalSpeed(glSpeed + 0.1f);
} else if (name.equals("Speed-")) {
float glSpeed = animComposer.getGlobalSpeed();
animComposer.setGlobalSpeed(glSpeed - 0.1f);
} else if (name.equals("ToggleArmature")) {
armatureDebugState.setEnabled(!armatureDebugState.isEnabled());
} else if (name.equals("ToggleShaderDebugMode")) {
shaderDebugMode++;
if(shaderDebugMode > 5){
shaderDebugMode = -1;
}
iceEffect.setParam("DebugValuesMode", VarType.Int, shaderDebugMode);
shaderDebugModeText.setText("Shader Debug Mode: " + shaderDebugMode);
}else if (name.equals("ToggleShaderDebugMode_Normals")) {
if(shaderDebugMode == 1){
shaderDebugMode = -1;
}else{
shaderDebugMode = 1;
}
iceEffect.setParam("DebugValuesMode", VarType.Int, shaderDebugMode);
shaderDebugModeText.setText("Shader Debug Mode: " + shaderDebugMode);
}
}
};
}
Presing the O key will cycle through each debug mode like in this example video (I also did a quick preview of the height scan effect so far since thats the last part I was working on):
Can you run that test case and take a video (or a series of screenshots) showing what each debug output looks like with all 6 different modes?
If just one of those debug layers is broken, then that would be a good indicator of where things are going wrong. And if they all work, then that is an indicator that the issue is in the final lighting equation. And if none work, then something is going wrong very early on in the shader code.
I also should’ve mentioned to check that your GPU drivers are up to date, although I highly doubt that’s the issue otherwise you probably would’ve had a ton of other issues with jme before this point. But that is usually the first important thing I would tell a non jme-dev to check if they were testing my app and had a similar issue where a shader is breaking without crashing or giving a catchable error.