gbadev.org forum archive

This is a read-only mirror of the content originally found on forum.gbadev.org (now offline), salvaged from Wayback machine copies. A new forum can be found here.

DS development > Problem with software sprite emulation via 3D engine

#79508 - NEiM0D - Fri Apr 14, 2006 1:48 am

Hello all,

I'm trying to figure out how to fix a problem which occurs when trying to draw a sprite (textured polygon) using the DS's 3D engine .

First I start with setting up the orthogonal projection matrix.
This orthogonal projection matrix sets up the camera and 3D system so that the X/Y coordinates of the 3D space (regardless of Z) are the same as the screen X/Y coordinates, which makes it very easy for 2D sprites to be drawn on the DS.

However, there is a slight problem with the texture mapping used by the 3D engine with DS:

When rotating the polygon, the polygon can be one texel off!

Consider drawing a sprite which is 32x32 in size.
For the polygon's vertex coordinates you give it these 4 points:

Code:

(0.0, 0.0)         (32.0, 0.0)
(0.0, 32.0)       (32.0, 32.0)


And for texture coordinates the same.

For example, see these two pictures:
Polygon which is not rotated (note the 4 differently coloured pixels) :
[Images not permitted - Click here to view it]

Now when you rotate this 90 degrees (see [1]) using the DS's 3D engine, you get:
[Images not permitted - Click here to view it]
(zoom in to see the problem clearly)

In the last image, the result you get which the 3D engine displays, is obviously not correct. It looks like the texture went up by 1 pixel!

I should mention that this texture was drawn with the "texture repeat" flags enabled. If this would not be the case, then the "1-pixel incorrect left side" of the polygon would simply not be drawn.

A possible correction would be to move the texture one pixel down when you know you are going to rotate 90 degrees, which would then display a correct image.

But consider a rotation of any degree, how does one fix it?
This is where I am stuck at the moment, maybe someone can enlighten me?

[1]: The matrix used to perform the rotation on the polygon is:

Code:

     
P = [cos(a)   sin(a)]
      [-sin(a)   cos(a)]

#79517 - ecurtz - Fri Apr 14, 2006 2:36 am

A couple things you could try:

Check to make sure your sin and cosine values are accurate (or just test with 0 and 1 to make sure the problem is the same.)

Offset the texel values slightly, try 0.5-32.5 or 0.1-32.1. Depending on how it determines what texel to use on border cases this may help.

#79736 - NEiM0D - Sat Apr 15, 2006 5:53 pm

No that seems not to have much effect. The result is still the same.

#79871 - blaisef01 - Sun Apr 16, 2006 5:18 pm

I don't know if this will help:

Keep vertex coordintes values between (-7, -7, -7) and (7, 7, 7) or things go screwed. If you need quads bgger than that use a glScale to increase the size of the object.

Here's a matrix that i was using to rotate things around the z-axis

P = [ca sa]
[-sa ca]

where ca = COS[a] and sa = SIN[a]

a = ((ang) + 2048) % LUT_SIZE

LUT_SIZE is 512, thats's how big the cosine and sine tables with libnds are

COS[a] and SIN[a] reference the sine and cosine tables shipped with libnds and the angles range from 0 to 4095 where 0 is 0 degress and 4095 is about 359.1 degrees

This isn't the best way to do this and this explanation probably makes no sense if you're still confused as i am now. find me on yahoo messenger at sevenforce_control@yahoo.co.uk or on msn messenger at blaisef01@hotmail.com and i'll attempt to explain it better

#79874 - NEiM0D - Sun Apr 16, 2006 5:30 pm

Thanks for the tip, but the vertex coordinates I use are 0.0 and 1.0. (multiplied by 4096 because of fixed point).

The elements of the internal matrix of the 3D engine is 32bits: 1bit sign, 19bits integer and 12bits decimal.

So yes, I use the scale command which scales the internal matrix by a factor of 32 because that's the size of my sprite.

#79945 - Chris Holmes - Mon Apr 17, 2006 1:39 pm

Are you actually rotating the sprite's vertices, or are you rotating the texture vertices? Generally speaking, if you're rendering sprites in 3d, your best bet is to leave the object vertices alone and only change the texture vertices.

It looks like the error you're getting is because you're using fixed/floating point math. If you're just trying to rotate by 90 degrees, then don't count on a lookup table working. Floating point math (and fixed point especially) is imprecise. The more you rotate things with fixed/floating point, the more the error builds up.

One easy way to fix this problem is to just keep an array of your current texture coordinates and your current rotation angle. Whenever you change the sprites rotation, reset the values in the texture coordinate array.

Assuming that you render your quad like this:
0 1
3 2

Then if you're rotating 90 degrees clockwise, then texcoord[0] = texcoord[3], [1] = [2], [2] = [1], and [3] = [2].

Since you're only copying values and not doing imprecise multiplication, then you're set and you won't see any graphical glitches.

Chris

#79948 - Mighty Max - Mon Apr 17, 2006 2:45 pm

Chris Holmes wrote:
It looks like the error you're getting is because you're using fixed/floating point math. If you're just trying to rotate by 90 degrees, then don't count on a lookup table working. Floating point math (and fixed point especially) is imprecise. The more you rotate things with fixed/floating point, the more the error builds up.


Multiplication by 0 or 1 (90? rotation) should not have loss at all, they are fully enclosed in every mathematical group:
a * 0 = 0 with 0 existing, for every existing a
a * 1 = a with 1 existing, for every existing a exists a :D

The only possible loss is the overflow at multiplying the greatest possible negative value with -1 (i.e. in 16 bit fixed point: 0x8000 * (-1) gives 0x8000 [~value +1 gives 0x7FFF +1 = 0x8000] , which is again the greatest possible negative value.



For rotating the texture instead of the quads vertices, it woudl really look odd when rotating in steps other then 90? later on. I.e. on 45? you'd get something like
Code:

    /\
  +----+
  |    |
 /|    |\
 \|    |/
  |    |
  +----+
    \/


where the texture does not have any ground to show its edges and it falsely repeated where the quad extends the texture
_________________
GBAMP Multiboot

#79960 - tepples - Mon Apr 17, 2006 6:00 pm

Mighty Max wrote:
Multiplication by 0 or 1 (90? rotation) should not have loss at all, they are fully enclosed in every mathematical group:
a * 0 = 0 with 0 existing, for every existing a
a * 1 = a with 1 existing, for every existing a exists a :D

Groups do not have a multiplicative zero, but rings do.
_________________
-- Where is he?
-- Who?
-- You know, the human.
-- I think he moved to Tilwick.

#79969 - Mighty Max - Mon Apr 17, 2006 8:20 pm

you'r right.
I was first about to say "R?ume" but as i don't know the correct english term, i reduced it a bit :p
_________________
GBAMP Multiboot

#79981 - NEiM0D - Mon Apr 17, 2006 11:02 pm

Mighty Max is right.

And my SIN/COS look up tables are precisely defined at 90 degrees, 180 degrees, etc.

Rotating the texture coordinates is not a good idea, because of what Mighty Max said.

Your polygon would *always* be an unrotated square, and only the texture on it would rotate. Definitely not what you want.

I've found an article by Chris Hecker which explains the problem better (but not necessarily more accurately): http://www.d6.com/users/checker/pdfs/gdmtex3.pdf

The problem is that the 3D engine of the DS does not behave exactly like Chris Hecker's texture mapper.

The difference from the top of my head at the moment is that in Chris Hecker's texture mapper, a "pixel" is representing a box with the coordinate of the pixel being the center.
On DS the coordinate of the pixel is not the center, but the top-left corner of the "pixel box".

#80024 - gladius - Tue Apr 18, 2006 3:36 am

I'm surprised (can't try it right now) that something like ecurtz suggested doesn't work:
Code:

fixed xoffset = fixedDiv(1, spriteWidth) / 2;
fixed yoffset = fixedDiv(1, spriteHeight) / 2;

// do rotation to sprite here

texture.topLeft.x -= xoffset; texture.topLeft.y -= yoffset;
topRight, bottomLeft, bottomRight, etc.

An alternative if it's only having a problem in the vertical direction is to do a vertical offset proportional to the angle of rotation. Something similar to this.
Code:

vector offsetVector = sin(angle) * vector2d(0, (1.0/spriteHeight) / 2.0);
texcoord.x += offsetvector.x;
texcoord.y += offsetvector.y;

This will have no effect when it is at 0 degrees, but will smoothly adjust the texture coordinates to be offset by 1.0/spriteHeight / 2.0 at 90 degree rotation.

#80058 - keldon - Tue Apr 18, 2006 11:55 am

Yes, I was thinking of that too but didn't like it as an implementation. Besides there is a specific point at which you will have to adjust the pixel, so why not precalculate it.

#80095 - gladius - Tue Apr 18, 2006 7:51 pm

keldon wrote:
Besides there is a specific point at which you will have to adjust the pixel, so why not precalculate it.

I'm not sure what you mean. The point of smoothly adjusting the offset is that so as you rotate, the image doesn't all of a sudden "pop" as you hit 90 degrees, or 180 degrees, etc.

#80105 - keldon - Tue Apr 18, 2006 8:43 pm

I was under the impression that the hardware was not handling inbetween pixels and would pop anyway since ecurtz's suggestion didn't work.

#80135 - NEiM0D - Wed Apr 19, 2006 2:54 am

I've uploaded a demo which shows the problem better (should have done this this first post):
http://neimod.com/softsprite.nds

L/R rotate, A+SELECT/START zoom

This is with no corrections whatsoever.
When moving from 0->45->90 degrees for example, you can see that the texture slips one texel up very noticable at certain points.

gladius: The first correction seems to mess up the rotation at 0 degrees, and the second correction is not much different from without texture correction.

#80148 - Payk - Wed Apr 19, 2006 6:28 am

I have same problem. There are some "ways" to work arround.
If u want to make a complete field with different textures, be sure to "draw" first all squarez with same texture. So that bug just apperes between squares with different texture. Next thing: U could have a Backgroungcolor which matchs to the texture so its hard to see the gab (that method is used in Infinity 3D World Generator). But in my project it couldnt be done because of that reflexions i want to use...They would look shity with a green bg-color behind the water...
And last dirty way: Make that squares a bit bigger. So they are "Over" EACHOTHER...Hmhm but then it flickers a bit on rotation like in that commercial 3D Helicopter game..I forget that name of it but the ground isnt still there, too.

#80184 - NEiM0D - Wed Apr 19, 2006 6:39 pm

Those workarounds don't do the quality good when you do have perfect texture display.

I'm still a firm believer that this is just a mathematical problem and it should certainly be possible to solve it.

#80193 - dovoto - Wed Apr 19, 2006 9:50 pm

Could you perhaps take ecurtz/gladius idea and use the texture matrix to shift the texture verticaly (you have a bit more bit depth using the matrix vs the coordinates iirc) in proportion to the angle of rotation?
_________________
www.drunkencoders.com

#80194 - gladius - Wed Apr 19, 2006 9:55 pm

Doh, for my method #2, you shouldn't divide the offset by 2. Perhaps try that? I'll give this a shot sometime today or later on this week. Can you post your source? I don't mind writing up my own little texture example if neccesary, but no need to overwork myself ;).

#80201 - NEiM0D - Wed Apr 19, 2006 11:02 pm

This code is for my own custom library I have written for DS. Should be possible to convert this to libnds.

Code:

#include <ds_types.h>
#include <ds_io.h>
#include <ds_g2d.h>
#include <ds_g3d.h>
#include <ds_pm.h>
#include <ds_debug.h>
#include <ds_sys.h>
#include <ds_pad.h>
#include <ds_irq.h>
#include <ds_swi.h>
#include <ds_card.h>
#include <ds_dma.h>
#include <ds_ipc.h>
#include <ds_rtc.h>
#include <ds_math.h>


void VBlankVector(void)
{

}

extern u8 texgfx[16384];
extern u16 texpal[256];

static int vtxtab[][2]=
{
   {-2048, -2048},
   {-2048, 2048},
   {2048, 2048},
   {2048, -2048},
};

static int textab[][2]=
{
   {0, 0},
   {0, 512},
   {512, 512},
   {512, 0},
};


int main()
{
   int i,j;


   // enable VRAM bank A, 128KB
   void *VRAM_A = G2D_EnableVRAM_A(VRAM_A_BG_A, 0);
   // enable VRAM bank C, 128KB
   void *VRAM_C = G2D_EnableVRAM_C(VRAM_C_BG_B, 0);
   // enable VRAM bank B, 128KB
   void *VRAM_B = G2D_EnableVRAM_B(VRAM_B_BITMAP, 0);
   // enable VRAM bank E, 64KB
   void *VRAM_E = G2D_EnableVRAM_E(VRAM_E_BITMAP, 0);
   
   // copy textures to vram (only way to map textures to arm9)
   SWI_CpuSet(texgfx, VRAM_B, CPUSET_MODE_WORD | CPUSET_MODE_COPY | (sizeof(texgfx)/4));   
   SWI_CpuSet(texpal, VRAM_E, CPUSET_MODE_WORD | CPUSET_MODE_COPY | (sizeof(texpal)/4));

   G2D_EnableVRAM_B(VRAM_B_TEXIMG, 0);
   G2D_EnableVRAM_E(VRAM_E_TEXPAL, 0);


   // enable power to these devices (and engine A should be at top screen)
   PM_Powerup(POWER_LCD | POWER_2D_ALL | POWER_3D_ALL | POWER_2D_A_TOP);


   // init interrupts
   IRQ_Init();
   IPC_Init();

   // enable ipc fifo recv interrupt
   IRQ_SetVector(IRQ_IPCRECV, IPC_RecvDispatcher, IRQ_SINGLE);
   IRQ_Enable(IRQ_IPCRECV);

   // enable ipc fifo recv interrupt
   IRQ_SetVector(IRQ_VBLANK, VBlankVector, IRQ_NESTED);
   IRQ_Enable(IRQ_VBLANK);

   // enable arm9 access to the gba and ds slot
   SYS_SetCartPriority(PROCESSOR_ARM9);
   SYS_SetCardPriority(PROCESSOR_ARM9);

   // let ARM7 know cartridge right is set to ARM7.
   IPC_SetState(0);
   // wait for ARM7 ack...
   IPC_Synchronize(0);

   REG_DISPCNT_A = A_DISPMODE_NORMAL | A_DISP_BG0_AS_3D | A_DISP_BG0;

   G3D_InitMatrixStack();
   G3D_Viewport(0, 0, 255, 191);
   G3D_ClearColor(makergb(01, 1, 1), 31, 0, CLEARIMAGE_FOG_OFF);
   G3D_ClearDepth(0x7FFF);

   for(;;)
   {

      REG_DISP3DCNT |= 1;


      G3D_Reset();

      G3D_MatrixMode(MTX_PROJECTION);
      G3D_LoadIdentity();

      static f32 zoom = makef32(32.0f);

      G3D_Ortho(makef32(0.0f), makef32(256.0f), makef32(192.0f), makef32(0.0f), makef32(-1024.0f), makef32( 1024.0f));

      G3D_MatrixMode(MTX_TEXTURE);
      G3D_LoadIdentity();

      // Do rotation/scaling/translation of texture here
      //G3D_RotateZInv(zrot)

      // Prepare current texture matrix for 3D engine
      
      m4x4 m;

      m[0][0] = makef32(1.0f);
      m[0][1] = 0;
      m[0][2] = 0;
      m[0][3] = 0;

      m[1][0] = 0;
      m[1][1] = makef32(1.0f);
      m[1][2] = 0;
      m[1][3] = 0;

      m[2][0] = 0;
      m[2][1] = 0;
      m[2][2] = makef32(16.0f);
      m[2][3] = 0;

      m[3][0] = 0;
      m[3][1] = 0;
      m[3][2] = 0;
      m[3][3] = makef32(16.0f);

      G3D_MatrixMultiply4x4(&m);

      G3D_MatrixMode(MTX_POSITION);
      G3D_LoadIdentity();

      static f32 xtrans = makef32(100.0f);
      static f32 ytrans = makef32(60.0f);

      G3D_Translate(xtrans, ytrans, makef32(0.0f));

      G3D_RotateZ(zrot);
      
      G3D_Scale(zoom, zoom, makef32(1.0f));

      

      G3D_PolygonAttr(0, 31, RENDER_ALL, POLYMODE_MODULATION, POLYLIGHT_NONE, POLYATTR_NONE);
      G3D_TexPalette(0);
      G3D_TexImage(TEXSOURCE_COORD, TEXTRANS_PALETTE, TEXFORMAT_256COL, TEXWIDTH_128, TEXHEIGHT_128, TEXATTR_REPEAT_S | TEXATTR_REPEAT_T /*| TEXATTR_FLIP_S | TEXATTR_FLIP_T*/, 0);
      G3D_Color(makergb(31, 31, 31));

      if (!(REG_KEY & KEY_A))
      {

         if (!(REG_KEY & KEY_DOWN))
            ytrans += makef32(1.0f);

         if (!(REG_KEY & KEY_UP))
            ytrans -= makef32(1.0f);

         if (!(REG_KEY & KEY_LEFT))
            xtrans -= makef32(1.0f);

         if (!(REG_KEY & KEY_RIGHT))
            xtrans += makef32(1.0f);

         if (!(REG_KEY & KEY_START))
            zoom += 4096;

         if (!(REG_KEY & KEY_SELECT))
            zoom -= 4096;

      }
      else if (!(REG_KEY & KEY_B))
      {
      } else
      {
      }

      if (!(REG_KEY & KEY_L))
         zrot++;
      if (!(REG_KEY & KEY_R))
         zrot--;

      DEBUG("zrot = %d\r\n", zrot)

      G3D_Begin(POLY_QUADS);

      for(i=0; i<4; i++)
      {
         G3D_TexCoord(textab[i][0], textab[i][1]);
         G3D_Vertex(vtxtab[i][0], vtxtab[i][1], makef32( 0.0f));
      }

      G3D_End();



      G3D_SwapBuffers(BUFFER_W, SORT_AUTO);


      // wait for vblank
      SWI_VBlankIntrWait();
   }

   return 0;
}


As for the offsetting without the divide by 2, the result is semi-same again.

#80202 - gladius - Wed Apr 19, 2006 11:14 pm

Cool, thanks. One thing I notice right off the bat is your viewport is using (0,0,255,191). Try changing that to (0,0,256,192) and see if that makes a difference.

#80204 - Dark Knight ez - Thu Apr 20, 2006 12:00 am

Actually, that is correct. Most (if not all) NDS 3D programs have that.
0 to 255 equals 256 (since you also count 0).

#80214 - gladius - Thu Apr 20, 2006 1:35 am

I realize that 0 is counted :). I wanted to see what happened when it was changed to 256 to see if the hardware is interpolating from the top left of the first pixel to the top left of the last pixel on the scanline. Or, instead if it was interpolating from the first pixel to one past the last pixel. In which case 255 would be incorrect.

#80226 - Payk - Thu Apr 20, 2006 6:36 am

Hey another solucion...Like i said the gab disappears if the naighbour squares have same texture... So each glBind isnt good for "field" of squares... So easy solucion if u want to have one ground with different gfx and no gab:
Use one texture including 16 textures. Then make for that square a function which uses only the wanted part of texture....
For me it runs perfectly...my opinion is also that its a mathicaly problem. But that doesnt mean u cant work arround it hehehe.
The hole ground in my projoect now doesnt have any gaps!! The ground looks like it should and doesnt flicker arround or something: Exactly like it should look like hehe fine for me...

#150384 - TwentySeven - Sun Feb 03, 2008 3:32 pm

I addressed exactly this in another post.. just trying to find it.