diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java index 6168132313..87ae344819 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/CustomContentManager.java @@ -34,10 +34,16 @@ import com.jme3.asset.AssetLoadException; import com.jme3.plugins.json.JsonArray; import com.jme3.plugins.json.JsonElement; - +import com.jme3.scene.plugins.gltf.ext.JME_speaker.SpeakerExtensionLoader; +import com.jme3.scene.plugins.gltf.ext.KHR_lights_punctual.LightsPunctualExtensionLoader; +import com.jme3.scene.plugins.gltf.ext.KHR_materials_pbrSpecularGlossiness.PBRSpecGlossExtensionLoader; +import com.jme3.scene.plugins.gltf.ext.KHR_materials_unlit.UnlitExtensionLoader; +import com.jme3.scene.plugins.gltf.ext.KHR_texture_transform.TextureTransformExtensionLoader; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -46,20 +52,26 @@ */ public class CustomContentManager { - private final static Logger logger = Logger.getLogger(CustomContentManager.class.getName()); + private static final Logger logger = Logger.getLogger(CustomContentManager.class.getName()); private GltfModelKey key; private GltfLoader gltfLoader; - private final Map defaultExtensionLoaders = new HashMap<>(); + static final Map> defaultExtensionLoaders = + new ConcurrentHashMap<>(); - public CustomContentManager() { - defaultExtensionLoaders.put("KHR_materials_pbrSpecularGlossiness", new PBRSpecGlossExtensionLoader()); - defaultExtensionLoaders.put("KHR_lights_punctual", new LightsPunctualExtensionLoader()); - defaultExtensionLoaders.put("KHR_materials_unlit", new UnlitExtensionLoader()); - defaultExtensionLoaders.put("KHR_texture_transform", new TextureTransformExtensionLoader()); + static { + defaultExtensionLoaders.put("KHR_materials_pbrSpecularGlossiness", PBRSpecGlossExtensionLoader.class); + defaultExtensionLoaders.put("KHR_lights_punctual", LightsPunctualExtensionLoader.class); + defaultExtensionLoaders.put("KHR_materials_unlit", UnlitExtensionLoader.class); + defaultExtensionLoaders.put("KHR_texture_transform", TextureTransformExtensionLoader.class); + defaultExtensionLoaders.put("JME_speaker", SpeakerExtensionLoader.class); } + private final Map loadedExtensionLoaders = new HashMap<>(); + + public CustomContentManager() {} + void init(GltfLoader gltfLoader) { this.gltfLoader = gltfLoader; @@ -72,8 +84,16 @@ void init(GltfLoader gltfLoader) { for (JsonElement extElem : extensionUsed) { String ext = extElem.getAsString(); if (ext != null) { - if (defaultExtensionLoaders.get(ext) == null && (this.key != null && this.key.getExtensionLoader(ext) == null)) { - logger.log(Level.WARNING, "Extension " + ext + " is not supported, please provide your own implementation in the GltfModelKey"); + if ( + defaultExtensionLoaders.get(ext) == null && + (this.key != null && this.key.getExtensionLoader(ext) == null) + ) { + logger.log( + Level.WARNING, + "Extension " + + ext + + " is not supported, please provide your own implementation in the GltfModelKey" + ); } } } @@ -83,15 +103,24 @@ void init(GltfLoader gltfLoader) { for (JsonElement extElem : extensionRequired) { String ext = extElem.getAsString(); if (ext != null) { - if (defaultExtensionLoaders.get(ext) == null && (this.key != null && this.key.getExtensionLoader(ext) == null)) { - logger.log(Level.SEVERE, "Extension " + ext + " is mandatory for this file, the loaded scene result will be unexpected."); + if ( + defaultExtensionLoaders.get(ext) == null && + (this.key != null && this.key.getExtensionLoader(ext) == null) + ) { + logger.log( + Level.SEVERE, + "Extension " + + ext + + " is mandatory for this file, the loaded scene result will be unexpected." + ); } } } } } - public T readExtensionAndExtras(String name, JsonElement el, T input) throws AssetLoadException, IOException { + public T readExtensionAndExtras(String name, JsonElement el, T input) + throws AssetLoadException, IOException { T output = readExtension(name, el, input); output = readExtras(name, el, output); return output; @@ -106,11 +135,34 @@ private T readExtension(String name, JsonElement el, T input) throws AssetLo for (Map.Entry ext : extensions.getAsJsonObject().entrySet()) { ExtensionLoader loader = null; + if (key != null) { loader = key.getExtensionLoader(ext.getKey()); } + if (loader == null) { - loader = defaultExtensionLoaders.get(ext.getKey()); + loader = loadedExtensionLoaders.get(ext.getKey()); + if (loader == null) { + try { + Class clz = defaultExtensionLoaders.get(ext.getKey()); + if (clz != null) { + loader = clz.getDeclaredConstructor().newInstance(); + } + } catch ( + InstantiationException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException + | NoSuchMethodException + | SecurityException e + ) { + logger.log(Level.WARNING, "Could not instantiate loader", e); + } + + if (loader != null) { + loadedExtensionLoaders.put(ext.getKey(), loader); + } + } } if (loader == null) { @@ -121,7 +173,15 @@ private T readExtension(String name, JsonElement el, T input) throws AssetLo try { return (T) loader.handleExtension(gltfLoader, name, el, ext.getValue(), input); } catch (ClassCastException e) { - throw new AssetLoadException("Extension loader " + loader.getClass().getName() + " for extension " + ext.getKey() + " is incompatible with type " + input.getClass(), e); + throw new AssetLoadException( + "Extension loader " + + loader.getClass().getName() + + " for extension " + + ext.getKey() + + " is incompatible with type " + + input.getClass(), + e + ); } } @@ -130,14 +190,16 @@ private T readExtension(String name, JsonElement el, T input) throws AssetLo @SuppressWarnings("unchecked") private T readExtras(String name, JsonElement el, T input) throws AssetLoadException { - if (key == null) { - return input; - } ExtrasLoader loader; - loader = key.getExtrasLoader(); - if (loader == null) { - return input; + if (key == null) { + loader = new UserDataLoader(); + } else { + loader = key.getExtrasLoader(); + if (loader == null) { + return input; + } } + JsonElement extras = el.getAsJsonObject().getAsJsonObject("extras"); if (extras == null) { return input; @@ -146,10 +208,15 @@ private T readExtras(String name, JsonElement el, T input) throws AssetLoadE try { return (T) loader.handleExtras(gltfLoader, name, el, extras, input); } catch (ClassCastException e) { - throw new AssetLoadException("Extra loader " + loader.getClass().getName() + " for " + name + " is incompatible with type " + input.getClass(), e); + throw new AssetLoadException( + "Extra loader " + + loader.getClass().getName() + + " for " + + name + + " is incompatible with type " + + input.getClass(), + e + ); } - } - - } diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java index a8c5070086..dd68056daa 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java @@ -31,21 +31,22 @@ */ package com.jme3.scene.plugins.gltf; -import com.jme3.plugins.json.JsonArray; -import com.jme3.plugins.json.JsonObject; -import com.jme3.plugins.json.JsonPrimitive; -import com.jme3.plugins.json.JsonElement; +import static com.jme3.scene.plugins.gltf.GltfUtils.*; + import com.jme3.anim.*; import com.jme3.asset.*; import com.jme3.material.Material; import com.jme3.material.RenderState; import com.jme3.math.*; +import com.jme3.plugins.json.JsonArray; +import com.jme3.plugins.json.JsonElement; +import com.jme3.plugins.json.JsonObject; +import com.jme3.plugins.json.JsonPrimitive; import com.jme3.renderer.Camera; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; import com.jme3.scene.control.CameraControl; import com.jme3.scene.mesh.MorphTarget; -import static com.jme3.scene.plugins.gltf.GltfUtils.*; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.util.IntMap; @@ -128,7 +129,11 @@ protected Object loadFromStream(AssetInfo assetInfo, InputStream stream) throws String version = getAsString(asset, "version"); String minVersion = getAsString(asset, "minVersion"); if (!isSupported(version, minVersion)) { - logger.log(Level.SEVERE, "Gltf Loader doesn''t support this gltf version: {0}{1}", new Object[]{version, minVersion != null ? ("/" + minVersion) : ""}); + logger.log( + Level.SEVERE, + "Gltf Loader doesn''t support this gltf version: {0}{1}", + new Object[] { version, minVersion != null ? ("/" + minVersion) : "" } + ); } scenes = docRoot.getAsJsonArray("scenes"); @@ -262,7 +267,6 @@ public Object readNode(int nodeIndex) throws IOException { spatial = node; } spatial.setName(readMeshName(meshIndex)); - } else { // no mesh, we have a node. Can be a camera node or a regular node. Integer camIndex = getAsInteger(nodeData, "camera"); @@ -341,24 +345,29 @@ public Transform readTransforms(JsonObject nodeData) { JsonArray translation = nodeData.getAsJsonArray("translation"); if (translation != null) { transform.setTranslation( - translation.get(0).getAsFloat(), - translation.get(1).getAsFloat(), - translation.get(2).getAsFloat()); + translation.get(0).getAsFloat(), + translation.get(1).getAsFloat(), + translation.get(2).getAsFloat() + ); } JsonArray rotation = nodeData.getAsJsonArray("rotation"); if (rotation != null) { - transform.setRotation(new Quaternion( + transform.setRotation( + new Quaternion( rotation.get(0).getAsFloat(), rotation.get(1).getAsFloat(), rotation.get(2).getAsFloat(), - rotation.get(3).getAsFloat())); + rotation.get(3).getAsFloat() + ) + ); } JsonArray scale = nodeData.getAsJsonArray("scale"); if (scale != null) { transform.setScale( - scale.get(0).getAsFloat(), - scale.get(1).getAsFloat(), - scale.get(2).getAsFloat()); + scale.get(0).getAsFloat(), + scale.get(1).getAsFloat(), + scale.get(2).getAsFloat() + ); } return transform; @@ -366,138 +375,151 @@ public Transform readTransforms(JsonObject nodeData) { public Geometry[] readMeshPrimitives(int meshIndex) throws IOException { Geometry[] geomArray = (Geometry[]) fetchFromCache("meshes", meshIndex, Object.class); - if (geomArray != null) { - // cloning the geoms. - Geometry[] geoms = new Geometry[geomArray.length]; - for (int i = 0; i < geoms.length; i++) { - geoms[i] = geomArray[i].clone(false); - } - return geoms; - } - JsonObject meshData = meshes.get(meshIndex).getAsJsonObject(); - JsonArray primitives = meshData.getAsJsonArray("primitives"); - assertNotNull(primitives, "Can't find any primitives in mesh " + meshIndex); - String name = getAsString(meshData, "name"); - - geomArray = new Geometry[primitives.size()]; - int index = 0; - for (JsonElement primitive : primitives) { - JsonObject meshObject = primitive.getAsJsonObject(); - Mesh mesh = new Mesh(); - addToCache("mesh", 0, mesh, 1); - Integer mode = getAsInteger(meshObject, "mode"); - mesh.setMode(getMeshMode(mode)); - Integer indices = getAsInteger(meshObject, "indices"); - if (indices != null) { - mesh.setBuffer(readAccessorData(indices, new VertexBufferPopulator(VertexBuffer.Type.Index))); - } - JsonObject attributes = meshObject.getAsJsonObject("attributes"); - assertNotNull(attributes, "No attributes defined for mesh " + mesh); - - skinBuffers.clear(); - - for (Map.Entry entry : attributes.entrySet()) { - // special case for joints and weights buffer. - // If there are more than 4 bones per vertex, there might be several of them - // we need to read them all and to keep only the 4 that have the most weight on the vertex. - String bufferType = entry.getKey(); - if (bufferType.startsWith("JOINTS")) { - SkinBuffers buffs = getSkinBuffers(bufferType); - SkinBuffers buffer - = readAccessorData(entry.getValue().getAsInt(), new JointArrayPopulator()); - buffs.joints = buffer.joints; - buffs.componentSize = buffer.componentSize; - } else if (bufferType.startsWith("WEIGHTS")) { - SkinBuffers buffs = getSkinBuffers(bufferType); - buffs.weights = readAccessorData(entry.getValue().getAsInt(), new FloatArrayPopulator()); - } else { - VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(), - new VertexBufferPopulator(getVertexBufferType(bufferType))); - if (vb != null) { - mesh.setBuffer(vb); + if (geomArray == null) { + JsonObject meshData = meshes.get(meshIndex).getAsJsonObject(); + JsonArray primitives = meshData.getAsJsonArray("primitives"); + assertNotNull(primitives, "Can't find any primitives in mesh " + meshIndex); + String name = getAsString(meshData, "name"); + + geomArray = new Geometry[primitives.size()]; + int index = 0; + for (JsonElement primitive : primitives) { + JsonObject meshObject = primitive.getAsJsonObject(); + Mesh mesh = new Mesh(); + addToCache("mesh", 0, mesh, 1); + Integer mode = getAsInteger(meshObject, "mode"); + mesh.setMode(getMeshMode(mode)); + Integer indices = getAsInteger(meshObject, "indices"); + if (indices != null) { + mesh.setBuffer( + readAccessorData(indices, new VertexBufferPopulator(VertexBuffer.Type.Index)) + ); + } + JsonObject attributes = meshObject.getAsJsonObject("attributes"); + assertNotNull(attributes, "No attributes defined for mesh " + mesh); + + skinBuffers.clear(); + + for (Map.Entry entry : attributes.entrySet()) { + // special case for joints and weights buffer. + // If there are more than 4 bones per vertex, there might be several of them + // we need to read them all and to keep only the 4 that have the most weight on the vertex. + String bufferType = entry.getKey(); + if (bufferType.startsWith("JOINTS")) { + SkinBuffers buffs = getSkinBuffers(bufferType); + SkinBuffers buffer = readAccessorData( + entry.getValue().getAsInt(), + new JointArrayPopulator() + ); + buffs.joints = buffer.joints; + buffs.componentSize = buffer.componentSize; + } else if (bufferType.startsWith("WEIGHTS")) { + SkinBuffers buffs = getSkinBuffers(bufferType); + buffs.weights = + readAccessorData(entry.getValue().getAsInt(), new FloatArrayPopulator()); + } else { + VertexBuffer vb = readAccessorData( + entry.getValue().getAsInt(), + new VertexBufferPopulator(getVertexBufferType(bufferType)) + ); + if (vb != null) { + mesh.setBuffer(vb); + } } } - } - handleSkinningBuffers(mesh, skinBuffers); - - if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) { - // the mesh has some skinning let's create needed buffers for HW skinning - // creating empty buffers for HW skinning - // the buffers will be setup if ever used. - VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight); - VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex); - // setting usage to cpuOnly so that the buffer is not sent empty to the GPU - indicesHW.setUsage(VertexBuffer.Usage.CpuOnly); - weightsHW.setUsage(VertexBuffer.Usage.CpuOnly); - mesh.setBuffer(weightsHW); - mesh.setBuffer(indicesHW); - mesh.generateBindPose(); - } - - // Read morph target names - LinkedList targetNames = new LinkedList<>(); - if (meshData.has("extras") && meshData.getAsJsonObject("extras").has("targetNames")) { - JsonArray targetNamesJson = meshData.getAsJsonObject("extras").getAsJsonArray("targetNames"); - for (JsonElement target : targetNamesJson) { - targetNames.add(target.getAsString()); + handleSkinningBuffers(mesh, skinBuffers); + + if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) { + // the mesh has some skinning let's create needed buffers for HW skinning + // creating empty buffers for HW skinning + // the buffers will be setup if ever used. + VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight); + VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex); + // setting usage to cpuOnly so that the buffer is not sent empty to the GPU + indicesHW.setUsage(VertexBuffer.Usage.CpuOnly); + weightsHW.setUsage(VertexBuffer.Usage.CpuOnly); + mesh.setBuffer(weightsHW); + mesh.setBuffer(indicesHW); + mesh.generateBindPose(); } - } - // Read morph targets - JsonArray targets = meshObject.getAsJsonArray("targets"); - if (targets != null) { - for (JsonElement target : targets) { - MorphTarget morphTarget = new MorphTarget(); - if (targetNames.size() > 0) { - morphTarget.setName(targetNames.pop()); + // Read morph target names + LinkedList targetNames = new LinkedList<>(); + if (meshData.has("extras") && meshData.getAsJsonObject("extras").has("targetNames")) { + JsonArray targetNamesJson = meshData + .getAsJsonObject("extras") + .getAsJsonArray("targetNames"); + for (JsonElement target : targetNamesJson) { + targetNames.add(target.getAsString()); } - for (Map.Entry entry : target.getAsJsonObject().entrySet()) { - String bufferType = entry.getKey(); - VertexBuffer.Type type = getVertexBufferType(bufferType); - VertexBuffer vb = readAccessorData(entry.getValue().getAsInt(), - new VertexBufferPopulator(type)); - if (vb != null) { - morphTarget.setBuffer(type, (FloatBuffer) vb.getData()); + } + + // Read morph targets + JsonArray targets = meshObject.getAsJsonArray("targets"); + if (targets != null) { + for (JsonElement target : targets) { + MorphTarget morphTarget = new MorphTarget(); + if (targetNames.size() > 0) { + morphTarget.setName(targetNames.pop()); + } + for (Map.Entry entry : target.getAsJsonObject().entrySet()) { + String bufferType = entry.getKey(); + VertexBuffer.Type type = getVertexBufferType(bufferType); + VertexBuffer vb = readAccessorData( + entry.getValue().getAsInt(), + new VertexBufferPopulator(type) + ); + if (vb != null) { + morphTarget.setBuffer(type, (FloatBuffer) vb.getData()); + } } + mesh.addMorphTarget(morphTarget); } - mesh.addMorphTarget(morphTarget); } - } - // Read mesh extras - mesh = customContentManager.readExtensionAndExtras("primitive", meshObject, mesh); - Geometry geom = new Geometry(null, mesh); + // Read mesh extras + mesh = customContentManager.readExtensionAndExtras("primitive", meshObject, mesh); + Geometry geom = new Geometry(null, mesh); - Integer materialIndex = getAsInteger(meshObject, "material"); - if (materialIndex == null) { - geom.setMaterial(defaultMat); - } else { - useNormalsFlag = false; - geom.setMaterial(readMaterial(materialIndex)); - if (geom.getMaterial().getAdditionalRenderState().getBlendMode() - == RenderState.BlendMode.Alpha) { - // Alpha blending is enabled for this material. Let's place the geom in the transparent bucket. - geom.setQueueBucket(RenderQueue.Bucket.Transparent); + Integer materialIndex = getAsInteger(meshObject, "material"); + if (materialIndex == null) { + geom.setMaterial(defaultMat); + } else { + useNormalsFlag = false; + geom.setMaterial(readMaterial(materialIndex)); + if ( + geom.getMaterial().getAdditionalRenderState().getBlendMode() == + RenderState.BlendMode.Alpha + ) { + // Alpha blending is enabled for this material. Let's place the geom in the transparent bucket. + geom.setQueueBucket(RenderQueue.Bucket.Transparent); + } + if (useNormalsFlag && mesh.getBuffer(VertexBuffer.Type.Tangent) == null) { + // No tangent buffer, but there is a normal map, we have to generate them using MiiktSpace + MikktspaceTangentGenerator.generate(geom); + } } - if (useNormalsFlag && mesh.getBuffer(VertexBuffer.Type.Tangent) == null) { - // No tangent buffer, but there is a normal map, we have to generate them using MiiktSpace - MikktspaceTangentGenerator.generate(geom); + + if (name != null) { + geom.setName(name + (primitives.size() > 1 ? ("_" + index) : "")); } - } - if (name != null) { - geom.setName(name + (primitives.size() > 1 ? ("_" + index) : "")); + geom.updateModelBound(); + geomArray[index] = geom; + index++; } - geom.updateModelBound(); - geomArray[index] = geom; - index++; - } + geomArray = customContentManager.readExtensionAndExtras("mesh", meshData, geomArray); - geomArray = customContentManager.readExtensionAndExtras("mesh", meshData, geomArray); + addToCache("meshes", meshIndex, geomArray, meshes.size()); + } - addToCache("meshes", meshIndex, geomArray, meshes.size()); - return geomArray; + // cloning the geoms. + Geometry[] geoms = new Geometry[geomArray.length]; + for (int i = 0; i < geoms.length; i++) { + geoms[i] = geomArray[i].clone(false); + } + return geoms; } private SkinBuffers getSkinBuffers(String bufferType) { @@ -533,8 +555,14 @@ private R readAccessorData(int accessorIndex, Populator populator) throws return data; } - public Object readBuffer(Integer bufferViewIndex, int byteOffset, int count, Object store, - int numComponents, VertexBuffer.Format format) throws IOException { + public Object readBuffer( + Integer bufferViewIndex, + int byteOffset, + int count, + Object store, + int numComponents, + VertexBuffer.Format format + ) throws IOException { JsonObject bufferView = bufferViews.get(bufferViewIndex).getAsJsonObject(); Integer bufferIndex = getAsInteger(bufferView, "buffer"); assertNotNull(bufferIndex, "No buffer defined for bufferView " + bufferViewIndex); @@ -594,7 +622,8 @@ protected byte[] getBytes(int bufferIndex, String uri, Integer bufferLength) thr String decoded = decodeUri(uri); if (!decoded.endsWith(".bin")) { throw new AssetLoadException( - "Cannot load " + decoded + ", a .bin extension is required."); + "Cannot load " + decoded + ", a .bin extension is required." + ); } BinDataKey key = new BinDataKey(info.getKey().getFolder() + decoded); @@ -630,8 +659,10 @@ public Material readMaterial(int materialIndex) throws IOException { adapter = customContentManager.readExtensionAndExtras("material", matData, adapter); if (adapter == null) { - logger.log(Level.WARNING, - "Couldn't find any matching material definition for material " + materialIndex); + logger.log( + Level.WARNING, + "Couldn't find any matching material definition for material " + materialIndex + ); adapter = defaultMaterialAdapters.get("pbrMetallicRoughness"); adapter.init(info.getManager()); setDefaultParams(adapter.getMaterial()); @@ -643,10 +674,13 @@ public Material readMaterial(int materialIndex) throws IOException { adapter.setParam("metallicFactor", getAsFloat(pbrMat, "metallicFactor", 1f)); adapter.setParam("roughnessFactor", getAsFloat(pbrMat, "roughnessFactor", 1f)); adapter.setParam("baseColorTexture", readTexture(pbrMat.getAsJsonObject("baseColorTexture"))); - adapter.setParam("metallicRoughnessTexture", - readTexture(pbrMat.getAsJsonObject("metallicRoughnessTexture"))); + adapter.setParam( + "metallicRoughnessTexture", + readTexture(pbrMat.getAsJsonObject("metallicRoughnessTexture")) + ); JsonObject metallicRoughnessJson = pbrMat.getAsJsonObject("metallicRoughnessTexture"); - metallicRoughnessIndex = metallicRoughnessJson != null ? getAsInteger(metallicRoughnessJson, "index") : null; + metallicRoughnessIndex = + metallicRoughnessJson != null ? getAsInteger(metallicRoughnessJson, "index") : null; } adapter.getMaterial().setName(getAsString(matData, "name")); @@ -672,7 +706,7 @@ public Material readMaterial(int materialIndex) throws IOException { Integer occlusionIndex = occlusionJson != null ? getAsInteger(occlusionJson, "index") : null; if (occlusionIndex != null && occlusionIndex.equals(metallicRoughnessIndex)) { adapter.getMaterial().setBoolean("AoPackedInMRMap", true); - } else { + } else { adapter.setParam("occlusionTexture", readTexture(matData.getAsJsonObject("occlusionTexture"))); } @@ -709,7 +743,6 @@ public void readCameras() throws IOException { cam.setFrustumPerspective(yfov * FastMath.RAD_TO_DEG, aspectRatio, zNear, zFar); cam = customContentManager.readExtensionAndExtras("camera.perspective", camData, cam); - } else { Float xmag = getAsFloat(camData, "xmag"); assertNotNull(xmag, "No xmag for orthographic camera"); @@ -776,7 +809,6 @@ public Texture2D readImage(int sourceIndex, boolean flip) throws IOException { String extension = mimeType.split("/")[1]; TextureKey key = new TextureKey("image" + sourceIndex + "." + extension, flip); result = (Texture2D) info.getManager().loadAssetFromStream(key, new ByteArrayInputStream(data)); - } else if (uri.startsWith("data:")) { // base64 encoded image String[] uriInfo = uri.split(","); @@ -817,13 +849,13 @@ public void readAnimation(int animationIndex) throws IOException { continue; } assertNotNull(targetPath, "No target path for channel"); -// -// if (targetPath.equals("weights")) { -// // Morph animation, not implemented in JME, let's warn the user and skip the channel -// logger.log(Level.WARNING, -// "Morph animation is not supported by JME yet, skipping animation track"); -// continue; -// } + // + // if (targetPath.equals("weights")) { + // // Morph animation, not implemented in JME, let's warn the user and skip the channel + // logger.log(Level.WARNING, + // "Morph animation is not supported by JME yet, skipping animation track"); + // continue; + // } TrackData trackData = tracks[targetNode]; if (trackData == null) { @@ -896,21 +928,27 @@ public void readAnimation(int animationIndex) throws IOException { if (node instanceof Spatial) { Spatial s = (Spatial) node; spatials.add(s); - if (trackData.rotations != null || trackData.translations != null - || trackData.scales != null) { - TransformTrack track = new TransformTrack(s, trackData.times, - trackData.translations, trackData.rotations, trackData.scales); + if ( + trackData.rotations != null || trackData.translations != null || trackData.scales != null + ) { + TransformTrack track = new TransformTrack( + s, + trackData.times, + trackData.translations, + trackData.rotations, + trackData.scales + ); aTracks.add(track); } if (trackData.weights != null && s instanceof Geometry) { Geometry g = (Geometry) s; int nbMorph = g.getMesh().getMorphTargets().length; -// for (int k = 0; k < trackData.weights.length; k++) { -// System.err.print(trackData.weights[k] + ","); -// if(k % nbMorph == 0 && k!=0){ -// System.err.println(" "); -// } -// } + // for (int k = 0; k < trackData.weights.length; k++) { + // System.err.print(trackData.weights[k] + ","); + // if(k % nbMorph == 0 && k!=0){ + // System.err.println(" "); + // } + // } MorphTrack track = new MorphTrack(g, trackData.times, trackData.weights, nbMorph); aTracks.add(track); } @@ -924,16 +962,30 @@ public void readAnimation(int animationIndex) throws IOException { // Check if all joints affected by this animation are from the same skin, // the track will be skipped. if (skinIndex != jw.skinIndex) { - logger.log(Level.WARNING, "Animation " + animationIndex + " (" + name - + ") applies to joints that are not from the same skin: skin " - + skinIndex + ", joint " + jw.joint.getName() - + " from skin " + jw.skinIndex); + logger.log( + Level.WARNING, + "Animation " + + animationIndex + + " (" + + name + + ") applies to joints that are not from the same skin: skin " + + skinIndex + + ", joint " + + jw.joint.getName() + + " from skin " + + jw.skinIndex + ); continue; } } - TransformTrack track = new TransformTrack(jw.joint, trackData.times, - trackData.translations, trackData.rotations, trackData.scales); + TransformTrack track = new TransformTrack( + jw.joint, + trackData.times, + trackData.translations, + trackData.rotations, + trackData.scales + ); aTracks.add(track); } } @@ -947,11 +999,11 @@ public void readAnimation(int animationIndex) throws IOException { for (Joint joint : skin.joints) { if (!usedJoints.contains(joint)) { // create a track - float[] times = new float[]{0}; + float[] times = new float[] { 0 }; - Vector3f[] translations = new Vector3f[]{joint.getLocalTranslation()}; - Quaternion[] rotations = new Quaternion[]{joint.getLocalRotation()}; - Vector3f[] scales = new Vector3f[]{joint.getLocalScale()}; + Vector3f[] translations = new Vector3f[] { joint.getLocalTranslation() }; + Quaternion[] rotations = new Quaternion[] { joint.getLocalRotation() }; + Vector3f[] scales = new Vector3f[] { joint.getLocalScale() }; TransformTrack track = new TransformTrack(joint, times, translations, rotations, scales); aTracks.add(track); } @@ -1089,8 +1141,12 @@ public void readSkins() throws IOException { } } - public Joint readNodeAsBone(int nodeIndex, int jointIndex, int skinIndex, Matrix4f inverseModelBindMatrix) - throws IOException { + public Joint readNodeAsBone( + int nodeIndex, + int jointIndex, + int skinIndex, + Matrix4f inverseModelBindMatrix + ) throws IOException { JointWrapper jointWrapper = fetchFromCache("nodes", nodeIndex, JointWrapper.class); if (jointWrapper != null) { return jointWrapper.joint; @@ -1114,8 +1170,7 @@ public Joint readNodeAsBone(int nodeIndex, int jointIndex, int skinIndex, Matrix private void findChildren(int nodeIndex) throws IOException { JointWrapper jw = fetchFromCache("nodes", nodeIndex, JointWrapper.class); if (jw == null) { - logger.log(Level.WARNING, - "No JointWrapper found for nodeIndex={0}.", nodeIndex); + logger.log(Level.WARNING, "No JointWrapper found for nodeIndex={0}.", nodeIndex); return; } @@ -1159,12 +1214,12 @@ private void setupControls() { if (spatials.size() >= 1) { spatial = findCommonAncestor(spatials); } -// if (spatial != skinData.parent) { -// skinData.rootBoneTransformOffset = spatial.getWorldTransform().invert(); -// if (skinData.parent != null) { -// skinData.rootBoneTransformOffset.combineWithParent(skinData.parent.getWorldTransform()); -// } -// } + // if (spatial != skinData.parent) { + // skinData.rootBoneTransformOffset = spatial.getWorldTransform().invert(); + // if (skinData.parent != null) { + // skinData.rootBoneTransformOffset.combineWithParent(skinData.parent.getWorldTransform()); + // } + // } if (skinData.animComposer != null && skinData.animComposer.getSpatial() == null) { spatial.addControl(skinData.animComposer); } @@ -1183,9 +1238,11 @@ private void setupControls() { SkinData skinData = fetchFromCache("skins", bw.skinIndex, SkinData.class); SkinningControl skinControl = skinData.skinningControl; if (skinControl.getSpatial() == null) { - logger.log(Level.WARNING, - "No skinned Spatial for joint \"{0}\" -- will skin the model's root node!", - jointName); + logger.log( + Level.WARNING, + "No skinned Spatial for joint \"{0}\" -- will skin the model's root node!", + jointName + ); rootNode.addControl(skinControl); } skinControl.getAttachmentsNode(jointName).attachChild(bw.attachedSpatial); @@ -1240,6 +1297,7 @@ private String decodeUri(String uri) { } public static class WeightData { + float value; short index; int componentSize; @@ -1252,6 +1310,7 @@ public WeightData(float value, short index, int componentSize) { } private class JointWrapper { + Joint joint; int jointIndex; int skinIndex; @@ -1267,6 +1326,7 @@ public JointWrapper(Joint joint, int jointIndex, int skinIndex) { } private class SkinData { + SkinningControl skinningControl; MorphControl morphControl; AnimComposer animComposer; @@ -1278,6 +1338,7 @@ private class SkinData { } public static class SkinBuffers { + short[] joints; float[] weights; int componentSize; @@ -1291,11 +1352,18 @@ public SkinBuffers() {} } private interface Populator { - T populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset, - boolean normalized) throws IOException; + T populate( + Integer bufferViewIndex, + int componentType, + String type, + int count, + int byteOffset, + boolean normalized + ) throws IOException; } private class VertexBufferPopulator implements Populator { + VertexBuffer.Type bufferType; public VertexBufferPopulator(VertexBuffer.Type bufferType) { @@ -1303,10 +1371,20 @@ public VertexBufferPopulator(VertexBuffer.Type bufferType) { } @Override - public VertexBuffer populate(Integer bufferViewIndex, int componentType, String type, int count, - int byteOffset, boolean normalized) throws IOException { + public VertexBuffer populate( + Integer bufferViewIndex, + int componentType, + String type, + int count, + int byteOffset, + boolean normalized + ) throws IOException { if (bufferType == null) { - logger.log(Level.WARNING, "could not assign data to any VertexBuffer type for buffer view {0}", bufferViewIndex); + logger.log( + Level.WARNING, + "could not assign data to any VertexBuffer type for buffer view {0}", + bufferViewIndex + ); return null; } @@ -1342,8 +1420,14 @@ public VertexBuffer populate(Integer bufferViewIndex, int componentType, String private class FloatArrayPopulator implements Populator { @Override - public float[] populate(Integer bufferViewIndex, int componentType, String type, int count, - int byteOffset, boolean normalized) throws IOException { + public float[] populate( + Integer bufferViewIndex, + int componentType, + String type, + int count, + int byteOffset, + boolean normalized + ) throws IOException { int numComponents = getNumberOfComponents(type); int dataSize = numComponents * count; float[] data = new float[dataSize]; @@ -1352,42 +1436,55 @@ public float[] populate(Integer bufferViewIndex, int componentType, String type, // no referenced buffer, specs says to pad the data with zeros. padBuffer(data, dataSize); } else { - readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, - getVertexBufferFormat(componentType)); + readBuffer( + bufferViewIndex, + byteOffset, + count, + data, + numComponents, + getVertexBufferFormat(componentType) + ); } return data; } } -// -// private class FloatGridPopulator implements Populator { -// -// @Override -// public float[][] populate(Integer bufferViewIndex, int componentType, String type, int count, -// int byteOffset, boolean normalized) throws IOException { -// -// int numComponents = getNumberOfComponents(type); -// int dataSize = numComponents * count; -// float[] data = new float[dataSize]; -// -// if (bufferViewIndex == null) { -// // no referenced buffer, specs says to pad the data with zeros. -// padBuffer(data, dataSize); -// } else { -// readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, -// getVertexBufferFormat(componentType)); -// } -// -// return data; -// } -// -// } + + // + // private class FloatGridPopulator implements Populator { + // + // @Override + // public float[][] populate(Integer bufferViewIndex, int componentType, String type, int count, + // int byteOffset, boolean normalized) throws IOException { + // + // int numComponents = getNumberOfComponents(type); + // int dataSize = numComponents * count; + // float[] data = new float[dataSize]; + // + // if (bufferViewIndex == null) { + // // no referenced buffer, specs says to pad the data with zeros. + // padBuffer(data, dataSize); + // } else { + // readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, + // getVertexBufferFormat(componentType)); + // } + // + // return data; + // } + // + // } private class Vector3fArrayPopulator implements Populator { @Override - public Vector3f[] populate(Integer bufferViewIndex, int componentType, String type, int count, - int byteOffset, boolean normalized) throws IOException { + public Vector3f[] populate( + Integer bufferViewIndex, + int componentType, + String type, + int count, + int byteOffset, + boolean normalized + ) throws IOException { int numComponents = getNumberOfComponents(type); int dataSize = numComponents * count; Vector3f[] data = new Vector3f[count]; @@ -1396,8 +1493,14 @@ public Vector3f[] populate(Integer bufferViewIndex, int componentType, String ty // no referenced buffer, specs says to pad the data with zeros. padBuffer(data, dataSize); } else { - readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, - getVertexBufferFormat(componentType)); + readBuffer( + bufferViewIndex, + byteOffset, + count, + data, + numComponents, + getVertexBufferFormat(componentType) + ); } return data; } @@ -1406,8 +1509,14 @@ public Vector3f[] populate(Integer bufferViewIndex, int componentType, String ty private class QuaternionArrayPopulator implements Populator { @Override - public Quaternion[] populate(Integer bufferViewIndex, int componentType, String type, int count, - int byteOffset, boolean normalized) throws IOException { + public Quaternion[] populate( + Integer bufferViewIndex, + int componentType, + String type, + int count, + int byteOffset, + boolean normalized + ) throws IOException { int numComponents = getNumberOfComponents(type); int dataSize = numComponents * count; Quaternion[] data = new Quaternion[count]; @@ -1416,8 +1525,14 @@ public Quaternion[] populate(Integer bufferViewIndex, int componentType, String // no referenced buffer, specs says to pad the data with zeros. padBuffer(data, dataSize); } else { - readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, - getVertexBufferFormat(componentType)); + readBuffer( + bufferViewIndex, + byteOffset, + count, + data, + numComponents, + getVertexBufferFormat(componentType) + ); } return data; @@ -1427,8 +1542,14 @@ public Quaternion[] populate(Integer bufferViewIndex, int componentType, String private class Matrix4fArrayPopulator implements Populator { @Override - public Matrix4f[] populate(Integer bufferViewIndex, int componentType, String type, int count, - int byteOffset, boolean normalized) throws IOException { + public Matrix4f[] populate( + Integer bufferViewIndex, + int componentType, + String type, + int count, + int byteOffset, + boolean normalized + ) throws IOException { int numComponents = getNumberOfComponents(type); int dataSize = numComponents * count; Matrix4f[] data = new Matrix4f[count]; @@ -1437,8 +1558,14 @@ public Matrix4f[] populate(Integer bufferViewIndex, int componentType, String ty // no referenced buffer, specs says to pad the data with zeros. padBuffer(data, dataSize); } else { - readBuffer(bufferViewIndex, byteOffset, count, data, numComponents, - getVertexBufferFormat(componentType)); + readBuffer( + bufferViewIndex, + byteOffset, + count, + data, + numComponents, + getVertexBufferFormat(componentType) + ); } return data; @@ -1448,8 +1575,14 @@ public Matrix4f[] populate(Integer bufferViewIndex, int componentType, String ty private class JointArrayPopulator implements Populator { @Override - public SkinBuffers populate(Integer bufferViewIndex, int componentType, String type, int count, - int byteOffset, boolean normalized) throws IOException { + public SkinBuffers populate( + Integer bufferViewIndex, + int componentType, + String type, + int count, + int byteOffset, + boolean normalized + ) throws IOException { int numComponents = getNumberOfComponents(type); // can be bytes or shorts. @@ -1471,4 +1604,12 @@ public SkinBuffers populate(Integer bufferViewIndex, int componentType, String t return new SkinBuffers(data, format.getComponentSize()); } } -} \ No newline at end of file + + public static void registerExtension(String name, Class ext) { + CustomContentManager.defaultExtensionLoaders.put(name, ext); + } + + public static void unregisterExtension(String name) { + CustomContentManager.defaultExtensionLoaders.remove(name); + } +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java index 34f43dde50..b02af8a334 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java @@ -53,7 +53,7 @@ public class GltfModelKey extends ModelKey { private Map materialAdapters = new HashMap<>(); private static Map extensionLoaders = new HashMap<>(); - private ExtrasLoader extrasLoader; + private ExtrasLoader extrasLoader = new UserDataLoader(); private boolean keepSkeletonPose = false; public GltfModelKey(String name) { diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/MaterialAdapter.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/MaterialAdapter.java index cb2639dc8f..d14f4cec43 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/MaterialAdapter.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/MaterialAdapter.java @@ -31,14 +31,12 @@ */ package com.jme3.scene.plugins.gltf; - import com.jme3.asset.AssetLoadException; import com.jme3.asset.AssetManager; import com.jme3.material.*; import com.jme3.math.*; import com.jme3.shader.VarType; import com.jme3.texture.Texture; - import java.util.HashMap; import java.util.Map; @@ -63,7 +61,7 @@ public abstract class MaterialAdapter { protected abstract MatParam adaptMatParam(MatParam param); - protected void init(AssetManager assetManager) { + public void init(AssetManager assetManager) { this.assetManager = assetManager; this.reset(); } @@ -89,15 +87,30 @@ public void setParam(String gltfParamName, Object value) { if (value instanceof Texture) { MatParam defParam = getMaterial().getMaterialDef().getMaterialParam(name); if (defParam == null) { - throw new AssetLoadException("Material definition " + getMaterialDefPath() + " has not param with name" + name); + throw new AssetLoadException( + "Material definition " + getMaterialDefPath() + " has not param with name" + name + ); } if (!(defParam instanceof MatParamTexture)) { - throw new AssetLoadException("param with name" + name + "in material definition " + getMaterialDefPath() + " should be a texture param"); + throw new AssetLoadException( + "param with name" + + name + + "in material definition " + + getMaterialDefPath() + + " should be a texture param" + ); } - param = new MatParamTexture(VarType.Texture2D, name, (Texture) value, ((MatParamTexture) defParam).getColorSpace()); + param = + new MatParamTexture( + VarType.Texture2D, + name, + (Texture) value, + ((MatParamTexture) defParam).getColorSpace() + ); param = adaptMatParam(param); if (param != null) { - getMaterial().setTextureParam(param.getName(), param.getVarType(), (Texture) param.getValue()); + getMaterial() + .setTextureParam(param.getName(), param.getVarType(), (Texture) param.getValue()); } } else { param = new MatParam(getVarType(value), name, value); @@ -127,6 +140,8 @@ private VarType getVarType(Object value) { if (value instanceof Matrix3f) return VarType.Matrix3; if (value instanceof Matrix4f) return VarType.Matrix4; if (value instanceof String) return VarType.Boolean; - throw new AssetLoadException("Unsupported material parameter type : " + value.getClass().getSimpleName()); + throw new AssetLoadException( + "Unsupported material parameter type : " + value.getClass().getSimpleName() + ); } } diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UserDataLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UserDataLoader.java new file mode 100644 index 0000000000..c2a43eac22 --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UserDataLoader.java @@ -0,0 +1,190 @@ +package com.jme3.scene.plugins.gltf; + +import com.jme3.plugins.json.JsonArray; +import com.jme3.plugins.json.JsonElement; +import com.jme3.plugins.json.JsonObject; +import com.jme3.plugins.json.JsonPrimitive; +import com.jme3.scene.Spatial; +/* + * $Id$ + * + * Copyright (c) 2019, Simsilica, LLC + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import java.lang.reflect.Array; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Attaches GLTF "extras" data to objects as appropriate. + * + * @author Paul Speed + */ +public class UserDataLoader implements ExtrasLoader { + + private static final Logger log = Logger.getLogger( + UserDataLoader.class.getName() + ); + + public UserDataLoader() {} + + @Override + public Object handleExtras( + GltfLoader loader, + String parentName, + JsonElement parent, + JsonElement extras, + Object input + ) { + log.fine( + "handleExtras(" + + loader + + ", " + + parentName + + ", " + + parent + + ", " + + extras + + ", " + + input + + ")" + ); + // Only interested in composite objects + if (!(extras instanceof JsonObject)) { + log.warning("Skipping extras:" + extras); + return input; + } + JsonObject jo = extras.getAsJsonObject(); + apply(input, jo); + return input; + } + + protected void apply(Object input, JsonObject extras) { + if (input == null) { + return; + } + if (input.getClass().isArray()) { + applyToArray(input, extras); + } else if (input instanceof Spatial) { + applyToSpatial((Spatial) input, extras); + } else { + log.warning("Unhandled input type:" + input.getClass()); + } + } + + protected void applyToArray(Object array, JsonObject extras) { + int size = Array.getLength(array); + for (int i = 0; i < size; i++) { + Object o = Array.get(array, i); + log.fine("processing array[" + i + "]:" + o); + apply(o, extras); + } + } + + protected void applyToSpatial(Spatial spatial, JsonObject extras) { + for (Map.Entry el : extras.entrySet()) { + log.fine(el.toString()); + Object val = toAttribute(el.getValue(), false); + + if (log.isLoggable(Level.FINE)) { + log.fine("setUserData(" + el.getKey() + ", " + val + ")"); + } + spatial.setUserData(el.getKey(), val); + } + } + + protected Object toAttribute(JsonElement el, boolean nested) { + if (el == null) { + return null; + } + if (el instanceof JsonObject) { + return toAttribute(el.getAsJsonObject(), nested); + } else if (el instanceof JsonArray) { + return toAttribute(el.getAsJsonArray(), nested); + } else if (el instanceof JsonPrimitive) { + return toAttribute(el.getAsJsonPrimitive(), nested); + } + log.fine("Unhandled extras element:" + el); + return null; + } + + protected Object toAttribute(JsonObject jo, boolean nested) { + Map result = new HashMap<>(); + for (Map.Entry el : jo.entrySet()) { + result.put(el.getKey(), toAttribute(el.getValue(), true)); + } + return result; + } + + protected Object toAttribute(JsonArray ja, boolean nested) { + List result = new ArrayList<>(); + for (JsonElement el : ja) { + result.add(toAttribute(el, true)); + } + return result; + } + + protected Object toAttribute(JsonPrimitive jp, boolean nested) { + if (jp.isBoolean()) { + return jp.getAsBoolean(); + } else if (jp.isNumber()) { + // JME doesn't save Maps properly and treats them as two + // separate Lists... and it doesn't like saving Doubles + // in lists so we'll just return strings in the case where + // the value would end up in a map. If users someday really + // need properly typed map values and JME map storage hasn't + // been fixed then perhaps we give the users the option of + // flattening the nested properties into dot notation, ie: + // all directly on UserData with no Map children. + if (nested) { + return jp.getAsString(); + } + Number num = jp.getAsNumber(); + // JME doesn't like to save GSON's LazilyParsedNumber so we'll + // convert it into a real number. I don't think we can reliably + // guess what type of number the user intended. It would take + // some expirimentation to determine if things like 0.0 are + // preserved + // during export or just get exported as 0. + // Rather than randomly flip-flop between number types depending + // on the inclusion (or not) of a decimal point, we'll just always + // return Double. + return num.doubleValue(); + } else if (jp.isString()) { + return jp.getAsString(); + } + log.fine("Unhandled primitive:" + jp); + return null; + } +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/JME_speaker/SpeakerExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/JME_speaker/SpeakerExtensionLoader.java new file mode 100644 index 0000000000..4aafef1782 --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/JME_speaker/SpeakerExtensionLoader.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2009-2023 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.scene.plugins.gltf.ext.JME_speaker; + + +import com.jme3.asset.AssetManager; +import com.jme3.audio.AudioData.DataType; +import com.jme3.plugins.json.JsonElement; +import com.jme3.plugins.json.JsonObject; +import com.jme3.audio.AudioNode; +import com.jme3.scene.Node; +import com.jme3.scene.plugins.gltf.ExtensionLoader; +import com.jme3.scene.plugins.gltf.GltfLoader; + +/** + * An extension for the GLTF loader that loads speaker nodes. + * (This extension requires the jme-extras addon for blender) + */ +public class SpeakerExtensionLoader implements ExtensionLoader { + + public SpeakerExtensionLoader(){} + + @Override + public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) { + if (input instanceof Node) { + JsonObject jsonObject = extension.getAsJsonObject(); + + final float volume = jsonObject.getAsJsonPrimitive("volume").getAsFloat(); + final float pitch = jsonObject.getAsJsonPrimitive("pitch").getAsFloat(); + final float attenuation = jsonObject.getAsJsonPrimitive("attenuation").getAsFloat(); // unused + final float distanceMax = jsonObject.getAsJsonPrimitive("distance_max").getAsFloat(); + final float distanceReference = jsonObject.getAsJsonPrimitive("distance_reference").getAsFloat(); + final float volume_min = jsonObject.getAsJsonPrimitive("volume_min").getAsFloat(); // unused + final float volume_max = jsonObject.getAsJsonPrimitive("volume_max").getAsFloat(); // unused + final float angleOuterCone = jsonObject.getAsJsonPrimitive("angle_outer_cone").getAsFloat(); + final float angleInnerCone = jsonObject.getAsJsonPrimitive("angle_inner_cone").getAsFloat(); + final float outerConeVolume = jsonObject.getAsJsonPrimitive("outer_cone_volume").getAsFloat(); // unused + final String soundPath = jsonObject.getAsJsonPrimitive("sound_path").getAsString(); + + String absSoundPath = loader.getInfo().getKey().getFolder() + soundPath; + + + AssetManager am = loader.getInfo().getManager(); + AudioNode audioSource = new AudioNode(am, absSoundPath, DataType.Buffer); + audioSource.setVolume(volume); + audioSource.setPitch(pitch); + audioSource.setRefDistance(distanceReference); + audioSource.setMaxDistance(distanceMax); + audioSource.setInnerAngle(angleInnerCone); + audioSource.setOuterAngle(angleOuterCone); + audioSource.setPositional(true); + + if (angleOuterCone == 360 && angleInnerCone == 360) { + audioSource.setDirectional(false); + } else { + audioSource.setDirectional(true); + } + + audioSource.setLooping(true); + audioSource.setReverbEnabled(true); + + ((Node) input).attachChild(audioSource); + + audioSource.play(); + + } + + return input; + + } + +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_lights_punctual/LightsPunctualExtensionLoader.java similarity index 88% rename from jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java rename to jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_lights_punctual/LightsPunctualExtensionLoader.java index b3df7dbc88..0860bcba50 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/LightsPunctualExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_lights_punctual/LightsPunctualExtensionLoader.java @@ -29,11 +29,8 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.jme3.scene.plugins.gltf; +package com.jme3.scene.plugins.gltf.ext.KHR_lights_punctual; -import com.jme3.plugins.json.JsonArray; -import com.jme3.plugins.json.JsonObject; -import com.jme3.plugins.json.JsonElement; import com.jme3.asset.AssetLoadException; import com.jme3.light.DirectionalLight; import com.jme3.light.Light; @@ -42,8 +39,14 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Vector3f; +import com.jme3.plugins.json.JsonArray; +import com.jme3.plugins.json.JsonElement; +import com.jme3.plugins.json.JsonObject; import com.jme3.scene.Node; import com.jme3.scene.control.LightControl; +import com.jme3.scene.plugins.gltf.ExtensionLoader; +import com.jme3.scene.plugins.gltf.GltfLoader; +import com.jme3.scene.plugins.gltf.GltfUtils; import java.util.HashMap; import java.util.HashSet; @@ -60,8 +63,16 @@ public class LightsPunctualExtensionLoader implements ExtensionLoader { private final HashSet pendingNodes = new HashSet<>(); private final HashMap lightDefinitions = new HashMap<>(); + public LightsPunctualExtensionLoader() {} + @Override - public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) { + public Object handleExtension( + GltfLoader loader, + String parentName, + JsonElement parent, + JsonElement extension, + Object input + ) { if (input instanceof Node) { //We are processing a node JsonObject jsonObject = extension.getAsJsonObject(); if (jsonObject.has("light")) { //These will get run first when loading the gltf file @@ -94,7 +105,9 @@ public Object handleExtension(GltfLoader loader, String parentName, JsonElement lightNode = buildSpotLight(light); break; default: - throw new AssetLoadException("KHR_lights_punctual unsupported light type: " + type); + throw new AssetLoadException( + "KHR_lights_punctual unsupported light type: " + type + ); } lightDefinitions.put(i, lightNode); @@ -125,14 +138,20 @@ private SpotLight buildSpotLight(JsonObject obj) { String name = obj.has("name") ? obj.get("name").getAsString() : ""; float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f; - ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White); + ColorRGBA color = obj.has("color") + ? GltfUtils.getAsColor(obj, "color") + : new ColorRGBA(ColorRGBA.White); color = lumensToColor(color, intensity); float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY; //Spot specific JsonObject spot = obj.getAsJsonObject("spot"); - float innerConeAngle = spot != null && spot.has("innerConeAngle") ? spot.get("innerConeAngle").getAsFloat() : 0f; - float outerConeAngle = spot != null && spot.has("outerConeAngle") ? spot.get("outerConeAngle").getAsFloat() : ((float) Math.PI) / 4f; + float innerConeAngle = spot != null && spot.has("innerConeAngle") + ? spot.get("innerConeAngle").getAsFloat() + : 0f; + float outerConeAngle = spot != null && spot.has("outerConeAngle") + ? spot.get("outerConeAngle").getAsFloat() + : ((float) Math.PI) / 4f; /* Correct floating point error on half PI, GLTF spec says that the outerConeAngle @@ -164,7 +183,9 @@ private DirectionalLight buildDirectionalLight(JsonObject obj) { String name = obj.has("name") ? obj.get("name").getAsString() : ""; float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f; - ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White); + ColorRGBA color = obj.has("color") + ? GltfUtils.getAsColor(obj, "color") + : new ColorRGBA(ColorRGBA.White); color = lumensToColor(color, intensity); DirectionalLight directionalLight = new DirectionalLight(); @@ -185,7 +206,9 @@ private PointLight buildPointLight(JsonObject obj) { String name = obj.has("name") ? obj.get("name").getAsString() : ""; float intensity = obj.has("intensity") ? obj.get("intensity").getAsFloat() : 1.0f; - ColorRGBA color = obj.has("color") ? GltfUtils.getAsColor(obj, "color") : new ColorRGBA(ColorRGBA.White); + ColorRGBA color = obj.has("color") + ? GltfUtils.getAsColor(obj, "color") + : new ColorRGBA(ColorRGBA.White); color = lumensToColor(color, intensity); float range = obj.has("range") ? obj.get("range").getAsFloat() : Float.POSITIVE_INFINITY; @@ -211,7 +234,9 @@ private void addLight(Node parent, Node node, int lightIndex) { LightControl control = new LightControl(light); node.addControl(control); } else { - throw new AssetLoadException("KHR_lights_punctual extension accessed undefined light at index " + lightIndex); + throw new AssetLoadException( + "KHR_lights_punctual extension accessed undefined light at index " + lightIndex + ); } } @@ -247,7 +272,7 @@ vec4 HDR_EncodeLum(in float lum){ */ float epsilon = 0.0001f; - double Le = 2f * Math.log(lumens * epsilon) / Math.log(2) + 127.0; + double Le = (2f * Math.log(lumens * epsilon)) / Math.log(2) + 127.0; ColorRGBA color = new ColorRGBA(); color.a = (float) (Le - Math.floor(Le)); //Get fractional part float val = (float) ((Le - (Math.floor(color.a * 255.0)) / 255.0) / 255.0); @@ -262,6 +287,7 @@ vec4 HDR_EncodeLum(in float lum){ * A bean to contain the relation between a node and a light index */ private static class NodeNeedingLight { + private Node node; private int lightIndex; diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_pbrSpecularGlossiness/PBRSpecGlossExtensionLoader.java similarity index 77% rename from jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossExtensionLoader.java rename to jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_pbrSpecularGlossiness/PBRSpecGlossExtensionLoader.java index 2ab3862db3..dd053d1d45 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_pbrSpecularGlossiness/PBRSpecGlossExtensionLoader.java @@ -29,15 +29,19 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.jme3.scene.plugins.gltf; +package com.jme3.scene.plugins.gltf.ext.KHR_materials_pbrSpecularGlossiness; -import com.jme3.asset.AssetKey; - -import java.io.IOException; -import com.jme3.plugins.json.JsonElement; import static com.jme3.scene.plugins.gltf.GltfUtils.getAsColor; import static com.jme3.scene.plugins.gltf.GltfUtils.getAsFloat; +import com.jme3.asset.AssetKey; +import com.jme3.plugins.json.JsonElement; +import com.jme3.scene.plugins.gltf.ExtensionLoader; +import com.jme3.scene.plugins.gltf.GltfLoader; +import com.jme3.scene.plugins.gltf.GltfModelKey; +import com.jme3.scene.plugins.gltf.MaterialAdapter; +import java.io.IOException; + /** * Material adapter for PBR Specular Glossiness pipeline * Created by Nehon on 20/08/2017. @@ -46,8 +50,16 @@ public class PBRSpecGlossExtensionLoader implements ExtensionLoader { private PBRSpecGlossMaterialAdapter materialAdapter = new PBRSpecGlossMaterialAdapter(); + public PBRSpecGlossExtensionLoader() {} + @Override - public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) throws IOException { + public Object handleExtension( + GltfLoader loader, + String parentName, + JsonElement parent, + JsonElement extension, + Object input + ) throws IOException { MaterialAdapter adapter = materialAdapter; AssetKey key = loader.getInfo().getKey(); //check for a custom adapter for spec/gloss pipeline @@ -64,8 +76,14 @@ public Object handleExtension(GltfLoader loader, String parentName, JsonElement adapter.setParam("diffuseFactor", getAsColor(extension.getAsJsonObject(), "diffuseFactor")); adapter.setParam("specularFactor", getAsColor(extension.getAsJsonObject(), "specularFactor")); adapter.setParam("glossinessFactor", getAsFloat(extension.getAsJsonObject(), "glossinessFactor")); - adapter.setParam("diffuseTexture", loader.readTexture(extension.getAsJsonObject().getAsJsonObject("diffuseTexture"))); - adapter.setParam("specularGlossinessTexture", loader.readTexture(extension.getAsJsonObject().getAsJsonObject("specularGlossinessTexture"))); + adapter.setParam( + "diffuseTexture", + loader.readTexture(extension.getAsJsonObject().getAsJsonObject("diffuseTexture")) + ); + adapter.setParam( + "specularGlossinessTexture", + loader.readTexture(extension.getAsJsonObject().getAsJsonObject("specularGlossinessTexture")) + ); return adapter; } diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossMaterialAdapter.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_pbrSpecularGlossiness/PBRSpecGlossMaterialAdapter.java similarity index 94% rename from jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossMaterialAdapter.java rename to jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_pbrSpecularGlossiness/PBRSpecGlossMaterialAdapter.java index bd4c25c8af..d9dfc60ceb 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRSpecGlossMaterialAdapter.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_pbrSpecularGlossiness/PBRSpecGlossMaterialAdapter.java @@ -29,9 +29,10 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.jme3.scene.plugins.gltf; +package com.jme3.scene.plugins.gltf.ext.KHR_materials_pbrSpecularGlossiness; import com.jme3.material.MatParam; +import com.jme3.scene.plugins.gltf.PBRMaterialAdapter; /** * Created by Nehon on 20/08/2017. diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UnlitExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_unlit/UnlitExtensionLoader.java similarity index 83% rename from jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UnlitExtensionLoader.java rename to jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_unlit/UnlitExtensionLoader.java index cb45fcc533..2c0285312c 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UnlitExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_unlit/UnlitExtensionLoader.java @@ -29,9 +29,14 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.jme3.scene.plugins.gltf; -import com.jme3.plugins.json.JsonElement; +package com.jme3.scene.plugins.gltf.ext.KHR_materials_unlit; + import com.jme3.asset.AssetKey; +import com.jme3.plugins.json.JsonElement; +import com.jme3.scene.plugins.gltf.ExtensionLoader; +import com.jme3.scene.plugins.gltf.GltfLoader; +import com.jme3.scene.plugins.gltf.GltfModelKey; +import com.jme3.scene.plugins.gltf.MaterialAdapter; /** * Material adapter for the Unlit pipeline @@ -41,8 +46,16 @@ public class UnlitExtensionLoader implements ExtensionLoader { private final UnlitMaterialAdapter materialAdapter = new UnlitMaterialAdapter(); + public UnlitExtensionLoader() {} + @Override - public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) { + public Object handleExtension( + GltfLoader loader, + String parentName, + JsonElement parent, + JsonElement extension, + Object input + ) { MaterialAdapter adapter = materialAdapter; AssetKey key = loader.getInfo().getKey(); //check for a custom adapter for spec/gloss pipeline diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UnlitMaterialAdapter.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_unlit/UnlitMaterialAdapter.java similarity index 96% rename from jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UnlitMaterialAdapter.java rename to jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_unlit/UnlitMaterialAdapter.java index 53e247bb0c..0ae08698b7 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/UnlitMaterialAdapter.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_materials_unlit/UnlitMaterialAdapter.java @@ -29,10 +29,11 @@ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package com.jme3.scene.plugins.gltf; +package com.jme3.scene.plugins.gltf.ext.KHR_materials_unlit; import com.jme3.material.MatParam; import com.jme3.material.RenderState; +import com.jme3.scene.plugins.gltf.MaterialAdapter; /** * @author Markil 3 diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/TextureTransformExtensionLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_texture_transform/TextureTransformExtensionLoader.java similarity index 84% rename from jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/TextureTransformExtensionLoader.java rename to jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_texture_transform/TextureTransformExtensionLoader.java index ea643cea0c..e316466ff9 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/TextureTransformExtensionLoader.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/ext/KHR_texture_transform/TextureTransformExtensionLoader.java @@ -1,164 +1,183 @@ -/* - * Copyright (c) 2009-2022 jMonkeyEngine - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * * Neither the name of 'jMonkeyEngine' nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package com.jme3.scene.plugins.gltf; - -import com.jme3.plugins.json.JsonArray; -import com.jme3.plugins.json.JsonObject; -import com.jme3.plugins.json.JsonElement; -import com.jme3.asset.AssetLoadException; -import com.jme3.math.Matrix3f; -import com.jme3.math.Vector3f; -import com.jme3.scene.Mesh; -import com.jme3.scene.VertexBuffer; -import static com.jme3.scene.plugins.gltf.GltfUtils.getAsInteger; -import static com.jme3.scene.plugins.gltf.GltfUtils.getVertexBufferType; -import com.jme3.texture.Texture2D; -import java.io.IOException; -import java.nio.FloatBuffer; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Thread-safe extension loader for KHR_texture_transform. - * It allows for UV coordinates to be scaled/rotated/translated - * based on transformation properties from textures in the glTF model. - * - * See spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform - * - * @author manuelrmo - Created on 11/20/2022 - */ -public class TextureTransformExtensionLoader implements ExtensionLoader { - - private final static Logger logger = Logger.getLogger(TextureTransformExtensionLoader.class.getName()); - - /** - * Scale/rotate/translate UV coordinates based on a transformation matrix. - * Code adapted from scaleTextureCoordinates(Vector2f) in jme3-core/src/main/java/com/jme3/scene/Mesh.java - * @param mesh The mesh holding the UV coordinates - * @param transform The matrix containing the scale/rotate/translate transformations - * @param verType The vertex buffer type from which to retrieve the UV coordinates - */ - private void uvTransform(Mesh mesh, Matrix3f transform, VertexBuffer.Type verType) { - if (!transform.isIdentity()) { // if transform is the identity matrix, there's nothing to do - VertexBuffer tc = mesh.getBuffer(verType); - if (tc == null) { - throw new IllegalStateException("The mesh has no texture coordinates"); - } - if (tc.getFormat() != VertexBuffer.Format.Float) { - throw new UnsupportedOperationException("Only float texture coord format is supported"); - } - if (tc.getNumComponents() != 2) { - throw new UnsupportedOperationException("Only 2D texture coords are supported"); - } - FloatBuffer fb = (FloatBuffer) tc.getData(); - fb.clear(); - for (int i = 0; i < fb.limit() / 2; i++) { - float x = fb.get(); - float y = fb.get(); - fb.position(fb.position() - 2); - Vector3f v = transform.mult(new Vector3f(x, y, 1)); - fb.put(v.getX()).put(v.getY()); - } - fb.clear(); - tc.updateData(fb); - } - } - - // The algorithm relies on the fact that the GltfLoader.class object - // loads all textures of a given mesh before doing so for the next mesh. - @Override - public Object handleExtension(GltfLoader loader, String parentName, JsonElement parent, JsonElement extension, Object input) throws IOException { - if (!(input instanceof Texture2D)) { - logger.log(Level.WARNING, "KHR_texture_transform extension added on an unsupported element, the loaded scene result will be unexpected."); - } - Mesh mesh = loader.fetchFromCache("mesh", 0, Mesh.class); - if (mesh != null) { - Matrix3f translation = new Matrix3f(); - Matrix3f rotation = new Matrix3f(); - Matrix3f scale = new Matrix3f(); - Integer texCoord = getAsInteger(parent.getAsJsonObject(), "texCoord"); - texCoord = texCoord != null ? texCoord : 0; - JsonObject jsonObject = extension.getAsJsonObject(); - if (jsonObject.has("offset")) { - JsonArray jsonArray = jsonObject.getAsJsonArray("offset"); - translation.set(0, 2, jsonArray.get(0).getAsFloat()); - translation.set(1, 2, jsonArray.get(1).getAsFloat()); - } - if (jsonObject.has("rotation")) { - float rad = jsonObject.get("rotation").getAsFloat(); - rotation.set(0, 0, (float) Math.cos(rad)); - rotation.set(0, 1, (float) Math.sin(rad)); - rotation.set(1, 0, (float) -Math.sin(rad)); - rotation.set(1, 1, (float) Math.cos(rad)); - } - if (jsonObject.has("scale")) { - JsonArray jsonArray = jsonObject.getAsJsonArray("scale"); - scale.set(0, 0, jsonArray.get(0).getAsFloat()); - scale.set(1, 1, jsonArray.get(1).getAsFloat()); - } - if (jsonObject.has("texCoord")) { - texCoord = jsonObject.get("texCoord").getAsInt(); // it overrides the parent's texCoord value - } - Matrix3f transform = translation.mult(rotation).mult(scale); - Mesh meshLast = loader.fetchFromCache("textureTransformData", 0, Mesh.class); - Map transformMap = loader.fetchFromCache("textureTransformData", 1, HashMap.class); - if (mesh != meshLast || (transformMap != null && transformMap.get(texCoord) == null)) { - // at this point, we're processing a new mesh or the same mesh as before but for a different UV set - if (mesh != meshLast) { // it's a new mesh - loader.addToCache("textureTransformData", 0, mesh, 2); - if (transformMap == null) { - transformMap = new HashMap<>(); // initialize transformMap - loader.addToCache("textureTransformData", 1, transformMap, 2); - } else { - transformMap.clear(); // reset transformMap - } - } - transformMap.put(texCoord, transform); // store the transformation matrix applied to this UV set - uvTransform(mesh, transform, getVertexBufferType("TEXCOORD_" + texCoord)); - logger.log(Level.FINE, "KHR_texture_transform extension successfully applied."); - } - else { - // at this point, we're processing the same mesh as before for an already transformed UV set - Matrix3f transformLast = transformMap.get(texCoord); - if (!transform.equals(transformLast)) { - logger.log(Level.WARNING, "KHR_texture_transform extension: use of different texture transforms for the same mesh's UVs is not supported, the loaded scene result will be unexpected."); - } - } - return input; - } - else { - throw new AssetLoadException("KHR_texture_transform extension applied to a null mesh."); - } - } -} +/* + * Copyright (c) 2009-2022 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.scene.plugins.gltf.ext.KHR_texture_transform; + +import static com.jme3.scene.plugins.gltf.GltfUtils.getAsInteger; +import static com.jme3.scene.plugins.gltf.GltfUtils.getVertexBufferType; + +import com.jme3.asset.AssetLoadException; +import com.jme3.math.Matrix3f; +import com.jme3.math.Vector3f; +import com.jme3.plugins.json.JsonArray; +import com.jme3.plugins.json.JsonElement; +import com.jme3.plugins.json.JsonObject; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.plugins.gltf.ExtensionLoader; +import com.jme3.scene.plugins.gltf.GltfLoader; +import com.jme3.texture.Texture2D; +import java.io.IOException; +import java.nio.FloatBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Thread-safe extension loader for KHR_texture_transform. + * It allows for UV coordinates to be scaled/rotated/translated + * based on transformation properties from textures in the glTF model. + * + * See spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_transform + * + * @author manuelrmo - Created on 11/20/2022 + */ +public class TextureTransformExtensionLoader implements ExtensionLoader { + + private static final Logger logger = Logger.getLogger(TextureTransformExtensionLoader.class.getName()); + + public TextureTransformExtensionLoader() {} + + /** + * Scale/rotate/translate UV coordinates based on a transformation matrix. + * Code adapted from scaleTextureCoordinates(Vector2f) in jme3-core/src/main/java/com/jme3/scene/Mesh.java + * @param mesh The mesh holding the UV coordinates + * @param transform The matrix containing the scale/rotate/translate transformations + * @param verType The vertex buffer type from which to retrieve the UV coordinates + */ + private void uvTransform(Mesh mesh, Matrix3f transform, VertexBuffer.Type verType) { + if (!transform.isIdentity()) { // if transform is the identity matrix, there's nothing to do + VertexBuffer tc = mesh.getBuffer(verType); + if (tc == null) { + throw new IllegalStateException("The mesh has no texture coordinates"); + } + if (tc.getFormat() != VertexBuffer.Format.Float) { + throw new UnsupportedOperationException("Only float texture coord format is supported"); + } + if (tc.getNumComponents() != 2) { + throw new UnsupportedOperationException("Only 2D texture coords are supported"); + } + FloatBuffer fb = (FloatBuffer) tc.getData(); + fb.clear(); + for (int i = 0; i < fb.limit() / 2; i++) { + float x = fb.get(); + float y = fb.get(); + fb.position(fb.position() - 2); + Vector3f v = transform.mult(new Vector3f(x, y, 1)); + fb.put(v.getX()).put(v.getY()); + } + fb.clear(); + tc.updateData(fb); + } + } + + // The algorithm relies on the fact that the GltfLoader.class object + // loads all textures of a given mesh before doing so for the next mesh. + @Override + public Object handleExtension( + GltfLoader loader, + String parentName, + JsonElement parent, + JsonElement extension, + Object input + ) throws IOException { + if (!(input instanceof Texture2D)) { + logger.log( + Level.WARNING, + "KHR_texture_transform extension added on an unsupported element, the loaded scene result will be unexpected." + ); + } + Mesh mesh = loader.fetchFromCache("mesh", 0, Mesh.class); + if (mesh != null) { + Matrix3f translation = new Matrix3f(); + Matrix3f rotation = new Matrix3f(); + Matrix3f scale = new Matrix3f(); + Integer texCoord = getAsInteger(parent.getAsJsonObject(), "texCoord"); + texCoord = texCoord != null ? texCoord : 0; + JsonObject jsonObject = extension.getAsJsonObject(); + if (jsonObject.has("offset")) { + JsonArray jsonArray = jsonObject.getAsJsonArray("offset"); + translation.set(0, 2, jsonArray.get(0).getAsFloat()); + translation.set(1, 2, jsonArray.get(1).getAsFloat()); + } + if (jsonObject.has("rotation")) { + float rad = jsonObject.get("rotation").getAsFloat(); + rotation.set(0, 0, (float) Math.cos(rad)); + rotation.set(0, 1, (float) Math.sin(rad)); + rotation.set(1, 0, (float) -Math.sin(rad)); + rotation.set(1, 1, (float) Math.cos(rad)); + } + if (jsonObject.has("scale")) { + JsonArray jsonArray = jsonObject.getAsJsonArray("scale"); + scale.set(0, 0, jsonArray.get(0).getAsFloat()); + scale.set(1, 1, jsonArray.get(1).getAsFloat()); + } + if (jsonObject.has("texCoord")) { + texCoord = jsonObject.get("texCoord").getAsInt(); // it overrides the parent's texCoord value + } + Matrix3f transform = translation.mult(rotation).mult(scale); + Mesh meshLast = loader.fetchFromCache("textureTransformData", 0, Mesh.class); + Map transformMap = loader.fetchFromCache( + "textureTransformData", + 1, + HashMap.class + ); + if (mesh != meshLast || (transformMap != null && transformMap.get(texCoord) == null)) { + // at this point, we're processing a new mesh or the same mesh as before but for a different UV set + if (mesh != meshLast) { // it's a new mesh + loader.addToCache("textureTransformData", 0, mesh, 2); + if (transformMap == null) { + transformMap = new HashMap<>(); // initialize transformMap + loader.addToCache("textureTransformData", 1, transformMap, 2); + } else { + transformMap.clear(); // reset transformMap + } + } + transformMap.put(texCoord, transform); // store the transformation matrix applied to this UV set + uvTransform(mesh, transform, getVertexBufferType("TEXCOORD_" + texCoord)); + logger.log(Level.FINE, "KHR_texture_transform extension successfully applied."); + } else { + // at this point, we're processing the same mesh as before for an already transformed UV set + Matrix3f transformLast = transformMap.get(texCoord); + if (!transform.equals(transformLast)) { + logger.log( + Level.WARNING, + "KHR_texture_transform extension: use of different texture transforms for the same mesh's UVs is not supported, the loaded scene result will be unexpected." + ); + } + } + return input; + } else { + throw new AssetLoadException("KHR_texture_transform extension applied to a null mesh."); + } + } +}