From 0f06c14c2887e1218f237dbe9be77281526bb746 Mon Sep 17 00:00:00 2001 From: kkolyan Date: Mon, 4 May 2020 04:35:45 +0300 Subject: [PATCH] OBJLoader enhancement: named groups support --- jme3-core/build.gradle | 4 + .../com/jme3/scene/plugins/MTLLoader.java | 1 + .../com/jme3/scene/plugins/OBJLoader.java | 93 +++++--- .../com/jme3/scene/plugins/OBJLoaderTest.java | 106 +++++++++ .../jme3test/model/TestObjGroupsLoading.java | 119 ++++++++++ .../resources/OBJLoaderTest/TwoChairs.mtl | 20 ++ .../resources/OBJLoaderTest/TwoChairs.obj | 219 ++++++++++++++++++ .../main/resources/OBJLoaderTest/dot_blue.png | Bin 0 -> 143 bytes .../resources/OBJLoaderTest/dot_green.png | Bin 0 -> 142 bytes .../resources/OBJLoaderTest/dot_purple.png | Bin 0 -> 143 bytes .../main/resources/OBJLoaderTest/dot_red.png | Bin 0 -> 142 bytes 11 files changed, 533 insertions(+), 29 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/scene/plugins/OBJLoaderTest.java create mode 100644 jme3-examples/src/main/java/jme3test/model/TestObjGroupsLoading.java create mode 100644 jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.mtl create mode 100644 jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.obj create mode 100644 jme3-testdata/src/main/resources/OBJLoaderTest/dot_blue.png create mode 100644 jme3-testdata/src/main/resources/OBJLoaderTest/dot_green.png create mode 100644 jme3-testdata/src/main/resources/OBJLoaderTest/dot_purple.png create mode 100644 jme3-testdata/src/main/resources/OBJLoaderTest/dot_red.png diff --git a/jme3-core/build.gradle b/jme3-core/build.gradle index c8b311739..162a2852b 100644 --- a/jme3-core/build.gradle +++ b/jme3-core/build.gradle @@ -17,6 +17,10 @@ sourceSets { } } +dependencies { + testCompile project(':jme3-testdata') +} + task updateVersionPropertiesFile { def versionFile = file('src/main/resources/com/jme3/system/version.properties') def versionFileText = "# THIS IS AN AUTO-GENERATED FILE..\n" + diff --git a/jme3-core/src/plugins/java/com/jme3/scene/plugins/MTLLoader.java b/jme3-core/src/plugins/java/com/jme3/scene/plugins/MTLLoader.java index 63a1f30e9..2a3a277f8 100644 --- a/jme3-core/src/plugins/java/com/jme3/scene/plugins/MTLLoader.java +++ b/jme3-core/src/plugins/java/com/jme3/scene/plugins/MTLLoader.java @@ -152,6 +152,7 @@ public class MTLLoader implements AssetLoader { material.setFloat("AlphaDiscardThreshold", 0.01f); } + material.setName(matName); matList.put(matName, material); } diff --git a/jme3-core/src/plugins/java/com/jme3/scene/plugins/OBJLoader.java b/jme3-core/src/plugins/java/com/jme3/scene/plugins/OBJLoader.java index 6457feb59..d523e45ff 100644 --- a/jme3-core/src/plugins/java/com/jme3/scene/plugins/OBJLoader.java +++ b/jme3-core/src/plugins/java/com/jme3/scene/plugins/OBJLoader.java @@ -66,10 +66,9 @@ public final class OBJLoader implements AssetLoader { protected final ArrayList verts = new ArrayList(); protected final ArrayList texCoords = new ArrayList(); protected final ArrayList norms = new ArrayList(); - - protected final ArrayList faces = new ArrayList(); - protected final HashMap> matFaces = new HashMap>(); - + + private final ArrayList groups = new ArrayList(); + protected String currentMatName; protected String currentObjectName; @@ -87,6 +86,16 @@ public final class OBJLoader implements AssetLoader { protected String objName; protected Node objNode; + private static class Group { + private String name; + private final ArrayList faces = new ArrayList(); + private final HashMap> matFaces = new HashMap>(); + + public Group(final String name) { + this.name = name; + } + } + protected static class Vertex { Vector3f v; @@ -164,8 +173,7 @@ public final class OBJLoader implements AssetLoader { verts.clear(); texCoords.clear(); norms.clear(); - faces.clear(); - matFaces.clear(); + groups.clear(); vertIndexMap.clear(); indexVertMap.clear(); @@ -289,10 +297,17 @@ public final class OBJLoader implements AssetLoader { f.verticies[i] = vertList.get(i); } - if (matList != null && matFaces.containsKey(currentMatName)){ - matFaces.get(currentMatName).add(f); + Group group = groups.get(groups.size() - 1); + + if (currentMatName != null && matList != null && matList.containsKey(currentMatName)){ + ArrayList matFaces = group.matFaces.get(currentMatName); + if (matFaces == null) { + matFaces = new ArrayList(); + group.matFaces.put(currentMatName, matFaces); + } + matFaces.add(f); }else{ - faces.add(f); // faces that belong to the default material + group.faces.add(f); // faces that belong to the default material } } @@ -337,13 +352,6 @@ public final class OBJLoader implements AssetLoader { } catch (AssetNotFoundException ex){ logger.log(Level.WARNING, "Cannot locate {0} for model {1}", new Object[]{name, key}); } - - if (matList != null){ - // create face lists for every material - for (String matName : matList.keySet()){ - matFaces.put(matName, new ArrayList()); - } - } } protected boolean nextStatement(){ @@ -387,8 +395,14 @@ public final class OBJLoader implements AssetLoader { // specify MTL lib to use for this OBJ file String mtllib = scan.nextLine().trim(); loadMtlLib(mtllib); - }else if (cmd.equals("s") || cmd.equals("g")){ + }else if (cmd.equals("s")) { + logger.log(Level.WARNING, "smoothing groups are not supported, statement ignored: {0}", cmd); + return nextStatement(); + }else if (cmd.equals("mg")) { + logger.log(Level.WARNING, "merge groups are not supported, statement ignored: {0}", cmd); return nextStatement(); + }else if (cmd.equals("g")) { + groups.add(new Group(scan.nextLine().trim())); }else{ // skip entire command until next line logger.log(Level.WARNING, "Unknown statement in OBJ! {0}", cmd); @@ -565,11 +579,14 @@ public final class OBJLoader implements AssetLoader { } objNode = new Node(objName + "-objnode"); + + Group defaultGroupStub = new Group(null); + groups.add(defaultGroupStub); if (!(info.getKey() instanceof ModelKey)) throw new IllegalArgumentException("Model assets must be loaded using a ModelKey"); - InputStream in = null; + InputStream in = null; try { in = info.openStream(); @@ -583,25 +600,43 @@ public final class OBJLoader implements AssetLoader { } } - if (matFaces.size() > 0){ - for (Entry> entry : matFaces.entrySet()){ - ArrayList materialFaces = entry.getValue(); - if (materialFaces.size() > 0){ - Geometry geom = createGeometry(materialFaces, entry.getKey()); + for (Group group : groups) { + if (group == defaultGroupStub) { + materializeGroup(group, objNode); + } else { + Node groupNode = new Node(group.name); + materializeGroup(group, groupNode); + if (groupNode.getQuantity() == 1) { + Spatial geom = groupNode.getChild(0); + geom.setName(groupNode.getName()); objNode.attachChild(geom); + } else if (groupNode.getQuantity() > 1) { + objNode.attachChild(groupNode); } } - }else if (faces.size() > 0){ - // generate final geometry - Geometry geom = createGeometry(faces, null); - objNode.attachChild(geom); } if (objNode.getQuantity() == 1) // only 1 geometry, so no need to send node - return objNode.getChild(0); + return objNode.getChild(0); else return objNode; } - + + private void materializeGroup(Group group, Node container) throws IOException { + if (group.matFaces.size() > 0) { + for (Entry> entry : group.matFaces.entrySet()){ + ArrayList materialFaces = entry.getValue(); + if (materialFaces.size() > 0){ + Geometry geom = createGeometry(materialFaces, entry.getKey()); + container.attachChild(geom); + } + } + } else if (group.faces.size() > 0) { + // generate final geometry + Geometry geom = createGeometry(group.faces, null); + container.attachChild(geom); + } + } + } diff --git a/jme3-core/src/test/java/com/jme3/scene/plugins/OBJLoaderTest.java b/jme3-core/src/test/java/com/jme3/scene/plugins/OBJLoaderTest.java new file mode 100644 index 000000000..4c3dd2ae9 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/scene/plugins/OBJLoaderTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2009-2020 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; + +import com.jme3.asset.AssetInfo; +import com.jme3.asset.AssetLoader; +import com.jme3.asset.AssetManager; +import com.jme3.asset.ModelKey; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.system.TestUtil; +import com.jme3.texture.Image; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class OBJLoaderTest { + private AssetManager assetManager; + + @Before + public void init() { + assetManager = TestUtil.createAssetManager(); + // texture loaders are outside of core, so creating stub + assetManager.registerLoader(PngLoaderStub.class, "png"); + + } + + @Test + public void testHappyPath() { + Node scene = (Node) assetManager.loadModel(new ModelKey("OBJLoaderTest/TwoChairs.obj")); + String sceneAsString = toDiffFriendlyString("", scene); + System.out.println(sceneAsString); + String expectedText = "" + + // generated root name (as before named groups support) + "TwoChairs-objnode\n" + + // unnamed geometry with generated name (as before named groups support). + // actually it's partially smoothed, but this fact is ignored. + " TwoChairs-geom-0 (material: dot_purple)\n" + + // named group as Geometry + " Chair 2 (material: dot_purple)\n" + + // named group as Geometry + " Pillow 2 (material: dot_red)\n" + + // named group as node with two dufferent Geometry instances, + // because two materials are used (as before named groups support) + " Podium\n" + + " TwoChairs-geom-3 (material: dot_red)\n" + + " TwoChairs-geom-4 (material: dot_blue)\n" + + // named group as Geometry + " Pillow 1 (material: dot_green)"; + assertEquals(expectedText, sceneAsString.trim()); + } + + private static String toDiffFriendlyString(String indent, Spatial spatial) { + if (spatial instanceof Geometry) { + return indent + spatial.getName() + " (material: "+((Geometry) spatial).getMaterial().getName()+")\n"; + } + if (spatial instanceof Node) { + StringBuilder s = new StringBuilder(); + s.append(indent).append(spatial.getName()).append("\n"); + Node node = (Node) spatial; + for (final Spatial child : node.getChildren()) { + s.append(toDiffFriendlyString(indent + " ", child)); + } + return s.toString(); + } + return indent + spatial + "\n"; + } + + public static class PngLoaderStub implements AssetLoader { + @Override + public Object load(final AssetInfo assetInfo) { + return new Image(); + } + } +} \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/model/TestObjGroupsLoading.java b/jme3-examples/src/main/java/jme3test/model/TestObjGroupsLoading.java new file mode 100644 index 000000000..79af30cde --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/TestObjGroupsLoading.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2009-2020 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 jme3test.model; + +import com.jme3.app.SimpleApplication; +import com.jme3.asset.ModelKey; +import com.jme3.collision.CollisionResult; +import com.jme3.collision.CollisionResults; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.font.Rectangle; +import com.jme3.light.AmbientLight; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Ray; +import com.jme3.math.Vector3f; +import com.jme3.scene.Spatial; + +public class TestObjGroupsLoading extends SimpleApplication { + + public static void main(String[] args) { + TestObjGroupsLoading app = new TestObjGroupsLoading(); + app.start(); + } + + private BitmapText pointerDisplay; + + @Override + public void simpleInitApp() { + + // load scene with following structure: + // Chair 1 (just mesh without name) and named groups: Chair 2, Pillow 2, Podium + Spatial scene = assetManager.loadModel(new ModelKey("OBJLoaderTest/TwoChairs.obj")); + // add light to make it visible + scene.addLight(new AmbientLight(ColorRGBA.White)); + // attach scene to the root + rootNode.attachChild(scene); + + // configure camera for best scene viewing + cam.setLocation(new Vector3f(-3, 4, 3)); + cam.lookAtDirection(new Vector3f(0, -0.5f, -1), Vector3f.UNIT_Y); + flyCam.setMoveSpeed(10); + + // create display to indicate pointed geometry name + pointerDisplay = new BitmapText(guiFont); + pointerDisplay.setBox(new Rectangle(0, settings.getHeight(), settings.getWidth(), settings.getHeight()/2)); + pointerDisplay.setAlignment(BitmapFont.Align.Center); + pointerDisplay.setVerticalAlignment(BitmapFont.VAlign.Center); + guiNode.attachChild(pointerDisplay); + + initCrossHairs(); + } + + @Override + public void simpleUpdate(final float tpf) { + + // ray to the center of the screen from the camera + Ray ray = new Ray(cam.getLocation(), cam.getDirection()); + + // find object at the center of the screen + + final CollisionResults results = new CollisionResults(); + rootNode.collideWith(ray, results); + + CollisionResult result = results.getClosestCollision(); + if (result == null) { + pointerDisplay.setText(""); + } else { + // display pointed geometry and it's parents names + StringBuilder sb = new StringBuilder(); + for (Spatial node = result.getGeometry(); node != null; node = node.getParent()) { + if (sb.length() > 0) { + sb.append(" < "); + } + sb.append(node.getName()); + } + pointerDisplay.setText(sb); + } + } + + private void initCrossHairs() { + guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt"); + BitmapText ch = new BitmapText(guiFont, false); + ch.setSize(guiFont.getCharSet().getRenderedSize() * 2); + ch.setText("+"); // crosshairs + ch.setLocalTranslation( // center + settings.getWidth() / 2 - guiFont.getCharSet().getRenderedSize() / 3 * 2, + settings.getHeight() / 2 + ch.getLineHeight() / 2, 0); + guiNode.attachChild(ch); + } +} diff --git a/jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.mtl b/jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.mtl new file mode 100644 index 000000000..c85a0ddaa --- /dev/null +++ b/jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.mtl @@ -0,0 +1,20 @@ +newmtl dot_purple +map_Kd -o 0 0 -s 1 1 dot_purple.png +Kd 1 1 1 +d 1 + +newmtl dot_red +map_Kd -o 0 0 -s 1 1 dot_red.png +Kd 1 1 1 +d 1 + +newmtl dot_blue +map_Kd -o 0 0 -s 1 1 dot_blue.png +Kd 1 1 1 +d 1 + +newmtl dot_green +map_Kd -o 0 0 -s 1 1 dot_green.png +Kd 1 1 1 +d 1 + diff --git a/jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.obj b/jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.obj new file mode 100644 index 000000000..727ec4619 --- /dev/null +++ b/jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.obj @@ -0,0 +1,219 @@ +# ProBuilder 4.2.3 +# https://unity3d.com/unity/features/worldbuilding/probuilder +# 5/10/2020 4:30:31 PM + +mtllib ./TwoChairs.mtl +o TwoChairs + +v -4 1 0 +v -5 1 0 +v -4 2 0 +v -5 2 0 +v -5 1 -1 +v -5 2 -1 +v -5 1 -1.25 +v -4 1 -1.25 + +s 1 +v -5 2 -1.25 +v -4 2 -1.25 +v -4 1 -1 +v -4 2 -1 +v -4 3 -1 +v -5 3 -1 +v -4 3 -1.25 +v -5 3 -1.25 +s off + +vt 0 0 +vt -1 0 +vt 0 1 +vt -1 1 +vt 1 0 +vt 1 1 +vt -1 -1 +vt 0 -1 +vt -1 -1.25 +vt 0 -1.25 +vt -1.25 1 +vt -1.25 0 +vt 1.25 0 +vt 1.25 1 +vt 1 -1 +vt 1 -1.25 +vt 0 2 +vt -1 2 +vt 1.25 2 +vt 1 2 +vt -1.25 2 + +vn 0 0 1 +vn -1 0 0 +vn 0 0 -1 +vn 1 0 0 +vn 0 -1 0 +vn 0 1 0 + +usemtl dot_purple +f 3/3/1 4/4/1 2/2/1 1/1/1 +f 4/3/2 6/4/2 5/2/2 2/1/2 +f 9/6/3 10/3/3 8/1/3 7/5/3 +f 12/6/4 3/3/4 1/1/4 11/5/4 +f 7/9/5 8/10/5 11/8/5 5/7/5 +f 9/11/2 7/12/2 5/2/2 6/4/2 +f 8/13/4 10/14/4 12/6/4 11/5/4 +f 15/10/6 16/16/6 14/15/6 13/8/6 +f 13/17/1 14/18/1 6/4/1 12/3/1 +f 15/19/4 13/20/4 12/6/4 10/14/4 +f 14/18/2 16/21/2 9/11/2 6/4/2 +f 16/20/3 15/17/3 10/3/3 9/6/3 + +g Chair 2 +v -2 1 0 +v -3 1 0 +v -2 2 0 +v -3 2 0 +v -3 1 -1 +v -3 2 -1 +v -3 1 -1.25 +v -2 1 -1.25 +v -3 2 -1.25 +v -2 2 -1.25 +v -2 1 -1 +v -2 2 -1 +v -2 3 -1 +v -3 3 -1 +v -2 3 -1.25 +v -3 3 -1.25 + +vt 0 0 +vt -1 0 +vt 0 1 +vt -1 1 +vt 1 0 +vt 1 1 +vt -1 -1 +vt 0 -1 +vt -1 -1.25 +vt 0 -1.25 +vt -1.25 1 +vt -1.25 0 +vt 1.25 0 +vt 1.25 1 +vt 1 -1 +vt 1 -1.25 +vt 0 2 +vt -1 2 +vt 1.25 2 +vt 1 2 +vt -1.25 2 + +vn 0 0 1 +vn -1 0 0 +vn 0 0 -1 +vn 1 0 0 +vn 0 -1 0 +vn 0 1 0 + +usemtl dot_purple +f 19/24/7 20/25/7 18/23/7 17/22/7 +f 20/24/8 22/25/8 21/23/8 18/22/8 +f 25/27/9 26/24/9 24/22/9 23/26/9 +f 28/27/10 19/24/10 17/22/10 27/26/10 +f 23/30/11 24/31/11 27/29/11 21/28/11 +f 25/32/8 23/33/8 21/23/8 22/25/8 +f 24/34/10 26/35/10 28/27/10 27/26/10 +f 31/31/12 32/37/12 30/36/12 29/29/12 +f 29/38/7 30/39/7 22/25/7 28/24/7 +f 31/40/10 29/41/10 28/27/10 26/35/10 +f 30/39/8 32/42/8 25/32/8 22/25/8 +f 32/41/9 31/38/9 26/24/9 25/27/9 + +g Pillow 2 +v -2 2 0 +v -3 2 0 +v -2 2 -1 +v -3 2 -1 + +vt 0 0 +vt 1 0 +vt 0 -1 +vt 1 -1 + +vn 0 1 0 + +usemtl dot_red +f 35/45/13 36/46/13 34/44/13 33/43/13 + +g Podium +v -1 0 1.5 +v -6 0 1.5 +v -1 1 1.5 +v -6 1 1.5 +v -6 0 -2 +v -6 1 -2 +v -1 0 -2 +v -1 1 -2 + +vt -1 0 +vt -6 0 +vt -1 1 +vt -6 1 +vt 1.5 0 +vt -2 0 +vt 1.5 1 +vt -2 1 +vt 6 0 +vt 1 0 +vt 6 1 +vt 1 1 + +s 1 +vt 2 0 +vt -1.5 0 +vt 2 1 +vt -1.5 1 +vt 1 1.5 +vt 6 1.5 +vt 1 -2 +vt 6 -2 +vt -1 -2 +vt -6 -2 +vt -1 1.5 +vt -6 1.5 +s off + +vn 0 0 1 +vn 0 0.7071068 0.7071068 +vn -1 0 0 +vn 0 0 -1 +vn 1 0 0 +vn 0 1 0 +vn 0 -1 0 + +usemtl dot_red +f 39/49/15 40/50/15 38/48/14 37/47/14 +f 40/53/16 42/54/16 41/52/16 38/51/16 +f 42/57/17 44/58/17 43/56/17 41/55/17 +f 44/61/18 39/62/18 37/60/18 43/59/18 +f 37/69/20 38/70/20 41/68/20 43/67/20 + +usemtl dot_blue +f 44/65/19 42/66/19 40/64/15 39/63/15 + +g Pillow 1 +v -4 2 0 +v -5 2 0 +v -4 2 -1 +v -5 2 -1 + +vt 0 0 +vt 1 0 +vt 0 -1 +vt 1 -1 + +vn 0 1 0 + +usemtl dot_green +f 47/73/21 48/74/21 46/72/21 45/71/21 + diff --git a/jme3-testdata/src/main/resources/OBJLoaderTest/dot_blue.png b/jme3-testdata/src/main/resources/OBJLoaderTest/dot_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..0a908db3786b684f11832f2c4f16fe788247f155 GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blmUKs7M+SzC{oH>NS%G}c0*}aI z1_r((Aj~*bn@<`jC{f}XQ4*Y=R#Ki=l*&+$n3-3imzP?iV4`QJXZF{R^94{1ucwP+ ih(vgDLc)*#@(j#OjQ>w=TKf(t!QkoY=d#Wzp$Pz0g(M*W literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/OBJLoaderTest/dot_green.png b/jme3-testdata/src/main/resources/OBJLoaderTest/dot_green.png new file mode 100644 index 0000000000000000000000000000000000000000..74f4a1707efcec03a740c95da05b88a2ef15e657 GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryEa{HEjtmSN`?>!lvI6;>1s;*b z3=Dh+K$tP>S|=w^P@=>&q9iy!t)x7$D3zfgF*C13FE6!3!9>qc&+M-s=L?`39#0p? h5Q*^Q4?oW{Ffcu1^kK^_R0fJOc)I$ztaD0e0syaUBewtm literal 0 HcmV?d00001 diff --git a/jme3-testdata/src/main/resources/OBJLoaderTest/dot_purple.png b/jme3-testdata/src/main/resources/OBJLoaderTest/dot_purple.png new file mode 100644 index 0000000000000000000000000000000000000000..4af4209e9811298b871b6afaac4e0ba203eae57f GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k8}blmUKs7M+SzC{oH>NS%G}c0*}aI z1_r(ZAk3I`t&!lvI6;>1s;*b z3=Dh+K$tP>S|=w^P@=>&q9iy!t)x7$D3zfgF*C13FE6!3!9>qc&+M-s=L?`39#0p? g5Q*^QALoHunHd=wl2t-41H~CUUHx3vIVCg!0G;I`{r~^~ literal 0 HcmV?d00001