Generate transparent icon from model


#1

OK before I spend time writing this from scratch I figured I would ask and see if someone has done this so I don’t re-invent the wheel.

The requirements:
Create a command line tool that takes a .j3o filename as an input parameter, renders the model in the center of a viewport, takes a snapshot based on a pre-defined width and height and saves the result to a .png file with the background being transparent.

What I am trying to do:
I need this because in my game, users can create their own space ships and then sell their designs. The in game store needs icons for each unique ship sold. After the ship it built and saved, I want the game to create a background transparent icon at say 64X64 pixels. Aside from that, I want to use this tool to create icons for the 400 or so ship components I have in game as well. Screenshot to Photoshop and then cutting out the background would take forever to do manually.

Has anyone solved for this? Or can you guys tell me what you use to convert models to transparent background icons grammatically?


#2

Not exactly what you are looking for but has a lot of the code you will need:

Making a command line tool will be interesting since it will need to initialize a display, etc… Like, it would have to be a real JME SimpleApplication.


#3

We did this for Lightspeed Frontier’s workshop functionallity. If I remember correctly, it took quite some code and still has problems if the ship is too big. It was also not a command line tool, but a part of the game.


#4

Two Ideas come to my mind:

  1. The dumb way would be to key out the glClearColor
  2. A more clever way is probably involving rendering to a frame buffer with alpha and then just grab all pixels out

#5

@zissis

i know how to do render, but transparency you would need to do removing background color (for example white) using shader or texture editor code.

but before, Why you cant put just 3d object into GUI node? (i understand there are a lot unique builds, but you can “auto-generate” only the ones that are visible in list) - if 3d model then you will be able to rotate it (good for someone to see all model, not one direction of it!) :slight_smile:

about texture rendering you got something like renderToTexture in Jme3 tests.
Im using this solution myself to pre-generate map texture, to not have camera rendering it all time on minimap/map.

but like said about transparency there are 2 solutions, one would be setting background to white(or black) and clear this color in shader, second will be same on start, but clear color programically by java libs.

Also important if you want have all models in same scale centered in view, you need compare Boundings of all creations and scale maximum bounding(x,y,z) to maximum size you want for render

Vector3f bound = Utility.getBoundDistance(obj);
float max = Math.max(Math.max(bound.x, bound.y), bound.z);
float resize = 50 / max;
obj.scale(resize);

where 50 was units that i needed (pixels in gui in this case). its not perfect calculation, but works


#6

Because in my game I use a combination of Lemur and JavaFX. The component screens are animated JavaFx screens so I need images for the icons. An example is shown below
Untitled


#7

Sorry i dont use Lemur and JavaFX

so there is no possibility Mixing 3d + 2d? ;/

Anyway like i said, you need use like in JME3Tests “renderToTexture” example,

set render background to white(or maybe there is transparent option if youi setup alpha 0 color :smiley: )

then center/scale objects like this:

Vector3f bound = Utility.getBoundDistance(obj);
float max = Math.max(Math.max(bound.x, bound.y), bound.z);
float resize = 50 / max;
obj.scale(resize);

and then remove White color from generated texture with some java code (here i would need to look too, but i think i did something before so possible)


#8

If you set the viewport’s color to ColorRGBA.BlackNoAlpha you get a transparent background. I do know how to code this. I was asking if anyone had done it before so that I don’t redo something that has already been done.


#9

Which, by the way, is exactly what the code I posted does… with transparency and everything.

Edit: the atlas generate code I posted essentially does everything OP wants except the ‘command line tool’ part. It renders tree imposters as an atlas that can also be saved to disk… sets the camera position the appropriate distance for the tree size, renders to a texture, etc…


#10

I also need to generate transparent icon from model.

This is my test code, but the generated png file has no transparency

public class TestIconGenerator extends SimpleApplication {

    public static final FunctionId F_GENERATE_ICON = new FunctionId("Generate Icon");

    private FrameBuffer fb;
    
    public TestIconGenerator() {

    }

    public static void main(String[] args) {
        TestIconGenerator main = new TestIconGenerator();
        AppSettings settings = new AppSettings(true);
        settings.setRenderer(AppSettings.LWJGL_OPENGL3);
        settings.setGammaCorrection(true);
        settings.setVSync(true);
        main.setSettings(settings);
        //main.setShowSettings(false);
        main.start();
    }

    @Override
    public void simpleInitApp() {
        GuiGlobals.initialize(this);
             
        viewPort.setBackgroundColor(ColorRGBA.BlackNoAlpha);
        
        //Camera camera = app.getCamera().clone();
        //camera.resize(256, 256, true);
        //camera.resize(1024, 256, false);

        fb = new FrameBuffer(getCamera().getWidth(), getCamera().getHeight(), 1);
        viewPort.setOutputFrameBuffer(fb);

        //Texture2D fbTex = new Texture2D(getCamera().getWidth(), getCamera().getHeight(), Format.RGBA8);
        fb.setDepthBuffer(Image.Format.Depth);
        fb.setColorBuffer(Format.BGRA8);
        //fb.setColorTexture(fbTex);

        initLight();
        //initFilters();
        initInput();

        Spatial model = getAssetManager().loadModel("tree/model-1/green-tree.gltf");
        rootNode.attachChild(model);
    }

    private void initLight() {
        Node probeNode = (Node) assetManager.loadModel("lightprobe/test-scene.j3o");
        LightProbe probe = (LightProbe) probeNode.getLocalLightList().iterator().next();
        rootNode.addLight(probe);

        /**
         * A white ambient light source.
         */
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White);
        rootNode.addLight(ambient);
    }

    private void initInput() {
        InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
        inputMapper.addDelegate(F_GENERATE_ICON, this, "generateIcon");
        inputMapper.map(F_GENERATE_ICON, KeyInput.KEY_SPACE);
    }

    private void initFilters() {
        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
        fpp.addFilter(new FXAAFilter());
        fpp.addFilter(new ToneMapFilter(Vector3f.UNIT_XYZ.mult(11.0f)));//4.0f
        fpp.addFilter(new SSAOFilter(0.5f, 3, 0.2f, 0.2f));
        viewPort.addProcessor(fpp);
    }

    public void savePng(File f, Image img) throws IOException {
        OutputStream out = new FileOutputStream(f);
        try {
            JmeSystem.writeImageFile(out, "png", img.getData(0), img.getWidth(), img.getHeight());
        } finally {
            out.close();
        }
    }

    protected Image createFrameBufferImage(FrameBuffer fb) {
        int width = fb.getWidth();
        int height = fb.getHeight();
        int size = width * height * 4;
        ByteBuffer buffer = BufferUtils.createByteBuffer(size);
        //Image.Format format = fb.getColorBuffer().getFormat();

        // I guess readFrameBuffer always writes in the same
        // format regardless of the frame buffer format
        Image.Format format = Image.Format.BGRA8;
        return new Image(format, width, height, buffer);
    }

    public void generateIcon() {

        Renderer renderer = getRenderer();
        Image image = createFrameBufferImage(fb);
        //Texture2D texture = new Texture2D(image);

        renderer.readFrameBuffer(fb, image.getData(0));
        //image.setUpdateNeeded();

        try {
            savePng(new File("/home/ali/Desktop/icon.png"), image);
        } catch (IOException ex) {
            Logger.getLogger(IconGeneratorState.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

this is how my model looks at runtime :

and this png file i get after generating icon :

It looks darker and there is no transparency.

Something should be wrong in my code, I have no deep understanding of how this FrameBuffer stuff works.

Will appreciate any help.


#11

I don’t know if it’s the issue… but have you tried it without the filters?


#12

Yes, I tried both with and without filters, the issue exist in both case.


#14

im not sure but i think i had dome something like this before.

first i thought you got no transparent background but blackNoalpha is 0,0,0,0 so should be fine.

have you tried change:

fb.setColorBuffer(Format.BGRA8);

to something else? it looks like it read background color properly, but alpha is not passed into image.

also i noticed in some old forum topics about:
gl.isExtensionAvailable(“GL_EXT_abgr”)
that was required for alpha render before. not sure if now.

also found some old topic(2011) using createPreview (that now still exist because i used it before)


#15

hmm… @oxplay2 I created a new test using an offViewPort created with renderManager.createPreView() , it seems transparency works fine at runtime if I set
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); on quad in simpleInitApp() but not exported when I create png file.

here is new test:

public class TestIconGenerator2 extends SimpleApplication {

    public static final FunctionId F_GENERATE_ICON = new FunctionId("Generate Icon");

    private static final String TOGGLE_UPDATE = "Toggle Update";
    private Spatial offModel;
    private float angle = 0;
    private ViewPort offView;
    private Texture offTex;
    private FrameBuffer offBuffer;

    public static void main(String[] args) {
        TestIconGenerator2 app = new TestIconGenerator2();
        AppSettings settings = new AppSettings(true);
        settings.setRenderer(AppSettings.LWJGL_OPENGL3);
        settings.setGammaCorrection(true);
        settings.setVSync(true);
        app.setSettings(settings);
        app.start();
    }

    public Texture setupOffscreenView() {
        Camera offCamera = new Camera(512, 512);

        offView = renderManager.createPreView("Offscreen View", offCamera);
        offView.setClearFlags(true, true, true);
        offView.setBackgroundColor(ColorRGBA.BlackNoAlpha);

        // create offscreen framebuffer
        offBuffer = new FrameBuffer(512, 512, 1);

        //setup framebuffer's cam
        offCamera.setFrustumPerspective(45f, 1f, 1f, 1000f);
        offCamera.setLocation(new Vector3f(0f, 0f, 15f));
        offCamera.lookAt(new Vector3f(0f, 0f, 0f), Vector3f.UNIT_Y);

        //setup framebuffer's texture
        Texture2D offTex = new Texture2D(512, 512, Format.RGBA8);
        offTex.setMinFilter(Texture.MinFilter.Trilinear);
        offTex.setMagFilter(Texture.MagFilter.Bilinear);

        //setup framebuffer to use texture
        offBuffer.setDepthBuffer(Format.Depth);
        offBuffer.setColorTexture(offTex);

        //set viewport to render to offscreen framebuffer
        offView.setOutputFrameBuffer(offBuffer);

        // setup framebuffer's scene
        offModel = getAssetManager().loadModel("tree/model-1/green-tree.gltf");

        // attach the scene to the viewport to be rendered
        offView.attachScene(offModel);

        return offTex;
    }

    protected Image createFrameBufferImage(FrameBuffer fb) {
        int width = fb.getWidth();
        int height = fb.getHeight();
        int size = width * height * 4;
        ByteBuffer buffer = BufferUtils.createByteBuffer(size);
        Image.Format format = fb.getColorBuffer().getFormat();

        // I guess readFrameBuffer always writes in the same
        // format regardless of the frame buffer format
        //format = Image.Format.BGRA8;
        return new Image(format, width, height, buffer);
    }
    
    public void savePng(File f, Image img) throws IOException {
        OutputStream out = new FileOutputStream(f);
        try {
            JmeSystem.writeImageFile(out, "png", img.getData(0), img.getWidth(), img.getHeight());
        } finally {
            out.close();
        }
    }
    
     public void generateIcon() {
        Renderer renderer = getRenderer();
        Image image = createFrameBufferImage(offBuffer);
        //Texture2D texture = new Texture2D(image);

        renderer.readFrameBuffer(offBuffer, image.getData(0));
        //image.setUpdateNeeded();
        
        try {
            savePng(new File("/home/ali/Desktop/icon.png"), image);
        } catch (IOException ex) {
            Logger.getLogger(IconGeneratorState.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    public void simpleInitApp() {
        GuiGlobals.initialize(this);

        cam.setLocation(new Vector3f(3, 3, 3));
        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);

        //setup main scene
        Quad mesh = new Quad(2, 2);
        Geometry quad = new Geometry("box", mesh);

        offTex = setupOffscreenView();

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setTexture("ColorMap", offTex);
        mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
        quad.setMaterial(mat);
        rootNode.attachChild(quad);

        viewPort.setBackgroundColor(ColorRGBA.White);
        initLight();
        initInput();
    }

    @Override
    public void simpleUpdate(float tpf) {
        Quaternion q = new Quaternion();

        if (offView.isEnabled()) {
            offModel.setLocalRotation(q);
            offModel.updateLogicalState(tpf);
            offModel.updateGeometricState();
        }
    }

    private void initLight() {
        Node probeNode = (Node) assetManager.loadModel("lightprobe/test-scene.j3o");
        LightProbe probe = (LightProbe) probeNode.getLocalLightList().iterator().next();
        offModel.addLight(probe);

        /**
         * A white ambient light source.
         */
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White);
        offModel.addLight(ambient);
    }

    private void initInput() {
        InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
        inputMapper.addDelegate(F_GENERATE_ICON, this, "generateIcon");
        inputMapper.map(F_GENERATE_ICON, KeyInput.KEY_G);
    }
} 

Result looks like this with BlendMode.Alpha in JME (runtime):

Without BlendMode.Alpha

but transparency is not exported when saving to png :


#16

I don’t know if you are in a position to run it, but I wonder if the export of tree atlas stuff works in the SimArboreal Editor. It also saves a PNG of rendered images and should have transparency.

Edit: and I guess there are still prebuilt releases so maybe it’s easy to try:

…I haven’t run it in a while.


#17

Yes tested also with RGBA8, ABGR8 but no difference.


#18

Going to try it now.


#19

Hmm… it has transparency

I doubt what is wrong with my code then ?


#20

Not sure. I looked through and didn’t spot any significant differences on quick glance.

…but maybe if you compare them you will spot a difference that I didn’t.


#21

Unfortunately I couldn’t spot difference either. I also moved image exporting code inside simpleRender(RenderManager rm) but no difference.

I also noticed even ScreenshotAppState does not export transparency on png files.

So there should be some magic happening in AtlasGeneratorState :slightly_smiling_face: