From 070b70c14738dbe9ce7d147fe760741e6adff7ad Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 16:04:26 +0100 Subject: [PATCH 1/2] feat(material): add MaterialDef inheritance support Allow MaterialDef to extend another MaterialDef using colon syntax: MaterialDef Child : parent.j3md { ... } Parent params and techniques are flattened into child at load time. Child can override param defaults, swap individual shaders, add defines/world params additively, and override render state. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/jme3/material/plugins/J3MLoader.java | 375 +++++++++++++++++- .../jme3/material/plugins/J3MLoaderTest.java | 248 ++++++++++++ .../test/resources/child-matdef-empty.j3md | 2 + .../resources/child-matdef-new-technique.j3md | 6 + .../resources/child-matdef-params-only.j3md | 5 + .../resources/child-matdef-renderstate.j3md | 8 + .../child-matdef-technique-only.j3md | 5 + .../resources/child-matdef-type-mismatch.j3md | 5 + .../resources/child-matdef-vertshader.j3md | 5 + .../resources/child-matdef-worldparams.j3md | 7 + .../src/test/resources/child-matdef.j3md | 12 + .../src/test/resources/parent-matdef.j3md | 20 + 12 files changed, 687 insertions(+), 11 deletions(-) create mode 100644 jme3-core/src/test/resources/child-matdef-empty.j3md create mode 100644 jme3-core/src/test/resources/child-matdef-new-technique.j3md create mode 100644 jme3-core/src/test/resources/child-matdef-params-only.j3md create mode 100644 jme3-core/src/test/resources/child-matdef-renderstate.j3md create mode 100644 jme3-core/src/test/resources/child-matdef-technique-only.j3md create mode 100644 jme3-core/src/test/resources/child-matdef-type-mismatch.j3md create mode 100644 jme3-core/src/test/resources/child-matdef-vertshader.j3md create mode 100644 jme3-core/src/test/resources/child-matdef-worldparams.j3md create mode 100644 jme3-core/src/test/resources/child-matdef.j3md create mode 100644 jme3-core/src/test/resources/parent-matdef.j3md diff --git a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java index 9999f5d3ec..b9b64ccb0a 100644 --- a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java +++ b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java @@ -713,6 +713,314 @@ private void readTechnique(Statement techStat) throws IOException{ presetDefines.clear(); } + private void readExtendingDefMaterialParams(List paramsList) throws IOException { + for (Statement statement : paramsList) { + readExtendingDefParam(statement.getLine()); + } + } + + private void readExtendingDefParam(String statement) throws IOException { + String name; + String defaultVal = null; + ColorSpace colorSpace = null; + + String[] split = statement.split(":"); + + // Parse default val + if (split.length == 1) { + // No default value + } else { + if (split.length != 2) { + throw new IOException("Parameter statement syntax incorrect"); + } + statement = split[0].trim(); + defaultVal = split[1].trim(); + } + + if (statement.endsWith("-LINEAR")) { + colorSpace = ColorSpace.Linear; + statement = statement.substring(0, statement.length() - "-LINEAR".length()); + } + + // Parse ffbinding + int startParen = statement.indexOf("("); + if (startParen != -1) { + int endParen = statement.indexOf(")", startParen); + statement = statement.substring(0, startParen); + } + + // Parse type + name + split = statement.split(whitespacePattern); + if (split.length != 2) { + throw new IOException("Parameter statement syntax incorrect"); + } + + VarType type; + if (split[0].equals("Color")) { + type = VarType.Vector4; + } else { + type = VarType.valueOf(split[0]); + } + + name = split[1]; + + MatParam existingParam = materialDef.getMaterialParam(name); + if (existingParam != null) { + // Validate type matches + if (existingParam.getVarType() != type) { + throw new IOException("Cannot override parameter '" + name + + "' with type '" + type + "', parent declares it as '" + existingParam.getVarType() + "'"); + } + // Update default value + if (defaultVal != null) { + Object defaultValObj = readValue(type, defaultVal); + existingParam.setValue(defaultValObj); + } + } else { + // New parameter + Object defaultValObj = null; + if (defaultVal != null) { + defaultValObj = readValue(type, defaultVal); + } + if (type.isTextureType()) { + materialDef.addMaterialParamTexture(type, name, colorSpace, (Texture) defaultValObj); + } else { + materialDef.addMaterialParam(type, name, defaultValObj); + } + } + } + + private void readExtendingDefTechnique(Statement techStat) throws IOException { + String[] split = techStat.getLine().split(whitespacePattern); + + String name; + if (split.length == 1) { + name = TechniqueDef.DEFAULT_TECHNIQUE_NAME; + } else if (split.length == 2) { + name = split[1]; + } else { + throw new IOException("Technique statement syntax incorrect"); + } + + List existingTechs = materialDef.getTechniqueDefs(name); + if (existingTechs == null || existingTechs.isEmpty()) { + // Brand new technique — delegate to normal readTechnique + readTechnique(techStat); + return; + } + + // Parse child's technique block into temporary state + EnumMap childShaderNames = new EnumMap<>(Shader.ShaderType.class); + List> childShaderLanguages = new ArrayList<>(); + List childDefineStatements = new ArrayList<>(); + List childWorldParamStatements = new ArrayList<>(); + RenderState childRenderState = null; + RenderState childForcedRenderState = null; + LightMode childLightMode = null; + ShadowMode childShadowMode = null; + TechniqueDef.LightSpace childLightSpace = null; + int childLangSize = 0; + + for (Statement statement : techStat.getContents()) { + String[] stSplit = statement.getLine().split("[ \\{]"); + if (stSplit[0].equals("VertexShader") + || stSplit[0].equals("FragmentShader") + || stSplit[0].equals("GeometryShader") + || stSplit[0].equals("TessellationControlShader") + || stSplit[0].equals("TessellationEvaluationShader")) { + // Parse shader statement into childShaderNames/childShaderLanguages + String[] sSplit = statement.getLine().split(":"); + if (sSplit.length != 2) { + throw new IOException("Shader statement syntax incorrect: " + statement.getLine()); + } + String[] typeAndLang = sSplit[0].split(whitespacePattern); + String shaderSource = sSplit[1].trim(); + + for (Shader.ShaderType shaderType : Shader.ShaderType.values()) { + if (typeAndLang[0].equals(shaderType.toString() + "Shader")) { + childShaderNames.put(shaderType, shaderSource); + String[] languages = Arrays.copyOfRange(typeAndLang, 1, typeAndLang.length); + if (childLangSize != 0 && childLangSize != languages.length) { + throw new AssetLoadException("Child technique must have the same number of languages for each shader type."); + } + childLangSize = languages.length; + for (int i = 0; i < languages.length; i++) { + if (i >= childShaderLanguages.size()) { + childShaderLanguages.add(new EnumMap<>(Shader.ShaderType.class)); + } + childShaderLanguages.get(i).put(shaderType, languages[i]); + } + } + } + } else if (stSplit[0].equals("LightMode")) { + String[] lmSplit = statement.getLine().split(whitespacePattern); + childLightMode = LightMode.valueOf(lmSplit[1]); + } else if (stSplit[0].equals("LightSpace")) { + String[] lsSplit = statement.getLine().split(whitespacePattern); + childLightSpace = TechniqueDef.LightSpace.valueOf(lsSplit[1]); + } else if (stSplit[0].equals("ShadowMode")) { + String[] smSplit = statement.getLine().split(whitespacePattern); + childShadowMode = ShadowMode.valueOf(smSplit[1]); + } else if (stSplit[0].equals("WorldParameters")) { + childWorldParamStatements.addAll(statement.getContents()); + } else if (stSplit[0].equals("RenderState")) { + childRenderState = new RenderState(); + renderState = childRenderState; + for (Statement rs : statement.getContents()) { + readRenderStateStatement(rs); + } + renderState = null; + } else if (stSplit[0].equals("ForcedRenderState")) { + childForcedRenderState = new RenderState(); + renderState = childForcedRenderState; + for (Statement rs : statement.getContents()) { + readRenderStateStatement(rs); + } + renderState = null; + } else if (stSplit[0].equals("Defines")) { + childDefineStatements.addAll(statement.getContents()); + } + } + + // Validate child language variant count matches parent variant count + if (!childShaderNames.isEmpty() && childShaderLanguages.size() != existingTechs.size()) { + throw new AssetLoadException("Child technique language variant count (" + + childShaderLanguages.size() + ") does not match parent variant count (" + + existingTechs.size() + ") for technique '" + name + "'"); + } + + // Process defines ONCE on first variant, then replicate + ArrayList savedPresetDefines = new ArrayList<>(presetDefines); + presetDefines.clear(); + + if (!childDefineStatements.isEmpty()) { + technique = existingTechs.get(0); + for (Statement defSt : childDefineStatements) { + readDefine(defSt.getLine()); + } + technique = null; + // Replicate mapped defines to remaining variants + for (int i = 1; i < existingTechs.size(); i++) { + TechniqueDef td = existingTechs.get(i); + for (Statement defSt : childDefineStatements) { + String[] defSplit = defSt.getLine().split(":"); + if (defSplit.length == 2) { + String defineName = defSplit[0].trim(); + String paramName = defSplit[1].trim(); + MatParam param = materialDef.getMaterialParam(paramName); + if (param != null) { + td.addShaderParamDefine(paramName, param.getVarType(), defineName); + } + } + } + } + } + + // Apply overrides to each variant by index + for (int i = 0; i < existingTechs.size(); i++) { + TechniqueDef td = existingTechs.get(i); + + // Shader merge + if (!childShaderNames.isEmpty()) { + EnumMap mergedNames = new EnumMap<>(td.getShaderProgramNames()); + EnumMap mergedLangs = new EnumMap<>(td.getShaderProgramLanguages()); + mergedNames.putAll(childShaderNames); + + // Find matching child language variant by comparing the child's declared + // language for an overridden shader type against the existing technique variant's + // language for that same type. Since parent variants have one language per variant + // (e.g. GLSL150 for all types in variant 0, GLSL100 for all types in variant 1), + // matching on any overridden shader type works. + EnumMap existingLangs = td.getShaderProgramLanguages(); + int matchedChildLangIdx = -1; + for (int ci = 0; ci < childShaderLanguages.size(); ci++) { + EnumMap childLangSet = childShaderLanguages.get(ci); + boolean matches = false; + for (Shader.ShaderType st : childShaderNames.keySet()) { + String existingLang = existingLangs.get(st); + String childLang = childLangSet.get(st); + if (existingLang != null && existingLang.equals(childLang)) { + matches = true; + break; + } + } + if (matches) { + matchedChildLangIdx = ci; + break; + } + } + if (matchedChildLangIdx == -1) { + // Fallback: match by index + matchedChildLangIdx = Math.min(i, childShaderLanguages.size() - 1); + } + mergedLangs.putAll(childShaderLanguages.get(matchedChildLangIdx)); + td.setShaderFile(mergedNames, mergedLangs); + } + + // World params — additive + for (Statement wpSt : childWorldParamStatements) { + td.addWorldParam(wpSt.getLine()); + } + + // RenderState override + if (childRenderState != null) { + td.setRenderState(childRenderState); + } + if (childForcedRenderState != null) { + td.setForcedRenderState(childForcedRenderState); + } + + // LightMode override + if (childLightMode != null) { + td.setLightMode(childLightMode); + switch (childLightMode) { + case Disable: + td.setLogic(new DefaultTechniqueDefLogic(td)); + break; + case MultiPass: + td.setLogic(new MultiPassLightingLogic(td)); + break; + case SinglePass: + td.setLogic(new SinglePassLightingLogic(td)); + break; + case StaticPass: + td.setLogic(new StaticPassLightingLogic(td)); + break; + case SinglePassAndImageBased: + td.setLogic(new SinglePassAndImageBasedLightingLogic(td)); + break; + default: + throw new IOException("Light mode not supported:" + childLightMode); + } + } + + // ShadowMode override + if (childShadowMode != null) { + td.setShadowMode(childShadowMode); + } + + // LightSpace override + if (childLightSpace != null) { + td.setLightSpace(childLightSpace); + } + + // Apply preset defines to shader prologue (append to existing) + if (!presetDefines.isEmpty()) { + String existingPrologue = td.getShaderPrologue(); + String newPrologue = createShaderPrologue(presetDefines); + if (existingPrologue != null && !existingPrologue.isEmpty()) { + td.setShaderPrologue(existingPrologue + newPrologue); + } else { + td.setShaderPrologue(newPrologue); + } + } + } + + // Restore saved presetDefines + presetDefines.clear(); + presetDefines.addAll(savedPresetDefines); + } + private void loadFromRoot(List roots) throws IOException{ if (roots.size() == 2){ Statement exception = roots.get(0); @@ -747,22 +1055,59 @@ private void loadFromRoot(List roots) throws IOException{ throw new MatParseException("Material name cannot be empty", materialStat); } + boolean extendingDef = false; + if (split.length == 2){ if (!extending){ - throw new MatParseException("Must use 'Material' when extending.", materialStat); - } + // MaterialDef with colon — this is MaterialDef inheritance + extendingDef = true; - String extendedMat = split[1].trim(); + String parentPath = split[1].trim(); + MaterialDef parentDef = assetManager.loadAsset(new AssetKey(parentPath)); + if (parentDef == null) { + throw new MatParseException("Parent MaterialDef " + parentPath + " cannot be found.", materialStat); + } - MaterialDef def = assetManager.loadAsset(new AssetKey(extendedMat)); - if (def == null) { - throw new MatParseException("Extended material " + extendedMat + " cannot be found.", materialStat); - } + materialDef = new MaterialDef(assetManager, split[0].trim()); + materialDef.setAssetName(key.getName()); + + // Copy all parent params + for (MatParam parentParam : parentDef.getMaterialParams()) { + if (parentParam instanceof MatParamTexture) { + MatParamTexture texParam = (MatParamTexture) parentParam; + materialDef.addMaterialParamTexture(texParam.getVarType(), texParam.getName(), + texParam.getColorSpace(), (Texture) texParam.getValue()); + } else { + materialDef.addMaterialParam(parentParam.getVarType(), parentParam.getName(), parentParam.getValue()); + } + } - material = new Material(def); - material.setKey(key); - material.setName(split[0].trim()); - + // Clone all parent techniques + for (String techName : parentDef.getTechniqueDefsNames()) { + List parentTechs = parentDef.getTechniqueDefs(techName); + if (parentTechs != null) { + for (TechniqueDef parentTech : parentTechs) { + try { + TechniqueDef cloned = parentTech.clone(); + materialDef.addTechniqueDef(cloned); + } catch (CloneNotSupportedException e) { + throw new AssetLoadException("Failed to clone technique: " + techName, e); + } + } + } + } + } else { + String extendedMat = split[1].trim(); + + MaterialDef def = assetManager.loadAsset(new AssetKey(extendedMat)); + if (def == null) { + throw new MatParseException("Extended material " + extendedMat + " cannot be found.", materialStat); + } + + material = new Material(def); + material.setKey(key); + material.setName(split[0].trim()); + } }else if (split.length == 1){ if (extending){ throw new MatParseException("Expected ':', got '{'", materialStat); @@ -787,6 +1132,14 @@ private void loadFromRoot(List roots) throws IOException{ } else if (statType.equals("ReceivesShadows")) { readReceivesShadowsStatement(statement.getLine()); } + } else if (extendingDef) { + if (statType.equals("MaterialParameters")) { + readExtendingDefMaterialParams(statement.getContents()); + } else if (statType.equals("Technique")) { + readExtendingDefTechnique(statement); + } else { + throw new MatParseException("Expected material statement, got '" + statType + "'", statement); + } } else { if (statType.equals("Technique")) { readTechnique(statement); diff --git a/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java b/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java index 7126d94fa8..a26c4e06bf 100644 --- a/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java +++ b/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java @@ -4,14 +4,20 @@ import com.jme3.asset.AssetKey; import com.jme3.asset.AssetManager; import com.jme3.asset.TextureKey; +import com.jme3.material.MatParam; import com.jme3.material.MatParamTexture; import com.jme3.material.Material; import com.jme3.material.MaterialDef; +import com.jme3.material.RenderState; +import com.jme3.material.TechniqueDef; import com.jme3.renderer.Caps; +import com.jme3.shader.Shader; +import com.jme3.shader.UniformBinding; import com.jme3.shader.VarType; import com.jme3.texture.Texture; import java.io.IOException; import java.util.EnumSet; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -145,4 +151,246 @@ private TextureKey setupMockForTexture(final String paramName, final String path return textureKey; } + + // ---- MaterialDef Inheritance helpers ---- + + private MaterialDef loadParentDef() throws IOException { + J3MLoader parentLoader = new J3MLoader(); + AssetInfo parentInfo = Mockito.mock(AssetInfo.class); + @SuppressWarnings("unchecked") + AssetKey parentKey = Mockito.mock(AssetKey.class); + when(parentKey.getExtension()).thenReturn("j3md"); + when(parentKey.getName()).thenReturn("parent-matdef.j3md"); + when(parentInfo.getManager()).thenReturn(assetManager); + when(parentInfo.getKey()).thenReturn(parentKey); + when(parentInfo.openStream()).thenReturn(J3MLoader.class.getResourceAsStream("/parent-matdef.j3md")); + return (MaterialDef) parentLoader.load(parentInfo); + } + + @SuppressWarnings("unchecked") + private MaterialDef loadChildDef(String resourcePath) throws IOException { + MaterialDef parentDef = loadParentDef(); + + when(assetManager.loadAsset(any(AssetKey.class))).thenReturn(parentDef); + + J3MLoader childLoader = new J3MLoader(); + AssetInfo childInfo = Mockito.mock(AssetInfo.class); + AssetKey childKey = Mockito.mock(AssetKey.class); + when(childKey.getExtension()).thenReturn("j3md"); + when(childKey.getName()).thenReturn(resourcePath); + when(childInfo.getManager()).thenReturn(assetManager); + when(childInfo.getKey()).thenReturn(childKey); + when(childInfo.openStream()).thenReturn(J3MLoader.class.getResourceAsStream("/" + resourcePath)); + return (MaterialDef) childLoader.load(childInfo); + } + + // ---- MaterialDef Inheritance tests ---- + + @Test + public void materialDefInheritance_shouldInheritParentParams() throws IOException { + MaterialDef def = loadChildDef("child-matdef.j3md"); + + // Should have parent's params + child's new param + assertNotNull(def.getMaterialParam("Roughness")); + assertNotNull(def.getMaterialParam("DiffuseMap")); + assertNotNull(def.getMaterialParam("Wetness")); + + // Roughness default overridden to 0.8 + assertEquals(0.8f, (float) def.getMaterialParam("Roughness").getValue(), 0.001f); + + // Wetness default is 0.0 + assertEquals(0.0f, (float) def.getMaterialParam("Wetness").getValue(), 0.001f); + } + + @Test(expected = IOException.class) + public void materialDefInheritance_paramTypeMismatch_shouldThrow() throws IOException { + loadChildDef("child-matdef-type-mismatch.j3md"); + } + + @Test + public void materialDefInheritance_shouldOverrideFragShaderOnly() throws IOException { + MaterialDef def = loadChildDef("child-matdef.j3md"); + + List defaultTechs = def.getTechniqueDefs("Default"); + assertNotNull(defaultTechs); + assertEquals(2, defaultTechs.size()); + + // Both variants should have child's frag shader and parent's vert shader + for (TechniqueDef td : defaultTechs) { + assertEquals("child.frag", td.getShaderProgramNames().get(Shader.ShaderType.Fragment)); + assertEquals("parent.vert", td.getShaderProgramNames().get(Shader.ShaderType.Vertex)); + } + + // Check languages per variant + // When parent is loaded, the clone() in TechniqueDef produces variants. + // After child inheritance cloning, variant order depends on addTechniqueDef order. + // Find each variant by checking its required caps. + TechniqueDef glsl150 = null, glsl100 = null; + for (TechniqueDef td : defaultTechs) { + if (td.getRequiredCaps().contains(Caps.GLSL150)) { + glsl150 = td; + } else { + glsl100 = td; + } + } + assertNotNull(glsl150); + assertNotNull(glsl100); + assertEquals("GLSL150", glsl150.getShaderProgramLanguages().get(Shader.ShaderType.Fragment)); + assertEquals("GLSL150", glsl150.getShaderProgramLanguages().get(Shader.ShaderType.Vertex)); + assertEquals("GLSL100", glsl100.getShaderProgramLanguages().get(Shader.ShaderType.Fragment)); + assertEquals("GLSL100", glsl100.getShaderProgramLanguages().get(Shader.ShaderType.Vertex)); + } + + @Test + public void materialDefInheritance_shouldMergeDefinesAdditively() throws IOException { + MaterialDef def = loadChildDef("child-matdef.j3md"); + + List defaultTechs = def.getTechniqueDefs("Default"); + assertNotNull(defaultTechs); + + // Parent's HAS_DIFFUSEMAP define should still exist + TechniqueDef td = defaultTechs.get(0); + assertNotNull(td.getShaderParamDefine("DiffuseMap")); + assertEquals("HAS_DIFFUSEMAP", td.getShaderParamDefine("DiffuseMap")); + + // Child's WETNESS define should be added + assertNotNull(td.getShaderParamDefine("Wetness")); + assertEquals("WETNESS", td.getShaderParamDefine("Wetness")); + } + + @Test + public void materialDefInheritance_shouldSupportNewTechniques() throws IOException { + MaterialDef def = loadChildDef("child-matdef-new-technique.j3md"); + + // Parent techniques should still exist + assertNotNull(def.getTechniqueDefs("Default")); + assertNotNull(def.getTechniqueDefs("PreShadow")); + + // New technique should be added + List glowTechs = def.getTechniqueDefs("Glow"); + assertNotNull(glowTechs); + assertEquals(2, glowTechs.size()); + assertEquals("glow.frag", glowTechs.get(0).getShaderProgramNames().get(Shader.ShaderType.Fragment)); + } + + @Test + public void materialDefInheritance_paramsOnly_shouldInheritTechniques() throws IOException { + MaterialDef def = loadChildDef("child-matdef-params-only.j3md"); + + // New param should exist + assertNotNull(def.getMaterialParam("Metallic")); + + // Parent params should exist + assertNotNull(def.getMaterialParam("Roughness")); + assertNotNull(def.getMaterialParam("DiffuseMap")); + + // Parent techniques should be inherited unchanged + assertNotNull(def.getTechniqueDefs("Default")); + assertNotNull(def.getTechniqueDefs("PreShadow")); + assertEquals(2, def.getTechniqueDefs("Default").size()); + assertEquals(2, def.getTechniqueDefs("PreShadow").size()); + } + + @Test + public void materialDefInheritance_techniqueOnly_shouldInheritParams() throws IOException { + MaterialDef def = loadChildDef("child-matdef-technique-only.j3md"); + + // Parent params should be inherited + assertNotNull(def.getMaterialParam("Roughness")); + assertNotNull(def.getMaterialParam("DiffuseMap")); + assertEquals(0.5f, (float) def.getMaterialParam("Roughness").getValue(), 0.001f); + + // Default technique should have overridden frag shader + List defaultTechs = def.getTechniqueDefs("Default"); + for (TechniqueDef td : defaultTechs) { + assertEquals("childtech.frag", td.getShaderProgramNames().get(Shader.ShaderType.Fragment)); + assertEquals("parent.vert", td.getShaderProgramNames().get(Shader.ShaderType.Vertex)); + } + } + + @Test + public void materialDefInheritance_emptyChild_shouldInheritEverything() throws IOException { + MaterialDef def = loadChildDef("child-matdef-empty.j3md"); + + // All parent params should be inherited + assertNotNull(def.getMaterialParam("Roughness")); + assertNotNull(def.getMaterialParam("DiffuseMap")); + assertEquals(0.5f, (float) def.getMaterialParam("Roughness").getValue(), 0.001f); + + // All parent techniques should be inherited + assertNotNull(def.getTechniqueDefs("Default")); + assertNotNull(def.getTechniqueDefs("PreShadow")); + assertEquals(2, def.getTechniqueDefs("Default").size()); + assertEquals(2, def.getTechniqueDefs("PreShadow").size()); + + // Shader files should be parent's + TechniqueDef td = def.getTechniqueDefs("Default").get(0); + assertEquals("parent.vert", td.getShaderProgramNames().get(Shader.ShaderType.Vertex)); + assertEquals("parent.frag", td.getShaderProgramNames().get(Shader.ShaderType.Fragment)); + } + + @Test + public void materialDefInheritance_shouldOverrideRenderState() throws IOException { + MaterialDef def = loadChildDef("child-matdef-renderstate.j3md"); + + List defaultTechs = def.getTechniqueDefs("Default"); + assertNotNull(defaultTechs); + + for (TechniqueDef td : defaultTechs) { + RenderState rs = td.getRenderState(); + assertNotNull(rs); + assertEquals(RenderState.FaceCullMode.Off, rs.getFaceCullMode()); + assertEquals(RenderState.BlendMode.Alpha, rs.getBlendMode()); + } + } + + @Test + public void materialDefInheritance_shouldMergeWorldParamsAdditively() throws IOException { + MaterialDef def = loadChildDef("child-matdef-worldparams.j3md"); + + List defaultTechs = def.getTechniqueDefs("Default"); + assertNotNull(defaultTechs); + + for (TechniqueDef td : defaultTechs) { + List worldBinds = td.getWorldBindings(); + assertTrue(worldBinds.contains(UniformBinding.WorldViewProjectionMatrix)); + assertTrue(worldBinds.contains(UniformBinding.ViewMatrix)); + } + } + + @Test + public void materialDefInheritance_shouldOverrideVertShaderOnly() throws IOException { + MaterialDef def = loadChildDef("child-matdef-vertshader.j3md"); + + List defaultTechs = def.getTechniqueDefs("Default"); + assertNotNull(defaultTechs); + + for (TechniqueDef td : defaultTechs) { + assertEquals("childvert.vert", td.getShaderProgramNames().get(Shader.ShaderType.Vertex)); + assertEquals("parent.frag", td.getShaderProgramNames().get(Shader.ShaderType.Fragment)); + } + } + + @Test + public void materialDefInheritance_definesOnBothVariants() throws IOException { + MaterialDef def = loadChildDef("child-matdef.j3md"); + + List defaultTechs = def.getTechniqueDefs("Default"); + assertNotNull(defaultTechs); + assertEquals(2, defaultTechs.size()); + + // Both variants should have the child's WETNESS define + for (TechniqueDef td : defaultTechs) { + assertNotNull("WETNESS define should be present on variant with caps " + td.getRequiredCaps(), + td.getShaderParamDefine("Wetness")); + assertEquals("WETNESS", td.getShaderParamDefine("Wetness")); + } + + // Both variants should still have the parent's HAS_DIFFUSEMAP define + for (TechniqueDef td : defaultTechs) { + assertNotNull("HAS_DIFFUSEMAP define should be present on variant with caps " + td.getRequiredCaps(), + td.getShaderParamDefine("DiffuseMap")); + assertEquals("HAS_DIFFUSEMAP", td.getShaderParamDefine("DiffuseMap")); + } + } } diff --git a/jme3-core/src/test/resources/child-matdef-empty.j3md b/jme3-core/src/test/resources/child-matdef-empty.j3md new file mode 100644 index 0000000000..f9e311630e --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-empty.j3md @@ -0,0 +1,2 @@ +MaterialDef ChildEmpty : parent-matdef.j3md { +} diff --git a/jme3-core/src/test/resources/child-matdef-new-technique.j3md b/jme3-core/src/test/resources/child-matdef-new-technique.j3md new file mode 100644 index 0000000000..4f2272f36c --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-new-technique.j3md @@ -0,0 +1,6 @@ +MaterialDef ChildWithNewTech : parent-matdef.j3md { + Technique Glow { + VertexShader GLSL150 GLSL100 : glow.vert + FragmentShader GLSL150 GLSL100 : glow.frag + } +} diff --git a/jme3-core/src/test/resources/child-matdef-params-only.j3md b/jme3-core/src/test/resources/child-matdef-params-only.j3md new file mode 100644 index 0000000000..a842e944db --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-params-only.j3md @@ -0,0 +1,5 @@ +MaterialDef ChildParamsOnly : parent-matdef.j3md { + MaterialParameters { + Float Metallic : 0.0 + } +} diff --git a/jme3-core/src/test/resources/child-matdef-renderstate.j3md b/jme3-core/src/test/resources/child-matdef-renderstate.j3md new file mode 100644 index 0000000000..410b60dd0c --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-renderstate.j3md @@ -0,0 +1,8 @@ +MaterialDef ChildRenderState : parent-matdef.j3md { + Technique { + RenderState { + FaceCull Off + Blend Alpha + } + } +} diff --git a/jme3-core/src/test/resources/child-matdef-technique-only.j3md b/jme3-core/src/test/resources/child-matdef-technique-only.j3md new file mode 100644 index 0000000000..e8c0ad3f86 --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-technique-only.j3md @@ -0,0 +1,5 @@ +MaterialDef ChildTechOnly : parent-matdef.j3md { + Technique { + FragmentShader GLSL150 GLSL100 : childtech.frag + } +} diff --git a/jme3-core/src/test/resources/child-matdef-type-mismatch.j3md b/jme3-core/src/test/resources/child-matdef-type-mismatch.j3md new file mode 100644 index 0000000000..86b37abf22 --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-type-mismatch.j3md @@ -0,0 +1,5 @@ +MaterialDef BadChild : parent-matdef.j3md { + MaterialParameters { + Vector4 Roughness : 1.0 1.0 1.0 1.0 + } +} diff --git a/jme3-core/src/test/resources/child-matdef-vertshader.j3md b/jme3-core/src/test/resources/child-matdef-vertshader.j3md new file mode 100644 index 0000000000..18673342b9 --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-vertshader.j3md @@ -0,0 +1,5 @@ +MaterialDef ChildVertShader : parent-matdef.j3md { + Technique { + VertexShader GLSL150 GLSL100 : childvert.vert + } +} diff --git a/jme3-core/src/test/resources/child-matdef-worldparams.j3md b/jme3-core/src/test/resources/child-matdef-worldparams.j3md new file mode 100644 index 0000000000..e9539b5137 --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef-worldparams.j3md @@ -0,0 +1,7 @@ +MaterialDef ChildWorldParams : parent-matdef.j3md { + Technique { + WorldParameters { + ViewMatrix + } + } +} diff --git a/jme3-core/src/test/resources/child-matdef.j3md b/jme3-core/src/test/resources/child-matdef.j3md new file mode 100644 index 0000000000..d51161071a --- /dev/null +++ b/jme3-core/src/test/resources/child-matdef.j3md @@ -0,0 +1,12 @@ +MaterialDef Child : parent-matdef.j3md { + MaterialParameters { + Float Wetness : 0.0 + Float Roughness : 0.8 + } + Technique { + FragmentShader GLSL150 GLSL100 : child.frag + Defines { + WETNESS : Wetness + } + } +} diff --git a/jme3-core/src/test/resources/parent-matdef.j3md b/jme3-core/src/test/resources/parent-matdef.j3md new file mode 100644 index 0000000000..b45f1d5ff6 --- /dev/null +++ b/jme3-core/src/test/resources/parent-matdef.j3md @@ -0,0 +1,20 @@ +MaterialDef Parent { + MaterialParameters { + Float Roughness : 0.5 + Texture2D DiffuseMap + } + Technique { + VertexShader GLSL150 GLSL100 : parent.vert + FragmentShader GLSL150 GLSL100 : parent.frag + WorldParameters { + WorldViewProjectionMatrix + } + Defines { + HAS_DIFFUSEMAP : DiffuseMap + } + } + Technique PreShadow { + VertexShader GLSL150 GLSL100 : shadow.vert + FragmentShader GLSL150 GLSL100 : shadow.frag + } +} From 80fb040816422ab85eeb3eb57fd85b2dc68166d5 Mon Sep 17 00:00:00 2001 From: zzuegg Date: Sat, 21 Mar 2026 19:11:40 +0100 Subject: [PATCH 2/2] feat(material): support multi-inheritance for MaterialDef MaterialDef can now extend multiple parents using comma-separated syntax: MaterialDef Child : parent1.j3md, parent2.j3md { ... } Conflict resolution: - Same param name, same type: OK (same param) - Same param name, different type: Error - Same technique name, same shaders: OK (same technique) - Same technique name, different shaders: Error Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/jme3/material/plugins/J3MLoader.java | 103 +++++++++---- .../jme3/material/plugins/J3MLoaderTest.java | 141 ++++++++++++++++++ .../resources/child-multi-conflict-param.j3md | 6 + .../child-multi-conflict-technique.j3md | 6 + .../test/resources/child-multi-inherit.j3md | 9 ++ .../resources/child-multi-same-technique.j3md | 6 + .../resources/child-multi-shared-param.j3md | 6 + .../test/resources/child-triple-inherit.j3md | 6 + jme3-core/src/test/resources/mixin-a.j3md | 13 ++ jme3-core/src/test/resources/mixin-b.j3md | 13 ++ .../test/resources/mixin-conflict-param.j3md | 9 ++ .../resources/mixin-conflict-technique.j3md | 9 ++ .../test/resources/mixin-same-technique.j3md | 12 ++ .../test/resources/mixin-shared-param.j3md | 10 ++ 14 files changed, 324 insertions(+), 25 deletions(-) create mode 100644 jme3-core/src/test/resources/child-multi-conflict-param.j3md create mode 100644 jme3-core/src/test/resources/child-multi-conflict-technique.j3md create mode 100644 jme3-core/src/test/resources/child-multi-inherit.j3md create mode 100644 jme3-core/src/test/resources/child-multi-same-technique.j3md create mode 100644 jme3-core/src/test/resources/child-multi-shared-param.j3md create mode 100644 jme3-core/src/test/resources/child-triple-inherit.j3md create mode 100644 jme3-core/src/test/resources/mixin-a.j3md create mode 100644 jme3-core/src/test/resources/mixin-b.j3md create mode 100644 jme3-core/src/test/resources/mixin-conflict-param.j3md create mode 100644 jme3-core/src/test/resources/mixin-conflict-technique.j3md create mode 100644 jme3-core/src/test/resources/mixin-same-technique.j3md create mode 100644 jme3-core/src/test/resources/mixin-shared-param.j3md diff --git a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java index b9b64ccb0a..c219f931fb 100644 --- a/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java +++ b/jme3-core/src/plugins/java/com/jme3/material/plugins/J3MLoader.java @@ -1059,39 +1059,81 @@ private void loadFromRoot(List roots) throws IOException{ if (split.length == 2){ if (!extending){ - // MaterialDef with colon — this is MaterialDef inheritance + // MaterialDef with colon — this is MaterialDef inheritance (single or multi) extendingDef = true; - String parentPath = split[1].trim(); - MaterialDef parentDef = assetManager.loadAsset(new AssetKey(parentPath)); - if (parentDef == null) { - throw new MatParseException("Parent MaterialDef " + parentPath + " cannot be found.", materialStat); - } + String parentsList = split[1].trim(); + String[] parentPaths = parentsList.split(","); materialDef = new MaterialDef(assetManager, split[0].trim()); materialDef.setAssetName(key.getName()); - // Copy all parent params - for (MatParam parentParam : parentDef.getMaterialParams()) { - if (parentParam instanceof MatParamTexture) { - MatParamTexture texParam = (MatParamTexture) parentParam; - materialDef.addMaterialParamTexture(texParam.getVarType(), texParam.getName(), - texParam.getColorSpace(), (Texture) texParam.getValue()); - } else { - materialDef.addMaterialParam(parentParam.getVarType(), parentParam.getName(), parentParam.getValue()); + for (String parentPathRaw : parentPaths) { + String parentPath = parentPathRaw.trim(); + if (parentPath.isEmpty()) { + continue; + } + + MaterialDef parentDef = assetManager.loadAsset(new AssetKey(parentPath)); + if (parentDef == null) { + throw new MatParseException("Parent MaterialDef " + parentPath + " cannot be found.", materialStat); } - } - // Clone all parent techniques - for (String techName : parentDef.getTechniqueDefsNames()) { - List parentTechs = parentDef.getTechniqueDefs(techName); - if (parentTechs != null) { - for (TechniqueDef parentTech : parentTechs) { - try { - TechniqueDef cloned = parentTech.clone(); - materialDef.addTechniqueDef(cloned); - } catch (CloneNotSupportedException e) { - throw new AssetLoadException("Failed to clone technique: " + techName, e); + // Merge params with conflict detection + for (MatParam parentParam : parentDef.getMaterialParams()) { + MatParam existing = materialDef.getMaterialParam(parentParam.getName()); + if (existing != null) { + // Same name — check type match + if (existing.getVarType() != parentParam.getVarType()) { + throw new IOException("Parameter '" + parentParam.getName() + + "' type conflict: declared as '" + existing.getVarType() + + "' but '" + parentPath + "' declares it as '" + parentParam.getVarType() + "'"); + } + // Same name, same type — OK, skip (already exists from previous parent) + } else { + // New param — add it + if (parentParam instanceof MatParamTexture) { + MatParamTexture texParam = (MatParamTexture) parentParam; + materialDef.addMaterialParamTexture(texParam.getVarType(), texParam.getName(), + texParam.getColorSpace(), (Texture) texParam.getValue()); + } else { + materialDef.addMaterialParam(parentParam.getVarType(), parentParam.getName(), + parentParam.getValue()); + } + } + } + + // Merge techniques with conflict detection + for (String techName : parentDef.getTechniqueDefsNames()) { + List parentTechs = parentDef.getTechniqueDefs(techName); + if (parentTechs == null) continue; + + List existingTechs = materialDef.getTechniqueDefs(techName); + if (existingTechs != null && !existingTechs.isEmpty()) { + // Same technique name exists — validate shaders match + for (TechniqueDef parentTech : parentTechs) { + boolean foundMatch = false; + for (TechniqueDef existingTech : existingTechs) { + if (shadersMatch(parentTech, existingTech)) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + throw new IOException("Technique '" + techName + + "' from '" + parentPath + "' conflicts with existing technique '" + + techName + "' (different shaders)"); + } + } + // All variants match — skip, technique already exists + } else { + // New technique — clone and add + for (TechniqueDef parentTech : parentTechs) { + try { + materialDef.addTechniqueDef(parentTech.clone()); + } catch (CloneNotSupportedException e) { + throw new AssetLoadException("Failed to clone technique: " + techName, e); + } } } } @@ -1333,4 +1375,15 @@ public void applyToTexture(final Texture texture) { textureOption.applyToTexture(value, texture); } } + + private boolean shadersMatch(TechniqueDef a, TechniqueDef b) { + EnumMap aNamesMap = a.getShaderProgramNames(); + EnumMap bNamesMap = b.getShaderProgramNames(); + if (aNamesMap.size() != bNamesMap.size()) return false; + for (Map.Entry entry : aNamesMap.entrySet()) { + String bName = bNamesMap.get(entry.getKey()); + if (!entry.getValue().equals(bName)) return false; + } + return true; + } } diff --git a/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java b/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java index a26c4e06bf..aa4ae94dd9 100644 --- a/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java +++ b/jme3-core/src/test/java/com/jme3/material/plugins/J3MLoaderTest.java @@ -17,13 +17,17 @@ import com.jme3.texture.Texture; import java.io.IOException; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; @@ -393,4 +397,141 @@ public void materialDefInheritance_definesOnBothVariants() throws IOException { assertEquals("HAS_DIFFUSEMAP", td.getShaderParamDefine("DiffuseMap")); } } + + // ---- Multi-Inheritance helpers ---- + + private MaterialDef loadMixinDef(String resourceName) throws IOException { + J3MLoader loader = new J3MLoader(); + AssetInfo info = Mockito.mock(AssetInfo.class); + @SuppressWarnings("unchecked") + AssetKey key = Mockito.mock(AssetKey.class); + when(key.getExtension()).thenReturn("j3md"); + when(key.getName()).thenReturn(resourceName); + when(info.getManager()).thenReturn(assetManager); + when(info.getKey()).thenReturn(key); + when(info.openStream()).thenReturn(J3MLoader.class.getResourceAsStream("/" + resourceName)); + return (MaterialDef) loader.load(info); + } + + @SuppressWarnings("unchecked") + private MaterialDef loadMultiInheritChildDef(String resourcePath, String... parentResources) throws IOException { + // Load each parent + final Map parentDefs = new HashMap<>(); + for (String parentResource : parentResources) { + parentDefs.put(parentResource, loadMixinDef(parentResource)); + } + + // Mock assetManager.loadAsset to return the correct parent based on path + when(assetManager.loadAsset(any(AssetKey.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + AssetKey k = (AssetKey) invocation.getArguments()[0]; + MaterialDef def = parentDefs.get(k.getName()); + if (def != null) return def; + return null; + } + }); + + J3MLoader childLoader = new J3MLoader(); + AssetInfo childInfo = Mockito.mock(AssetInfo.class); + AssetKey childKey = Mockito.mock(AssetKey.class); + when(childKey.getExtension()).thenReturn("j3md"); + when(childKey.getName()).thenReturn(resourcePath); + when(childInfo.getManager()).thenReturn(assetManager); + when(childInfo.getKey()).thenReturn(childKey); + when(childInfo.openStream()).thenReturn(J3MLoader.class.getResourceAsStream("/" + resourcePath)); + return (MaterialDef) childLoader.load(childInfo); + } + + // ---- Multi-Inheritance tests ---- + + @Test + public void multiInheritance_shouldInheritFromMultipleParents() throws IOException { + MaterialDef def = loadMultiInheritChildDef("child-multi-inherit.j3md", + "mixin-a.j3md", "mixin-b.j3md"); + + // Params from mixin-a + assertNotNull(def.getMaterialParam("AlphaDiscardThreshold")); + assertNotNull(def.getMaterialParam("NumberOfBones")); + // Params from mixin-b + assertNotNull(def.getMaterialParam("GlowMap")); + assertNotNull(def.getMaterialParam("GlowColor")); + // Own param + assertNotNull(def.getMaterialParam("BaseColor")); + + // Own Default technique + assertNotNull(def.getTechniqueDefs("Default")); + // PreShadow from mixin-a + assertNotNull(def.getTechniqueDefs("PreShadow")); + // Glow from mixin-b + assertNotNull(def.getTechniqueDefs("Glow")); + } + + @Test + public void multiInheritance_sharedParamSameType_shouldSucceed() throws IOException { + MaterialDef def = loadMultiInheritChildDef("child-multi-shared-param.j3md", + "mixin-a.j3md", "mixin-shared-param.j3md"); + + // AlphaDiscardThreshold shared between both parents (same type Float) — should succeed + assertNotNull(def.getMaterialParam("AlphaDiscardThreshold")); + assertEquals(VarType.Float, def.getMaterialParam("AlphaDiscardThreshold").getVarType()); + + // Params from mixin-a + assertNotNull(def.getMaterialParam("NumberOfBones")); + // Params from mixin-shared-param + assertNotNull(def.getMaterialParam("UseFog")); + + // Techniques from both + assertNotNull(def.getTechniqueDefs("PreShadow")); + assertNotNull(def.getTechniqueDefs("Fog")); + } + + @Test(expected = IOException.class) + public void multiInheritance_conflictingParamType_shouldThrow() throws IOException { + loadMultiInheritChildDef("child-multi-conflict-param.j3md", + "mixin-a.j3md", "mixin-conflict-param.j3md"); + } + + @Test(expected = IOException.class) + public void multiInheritance_conflictingTechnique_shouldThrow() throws IOException { + loadMultiInheritChildDef("child-multi-conflict-technique.j3md", + "mixin-a.j3md", "mixin-conflict-technique.j3md"); + } + + @Test + public void multiInheritance_sameTechniqueSameShaders_shouldSucceed() throws IOException { + MaterialDef def = loadMultiInheritChildDef("child-multi-same-technique.j3md", + "mixin-a.j3md", "mixin-same-technique.j3md"); + + // PreShadow from both parents has same shaders — should succeed without duplicating + List preShadowTechs = def.getTechniqueDefs("PreShadow"); + assertNotNull(preShadowTechs); + assertEquals(2, preShadowTechs.size()); // 2 variants (GLSL150 + GLSL100), not 4 + + // Params from both + assertNotNull(def.getMaterialParam("AlphaDiscardThreshold")); + assertNotNull(def.getMaterialParam("NumberOfBones")); + assertNotNull(def.getMaterialParam("Roughness")); + } + + @Test + public void multiInheritance_tripleInheritance_shouldWork() throws IOException { + MaterialDef def = loadMultiInheritChildDef("child-triple-inherit.j3md", + "mixin-a.j3md", "mixin-b.j3md", "mixin-shared-param.j3md"); + + // Params from all three parents + assertNotNull(def.getMaterialParam("AlphaDiscardThreshold")); // shared A + shared-param + assertNotNull(def.getMaterialParam("NumberOfBones")); // from A + assertNotNull(def.getMaterialParam("GlowMap")); // from B + assertNotNull(def.getMaterialParam("GlowColor")); // from B + assertNotNull(def.getMaterialParam("UseFog")); // from shared-param + + // Techniques from all parents + assertNotNull(def.getTechniqueDefs("PreShadow")); // from A + assertNotNull(def.getTechniqueDefs("Glow")); // from B + assertNotNull(def.getTechniqueDefs("Fog")); // from shared-param + + // Own Default technique + assertNotNull(def.getTechniqueDefs("Default")); + } } diff --git a/jme3-core/src/test/resources/child-multi-conflict-param.j3md b/jme3-core/src/test/resources/child-multi-conflict-param.j3md new file mode 100644 index 0000000000..01988586ee --- /dev/null +++ b/jme3-core/src/test/resources/child-multi-conflict-param.j3md @@ -0,0 +1,6 @@ +MaterialDef ChildConflict : mixin-a.j3md, mixin-conflict-param.j3md { + Technique { + VertexShader GLSL150 GLSL100 : child.vert + FragmentShader GLSL150 GLSL100 : child.frag + } +} diff --git a/jme3-core/src/test/resources/child-multi-conflict-technique.j3md b/jme3-core/src/test/resources/child-multi-conflict-technique.j3md new file mode 100644 index 0000000000..954fd5a9c9 --- /dev/null +++ b/jme3-core/src/test/resources/child-multi-conflict-technique.j3md @@ -0,0 +1,6 @@ +MaterialDef ChildTechConflict : mixin-a.j3md, mixin-conflict-technique.j3md { + Technique { + VertexShader GLSL150 GLSL100 : child.vert + FragmentShader GLSL150 GLSL100 : child.frag + } +} diff --git a/jme3-core/src/test/resources/child-multi-inherit.j3md b/jme3-core/src/test/resources/child-multi-inherit.j3md new file mode 100644 index 0000000000..ffabf8ad17 --- /dev/null +++ b/jme3-core/src/test/resources/child-multi-inherit.j3md @@ -0,0 +1,9 @@ +MaterialDef ChildMulti : mixin-a.j3md, mixin-b.j3md { + MaterialParameters { + Color BaseColor : 1.0 1.0 1.0 1.0 + } + Technique { + VertexShader GLSL150 GLSL100 : child.vert + FragmentShader GLSL150 GLSL100 : child.frag + } +} diff --git a/jme3-core/src/test/resources/child-multi-same-technique.j3md b/jme3-core/src/test/resources/child-multi-same-technique.j3md new file mode 100644 index 0000000000..45ba63c076 --- /dev/null +++ b/jme3-core/src/test/resources/child-multi-same-technique.j3md @@ -0,0 +1,6 @@ +MaterialDef ChildSameTech : mixin-a.j3md, mixin-same-technique.j3md { + Technique { + VertexShader GLSL150 GLSL100 : child.vert + FragmentShader GLSL150 GLSL100 : child.frag + } +} diff --git a/jme3-core/src/test/resources/child-multi-shared-param.j3md b/jme3-core/src/test/resources/child-multi-shared-param.j3md new file mode 100644 index 0000000000..aa662a6177 --- /dev/null +++ b/jme3-core/src/test/resources/child-multi-shared-param.j3md @@ -0,0 +1,6 @@ +MaterialDef ChildShared : mixin-a.j3md, mixin-shared-param.j3md { + Technique { + VertexShader GLSL150 GLSL100 : child.vert + FragmentShader GLSL150 GLSL100 : child.frag + } +} diff --git a/jme3-core/src/test/resources/child-triple-inherit.j3md b/jme3-core/src/test/resources/child-triple-inherit.j3md new file mode 100644 index 0000000000..15026f2875 --- /dev/null +++ b/jme3-core/src/test/resources/child-triple-inherit.j3md @@ -0,0 +1,6 @@ +MaterialDef ChildTriple : mixin-a.j3md, mixin-b.j3md, mixin-shared-param.j3md { + Technique { + VertexShader GLSL150 GLSL100 : child.vert + FragmentShader GLSL150 GLSL100 : child.frag + } +} diff --git a/jme3-core/src/test/resources/mixin-a.j3md b/jme3-core/src/test/resources/mixin-a.j3md new file mode 100644 index 0000000000..0abaed98c0 --- /dev/null +++ b/jme3-core/src/test/resources/mixin-a.j3md @@ -0,0 +1,13 @@ +MaterialDef MixinA { + MaterialParameters { + Float AlphaDiscardThreshold + Int NumberOfBones + } + Technique PreShadow { + VertexShader GLSL150 GLSL100 : shadow.vert + FragmentShader GLSL150 GLSL100 : shadow.frag + WorldParameters { + WorldViewProjectionMatrix + } + } +} diff --git a/jme3-core/src/test/resources/mixin-b.j3md b/jme3-core/src/test/resources/mixin-b.j3md new file mode 100644 index 0000000000..cb97cd32cc --- /dev/null +++ b/jme3-core/src/test/resources/mixin-b.j3md @@ -0,0 +1,13 @@ +MaterialDef MixinB { + MaterialParameters { + Texture2D GlowMap + Color GlowColor + } + Technique Glow { + VertexShader GLSL150 GLSL100 : glow.vert + FragmentShader GLSL150 GLSL100 : glow.frag + WorldParameters { + WorldViewProjectionMatrix + } + } +} diff --git a/jme3-core/src/test/resources/mixin-conflict-param.j3md b/jme3-core/src/test/resources/mixin-conflict-param.j3md new file mode 100644 index 0000000000..080096fd05 --- /dev/null +++ b/jme3-core/src/test/resources/mixin-conflict-param.j3md @@ -0,0 +1,9 @@ +MaterialDef MixinConflictParam { + MaterialParameters { + Vector4 AlphaDiscardThreshold + } + Technique SomePass { + VertexShader GLSL150 GLSL100 : some.vert + FragmentShader GLSL150 GLSL100 : some.frag + } +} diff --git a/jme3-core/src/test/resources/mixin-conflict-technique.j3md b/jme3-core/src/test/resources/mixin-conflict-technique.j3md new file mode 100644 index 0000000000..d7191cbd8c --- /dev/null +++ b/jme3-core/src/test/resources/mixin-conflict-technique.j3md @@ -0,0 +1,9 @@ +MaterialDef MixinConflictTechnique { + MaterialParameters { + Float Roughness + } + Technique PreShadow { + VertexShader GLSL150 GLSL100 : other-shadow.vert + FragmentShader GLSL150 GLSL100 : other-shadow.frag + } +} diff --git a/jme3-core/src/test/resources/mixin-same-technique.j3md b/jme3-core/src/test/resources/mixin-same-technique.j3md new file mode 100644 index 0000000000..e172c83f26 --- /dev/null +++ b/jme3-core/src/test/resources/mixin-same-technique.j3md @@ -0,0 +1,12 @@ +MaterialDef MixinSameTechnique { + MaterialParameters { + Float Roughness + } + Technique PreShadow { + VertexShader GLSL150 GLSL100 : shadow.vert + FragmentShader GLSL150 GLSL100 : shadow.frag + WorldParameters { + WorldViewProjectionMatrix + } + } +} diff --git a/jme3-core/src/test/resources/mixin-shared-param.j3md b/jme3-core/src/test/resources/mixin-shared-param.j3md new file mode 100644 index 0000000000..2921c39779 --- /dev/null +++ b/jme3-core/src/test/resources/mixin-shared-param.j3md @@ -0,0 +1,10 @@ +MaterialDef MixinSharedParam { + MaterialParameters { + Float AlphaDiscardThreshold + Boolean UseFog + } + Technique Fog { + VertexShader GLSL150 GLSL100 : fog.vert + FragmentShader GLSL150 GLSL100 : fog.frag + } +}