This tutorial illustrates the loading and use of resources such as textures and vertex buffers in 2D drawing using default shaders. Examples include drawing solid primitives, images and subimages, clipping, scaling, and offscreen rendering. You are expected to have read the previous tutorial, "Hello world!"; the topics covered there will not be repeated. The source code for the image viewer is in tut/rgliv. It is built by default when you build GLERI, and can be run as tut/rgliv/rgliv test/pgcat.jpg. Please read the heavily commented source for full details on the image viewer operations. This document explains only the GLERI-specific parts.

Resource Loading

Buffers, textures, fonts, datapaks, shaders, and framebuffers are collectively called resources and can be loaded using Load calls in PRGL. There are three versions - first to load directly from data you specify by pointer, second to load from file, third to load from a datapak. (Datapaks are not covered in this tutorial, they are .cpio.gz archives you can send over in a single call. Handy for aggregating all your resources together for faster loading)

When connecting to gleris over a TCP socket, the resource data is always sent over the wire. When a local socket is used, files are sent by directly passing their file descriptors. This ensures that you do not suffer the copy penalty when running locally. Optimizing an application for best network performance can be tricky, and this image viewer does not really try. Generating thumbnails locally and keeping them stored somewhere in .cache can be good optimizations to avoid excessive network traffic.

void CImageViewer::OnInit (void)
{
    CWindow::OnInit();
    Open ("GLERI Image Viewer", WinInfo (0,0,800,600,0,0x33,0,WinInfo::MSAA_OFF,WinInfo::type_Normal,WinInfo::state_Fullscreen));
    TexParameter (G::Texture::MIN_FILTER, G::Texture::LINEAR);
    TexParameter (G::Texture::MAG_FILTER, G::Texture::LINEAR);
    static const coord_t c_Vertices[] = {
        VGEN_TSRECT (0,0, c_EntryWidth,c_EntryWidth),
        0,2, 0,15, 15,15, 15,2, 8,2, 7,0, 1,0,
        0,0, 0,15, 15,15, 15,2, 13,0, 13,2, 15,2, 13,0
    };
    _vertices = BufferData (G::ARRAY_BUFFER, c_Vertices, sizeof(c_Vertices));
    _thumbsDepth = CreateDepthTexture (c_CacheWidth, c_CacheHeight);
    _thumbs = CreateTexture (G::TEXTURE_2D, c_CacheWidth, c_CacheHeight, 0, G::Pixel::RGBA);
    _thumbsFb = CreateFramebuffer (_thumbsDepth, _thumbs);
    _caching.ImageH = _caching.ImageW = 16;
    for (_caching.ThumbIndex = 0; _caching.ThumbIndex < 2; ++_caching.ThumbIndex)
        DrawThumbCache (_thumbsFb);
    LoadEntry();
}

You can load resources only after you open the target window in OnInit. In this example the long form of Open is used in order to make the viewer window fullscreen for most comfortable viewing. Other parameters of note are the OpenGL version, here set to 0x33,0 meaning minimum GL 3.3, with no maximum version. GLERI always requires at least 3.3, core profile, so you would set a higher version if you use its features, such as tesselation shaders. A maximum version can be set to avoid forward compatibility issues.

TexParameter, like its OpenGL equivalent, sets various parameters for textures. For the image viewer it is nice to enable linear filtering to smooth the images when zooming.

The first resource loaded is a vertex buffer containing coordinates for primitives. Three primitives are specified in a single buffer, for efficiency. The first is the selection rectangle, specified in 4 vertices generated by the VGEN_TSRECT macro. A macro is used because specifying a primitive in OpenGL can be rather tricky.

First remember that points must be specified in a counter-clockwise direction. Even though you are only drawing in 2D, OpenGL treats all primitives as 3D, and will see a clockwise polygon as facing backward. Backward-facing polygons are not drawn.

The same set of vertices will render differently depending on what type of primitive you are drawing. Here a triangle strip will be used, a filled primitive, so you must specify the far boundary point one past the pixel you intend to fill, taking into account that the last row and column of pixels will not be rendered. If this were rendered as a line strip, the line will go directly through the vertices.

The final complication is that OpenGL uses a left handed coordinate system with y increasing upward from the bottom of the screen, while pretty much everybody else wants y to increase downward from the top. GLERI inverts the y by default, putting 0,0 in the top left corner. Unfortunately, when filling primitives, the "far edge" is determined by OpenGL to be the top one, so that when you draw a triangle strip through points 0,0, 0,10, 10,0, 10,10, you will actually get a 9x9 square at 0,1.

And that's why there are the VGEN macros for defining rectangular areas for line loops, triangle strips, and triangle fans - VGEN_LLRECT, VGEN_TSRECT, and VGEN_TFRECT, taking position and dimensions, and emitting the correct vertex sequence for each. If only life were simpler...

The other two primitives are line loops for drawing default entry icons, one line in 7 points for the folder icon, and one line in 8 points for the file icon. These are drawn in the folder view when thumbnails are unavailable.

The second resource created is the thumbnail cache texture and the framebuffer used to draw onto it. Note that both the color and depth texture are required, even though only 2D rendering is done. The color texture is specified as RGBA to make the border around the thumbnail transparent.

Then, the two default thumbnails are rendered into slots 0 and 1. DrawThumbCache is defined similarly to the regular OnDraw, with a double-called template, so macros are provided for this purpose. DRAWFBDECL(ThumbCache); is placed in the class declaration, and DRAWFBIMPL(CImageViewer,ThumbCache) {} is used to define the body. Drawing is done just as in OnDraw, through the drw object.

_loadingImg = LoadTexture (G::TEXTURE_2D, CurEntry().Name());

Finally, LoadEntry is called to load the initial image or folder. LoadEntry calls LoadTexture as above with the image filename. When gleris finishes loading the texture, it will send a resource info back to the client.

Resource Info Events

void CImageViewer::OnTextureInfo (goid_t tid, const G::Texture::Header& ih)
{
    CWindow::OnTextureInfo (tid, ih);
    if (tid == _loadingImg) {
        _iw = ih.w; _ih = ih.h;
        if (_img != G::GoidNull)
            FreeTexture (_img);
        _img = _loadingImg;
        _loadingImg = G::GoidNull;
        _view = ImageView;
    } else if (tid == _caching.Image) {
        _caching.ImageW = ih.w; _caching.ImageH = ih.h;
        if (_caching.FileIndex < _files.size()) {
            DrawThumbCache (_thumbsFb);
            _files[_caching.FileIndex].SetThumbIndex (_caching.ThumbIndex);
        }
        FreeTexture (_caching.Image);
        _caching.Image = G::GoidNull;
        BeginThumbUpdate();
    }
    OnKey ('m');
}

When a resource is loaded, gleris sends a resource info event to client. Here is the the texture info handler. G::Texture::Header is defined in gleri/gldefs.h. Here only the image dimensions are of interest. A separate texture object is used to keep the image currently being loaded to avoid flicker when changing images.

Here you also see the receipt of the image loaded for thumbnailing. DrawThumbCache is then called with parameters in _caching to generate the thumbnail, followed by freeing the image and continuing thumbnailing.

Drawing

ONDRAWIMPL(CImageViewer)::OnDraw (Drw& drw) const
{
    CWindow::OnDraw (drw);
    if (_view == ImageView) {
        drw.Clear (color_ImageViewBackground);
        if (_img != G::GoidNull) {
            drw.Scale (_iscale, _iscale);
            drw.Image (_ix/_iscale, _iy/_iscale, _img);
        }
    } else {
        drw.Clear (color_FolderViewBackground);
        const coord_t filenameX = Font()->Width(), filenameY = c_EntryHeight-Font()->Height()*5/2;
        drw.Color (color_FolderViewText);
        drw.VertexPointer (_vertices);
        for (unsigned y = 0, ie = _firstentry; y <= Info().h-c_EntryHeight; y += c_EntryHeight) {
            for (unsigned x = 0; x <= Info().w-c_EntryWidth; ++ie, x += c_EntryWidth) {
                drw.Viewport (x,y,c_EntryWidth,c_EntryHeight);
                if (ie >= _files.size())
                    return;
                if (ie == _selection) {
                    drw.Color (color_FolderViewSelection);
                    drw.TriangleStrip (0, 4);
                    drw.Color (color_FolderViewText);
                }
                uint8_t thumbIndex = _files[ie].ThumbIndex();
                drw.Sprite (c_ThumbX, c_ThumbY, _thumbs, CacheThumbX (thumbIndex), CacheThumbY (thumbIndex), c_ThumbWidth, c_ThumbHeight);
                const char* filename = _files[ie].Name();
                drw.Text (max (filenameX, (c_EntryWidth-Font()->Width(filename))/2), filenameY, _files[ie].Name());
            }
        }
    }
}

Here a variety of drawing methods is illustrated. First is the simple image drawing with the Image call that draws the full image at given coordinates. The transformation matrix for the default shaders can be manipulated by the Scale and Offset calls to stretch and move objects on the screen. Here such scaling is used to zoom into the image.

Both the image view and the folder thumbnail views are drawn, switched on _view. In the folder view, thumbnail and filename are drawn for each entry. The thumbnail is drawn with the Sprite call, that takes the screen position, texture, and the area from the texture to draw. The thumbnail cache is a single large texture with all the thumbnails in a grid, and the appropriate area is selected from it. Text is then drawn centered under the thumbnail, just like in the previous tutorial.

To keep the text from overflowing outside the entry box the Viewport is set to the entry rectangle. This offsets all primitives to the viewport as well as clips them to its edges.

The bright selection rectangle is drawn using a triangle strip defined in the _vertices buffer. To draw it you must first bind a vertex buffer with VertexPointer, and then call the appropriate primitive function. TriangleStrip(0,4) means "draw a triangle strip using 4 vertices, starting at vertex 0, from the currently selected vertex buffer".

Both VertexPointer and TriangleStrip are convenience functions for working with default shaders. For your own shaders you would set parameters manually with the Parameter call that is the equivalent of glVertexAttribPointer, and then use DrawArrays and friends to render a specified primitive type. TriangleStrip(0,4) is equivalent to DrawArrays(G::TRIANGLE_STRIP,0,4), defined because this is what you use most of the time for 2D drawing. Drawing by index with DrawElements, instancing, and indirect buffers are advanced OpenGL topics. Please consult OpenGL tutorials on the appropriate uses for those.

DRAWFBIMPL(CImageViewer,ThumbCache)
{
    drw.VertexPointer (_vertices);
    drw.Viewport (CacheThumbX (_caching.ThumbIndex), CacheThumbY (_caching.ThumbIndex), c_ThumbWidth, c_ThumbHeight);
    drw.Clear (color_ThumbBackground);
    drw.Color (color_FolderViewText);
    float scale = min (float(c_ThumbWidth)/_caching.ImageW, float(c_ThumbHeight)/_caching.ImageH);
    drw.Scale (scale, scale);
    if (_caching.ThumbIndex == CFolderEntry::Folder)
        drw.LineLoop (4, 7);
    else if (_caching.ThumbIndex == CFolderEntry::Image)
        drw.LineLoop (11, 8);
    else if (_caching.Image != G::GoidNull)
        drw.Image ((c_ThumbWidth/scale-_caching.ImageW)/2,
                   (c_ThumbWidth/scale-_caching.ImageH)/2,
                   _caching.Image);
}

Offscreen drawing is implemented exactly like the onscreen OnDraw, but with a different suffix on the names. The standard draw command creation code is also generated by macros, with the above code implementing the DrawThumbCache call and the OnDrawThumbCache template containing drawing code.

Here offscreen rendering is used to draw thumbnails onto the cache texture. Default thumbnails are drawn as line loops onto the first two slots. Just as in the OnDraw above, the primitives are drawn with VertexPointer/LineLoop, scaling up from 16x16, and the loaded image is drawn, scaling down to thumb size.

OnDraw draws to a double-buffered surface, so each call must render an entire frame before the buffers are swapped. With offscreen rendering, incremental drawing is possible. Here, only one thumbnail is drawn per call.