How to draw a simple 2D line onto my jPCT Framebuffer?

Started by HammerNL, September 04, 2013, 02:53:21 PM

Previous topic - Next topic

HammerNL

Hi,

I'm developing a little 3D game in which you drive a car through a 3D world. In the upper left corner I'm displaying a fuel indicator, which is of course in simple flat 2D.

I'm using framebuffer.blit() to put the indicator itself directly on top of every frame, which is working fine.
However, inside the fuel indicator I need to draw a line, that actually indicates the level of fuel.

In the desktop version of jPCT, FrameBuffer has a getGraphics() method. But, since on Android there is no AWT, this method does not exist there. Now how do I draw my simple line?

Tnx!

Irony

I suppose you want this to work like a real fuel indicator, where the line/indicator should rotate. I believe there is no alternative to create a bitmap showing the arrow, use it as a texture on a plane, and rotate this plane accordingly.
If rotation is not necessary, simply create a 8x8 texture out of a color, and blit it in the format 1x20 or something on the correct position.

HammerNL

#2
Yes, like a real fuel indicator you find in most cars. However, I do not want to draw this line in 3D space. It must somehow be possible to blit a line directly into jPCT's Framebuffer, so in real plain old 2D.

I've come up with the following:

Bitmap bmp = Bitmap.createBitmap(100,100,Bitmap.Config.RGB_565);
Canvas c = new Canvas(bmp);
Paint p = new Paint();
p.setColor(0xffffffff);


then in the onDrawFrame() method:

framebuffer.blit(fuelMeter,0,0,10,10,100,100,true); // blit the fuel meter without the line-indicator
// then make the line-indicator
bmp.eraseColor(0);
c.drawLine(10,10,90,90,p); // this is a static line, but of course the coordinates should move
Texture t = new Texture(bmp);
framebuffer.blit(t,0,0,10,10,100,100,true);


It works OK, except after a few seconds I get an Out of Memory error:

09-04 16:40:53.240: E/dalvikvm-heap(18607): Out of memory on a 65552-byte allocation.
09-04 16:40:53.240: I/dalvikvm(18607): "GLThread 6483" prio=5 tid=13 RUNNABLE
09-04 16:40:53.240: I/dalvikvm(18607):   | group="main" sCount=0 dsCount=0 obj=0x41244200 self=0x2170230
09-04 16:40:53.240: I/dalvikvm(18607):   | sysTid=18628 nice=0 sched=0/0 cgrp=default handle=34429144
09-04 16:40:53.240: I/dalvikvm(18607):   | schedstat=( 0 0 0 ) utm=874 stm=256 core=0
09-04 16:40:53.240: I/dalvikvm(18607):   at com.threed.jpct.GLRenderer.convertTexture(GLRenderer.java:~868)
09-04 16:40:53.240: I/dalvikvm(18607):   at com.threed.jpct.GLRenderer.blit(GLRenderer.java:1381)
09-04 16:40:53.240: I/dalvikvm(18607):   at com.threed.jpct.GLRenderer.blitTexture(GLRenderer.java:1747)


So probably instantiating a new Texture on every frame is not a good idea, but I see no possibility to re-use an existing Texture. Or maybe I'm using an incorrect approach alltogether.

Any help would be appreciated!

EgonOlsen

You are constantly creating new textures that way. No wonder that you'll run into an OOM that way. You could fix this by using an ITextureEffect, but i don't think that this is a good idea anyway. It will perform very badly. I can think you various solutions for this:


  • Use a fuel bar instead. Much easier to implement.
  • As Irony suggested: Use a texture for that. You don't have to go into 3d space for this, you can use an Overlay for that. Or you can so in 3d and use an Object3D for it. That requires a bit more coding. Have a look at my minimap here: http://www.youtube.com/watch?v=oew62wECxqY...it uses this approach.
  • You can use a single Polyline. However, you are in 3d world space again with that solution, but that's actually pretty simple: You can use Interact2D to get the required 3d coordinates in world space and apply them to the Polyline.

HammerNL

Hi Egon,

So I went for the Polyline/Interact2D solution... spend hours and hours, but I can't get it right. I must be missing something...
Here's basically what I'm doing:

init function:

Polyline fuelLine = new Polyline(new SimpleVector[] { new SimpleVector(-1,0,1), new SimpleVector(1,1,1) }, RGBColor.RED);
world.addPolyline(fuelLine);


Then in onDrawFrame():

// determine center, radius and the line-angle of the fuel meter
int x = 100;
int y = 100;
float r = 40;
float angle = ...;

world.renderScene(framebuffer);
cam = world.getCamera();
SimpleVector[] vectors = new SimpleVector[]
{
  Interact2D.reproject2D3D(cam,framebuffer,x,y),
  Interact2D.reproject2D3D(cam,framebuffer,(int)(x+Math.sin(angle)*r+0.5),(int)(y-Math.cos(angle)*r+0.5))
};
fuelLine.update(vectors);

world.draw(framebuffer);


It does produce a line, but it's nowhere near the 2D x,y coordinates that I'm using, but somewhere in the middle of the screen (size 480x800px). Also the resulting line is much too short. I expect a line of 40 pixels, but it's actually like 2 or 3 pixels.

What am I doing wrong?

EgonOlsen

Use

Interact2D.reproject2D3DWS(...)

instead of


Interact2D.reproject2D3D(...)


With your code, you are calculating the coordinates in camera space, which is not what you want.

HammerNL

#6
Hi Egon,

Using reproject2D3DWS() does not seem to change a lot, my line is still just 3 or 4 pixels in the middle of the screen. Also, I've noticed that when I move the cam around, the reprojected lines move as well. Even though I am calling reproject2D3DWS() after every camera move. The idea is that the lines remain static on screen, like an overlay.

As the camera is passed as an argument into the reproject2D3DWS() method, I expected the method to come up with 3D coordinates that result in the exact same 2D coordinates, even when the cam is moved.

I'm still missing a vital piece. I think we need an example here to clarify things. I used your "hello world" app to demonstrate what I'm doing:

package com.example.line2don3d;

import java.lang.reflect.Field;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.opengles.GL10;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.MotionEvent;

import com.threed.jpct.*;

public class HelloWorld extends Activity {

  public final static float PI = (float)Math.PI;
  public final static SimpleVector XAXIS = new SimpleVector(1,0,0);
  public final static SimpleVector YAXIS = new SimpleVector(0,1,0);
  public final static SimpleVector ZAXIS = new SimpleVector(0,0,1);
  public final static float DEGtoRAD = PI/180;

  // Used to handle pause and resume...
  private static HelloWorld master = null;

  private GLSurfaceView mGLView;
  private MyRenderer renderer = null;
  private FrameBuffer fb = null;
  private World meterWorld = null;
  private RGBColor back = new RGBColor(0,0,0);

  private float touchTurn = 0;
  private float touchTurnUp = 0;

  private float xpos = -1;
  private float ypos = -1;

  private Camera cam = null;
  private float camX = 100;
  private float camY = 0;

  private Polyline fuelLine = null;

  protected void onCreate(Bundle savedInstanceState) {

    Logger.log("onCreate");

    if (master != null) {
      copy(master);
    }

    super.onCreate(savedInstanceState);
    mGLView = new GLSurfaceView(getApplication());

    mGLView.setEGLConfigChooser(new GLSurfaceView.EGLConfigChooser() {
      public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
        // Ensure that we get a 16bit framebuffer. Otherwise, we'll fall
        // back to Pixelflinger on some device (read: Samsung I7500)
        int[] attributes = new int[] { EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_NONE };
        EGLConfig[] configs = new EGLConfig[1];
        int[] result = new int[1];
        egl.eglChooseConfig(display, attributes, configs, 1, result);
        return configs[0];
      }
    });

    renderer = new MyRenderer();
    mGLView.setRenderer(renderer);
    setContentView(mGLView);
  }

  @Override
  protected void onPause() {
    super.onPause();
    mGLView.onPause();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mGLView.onResume();
  }

  @Override
  protected void onStop() {
    super.onStop();
  }

  private void copy(Object src) {
    try {
      Logger.log("Copying data from master Activity!");
      Field[] fs = src.getClass().getDeclaredFields();
      for (Field f : fs) {
        f.setAccessible(true);
        f.set(this, f.get(src));
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public boolean onTouchEvent(MotionEvent me) {

    if (me.getAction() == MotionEvent.ACTION_DOWN) {
      xpos = me.getX();
      ypos = me.getY();
      return true;
    }

    if (me.getAction() == MotionEvent.ACTION_UP) {
      xpos = -1;
      ypos = -1;
      touchTurn = 0;
      touchTurnUp = 0;
      return true;
    }

    if (me.getAction() == MotionEvent.ACTION_MOVE) {
      float xd = me.getX() - xpos;
      float yd = me.getY() - ypos;

      xpos = me.getX();
      ypos = me.getY();

      touchTurn = xd / -100f;
      touchTurnUp = yd / -100f;
      return true;
    }

    try {
      Thread.sleep(15);
    } catch (Exception e) {
      // No need for this...
    }

    return super.onTouchEvent(me);
  }

  protected boolean isFullscreenOpaque() {
    return true;
  }

  class MyRenderer implements GLSurfaceView.Renderer {

    public MyRenderer() {
    }

    public void onSurfaceChanged(GL10 gl, int w, int h) {
      if (fb != null) {
        fb.dispose();
      }
      fb = new FrameBuffer(gl, w, h);

      if (master == null) {

        meterWorld = new World();
        meterWorld.setAmbientLight(100, 100, 100);

        Light sun = new Light(meterWorld);
        sun.setIntensity(255,255,255);
        sun.setPosition(new SimpleVector(100,0,0));

        cam = meterWorld.getCamera();
        cam.setPosition(camX,camY,0);
        cam.setOrientation(new SimpleVector(-1,0,0),ZAXIS);

        Object3D cube = Primitives.getCube(10);
        cube.setAdditionalColor(0xff,0xff,0);
        cube.setOrigin(SimpleVector.ORIGIN);
        cube.build();
        meterWorld.addObject(cube);

        fuelLine = new Polyline(new SimpleVector[] { new SimpleVector(-20,0,20), new SimpleVector(20,20,20) }, RGBColor.RED);
        fuelLine.setWidth(3);
        meterWorld.addPolyline(fuelLine);

        if (master == null) {
          Logger.log("Saving master Activity!");
          master = HelloWorld.this;
        }
      }
    }

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    }

    private float fuel = 0;

    public void onDrawFrame(GL10 gl) {
      if (touchTurn != 0)
      {
        camY += touchTurn*10;
        cam.setPosition(camX,camY,0);
        cam.setOrientation(new SimpleVector(-1,0,0),ZAXIS);
        touchTurn = 0;
      }

      if (touchTurnUp != 0)
      {
        camX += touchTurnUp*10;
        cam.setPosition(camX,camY,0);
        cam.setOrientation(new SimpleVector(-1,0,0),ZAXIS);
        touchTurnUp = 0;
      }

      fuel += 0.01; // make the fuel line spin around and  around

      float angle = (-60+fuel*120)*DEGtoRAD;
      int x = 200;
      int y = 200;
      float r = 50;

      SimpleVector[] vectors = new SimpleVector[]
        {
          Interact2D.reproject2D3DWS(cam,fb,x,y),
          Interact2D.reproject2D3DWS(cam,fb,(int)(x+Math.sin(angle)*r+0.5),(int)(y-Math.cos(angle)*r+0.5))
        };
      fuelLine.update(vectors);

      fb.clear(back);
      meterWorld.renderScene(fb);
      meterWorld.draw(fb);
      fb.display();
    }
  }
}


What I'm getting with this, is a yellow cube on the origin, and inside the cube (so zoom in A LOT by sliding down) I find my little line that is spinning around. What I want is a spinning line around the actual sceen coordinate 200,200 with a radius of 50 pixels. What to do?

EgonOlsen

You have to keep in mind that this method returns a direction vector, not a position vector. Like the docs state, it's the direction vector from the camera's position to that given point in 3d. To make this a renderable Polyline, you have to add the position vector of the camera to both results.

HammerNL

That's it! I missed that line in the docs, see it now.
So simple. Tanx, working perfect now!

HammerNL

Almost there... only one tiny problem. I'm doing this in my onDrawFrame() method:

      fb.clear(back);
      world.renderScene(fb);
      world.draw(fb);

      fb.blit(fuelMeter,0,0,0,0,256,256,200,200,100,false);

      meterWorld.renderScene(fb);
      meterWorld.draw(fb);
      fb.display();


So first I draw my world containing the actual game objects, then I blit the static part of the fuel meter on top of it, and finally I draw the world containing nothing but my fuel indicator line on top of that. It all works great, except for 1 tiny detail:

The color of the polyline in the "meterWorld" is always white!? Even though it is defined as red:

fuelLine = new Polyline(new SimpleVector[] { new SimpleVector(-20,0,20), new SimpleVector(20,20,20) }, RGBColor.RED);

After some trail-and-error I found that removing the fb.blit() call fixes the issue, thus my Polyline is actually red then.

Why does this blit affect the color of the polyline? And how can I prevent that from happening?

EgonOlsen

Sounds like either a driver or an engine bug. Which OpenGL ES version are you using? 1.1 or 2.0? Can you provide a test case that shows the problem?

HammerNL

Hi Egon,

Yes of course: Here it is:

package com.example.line2don3d;

import java.lang.reflect.Field;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.opengles.GL10;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.MotionEvent;

import com.threed.jpct.*;
import com.threed.jpct.util.MemoryHelper;

public class HelloWorld extends Activity {

  public final static float PI = (float)Math.PI;
  public final static SimpleVector XAXIS = new SimpleVector(1,0,0);
  public final static SimpleVector YAXIS = new SimpleVector(0,1,0);
  public final static SimpleVector ZAXIS = new SimpleVector(0,0,1);
  public final static float DEGtoRAD = PI/180;

  // Used to handle pause and resume...
  private static HelloWorld master = null;

  private GLSurfaceView mGLView;
  private MyRenderer renderer = null;
  private FrameBuffer fb = null;
  private World world = null;
  private World meterWorld = null;
  private RGBColor back = new RGBColor(0xaa,0xaa,0xaa);

  private float touchTurn = 0;
  private float touchTurnUp = 0;

  private float xpos = -1;
  private float ypos = -1;

  private Camera cam = null;
  private float camX = 100;
  private float camY = 0;
  private Camera mCam = null;
  private float mCamX = 10;
  private float mCamY = 0;

  private Polyline fuelLine = null;
  private Texture fuelMeter = null;

  protected void onCreate(Bundle savedInstanceState) {

    Logger.log("onCreate");

    if (master != null) {
      copy(master);
    }

    super.onCreate(savedInstanceState);
    mGLView = new GLSurfaceView(getApplication());

    mGLView.setEGLConfigChooser(new GLSurfaceView.EGLConfigChooser() {
      public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
        // Ensure that we get a 16bit framebuffer. Otherwise, we'll fall
        // back to Pixelflinger on some device (read: Samsung I7500)
        int[] attributes = new int[] { EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_NONE };
        EGLConfig[] configs = new EGLConfig[1];
        int[] result = new int[1];
        egl.eglChooseConfig(display, attributes, configs, 1, result);
        return configs[0];
      }
    });

    renderer = new MyRenderer();
    mGLView.setRenderer(renderer);
    setContentView(mGLView);
  }

  @Override
  protected void onPause() {
    super.onPause();
    mGLView.onPause();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mGLView.onResume();
  }

  @Override
  protected void onStop() {
    super.onStop();
  }

  private void copy(Object src) {
    try {
      Logger.log("Copying data from master Activity!");
      Field[] fs = src.getClass().getDeclaredFields();
      for (Field f : fs) {
        f.setAccessible(true);
        f.set(this, f.get(src));
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public boolean onTouchEvent(MotionEvent me) {

    if (me.getAction() == MotionEvent.ACTION_DOWN) {
      xpos = me.getX();
      ypos = me.getY();
      return true;
    }

    if (me.getAction() == MotionEvent.ACTION_UP) {
      xpos = -1;
      ypos = -1;
      touchTurn = 0;
      touchTurnUp = 0;
      return true;
    }

    if (me.getAction() == MotionEvent.ACTION_MOVE) {
      float xd = me.getX() - xpos;
      float yd = me.getY() - ypos;

      xpos = me.getX();
      ypos = me.getY();

      touchTurn = xd / -100f;
      touchTurnUp = yd / -100f;
      return true;
    }

    try {
      Thread.sleep(15);
    } catch (Exception e) {
      // No need for this...
    }

    return super.onTouchEvent(me);
  }

  protected boolean isFullscreenOpaque() {
    return true;
  }

  class MyRenderer implements GLSurfaceView.Renderer {

    public MyRenderer() {
    }

    public void onSurfaceChanged(GL10 gl, int w, int h) {
      if (fb != null) {
        fb.dispose();
      }
      fb = new FrameBuffer(gl, w, h);

      if (master == null) {

        world = new World();
        world.setAmbientLight(100, 100, 100);

        Light sun = new Light(world);
        sun.setIntensity(255,255,255);
        sun.setPosition(new SimpleVector(-100,0,50));

        cam = world.getCamera();
        cam.setPosition(camX,camY,0);
        cam.setOrientation(new SimpleVector(-1,0,0),ZAXIS);

        Object3D cube = Primitives.getCube(10);
        cube.setAdditionalColor(0xff,0xff,0);
        cube.setOrigin(SimpleVector.ORIGIN);
        cube.build();
        world.addObject(cube);

        MemoryHelper.compact();
        world.compileAllObjects();

        meterWorld = new World();
        meterWorld.setAmbientLight(100, 100, 100);

        mCam = meterWorld.getCamera();
        mCam.setPosition(mCamX,mCamY,0);
        mCam.setOrientation(new SimpleVector(-1,0,0),ZAXIS);

        fuelLine = new Polyline(new SimpleVector[] { new SimpleVector(-20,0,20), new SimpleVector(20,20,20) }, RGBColor.RED);
        fuelLine.setWidth(5);
        meterWorld.addPolyline(fuelLine);

        meterWorld.compileAllObjects();

        fuelMeter = new Texture(getResources().openRawResource(R.raw.fuelmeter_256),true);
        fuelMeter.setMipmap(false);

        if (master == null) {
          Logger.log("Saving master Activity!");
          master = HelloWorld.this;
        }
      }
    }

    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    }

    private float fuel = 0;

    public void onDrawFrame(GL10 gl) {
      if (touchTurn != 0)
      {
        camY += touchTurn*10;
        cam.setPosition(camX,camY,0);
        cam.setOrientation(new SimpleVector(-1,0,0),ZAXIS);
        touchTurn = 0;
      }

      if (touchTurnUp != 0)
      {
        camX += touchTurnUp*10;
        cam.setPosition(camX,camY,0);
        cam.setOrientation(new SimpleVector(-1,0,0),ZAXIS);
        touchTurnUp = 0;
      }

      fuel += 0.01;

      float angle = (-60+fuel*120)*DEGtoRAD;
      int x = 200;
      int y = 200;
      float r = 100;

      SimpleVector v1 = Interact2D.reproject2D3DWS(mCam,fb,x,y);
      v1.add(new SimpleVector(mCamX,mCamY,0));
      SimpleVector v2 = Interact2D.reproject2D3DWS(mCam,fb,(int)(x+Math.sin(angle)*r+0.5),(int)(y-Math.cos(angle)*r+0.5));
      v2.add(new SimpleVector(mCamX,mCamY,0));
      SimpleVector[] vectors = new SimpleVector[] { v1, v2 };
      fuelLine.update(vectors);

      fb.clear(back);
      world.renderScene(fb);
      world.draw(fb);

      fb.blit(fuelMeter,0,0,0,0,256,256,200,200,100,false);

      meterWorld.renderScene(fb);
      meterWorld.draw(fb);
      fb.display();
    }
  }
}


I have no idea which version of GL I'm using. I'm running this on an Android 2.3.5 and an Android 4.0.3 device.

Hope you can find anything. Tanx!

[attachment deleted by admin]

EgonOlsen

It's OpenGL ES 1.1 then. BTW: Which version of jPCT-AE are you using?

HammerNL


EgonOlsen