From 3bbfabed5e0b4760bb049faa9073e5c02fc4fb85 Mon Sep 17 00:00:00 2001 From: Nehon Date: Wed, 9 Aug 2017 00:40:54 +0200 Subject: [PATCH] Gltf: added support for PBR colored material --- .../java/jme3test/model/TestGltfLoading.java | 5 +- .../jme3/scene/plugins/gltf/GltfLoader.java | 125 ++++++++++++------ .../jme3/scene/plugins/gltf/GltfModelKey.java | 29 ++++ .../jme3/scene/plugins/gltf/GltfUtils.java | 29 +++- .../scene/plugins/gltf/MaterialAdapter.java | 86 ++++++++++++ .../plugins/gltf/PBRMaterialAdapter.java | 58 ++++++++ 6 files changed, 288 insertions(+), 44 deletions(-) create mode 100644 jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java create mode 100644 jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/MaterialAdapter.java create mode 100644 jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRMaterialAdapter.java diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index 5ad4afae3..ff0424fb7 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java @@ -37,6 +37,7 @@ import com.jme3.light.PointLight; import com.jme3.math.*; import com.jme3.scene.Geometry; import com.jme3.scene.Spatial; +import com.jme3.scene.plugins.gltf.GltfModelKey; import com.jme3.scene.shape.Sphere; public class TestGltfLoading extends SimpleApplication { @@ -67,8 +68,8 @@ public class TestGltfLoading extends SimpleApplication { PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50); rootNode.addLight(pl1); - //rootNode.attachChild(assetManager.loadModel("Models/gltf/box/box.gltf")); - rootNode.attachChild(assetManager.loadModel("Models/gltf/duck/Duck.gltf")); + rootNode.attachChild(assetManager.loadModel("Models/gltf/box/box.gltf")); + //rootNode.attachChild(assetManager.loadModel(new GltfModelKey("Models/gltf/duck/Duck.gltf"))); //rootNode.attachChild(assetManager.loadModel("Models/gltf/hornet/scene.gltf")); } 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 99473a8f1..4a321362b 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 @@ -4,13 +4,17 @@ import com.google.gson.*; import com.google.gson.stream.JsonReader; import com.jme3.asset.*; import com.jme3.material.Material; +import com.jme3.material.RenderState; import com.jme3.math.*; +import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; import java.io.*; import java.nio.Buffer; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import static com.jme3.scene.plugins.gltf.GltfUtils.*; @@ -20,6 +24,8 @@ import static com.jme3.scene.plugins.gltf.GltfUtils.*; */ public class GltfLoader implements AssetLoader { + private static final Logger logger = Logger.getLogger(GltfLoader.class.getName()); + //Data cache for already parsed JME objects private Map dataCache = new HashMap<>(); private JsonArray scenes; @@ -30,9 +36,14 @@ public class GltfLoader implements AssetLoader { private JsonArray buffers; private JsonArray materials; private Material defaultMat; - private byte[] tmpByteArray; private AssetInfo info; + private static Map defaultMaterialAdapters = new HashMap<>(); + + static { + defaultMaterialAdapters.put("pbrMetallicRoughness", new PBRMaterialAdapter()); + } + @Override public Object load(AssetInfo assetInfo) throws IOException { try { @@ -46,7 +57,6 @@ public class GltfLoader implements AssetLoader { defaultMat.setFloat("Roughness", 1f); } - JsonObject root = new JsonParser().parse(new JsonReader(new InputStreamReader(assetInfo.openStream()))).getAsJsonObject(); JsonObject asset = root.getAsJsonObject().get("asset").getAsJsonObject(); @@ -66,8 +76,6 @@ public class GltfLoader implements AssetLoader { buffers = root.getAsJsonArray("buffers"); materials = root.getAsJsonArray("materials"); - allocatedTmpByteArray(); - JsonPrimitive defaultScene = root.getAsJsonPrimitive("scene"); Node n = loadScenes(defaultScene); @@ -85,21 +93,6 @@ public class GltfLoader implements AssetLoader { } } - private void allocatedTmpByteArray() { - //Allocate the tmpByteArray to the biggest bufferView - if (bufferViews == null) { - throw new AssetLoadException("No buffer view defined but one is referenced by an accessor"); - } - int maxLength = 0; - for (JsonElement bufferView : bufferViews) { - Integer byteLength = getAsInteger(bufferView.getAsJsonObject(), "byteLength"); - if (byteLength != null && maxLength < byteLength) { - maxLength = byteLength; - } - } - tmpByteArray = new byte[maxLength]; - } - private boolean isSupported(String version, String minVersion) { return "2.0".equals(version); } @@ -152,26 +145,19 @@ public class GltfLoader implements AssetLoader { //there is a mesh in this node, however gltf can split meshes in primitives (some kind of sub meshes), //We don't have this in JME so we have to make one mesh and one Geometry for each primitive. - Mesh[] primitives = loadMeshPrimitives(meshIndex); + Geometry[] primitives = loadMeshPrimitives(meshIndex); if (primitives.length > 1) { - //only one mesh, lets just make a geometry. - Geometry geometry = new Geometry(null, primitives[0]); - geometry.setMaterial(mat); - geometry.updateModelBound(); - spatial = geometry; + //only one geometry, let's not wrap it in another node. + spatial = primitives[0]; } else { - //several meshes, let's make a parent Node and attach several geometries to it + //several geometries, let's make a parent Node and attach them to it Node node = new Node(); - for (Mesh primitive : primitives) { - Geometry geom = new Geometry(null, primitive); - geom.setMaterial(mat); - geom.updateModelBound(); - node.attachChild(geom); + for (Geometry primitive : primitives) { + node.attachChild(primitive); } spatial = node; } - spatial.setName(loadMeshName(meshIndex)); } else { @@ -235,10 +221,15 @@ public class GltfLoader implements AssetLoader { return transform; } - private Mesh[] loadMeshPrimitives(int meshIndex) throws IOException { - Mesh[] meshArray = (Mesh[]) fetchFromCache("meshes", meshIndex, Object.class); - if (meshArray != null) { - return meshArray; + private Geometry[] loadMeshPrimitives(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"); @@ -246,7 +237,7 @@ public class GltfLoader implements AssetLoader { throw new AssetLoadException("Can't find any primitives in mesh " + meshIndex); } - meshArray = new Mesh[primitives.size()]; + geomArray = new Geometry[primitives.size()]; int index = 0; for (JsonElement primitive : primitives) { JsonObject meshObject = primitive.getAsJsonObject(); @@ -263,14 +254,28 @@ public class GltfLoader implements AssetLoader { for (Map.Entry entry : attributes.entrySet()) { mesh.setBuffer(loadVertexBuffer(entry.getValue().getAsInt(), getVertexBufferType(entry.getKey()))); } - meshArray[index] = mesh; + Geometry geom = new Geometry(null, mesh); + + Integer materialIndex = getAsInteger(meshObject, "material"); + if (materialIndex == null) { + geom.setMaterial(defaultMat); + } else { + geom.setMaterial(loadMaterial(materialIndex)); + if (geom.getMaterial().getAdditionalRenderState().getBlendMode() == RenderState.BlendMode.Alpha) { + //Alpha blending is on on this material let's place the geom in the transparent bucket + geom.setQueueBucket(RenderQueue.Bucket.Transparent); + } + } + + geom.updateModelBound(); + geomArray[index] = geom; index++; //TODO material, targets(morph anim...) } - addToCache("meshes", meshIndex, meshArray, meshes.size()); - return meshArray; + addToCache("meshes", meshIndex, geomArray, meshes.size()); + return geomArray; } private VertexBuffer loadVertexBuffer(int accessorIndex, VertexBuffer.Type bufferType) throws IOException { @@ -373,6 +378,46 @@ public class GltfLoader implements AssetLoader { } + private Material loadMaterial(int materialIndex) { + if (materials == null) { + throw new AssetLoadException("There is no material defined yet a mesh references one"); + } + JsonObject matData = materials.get(materialIndex).getAsJsonObject(); + JsonObject pbrMat = matData.getAsJsonObject("pbrMetallicRoughness"); + + if (pbrMat == null) { + logger.log(Level.WARNING, "Unable to find any pbrMetallicRoughness material entry in material " + materialIndex + ". Only PBR material is supported for now"); + return defaultMat; + } + MaterialAdapter adapter = null; + if (info.getKey() instanceof GltfModelKey) { + adapter = ((GltfModelKey) info.getKey()).getAdapterForMaterial("pbrMetallicRoughness"); + } + if (adapter == null) { + adapter = defaultMaterialAdapters.get("pbrMetallicRoughness"); + } + + Material mat = adapter.getMaterial(info.getManager()); + mat.setName(getAsString(matData, "name")); + + adapter.setParam(mat, "baseColorFactor", getAsColor(pbrMat, "baseColorFactor", ColorRGBA.White)); + adapter.setParam(mat, "metallicFactor", getAsFloat(pbrMat, "metallicFactor", 1f)); + adapter.setParam(mat, "roughnessFactor", getAsFloat(pbrMat, "roughnessFactor", 1f)); + adapter.setParam(mat, "emissiveFactor", getAsColor(matData, "emissiveFactor", ColorRGBA.Black)); + adapter.setParam(mat, "alphaMode", getAsString(matData, "alphaMode")); + adapter.setParam(mat, "alphaCutoff", getAsFloat(matData, "alphaCutoff")); + adapter.setParam(mat, "doubleSided", getAsBoolean(matData, "doubleSided")); + + //TODO textures + //adapter.setParam(mat, "baseColorTexture", readTexture); + //adapter.setParam(mat, "metallicRoughnessTexture", readTexture); + //adapter.setParam(mat, "normalTexture", readTexture); + //adapter.setParam(mat, "occlusionTexture", readTexture); + //adapter.setParam(mat, "emissiveTexture", readTexture); + + return mat; + } + private String loadMeshName(int meshIndex) { JsonObject meshData = meshes.get(meshIndex).getAsJsonObject(); return getAsString(meshData, "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 new file mode 100644 index 000000000..67c076e30 --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfModelKey.java @@ -0,0 +1,29 @@ +package com.jme3.scene.plugins.gltf; + +import com.jme3.asset.ModelKey; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by Nehon on 08/08/2017. + */ +public class GltfModelKey extends ModelKey { + + private Map materialAdapters = new HashMap<>(); + + public GltfModelKey(String name) { + super(name); + } + + public GltfModelKey() { + } + + public void registerMaterialAdapter(String gltfMaterialName, MaterialAdapter adapter) { + materialAdapters.put(gltfMaterialName, adapter); + } + + public MaterialAdapter getAdapterForMaterial(String gltfMaterialName) { + return materialAdapters.get(gltfMaterialName); + } +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java index 254a6bda6..2d24fcbfc 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java @@ -1,8 +1,8 @@ package com.jme3.scene.plugins.gltf; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.google.gson.*; import com.jme3.asset.AssetLoadException; +import com.jme3.math.ColorRGBA; import com.jme3.scene.Mesh; import com.jme3.scene.VertexBuffer; import com.jme3.util.LittleEndien; @@ -229,6 +229,16 @@ public class GltfUtils { return el == null ? defaultValue : el.getAsInt(); } + public static Float getAsFloat(JsonObject parent, String name) { + JsonElement el = parent.get(name); + return el == null ? null : el.getAsFloat(); + } + + public static Float getAsFloat(JsonObject parent, String name, float defaultValue) { + JsonElement el = parent.get(name); + return el == null ? defaultValue : el.getAsFloat(); + } + public static Boolean getAsBoolean(JsonObject parent, String name) { JsonElement el = parent.get(name); return el == null ? null : el.getAsBoolean(); @@ -239,6 +249,21 @@ public class GltfUtils { return el == null ? defaultValue : el.getAsBoolean(); } + public static ColorRGBA getAsColor(JsonObject parent, String name) { + JsonElement el = parent.get(name); + if (el == null) { + return null; + } + JsonArray color = el.getAsJsonArray(); + return new ColorRGBA(color.get(0).getAsFloat(), color.get(1).getAsFloat(), color.get(2).getAsFloat(), color.get(3).getAsFloat()); + } + + public static ColorRGBA getAsColor(JsonObject parent, String name, ColorRGBA defaultValue) { + ColorRGBA color = getAsColor(parent, name); + return color == null ? defaultValue : color; + } + + public static void assertNotNull(Object o, String errorMessage) { if (o == null) { throw new AssetLoadException(errorMessage); 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 new file mode 100644 index 000000000..069b388a6 --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/MaterialAdapter.java @@ -0,0 +1,86 @@ +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; + +/** + * A MaterialAdapter allows to map a GLTF material to a JME material. + * It maps each gltf parameter to it's matching parameter in the JME material, + * and allows for some conversion if the JME material model doesn't exactly match the gltf material model + * Created by Nehon on 08/08/2017. + */ +public abstract class MaterialAdapter { + + private Map paramsMapping = new HashMap<>(); + + /** + * Should return the material definition used by this material adapter + * + * @return + */ + protected abstract String getMaterialDefPath(); + + protected abstract MatParam adaptMatParam(Material mat, MatParam param); + + public Material getMaterial(AssetManager assetManager) { + return new Material(assetManager, getMaterialDefPath()); + } + + public void setParam(Material mat, String gltfParamName, Object value) { + String name = getJmeParamName(gltfParamName); + if (name == null || value == null) { + //no mapping registered or value is null, let's ignore this param + return; + } + MatParam param; + if (value instanceof Texture) { + MatParam defParam = mat.getMaterialDef().getMaterialParam(name); + if (defParam == null) { + 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"); + } + param = new MatParamTexture(VarType.Texture2D, name, (Texture) value, ((MatParamTexture) defParam).getColorSpace()); + param = adaptMatParam(mat, param); + if (param != null) { + mat.setTextureParam(param.getName(), param.getVarType(), (Texture) param.getValue()); + } + } else { + param = new MatParam(getVarType(value), name, value); + param = adaptMatParam(mat, param); + if (param != null) { + mat.setParam(param.getName(), param.getVarType(), param.getValue()); + } + } + } + + protected void addParamMapping(String gltfParamName, String jmeParamName) { + paramsMapping.put(gltfParamName, jmeParamName); + } + + protected String getJmeParamName(String gltfParamName) { + return paramsMapping.get(gltfParamName); + } + + private VarType getVarType(Object value) { + if (value instanceof Float) return VarType.Float; + if (value instanceof Integer) return VarType.Int; + if (value instanceof Boolean) return VarType.Boolean; + if (value instanceof ColorRGBA) return VarType.Vector4; + if (value instanceof Vector4f) return VarType.Vector4; + if (value instanceof Vector3f) return VarType.Vector3; + if (value instanceof Vector2f) return VarType.Vector2; + if (value instanceof Matrix3f) return VarType.Matrix3; + if (value instanceof Matrix4f) return VarType.Matrix4; + throw new AssetLoadException("Unsupported material parameter type : " + value.getClass().getSimpleName()); + } +} diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRMaterialAdapter.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRMaterialAdapter.java new file mode 100644 index 000000000..d783e925e --- /dev/null +++ b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/PBRMaterialAdapter.java @@ -0,0 +1,58 @@ +package com.jme3.scene.plugins.gltf; + +import com.jme3.material.*; + +/** + * Created by Nehon on 08/08/2017. + */ +public class PBRMaterialAdapter extends MaterialAdapter { + + + public PBRMaterialAdapter() { + addParamMapping("baseColorFactor", "BaseColor"); + addParamMapping("baseColorTexture", "BaseColorMap"); + addParamMapping("metallicFactor", "Metallic"); + addParamMapping("roughnessFactor", "Roughness"); + addParamMapping("metallicRoughnessTexture", "MetallicRoughnessMap"); + addParamMapping("normalTexture", "NormalMap"); + addParamMapping("occlusionTexture", "LightMap"); + addParamMapping("emisiveTexture", "EmissiveMap"); + addParamMapping("emisiveFactor", "Emissive"); + addParamMapping("alphaMode", "alpha"); + addParamMapping("alphaCutoff", "AlphaDiscardThreshold"); + addParamMapping("doubleSided", "doubleSided"); + } + + @Override + protected String getMaterialDefPath() { + return "Common/MatDefs/Light/PBRLighting.j3md"; + } + + @Override + protected MatParam adaptMatParam(Material mat, MatParam param) { + if (param.getName().equals("alpha")) { + String alphaMode = (String) param.getValue(); + switch (alphaMode) { + case "MASK": + case "BLEND": + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + } + return null; + } + if (param.getName().equals("doubleSided")) { + boolean doubleSided = (boolean) param.getValue(); + if (doubleSided) { + //Note that this is not completely right as normals on the back side will be in the wrong direction. + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + } + return null; + } + if (param.getName().equals("MetallicRoughnessMap")) { + //use packed Metallic/Roughness + mat.setBoolean("UsePackedMR", true); + } + + + return param; + } +}