Adding full support for custom materials to GltfLoader
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
GltfLoaderwhen 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
CustomContentManageronly 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 anExtensionLoaderinstance.
GLTFLoader
- Major flaw: There is currently no direct way to add custom material creation logic to the
GltfLoader.- Internally, there is a
defaultMaterialAdaptersmap, but there is no way to add newMaterialAdapters to it. In fact, it always contains exactly one adapter that cannot be changed. In its current state, this could be replaced with a singledefaultMaterialAdapterfield. - One way to provide custom
MaterialAdapters is viaGltfModelKey.materialAdapters, but this requires loading models using aGltfModelKeyinstead 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 currentMaterialAdapter. 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.
- Internally, there is a
Existing material-based ExtensionLoaders
Conceptual issue: Instead of modifying existing data, all current material-related
ExtensionLoaders replace theMaterialAdapter. Ideally, they should build upon the previous adapter, but instead they discard it entirely. As a result, only the last loader has any effect. (PBRSpecGlossExtensionLoaderandPBREmissiveStrengthExtensionLoaderare 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:
- Create a container object to collect all GLTF material parameters and extensions (e.g.,
GltfMaterialData) - Add all standard GLTF material parameters to it
- Process all ExtensionLoader and ExtrasLoader, allowing them to add data to this container
(They no longer operate on a
MaterialAdapter) - Select an appropriate material factory from a configurable list (e.g.,
GltfMaterialFactory) (Find the first one that supports the given data) - 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
GltfMaterialFactoryis 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:
GltfMaterialDataacts 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
GltfMaterialFactoryinstances (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
GltfMaterialDatainstead ofMaterialAdapter - Their role becomes purely additive (no more overriding)
GltfModelKey
- Optional: Allow adding custom
GltfMaterialFactoryinstances - Optional: Add “material hints” to guide factory selection
CustomContentManager
- Fix handling of multiple extensions (critical)
- 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:
MaterialAdapterGltfLoader.defaultMaterialAdaptersGltfModelKey.materialAdapters- Old material-based
ExtensionLoaders
- Add a flag in
GltfLoaderto 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