OBJLoader enhancement: named groups support

master
kkolyan 5 years ago committed by Stephen Gold
parent 6ade1a027e
commit 0f06c14c28
  1. 4
      jme3-core/build.gradle
  2. 1
      jme3-core/src/plugins/java/com/jme3/scene/plugins/MTLLoader.java
  3. 83
      jme3-core/src/plugins/java/com/jme3/scene/plugins/OBJLoader.java
  4. 106
      jme3-core/src/test/java/com/jme3/scene/plugins/OBJLoaderTest.java
  5. 119
      jme3-examples/src/main/java/jme3test/model/TestObjGroupsLoading.java
  6. 20
      jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.mtl
  7. 219
      jme3-testdata/src/main/resources/OBJLoaderTest/TwoChairs.obj
  8. BIN
      jme3-testdata/src/main/resources/OBJLoaderTest/dot_blue.png
  9. BIN
      jme3-testdata/src/main/resources/OBJLoaderTest/dot_green.png
  10. BIN
      jme3-testdata/src/main/resources/OBJLoaderTest/dot_purple.png
  11. BIN
      jme3-testdata/src/main/resources/OBJLoaderTest/dot_red.png

@ -17,6 +17,10 @@ sourceSets {
} }
} }
dependencies {
testCompile project(':jme3-testdata')
}
task updateVersionPropertiesFile { task updateVersionPropertiesFile {
def versionFile = file('src/main/resources/com/jme3/system/version.properties') def versionFile = file('src/main/resources/com/jme3/system/version.properties')
def versionFileText = "# THIS IS AN AUTO-GENERATED FILE..\n" + def versionFileText = "# THIS IS AN AUTO-GENERATED FILE..\n" +

@ -152,6 +152,7 @@ public class MTLLoader implements AssetLoader {
material.setFloat("AlphaDiscardThreshold", 0.01f); material.setFloat("AlphaDiscardThreshold", 0.01f);
} }
material.setName(matName);
matList.put(matName, material); matList.put(matName, material);
} }

@ -67,8 +67,7 @@ public final class OBJLoader implements AssetLoader {
protected final ArrayList<Vector2f> texCoords = new ArrayList<Vector2f>(); protected final ArrayList<Vector2f> texCoords = new ArrayList<Vector2f>();
protected final ArrayList<Vector3f> norms = new ArrayList<Vector3f>(); protected final ArrayList<Vector3f> norms = new ArrayList<Vector3f>();
protected final ArrayList<Face> faces = new ArrayList<Face>(); private final ArrayList<Group> groups = new ArrayList<Group>();
protected final HashMap<String, ArrayList<Face>> matFaces = new HashMap<String, ArrayList<Face>>();
protected String currentMatName; protected String currentMatName;
protected String currentObjectName; protected String currentObjectName;
@ -87,6 +86,16 @@ public final class OBJLoader implements AssetLoader {
protected String objName; protected String objName;
protected Node objNode; protected Node objNode;
private static class Group {
private String name;
private final ArrayList<Face> faces = new ArrayList<Face>();
private final HashMap<String, ArrayList<Face>> matFaces = new HashMap<String, ArrayList<Face>>();
public Group(final String name) {
this.name = name;
}
}
protected static class Vertex { protected static class Vertex {
Vector3f v; Vector3f v;
@ -164,8 +173,7 @@ public final class OBJLoader implements AssetLoader {
verts.clear(); verts.clear();
texCoords.clear(); texCoords.clear();
norms.clear(); norms.clear();
faces.clear(); groups.clear();
matFaces.clear();
vertIndexMap.clear(); vertIndexMap.clear();
indexVertMap.clear(); indexVertMap.clear();
@ -289,10 +297,17 @@ public final class OBJLoader implements AssetLoader {
f.verticies[i] = vertList.get(i); f.verticies[i] = vertList.get(i);
} }
if (matList != null && matFaces.containsKey(currentMatName)){ Group group = groups.get(groups.size() - 1);
matFaces.get(currentMatName).add(f);
if (currentMatName != null && matList != null && matList.containsKey(currentMatName)){
ArrayList<Face> matFaces = group.matFaces.get(currentMatName);
if (matFaces == null) {
matFaces = new ArrayList<Face>();
group.matFaces.put(currentMatName, matFaces);
}
matFaces.add(f);
}else{ }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){ } catch (AssetNotFoundException ex){
logger.log(Level.WARNING, "Cannot locate {0} for model {1}", new Object[]{name, key}); 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<Face>());
}
}
} }
protected boolean nextStatement(){ protected boolean nextStatement(){
@ -387,8 +395,14 @@ public final class OBJLoader implements AssetLoader {
// specify MTL lib to use for this OBJ file // specify MTL lib to use for this OBJ file
String mtllib = scan.nextLine().trim(); String mtllib = scan.nextLine().trim();
loadMtlLib(mtllib); 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(); 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{ }else{
// skip entire command until next line // skip entire command until next line
logger.log(Level.WARNING, "Unknown statement in OBJ! {0}", cmd); logger.log(Level.WARNING, "Unknown statement in OBJ! {0}", cmd);
@ -566,6 +580,9 @@ public final class OBJLoader implements AssetLoader {
objNode = new Node(objName + "-objnode"); objNode = new Node(objName + "-objnode");
Group defaultGroupStub = new Group(null);
groups.add(defaultGroupStub);
if (!(info.getKey() instanceof ModelKey)) if (!(info.getKey() instanceof ModelKey))
throw new IllegalArgumentException("Model assets must be loaded using a ModelKey"); throw new IllegalArgumentException("Model assets must be loaded using a ModelKey");
@ -583,18 +600,20 @@ public final class OBJLoader implements AssetLoader {
} }
} }
if (matFaces.size() > 0){ for (Group group : groups) {
for (Entry<String, ArrayList<Face>> entry : matFaces.entrySet()){ if (group == defaultGroupStub) {
ArrayList<Face> materialFaces = entry.getValue(); materializeGroup(group, objNode);
if (materialFaces.size() > 0){ } else {
Geometry geom = createGeometry(materialFaces, entry.getKey()); 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); 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) if (objNode.getQuantity() == 1)
@ -604,4 +623,20 @@ public final class OBJLoader implements AssetLoader {
return objNode; return objNode;
} }
private void materializeGroup(Group group, Node container) throws IOException {
if (group.matFaces.size() > 0) {
for (Entry<String, ArrayList<Face>> entry : group.matFaces.entrySet()){
ArrayList<Face> 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);
}
}
} }

@ -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();
}
}
}

@ -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);
}
}

@ -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

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Loading…
Cancel
Save