DeepTokens: a library to turn 2D images into jMonkey 3d-ish meshes

Thing I’m trying to solve

I’ve always liked board-game-token like objects. Ones that are kind of 2D but have some depth to them (a Minecraft sword is a good example of this). When I’ve created these in the past I’ve turned the 2D image into voxels and let my voxel builder turn them 3D. This works but has a couple of downsides:

  • You get a jagged edge (it follows the pixel edges)
  • The number of vertexes is INSANE; 8 per pixel. So its only really suitable for very small images.

So I decided to do it properly:

  1. Take an image where transparent means “not present”
  2. Do edge detection on it
  3. Simpilify the edge
  4. Triangularize it

Deep tokens Library

I’ve created a library to do just that. It produces things like this:

weather

(real thing on left, triangles representation on right)

Starting from images like this

weatherIcon

Here you can see that the traditional jMonkeyEngine logo has been turned into a 3D shape.

Monkey

Features

Performance

The library creates these images pretty fast. Using the sun and clouds (512 by 362 px) as a test case the time it takes from png image finished loading to geometry created is on average; 45 milliseconds. That’s pretty fast but if you really care about start up time you could create the geometry ahead of time then save it as a j3o. (I have plans to improve this performance, so it will probably get better in a new release)

Edit; got it down to 10ms for version 1.0.2 :partying_face:

No transparency

No transparency; holes really are holes. This means no worrying about render order.

Normal calculation

The library automatically calculates appropriate normals, so the tokens look good when lit. Sharp edges are automatically detected (by default a greater than 30 degree change at a vertex is “sharp”) leading to sharp changes in normals at corners whereas curved surfaces get gentle changes in normals.

See in the below test shape the difference between the corners (sharp normal change) and the curved sections

Arch

Edge tinting

If you aren’t using lighting (or you want to make the edge more distinct) vertex colours can be used to darken it.

    DeepTokenBuilder deepTokenBuilder = new DeepTokenBuilder(1, 0.1f);
    deepTokenBuilder.setEdgeTint(new ColorRGBA(0.75f, 0.75f, 0.75f, 1));

Holes, islands, concave shapes

There are very few limitations on the starting images it can be given. They can be concave, convex, have holes, have islands in the holes (and holes in those islands etc).

E.g. this complex image (white is actually transparent and becomes “hole”)

Which outputs as this

IslandsAndHoles

As a fairly complex shape this has a decent number of triangles, but the amount of simplification of the edge is a configurable parameter so this could be reduced (with obvious downsides)

IslandsAndHolesGeometry

Limitations

There are a couple of things that can give bad results in the output

Single pixel wide regions

This is the big one, the library hates single pixel regions (like long thin hairs). Triangularisation will fail if DeepTokens encounters any of these. The jMonkeyEngine logo has one region where this happens (the hairs come out to the very thin, long, point) and I had to give it a hair cut. I might be able to pre-process the image to avoid that but it’s a job for another day.

Dirty edges

Often 2D images fade to transparent over a few pixels (anti aliasing). This won’t prevent a deep token being generated but tend to look a bit rubbish. DeepTokens likes sharp aliased edges; where a fully transparent pixel is next to a fully opaque pixel. After all DeepTokens is going to create a sharp edge in 3D and doesn’t know what to do with these partially transparent pixels

Solid edge

This isn’t a limitation exactly just an FYI; the edge is coloured by the last pixel, so if (cartoon style) you have a solid border round the image that will be the colour of the edge.

Example

Create a DeepTokenBuilder and specify its width and depth (height is implicit from the image shape)

DeepTokenBuilder deepTokenBuilder = new DeepTokenBuilder(1, 0.1f);

Load an image

BufferedImage tokenImage = ImageIO.read(TestApplication.class.getResourceAsStream("/pathToImage.png"));

Request this be turned into a deep token

Geometry deepToken = deepTokenBuilder.bufferedImageToLitGeometry(tokenImage, assetManager);

Or just get the mesh and texture and put it together yourself (if you want to use an unusual material)

Mesh mesh = deepTokenBuilder.bufferedImageToMesh(tokenImage);
Texture texture = deepTokenBuilder.imageToTexture(tokenImage);

Release

This library is on Maven Central, current dependency is

 implementation "com.onemillionworlds:deeptokens:1.0.2"

But keep an eye on maven central for new versions [there is some caching, so 24 hour delay on new versions]

17 Likes

cool lib :slight_smile:

not sure where i would need it for myself, but still might be usefull for some people.

It would be cool to use this library to make a 3d version of a game logo, and then that could be used on load screens for a rotating logo icon.

I was about to try this, but it looks like gradle is saying the dependency can’t be found:

Execution failed for task ':compileJava'.
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not resolve com.onemillionworlds:deeptokens:1.0.2.
     Required by:
         project :
      > No matching variant of com.onemillionworlds:deeptokens:1.0.2 was found. The consumer was configured to find a library for use during compile-time, compatible with Java 8, preferably in the form of class files, preferably optimized for standard JVMs, and its dependencies declared externally but:
          - Variant 'apiElements' capability com.onemillionworlds:deeptokens:1.0.2 declares a library for use during compile-time, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component, compatible with Java 17 and the consumer needed a component, compatible with Java 8
              - Other compatible attribute:
                  - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
          - Variant 'javadocElements' capability com.onemillionworlds:deeptokens:1.0.2 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
                  - Doesn't say anything about its target Java version (required compatibility with Java 8)
                  - Doesn't say anything about its elements (required them preferably in the form of class files)
          - Variant 'runtimeElements' capability com.onemillionworlds:deeptokens:1.0.2 declares a library for use during runtime, packaged as a jar, and its dependencies declared externally:
              - Incompatible because this component declares a component, compatible with Java 17 and the consumer needed a component, compatible with Java 8
              - Other compatible attribute:
                  - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
          - Variant 'sourcesElements' capability com.onemillionworlds:deeptokens:1.0.2 declares a component for use during runtime, and its dependencies declared externally:
              - Incompatible because this component declares documentation and the consumer needed a library
              - Other compatible attributes:
                  - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
                  - Doesn't say anything about its target Java version (required compatibility with Java 8)
                  - Doesn't say anything about its elements (required them preferably in the form of class files)

I have mavenCentral() in my repositories{} and added implementation "com.onemillionworlds:deeptokens:1.0.2". Is there anything else I still need to do?

What version of java are you using? I think thats saying you are running java 8 but DeepTokens uses java 17 (which it does)

Edit; although, downgrading it to use java 8 doesn’t look that hard if that’s a problem, just means I can’t use the nice records of later java

1 Like

I think it should be using the version that comes with the SDK, since I’m using an sdk gradle project (and on launch it says "JAVA_HOME="C:\Program Files\jmonkeyplatform_3.6.1-stable\jdk")

The SDK release page says its using java 17 since the 3.6 sdk release so I think it should be running with java 17 then

Could you try using a new feature in your project (like a record) and see if it complains?

1 Like

It looks like the app still compiles and runs with a Record variable set:

image

That’s strange that it’s saying that the “consumer” needed java 8, but it must be referring to something else as the consumer then. I am not sure though, I haven’t seen anything like this before.

Ah, sorry, I didn’t mean a java.lang.Record I meant something like this

public record SomeRecord(int valueA,String valueB){
}

In any case, I was requiring java 17 for basically no reason (I tend to default to it these days) so I’ve published a 1.0.3 that only requires java 8 or later.

[What with caching it will be several hours before it shows up on public repositories]

1 Like

Oh my bad, I’m not too familiar with java versioning and the different features, but with the corrected syntax it does not appear to compile.

So you’re right, my app must not be using java 17 then.

That is strange since I thought its using 17 with the sdk, since the latest 3.6 releases comes with jdk17.

Either way, I appreciate the help. I’ll be sure to post a screenshot of my games logo in 3d once I get it running

1 Like

The SDK itself is run with the said JDK version and by default it is used in projects. But this varies by your project. You should check your project (and Gradle build) settings.

Upgrading IDE doesn’t upgrade your project.

1 Like

It looks like I had 17 set in the project settings, but im still fairly new to gradle and my brain was still in Ant mode so I had never updated the “sourceCompatibility = 1.8” line in my gradle script. Thanks for the clarification, and sorry for the silly mistake :sweat_smile:

3 Likes

I tried to convert 2 variations of my logo, and I was able to get a circular image (the logo in my circular jme profile picture) to work.

However I get the following error when I tried to convert the full-text version of my logo to a mesh (the black areas are all transparent):

com.onemillionworlds.deeptokens.Triangulariser$TriangularisationFailureException: Triangulation failed. Do you have single pixel wide regions? Problematic points were (remember y may be flipped): 
. 443,906
447,875
446,873
444,877
445,882
447,876
447,874
470,847
470,836
464,809
462,809
446,870
447,874
361,1302
499,517
644,835

	at com.onemillionworlds.deeptokens.Triangulariser.triangulate(Triangulariser.java:28)
	at com.onemillionworlds.deeptokens.DeepTokenBuilder.bufferedImageToMesh(DeepTokenBuilder.java:142)
	at Core.SceneState.<init>(SceneState.java:386)
	at Core.AppController_ASD.initSingleScene(AppController_ASD.java:416)
	at Core.Interface.MainMenu.SingleSceneSelectionDisplay.lambda$new$0(SingleSceneSelectionDisplay.java:70)
	at com.simsilica.lemur.core.CommandMap.runCommands(CommandMap.java:61)
	at com.simsilica.lemur.Button.runClick(Button.java:359)
	at com.simsilica.lemur.Button$ButtonMouseHandler.mouseButtonEvent(Button.java:436)
	at com.simsilica.lemur.event.MouseEventControl.mouseButtonEvent(MouseEventControl.java:122)
	at com.simsilica.lemur.event.PickEventSession.buttonEvent(PickEventSession.java:657)
	at com.simsilica.lemur.event.MouseAppState.dispatch(MouseAppState.java:98)
	at com.simsilica.lemur.event.MouseAppState$MouseObserver.onMouseButtonEvent(MouseAppState.java:114)
	at com.jme3.input.InputManager.processQueue(InputManager.java:847)
	at com.jme3.input.InputManager.update(InputManager.java:923)
	at com.jme3.app.LegacyApplication.update(LegacyApplication.java:785)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:248)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:160)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:225)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:242)
	at java.base/java.lang.Thread.run(Thread.java:833)

Is the image I’m using possibly too large or incompatible in some way?


And then here is the other successful 3d logo. I didn’t code it into any loading screen yet, but I spun it manually in my scene editor for an early preview with a pbr material for some shininess, and I really like the result:

1 Like

Ooo, the rotating logo is very pretty.

On the image that fails triangularisation, it’s the thin region between the A and the F that’s upsetting it

image

I am trying to solve this problem as we speak, as it seems these thin regions come up a lot (I’ve had to fix several of my own images). I’ve edited your logo to eliminate this region and this should now work:

Imgur

2 Likes

Triangulation failure fix

I have put in a partial fix for the triangulation failure. If it experiences issues triangulating it just starts making triangles from vertexes near each other until it’s worked past the problem. Theoretically this might give bad results but for [The Afflicted Forests]¹ it gives what looks like a perfect result. And it will always finish so if there are problem regions you can visibly see them. This is in version 1.0.4.

¹ cool logo by the way

The things I wanted to use this for

The current game I’m working on, Starlight, is a VR space fleet game and the way you customise your fleet is by moving tokens onto a blueprint of your ship (it’s very physical, ideal for VR). Here are some of these tokens ready to be placed onto the blueprint (blueprint visible in the background). It’s a zoomed in view, these tokens are maybe 3 cm wide.

For comparison, here are the same tokens created with the voxel based approach

You may be saying “that’s not a fair comparison, the voxel ones are clearly lower resolution” and that’s true but also the whole point, the voxel based couldn’t really handle higher resolution than that (In fact I accidentally gave it the new high res images and it more or less locked up).

6 Likes