External Publication
Visit Post

Adding full support for custom materials to GltfLoader

jMonkeyEngine Hub March 31, 2026
Source

I would like to discuss a possible improvement to the material creation process inside the GltfLoader. There are currently a number of problems, especially if one wants to use custom materials for all loaded GLTF models.


What I want to achieve

  • Make it possible for custom materials to be created directly by the GltfLoader when loading GLTF models.
  • Custom materials should be completely independent from jME3’s standard materials. This means they can use different material definitions and parameters.

Problems with the current implementation

CustomContentManager

  • Major bug: If multiple GLTF extensions are present on an element, the CustomContentManager only processes the first supported one instead of all supported ones. This should be easy to fix.

  • Minor flaw: To add a new ExtensionLoader, one only provides its class rather than an instance. The manager then instantiates it using the default constructor. This complicates implementations that require dependencies or configuration. This could be improved by adding an additional method that accepts an ExtensionLoader instance.

GLTFLoader

  • Major flaw: There is currently no direct way to add custom material creation logic to the GltfLoader.
    • Internally, there is a defaultMaterialAdapters map, but there is no way to add new MaterialAdapters to it. In fact, it always contains exactly one adapter that cannot be changed. In its current state, this could be replaced with a single defaultMaterialAdapter field.
    • One way to provide custom MaterialAdapters is via GltfModelKey.materialAdapters, but this requires loading models using a GltfModelKey instead of a simple asset path. Additionally, adapters must be added separately for every key.
    • Another approach is to use custom ExtensionLoaders, since they can replace the current MaterialAdapter. However, this has several issues:
      • It only works if the corresponding GLTF extension is present in the file. Without it, the loader is never invoked.
      • You must remove or replace all existing material-related ExtensionLoaders to prevent them from overriding your adapter. This may result in losing useful built-in extension handling.

Existing material-based ExtensionLoaders

  • Conceptual issue: Instead of modifying existing data, all current material-related ExtensionLoaders replace the MaterialAdapter. Ideally, they should build upon the previous adapter, but instead they discard it entirely. As a result, only the last loader has any effect. (PBRSpecGlossExtensionLoader and PBREmissiveStrengthExtensionLoader are good exaples, that cancel each other out)

  • Control issue: The final material type depends on the order of ExtensionLoaders. Since execution order is not well controlled, defining a reliable material selection process is difficult.

  • Fragility: New ExtensionLoaders added in future jME3 versions could easily break custom setups if they override adapters.

MaterialAdapter

  • Minor issue: These are often stored in maps with inconsistent key naming, making them difficult to work with when adding custom adapters.

My suggested solution

GltfLoader

I suggest changing the material creation process in GltfLoader.createMaterial(...) as follows:

  1. Create a container object to collect all GLTF material parameters and extensions (e.g., GltfMaterialData)
  2. Add all standard GLTF material parameters to it
  3. Process all ExtensionLoader and ExtrasLoader, allowing them to add data to this container (They no longer operate on aMaterialAdapter)
  4. Select an appropriate material factory from a configurable list (e.g., GltfMaterialFactory) (Find the first one that supports the given data)
  5. Use that factory to create the final material

public interface GltfMaterialFactory {

    /**
     * Checks, if the factory is able to create a new material from the given set of available material params.
     * If it accepts the params, the {@link #createMaterial(GltfMaterialData)} method can be used to create a new material.
     *
     * @param gltfMaterialData The {@link GltfMaterialData} containing all available GLTF material params.
     * @return true if the factory is able to create a material from the given params, otherwise false.
     */
    boolean accepts(GltfMaterialData gltfMaterialData);

    /**
     * Creates a new material from the given set of available material params.
     *
     * @param gltfMaterialData The {@link GltfMaterialData} containing all available GLTF material params.
     * @return The new created {@link Material}.
     */
    Material createMaterial(GltfMaterialData gltfMaterialData);

}

GltfMaterialFactory:

  • Each GltfMaterialFactory is responsible for creating materials for a specific definition (e.g., PBRLighting, Unshaded)
  • A factory may support multiple definitions if needed
  • It is also possible (though not recommended) to use a single factory for everything

public class GltfMaterialData {

    private Map<String, MatParam> gltfParamMap = new HashMap<>();

    private Set<String> gltfExtensions = new HashSet<>();


    /**
     * Checks if the material provides the given GLTF extension.
     *
     * @param gltfExtension The GLTF extension name.
     * @return true if the material provides the given GLTF extension, otherwise false.
     */
    public boolean hasGltfExtension(String gltfExtension) {
        return gltfExtensions.contains(gltfExtension);
    }

    /**
     * Adds the given GLTF extension name.
     *
     * @param gltfExtension The GLTF extension name.
     */
    public void addGltfExtension(String gltfExtension) {
        gltfExtensions.add(gltfExtension);
    }


    /**
     * Checks if the material provides a material param with the given name.
     *
     * @param gltfParamName The GLTF param name.
     * @return true if the material provides a material param with the given name, otherwise false.
     */
    public boolean containsGltfParam(String gltfParamName) {
        return gltfParamMap.containsKey(gltfParamName);
    }

    /**
     * Gets the material parameter with the given name.
     *
     * @param gltfParamName The GLTF param name.
     * @return The {@link MatParam} with the given name, or null if no such param exists.
     */
    public MatParam getGltfParam(String gltfParamName) {
        return gltfParamMap.get(gltfParamName);
    }

    /**
     * Adds a material param with the given name and value.
     *
     * @param gltfParamName The GLTF param name.
     * @param value         The value of the param. Does nothing, if value is null.
     */
    public void setGltfParam(String gltfParamName, Object value) {
        if (value != null) {
            MatParam gltfParam = new MatParam(getVarType(value), gltfParamName, value);
            gltfParamMap.put(gltfParamName, gltfParam);
        }
    }


    // Maybe more convenience methods?
    // * getGltfParam(gltfParamName, defaultValue)
    // * setGltfParam(gltfParamName, value, defaultValue) ???
    // * removeGltfParam(gltfParamName)


    private VarType getVarType(Object value) {
        return switch (value) {
            case Float __ -> VarType.Float;
            case Integer __ -> VarType.Int;
            case Boolean __ -> VarType.Boolean;
            case ColorRGBA __ -> VarType.Vector4;
            case Vector4f __ -> VarType.Vector4;
            case Vector3f __ -> VarType.Vector3;
            case Vector2f __ -> VarType.Vector2;
            case Matrix3f __ -> VarType.Matrix3;
            case Matrix4f __ -> VarType.Matrix4;
            case String __ -> VarType.Boolean;   // WTF ???
            default -> throw new AssetLoadException("Unsupported material parameter type : " + value.getClass().getSimpleName());
        };
    }

}

GltfMaterialData:

  • GltfMaterialData acts as a container for parameters and extensions
  • Similar to MaterialAdapter, but does not create materials
  • Factories handle the actual material creation
  • Naming conventions for parameters still need to be defined carefully to avoid collisions (I think naming should be close to GLTF)

GltfLoader

  • Maintain a configurable list of GltfMaterialFactory instances (Effectively replacesdefaultMaterialAdapters)
  • Include default factories (e.g., PBRLighting, Unshaded)
  • Allow users to add, remove, or replace factories

Existing ExtensionLoaders

  • Must be updated to work with GltfMaterialData instead of MaterialAdapter
  • Their role becomes purely additive (no more overriding)

GltfModelKey

  • Optional: Allow adding custom GltfMaterialFactory instances
  • Optional: Add “material hints” to guide factory selection

CustomContentManager

  1. Fix handling of multiple extensions (critical)
  2. Add methods that accept loader instances instead of only classes (minor)

Backward compatibility

This is the tricky part. The new system is not compatible with:

  • MaterialAdapter
  • Existing material-based ExtensionLoaders

This has the potential to break some existing projects, if they use custom MaterialAdapter in GltfModelKeys. It also breaks all existing material-based ExtensionLoader, because they no longer get a MaterialAdapter, but instead the new GltfMaterialData as input. While this may break some projects, the number is likely small due to current limitations of the system.

Suggested approach:

  • Mark old components as deprecated:
    • MaterialAdapter
    • GltfLoader.defaultMaterialAdapters
    • GltfModelKey.materialAdapters
    • Old material-based ExtensionLoaders
  • Add a flag in GltfLoader to switch between old and new behavior
    • Default to the new system
  • Remove the old system in a future major release

What do you think?

I created this thread to gather feedback. Please share:

  • Suggestions for improvement
  • Potential issues I may have missed
  • Alternative approaches

If we arrive at a solid solution, I would be happy to implement it and open a PR.

Discussion in the ATmosphere

Loading comments...