[SOLVED] Vlog 02: Mixamo FBX models now load perfectly with jMonkeyEngine
github.com/stephengold/MonkeyWrench
#### Proposal: Replace dedicated globalScale field with a property map in LwjglAssetKey
opened 10:38AM - 06 Aug 26 UTC
capdevon
### Context **LwjglAssetKey** currently stores _globalScale_ as a dedicated fin…al float field. This value is used exclusively to set the `Assimp.AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY` property on the `AIPropertyStore` before importing a scene. While this works for the global scale, Assimp exposes many other configuration properties. Adding each of them as a separate field would bloat the API and require new constructors for every combination, making the class hard to maintain and extend. ### Proposal Replace the `globalScale` field with a general-purpose `Map<String, Object> storeProperties` that can hold any Assimp import property. The loader would iterate over the map and set each property on the `AIPropertyStore` using the appropriate `aiSetImportProperty*` function. Optional: A Builder inner class is also proposed to keep the construction readable and allow setting multiple properties fluently. ### Implementation summary **LwjglAssetKey:** * remove globalScale * add private Map<String, Object> storeProperties * add constructor accepting Map<String, Object> * add Builder with storeProperty(String, Object) * update equals()/hashCode() to include the map * add getStoreProperties() returning unmodifiable view ```java public class LwjglAssetKey extends ModelKey { // ... others constants and loggers ... /** * Additional properties to be set on the AIPropertyStore. */ private Map<String, Object> storeProperties; private LwjglAssetKey(Builder builder) { super(builder.assetPath); this.flags = builder.flags; this.textureLoader = builder.textureLoader; this.storeProperties = Map.copyOf(builder.storeProperties); } public LwjglAssetKey(AssetKey<?> assetKey) { this(assetKey.getName()); } public LwjglAssetKey(String assetPath) { this(assetPath, defaultTextureLoader, defaultFlags); } public LwjglAssetKey(String assetPath, int flags) { this(assetPath, defaultTextureLoader, flags); } public LwjglAssetKey(String assetPath, TextureLoader textureLoader) { this(assetPath, textureLoader, defaultFlags); } public LwjglAssetKey(String assetPath, TextureLoader textureLoader, int flags) { this(assetPath, textureLoader, flags, null); } /** * Instantiate a key with the specified post-processing options and texture * loader. * * @param assetPath the name of (path to) the asset (not null) * @param textureLoader the desired texture-load options (not null) * @param flags the desired post-processing flag values, ORed together * (default=0x942b) * @param storeProperties additional properties to be set on the AIPropertyStore (may be null) */ public LwjglAssetKey(String assetPath, TextureLoader textureLoader, int flags, Map<String, Object> storeProperties) { super(assetPath); assert assetPath != null; assert textureLoader != null; this.flags = flags; this.textureLoader = textureLoader; this.storeProperties = storeProperties != null ? Map.copyOf(storeProperties) : Map.of(); } // ... others public methods ... /** * Return an unmodifiable view of the custom Assimp store properties. */ public Map<String, Object> getStoreProperties() { return storeProperties; } @Override public boolean equals(Object other) { // ... && storeProperties.equals(otherKey.storeProperties); } @Override public int hashCode() { // ... result = 31 * result + storeProperties.hashCode(); } /** * Builder for {@link LwjglAssetKey}. */ public static class Builder { private final String assetPath; private TextureLoader textureLoader = defaultTextureLoader; private int flags = defaultFlags; private final Map<String, Object> storeProperties = new HashMap<>(); public Builder(String assetPath) { this.assetPath = Objects.requireNonNull(assetPath, "assetPath must not be null"); } public Builder textureLoader(TextureLoader textureLoader) { this.textureLoader = Objects.requireNonNull(textureLoader); return this; } public Builder flags(int flags) { this.flags = flags; return this; } public Builder storeProperty(String key, Object value) { this.storeProperties.put(key, value); return this; } public Builder storeProperties(Map<String, Object> properties) { if (properties != null) { this.storeProperties.putAll(properties); } return this; } public LwjglAssetKey build() { return new LwjglAssetKey(this); } } } ``` **LwjglAssetLoader:** * Iterates over `assetKey.getStoreProperties()` and calls a helper setProperty() that dispatches to the correct Assimp setter based on the value type (currently `Integer` and `Float`; others can be added as needed).. ```java AIPropertyStore propertyStore = Assimp.aiCreatePropertyStore(); for (Map.Entry<String, Object> entry : assetKey.getStoreProperties().entrySet()) { setProperty(propertyStore, entry.getKey(), entry.getValue()); } AIScene aiScene = Assimp.aiImportFileExWithProperties( filename, postFlags, aiFileIo, propertyStore); Assimp.aiReleasePropertyStore(propertyStore); ``` ```java /** * Set a property in an Assimp property store. * * @param propertyStore the store (not null) * @param propKey the property key (not null) * @param value the property value (not null) */ private static void setProperty(AIPropertyStore propertyStore, String propKey, Object value) { if (value instanceof Integer) { Assimp.aiSetImportPropertyInteger(propertyStore, propKey, (int) value); } else if (value instanceof Float) { Assimp.aiSetImportPropertyFloat(propertyStore, propKey, (float) value); } else { logger.warning("Unsupported property type for key " + propKey + ": " + value.getClass().getName()); } } ``` **Example usage:** ```java TextureLoader textureLoader = new TextureLoader(...); int ppFlags = LwjglAssetKey.defaultFlags | Assimp.aiProcess_GlobalScale; LwjglAssetKey key = new LwjglAssetKey.Builder(MODEL_PATH) .textureLoader(textureLoader) .storeProperty(Assimp.AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY, 0.01f) .flags(ppFlags) .build(); key.setVerboseLogging(true); Spatial model = assetManager.loadModel(key); ``` Benefits * **Extensibility** – any future Assimp property can be supported without modifying LwjglAssetKey or its constructors. * **Flexibility** – users can set properties per asset without subclassing or creating many overloaded constructors. * **Cleaner API** – the Builder pattern keeps the construction readable and type‑safe (as far as possible with Object values). * **Alignment** – it mirrors the underlying AIPropertyStore concept and is consistent with how other game engines handle import options.