OBJLoader enhancement: named groups support

What’ve done

OBJ model format has markup element to organize objects in groups (without nesting). Currently, this markup is ignored by jME’s OBJLoader - only shape and material is loaded. This PR implements loading of groups as dedicated children of resulting Spatial.

Justification

In current verison there is no legitimate way of transfering any “tags” from model editor to game code with OBJ format models. This feature is vital for loading interactive models from OBJ format. Though OBJ format may be too primitive for serious modern character models, it’s easy to support, it’s tools-friendly, it’s human-friendly, it’s still supported by many editors and its capabilities are enough for static geometry modeling such as game areas for non-high-end games.

Example of use

in level editor

Object names are in “Hierarchy view” by left-top
image
sorry for referencing competitors - there just a nice level-editing plugin…

in OBJ file

groups defined by “g” command:


mtllib ./TwoChairs.mtl
o TwoChairs

g Pillow 2
v -2.791 0.8468804 0.04495001
v -3.791 0.8468804 0.04495001
v -2.791 0.8468804 -0.95505
v -3.791 0.8468804 -0.95505

vt 0 0
vt 1 0
vt 0 -1
vt 1 -1

vn 0 1 0

usemtl dot_red
f 3/3/1 4/4/1 2/2/1 1/1/1

g Chair 1
v -0.7912173 -0.1531196 0.29495
v -1.791217 -0.1531196 0.29495
v -0.7912173 0.8468804 0.29495
v -1.791217 0.8468804 0.29495
v -1.791217 -0.1531196 0.04495001
v -1.791217 -0.1531196 -0.95505
v -1.791217 0.8468804 0.04495001
v -1.791217 0.8468804 -0.95505
v -0.7912173 -0.1531196 -0.95505
v -0.7912173 0.8468804 -0.95505
v -0.7912173 -0.1531196 0.04495001
v -0.7912173 0.8468804 0.04495001
v -1.791217 1.84688 0.04495001
v -0.7912173 1.84688 0.04495001
v -1.791217 1.84688 0.29495
v -0.7912173 1.84688 0.29495

vt 0 0
vt -1 0
vt 0 1
vt -1 1
vt 1 0
vt 1 1
vt 0 -1
vt -1 -1
vt 0 0.25
vt -1 0.25
vt -0.25 1
vt -0.25 0
vt 0.25 0
vt 0.25 1
vt 1 0.25
vt 1 2
vt 0 2
vt 0.25 2
vt -0.25 2
vt -1 2
vt 1 -1

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_black
f 7/7/2 8/8/2 6/6/2 5/5/2
f 11/7/3 12/8/3 10/6/3 9/5/3
f 12/10/4 14/7/4 13/5/4 10/9/4
f 14/10/5 16/7/5 15/5/5 13/9/5
f 15/5/6 9/6/6 10/12/6 13/11/6
f 5/13/6 6/14/6 9/6/6 15/5/6
f 7/15/5 5/16/5 15/5/5 16/7/5
f 6/17/3 8/18/3 11/7/3 9/5/3
f 19/19/7 20/13/7 18/5/7 17/9/7
f 17/20/4 18/21/4 16/7/4 11/10/4
f 19/22/3 17/21/3 11/7/3 8/18/3
f 18/21/5 20/23/5 7/15/5 16/7/5
f 20/21/2 19/24/2 8/8/2 7/7/2

usemtl dot_green
f 14/11/7 12/25/7 11/9/7 16/5/7

g Chair 2
v -2.791 -0.1531196 0.29495
v -3.791 -0.1531196 0.29495
v -2.791 0.8468804 0.29495
v -3.791 0.8468804 0.29495
v -3.791 -0.1531196 0.04495001
v -3.791 -0.1531196 -0.95505
v -3.791 0.8468804 0.04495001
v -3.791 0.8468804 -0.95505
v -2.791 -0.1531196 -0.95505
v -2.791 0.8468804 -0.95505
v -2.791 -0.1531196 0.04495001
v -2.791 0.8468804 0.04495001
v -3.791 1.84688 0.04495001
v -2.791 1.84688 0.04495001
v -3.791 1.84688 0.29495
v -2.791 1.84688 0.29495

vt 0 0
vt -1 0
vt 0 1
vt -1 1
vt 1 0
vt 1 1
vt 0 -1
vt -1 -1
vt 0 0.25
vt -1 0.25
vt -0.25 1
vt -0.25 0
vt 0.25 0
vt 0.25 1
vt 1 0.25
vt 1 2
vt 0 2
vt 0.25 2
vt -0.25 2
vt -1 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_black
f 23/28/8 24/29/8 22/27/8 21/26/8
f 27/28/9 28/29/9 26/27/9 25/26/9
f 28/31/10 30/28/10 29/26/10 26/30/10
f 30/31/11 32/28/11 31/26/11 29/30/11
f 31/26/12 25/27/12 26/33/12 29/32/12
f 21/34/12 22/35/12 25/27/12 31/26/12
f 23/36/11 21/37/11 31/26/11 32/28/11
f 22/38/9 24/39/9 27/28/9 25/26/9
f 35/40/13 36/34/13 34/26/13 33/30/13
f 33/41/10 34/42/10 32/28/10 27/31/10
f 35/43/9 33/42/9 27/28/9 24/39/9
f 34/42/11 36/44/11 23/36/11 32/28/11
f 36/42/8 35/45/8 24/29/8 23/28/8


in Java code

        Node scene = (Node) assetManager.loadModel(new ModelKey("OBJLoaderTest/TwoChairs.obj"));
        assertEquals(3, scene.getQuantity());
        {
            Geometry pillow2 = (Geometry) scene.getChild(0);
            assertEquals("Pillow 2", pillow2.getName());
            assertEquals("dot_red", pillow2.getMaterial().getName());
        
            Node chair1 = (Node) scene.getChild(1);
            assertEquals("Chair 1", chair1.getName());
            assertEquals(2, chair1.getQuantity());
            {
                Geometry chairMat1 = (Geometry) chair1.getChild(0);
                assertEquals("dot_green", chairMat1.getMaterial().getName());
    
                Geometry chairMat2 = (Geometry) chair1.getChild(1);
                assertEquals("dot_black", chairMat2.getMaterial().getName());
            }
            Geometry chair2 = (Geometry) scene.getChild(2);
            assertEquals("Chair 2", chair2.getName());
            assertEquals("dot_black", chair2.getMaterial().getName());
        }

Code Changes

https://github.com/jMonkeyEngine/jmonkeyengine/pull/1369

Note: this topic was deleted during recent downtime issue. This is manually restored copy.

5 Likes

Hello

Smoothing groups, merge groups and (plain) groups shouldn’t be managed the same way. Before your change, lines starting with s or g were skipped and the parser went to the next statement. Now, you manage the (plain) groups. I advise you to use other importers written in Java as sources of inspiration when their respective licenses are compatible with JMonkeyEngine’s license to avoid reinventing the wheel in worse.

Hi. Thanks for reasonable notice.

The goal of my change is to extend it at the minimal sufficient extent to support necessary features.
Such minimalistic approach gives us two benefits:

  1. risk of break is minimal
  2. test coverage is maximum

And, as I see, current OBJLoader implementation developed with the same attitude, so such approach doesn’t seem bad in this case.

But, taking your notice in attention, I think I need to reimplement my solution to take into account plain groups only.

1 Like

I thought about how to correctly handle merge and smoothing groups and…

Options we have:

  1. Leave as is
    • Limited spec compliance
    • Free
    • Limited use cases support
    • Absolutely Safe
  2. Handle all group types just as name holders (proposed and implemented solution)
    • The same spec compiance
    • Easy
    • Extends OBJ usage for some real use cases
    • Possible break for users who hacks OBJLoader by using it’s protected fields or relies hard on current flat structure of resulting node tree
  3. Implement merging and smooting
    • Good spec compiance
    • Very complex
    • Extends OBJ usage for the same real use cases and for some hypothetical
    • Possible break at the same cases
  4. Ignore non-plain groups (leave “smooth” and “merge” groups at the same place as before - under the material-divided nodes with meaningless names)
    • The same spec compiance as proposed solution
    • Complex (much more corner cases)
    • Extends OBJ Usage at the same extent as proposed solution
    • Possible break in similar cases: will not break for users who relies hard on flat structure as long as their OBJ files contain only non-plain groups.

For me option 2 sounds the most reasonable.

PS: demo added in jme3-examples module.

3 Likes

option 5.

Write your own custom obj loader, create a geometry/mesh/material and write it out using j3o?

@mitm Maybe I missed something obvious, are the limitations of this importer documented?

@NikolayPlekhanov Do as you wish but in my humble opinion, smoothing groups should be parsed into a separate block with a comment explaining that they are unfortunately not smoothed by the engine.

as I understand, j3o is internal format used by jME SDK to store models imported from another formats. But I do not use jME SDK. What are the benefits of this double conversion for cases when developer is ok to work without SDK?

j3o loads fast.

everything else loads slow, at least doubles ram while loading.

Edit: using other loaders at runtime is like shipping .java files and compiling them on demand.

Edit2: and you don’t need the SDK to convert models. You just need to load them and save them as j3o. Or use JmeConvert. GitHub - Simsilica/JmeConvert: A command line utility for converting models to J3O and copying their dependencies to a new target structure.

1 Like

Actually, there are two different questions:

  1. how to handle smooth and merge groups
  2. where to place new code

List of options above are targeted only to the first question. Let’s do not mix them. at least now.

About the smooth and merge groups

@gouessej
Ok, I’ll add informative warning if these statements detected in OBJ file.

About the place where new code should be

@pspeed, @mitm
Now I understand. j3o is optimized for jME and it follows the idea to avoid unnecessary dependencies in final distribution.
Then I’d like to apply my changes to the place where it’s available for JmeConvert and JME SDK.

After examination of sources of SDK and JmeConvert I discovered that they both use jme3-core’s OBJLoader to read OBJ files during import (indirectly via DesktopAssetManager). So, looks like proposed solution is already compliant with your idea to use only j3o in runtime.

@NikolayPlekhanov At first, I don’t know whether it’s in the spec but a group can have no name. A smoothing group is different, “off” means 0, otherwise you should find a smoothing value. You have to use this information when removing duplicate vertices, the candidates must have the same texture (UV) coordinates AND come from the same smoothing group.

I agree with pspeed, the format of the engine should be used at runtime most of the time except if the purpose of the software is to perform conversions like MeshLab and jmeConvert. For long term storage, I use an exchange format (Collada in my case but GLTF is fine too, OBJ is enough for basic models without animations and is supported by tons of modelers).

I’d prefer avoid increasing spec compliance of OBJLoader more, because:

  1. it will make code more complex and thus more expensive in support.
  2. it is not demanded by anyone.
  3. Proposed implementation doesn’t make such enhancement more difficult in future.

as I see from this spec (I’m not sure it’s official, but looks very serious), s statements always used in conjunction with g statement. Anyway, if not - current implementation just ignores it. So it’s safe to continue ignore it and it will not make spec compiance less than before.

I agree too. And proposed solution supports such practice.

The example uses s in conjunction with g but it’s not mandatory. Yes you can go on ignoring them but in this case, don’t create a group for each smoothing group, just skip them and add a comment into your source code to state that smoothing groups aren’t taken into account.

you mean skip geometry of skipped group at all, or just handle it as before (place under a node with meaningless name)?

Just handle it as before. You’re right, my reply was inaccurate. “off” or a long isn’t a group name, it makes no sense to make a plain group for a smoothing group, especially if you don’t perform smoothing later. Those are two distinct notions.

So, @gouessej confincing me to follow option 4 from this post . I’m in doubt, because from my POV it will unreasonably complicate very simple code.

I’d appreciate intervention of other team members with their opinion :slight_smile:

To be honest, I care so little about the OBJ format other than “we should have it” that I’m actually fine with the way it already is. It’s an ancient format that is as simple as a format gets… and our current implementation is as simple as a format reader gets.

I would be willing to bet that within the next year or two, even you will have abandoned this format for something better.

To complicate it to support a feature that probably only you will use… and then only for a while… I just don’t know.

2 Likes

@gouessej
Could you please clarify how “s” statement works. As I see in Wavefront .obj file - Wikipedia, it just switches on/off smoothing and sounds like it doesn’t clash with “g” command. so we can smooth part of named group. Is my understadning right?

https://www.cs.cmu.edu/~mbz/personal/graphics/obj.html

Look at very bottom of page.

2 Likes

thanks for reference. actually text there confirms by assumption that s works in parallel with g. It means that my implementation already fit @gouessej’s claim.

Actually, I hope to abandon OBJ format in a couple of months, because I do not plan to create a lot of demo levels.

But from another side, this change is very simple and improves format support so much. And there are lot of tools that supports OBJ export. So I thought it worth to be improved even for rare cases, especially counting on growth of jME auditory.

2 Likes