Wednesday, May 29, 2013

Loading an external image into Emscripten in ARGB

I have been working on an archery game and was investigating the ability to dynamically load images into the app. Most Emscripten apps are designed where the files are prepackaged into the game - a loading bar shows when starting the app and this loads all of the assets. I could have done it this way as well, but I wanted to explore if I could load an image externally and then bring it into emscripten.

The advantages are at least a couple:
  • I don't need any image loading code - jpeg decoder libraries etc and the image will be loaded in ARGB32 format (Note, I am not using opengl either)
  • I can have dynamic content loaded from another website or service.

So let's begin, if you haven't followed my other posts at this point I have a few handy things setup -
  • I am working on an amazon ami, which builds and publishes to a web folder
  • I wrote a program to combine html, javascript, into emscripten output so that I can run a build script and have all of these pieces combined into the output
The first thing to do is add the javascript and html to your page which will load the image:

<img id="funnyturtle" style="display:none" src="turtle.jpg"/>
<canvas id="imageloader" style="display:none;width:250px;height:200px"></canvas>


<script type='text/javascript'>
setTimeout(loadImage,2000);

var context;
function loadImage()
{
    var elem = document.getElementById('imageloader');
    if (elem && elem.getContext)
    {
        elem.setAttribute('width', '250px'); //###IMPORTANT
        elem.setAttribute('height', '200px');

        context = elem.getContext('2d');
        if (context)
        {
            var img=document.getElementById('funnyturtle');

            var width = 250;
            var height = 200;
           
            context.drawImage(img,0,0, img.width, img.height, 0, 0, width, height);

            var imgd = context.getImageData(0, 0, width, height);

            var numBytes = width * height * 4;
            var ptr= Module._malloc(numBytes);
            var heapBytes= new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes);

            // copy data into heapBytes
            heapBytes.set(new Uint8Array(imgd.data));

            var imageloaded = Module.cwrap('imageloaded', 'number', ['number','number','number'])
            var n = imageloaded(heapBytes.byteOffset, width, height );

            Module._free(heapBytes.byteOffset);
        }
    }
}
</script>


What goes on here is 2 seconds after the page is loaded it will call loadImage(). 2 seconds is because for some reason the javascript for the Emscripten engine is not ready in the normal ready() from jquery. Once loadimage() gets called, we assume that the image is loaded, and basically draw the image to a hidden canvas element. From the hidden canvas we can get the ARGB32 values for the image and call imageLoaded() passing the image rgb values into our c++ code.

Now about the C++ code - one big gotcha is that when you are exporting functions via build options, remember that everything is lopped off unless you add it to your export list. I spent some time figuring out why Module._malloc did not exist and hopefully I can save you some time. So in your build script make sure you have something similar to this:

~/emscripten/emscripten/emcc main.cpp image.cpp game.cpp ShootingScreen.cpp drawing.cpp mainmenu.cpp -o /var/www/html/archery.html -s EXPORTED_FUNCTIONS="['_imageloaded','_main','_malloc','_free']"

From the above, you can see I exported _imageloaded and the other more standard functions. Here is the code in c++ that I have:

extern "C" int imageloaded( char * buffer, int width, int height )
{
    g_game.SetImageLoaded( buffer, width, height );
    return 0;
}


void CGame::SetImageLoaded( char * buffer, int width, int height )
{
    for ( int n = 0; n < IMAGE_CACHE_COUNT; n++ )
    {
        if ( m_images[n] == NULL )
        {
            m_images[n] = new CImage("mike.jpg", buffer, width, height, 200 );
        }
    }
}


CImage::CImage( const char * name, char * imageData, int width, int height, int alpha /*255*/ )
{
    m_imageData = (unsigned int*)malloc( width*height*4 );
    memcpy( m_imageData, imageData, width * height * 4 );

    m_name = strdup( name );
    m_width = width;
    m_height = height;
    m_alpha = alpha;
}


You can see that imageloaded gets called with my rgb32 buffer. I then call into my global game object, to save off the image. To be stupid simple I have an array of CImage* (I haven't gotten std::vector to work yet!). Each image object allocates the size of the image and then makes a copy.

Just for added kicks, here is my routine to draw the image.
void CDrawing::DrawImage( SURFACE & surface, CImage * image, int x, int y )
{
    if ( image->m_alpha == 255 )
    {
        int bufferOffset = x + y * surface.screenWidth;
       
        for ( int n = 0; n < image->m_height; n++ )
        {
            memcpy( surface.buffer + bufferOffset, image->m_imageData + n * image->m_width, image->m_width * 4 );
            bufferOffset += surface.screenWidth;
        }
    }
    else
    {
        unsigned int * src = image->m_imageData;
        unsigned int * dest = surface.buffer + x + y * surface.screenWidth;
       
        double alphaPercent = image->m_alpha / 255.0;
        double destAlpha = 1.0 - alphaPercent;
       
        for ( int n = 0; n < image->m_height; n++ )
        {
            for ( int a = 0; a < image->m_width; a++ )
            {
                *dest = ARGB(   0,
                                (int)(GET_RED(*src) * alphaPercent + GET_RED(*dest) * destAlpha),
                                (int)(GET_GREEN(*src) * alphaPercent + GET_GREEN(*dest) * destAlpha),
                                (int)(GET_BLUE(*src) * alphaPercent + GET_BLUE(*dest) * destAlpha) );
           
                src++;
                dest++;
            }
           
            dest += surface.screenWidth - image->m_width;
        }
    }
}


This just draws the image without stretching, nothing fancy. The alpha blending is split out as it should be slower! That's pretty much all there is too it.

No comments:

Post a Comment