Skip to content
Justin Gasper edited this page Apr 24, 2017 · 1 revision

Changes in the _topcoder_graphics_and_text_updates branch

This document covers the changes we've implemented in the branch to support Pixi.js features that weren't originally available in pxscene.

The changes affect three main areas at the current time:

  • Graphics primitive drawing (rectangles, circles)
  • Rich text support
  • Animation optimisations

Risks:

  • All features are currently only implemented under the OpenGL context.

Pixi.js testing:

Graphics primitives

The graphics primitive changes revolve around pxRectangle.cpp, pxGraphic.cpp, and pxContextGL.cpp

  • Simple rectangles and circles use the pxRectangle object to render.
  • Poloygon and lines use the pxGraphic object to render.

Example:

  1. Here's where we expanded pxScene2d to include support for rect and graphic

      // pxScene2d.cpp line:1420
      if (!strcmp("rect",t.cString()))
        e = createRectangle(p,o);
      ...
      else if (!strcmp("graphic",t.cString()))
        e = createGraphic(p,o);
    
  2. When graphics objects are created , pxscene2d will invoke its draw method, and those draw methods invoke opengl context methods. To support the graphics render , we updated the drawRect method to support radius and outline, and added drawLines and drawPolygon methods.

  • The drawRect method, pxContextGL.cpp line:2490, draw rectangle with circle and outline.

     if (fillColor != NULL && fillColor[3] > 0.0) // with non-transparent color
           {
    
             float half = lineWidth / 2;
             if (radius > 0)
             {
               drawRectWithRounded(half, half, w - lineWidth, h - lineWidth, fillColor, radius);
             }
             else
             {
               drawRect2(half, half, w - lineWidth, h - lineWidth, fillColor);
             }
           }
    
           // Frame ...
           if (lineColor != NULL && lineColor[3] > 0.0 && lineWidth > 0) // with non-transparent color and non-zero stroke
           {
             drawRectOutline(0, 0, w, h, lineWidth, lineColor, radius, false);
           }
  • The drawLines method, pxContextGL.cpp line:2627, Draw lines with width and color.

     void pxContext::drawLines(GLfloat* verts, int count, float* lineColor, float lineWidth)
         {
           float colorPM[4];
           premultiply(colorPM, lineColor);
           GLfloat* newVerts = (GLfloat*) malloc(count * 4 * sizeof(GLfloat));
           int vertIndex = 0;
           // ... calculator lines verts
           gSolidShader->draw(gResW, gResH, gMatrix.data(), gAlpha, GL_TRIANGLE_STRIP, newVerts, vertIndex / 2, colorPM);
           // ... free some mem
         }
    • The drawPolygon method , pxContextGL.cpp line:2660 , It will first calculator the outline verts and draw it if outline width > 0 and alpha > 0, then draw solid polygon.
       if (lineWidth > 0 && lineColor[4] > 0)  // need draw outline
         {  
           //... calculater the outline verts
           gSolidShader->draw(gResW, gResH, gMatrix.data(), gAlpha, GL_TRIANGLE_STRIP, outLineVerts, outlineIndex / 2,
                              colorPM);
           //... free some mem
         }
      
         if (fillColor[4] > 0) // draw solid polygon
         {  
           premultiply(colorPM,fillColor);
           gSolidShader->draw(gResW,gResH,gMatrix.data(),gAlpha,GL_TRIANGLES,verts,count,colorPM);
         }

Text features

These changes focus on:

* pxFont
* pxText
* pxTextBox

We use the Freetype2 library to read TTF font files and generate textures using different parameters, and then the opengl context uses this texture and shader renders it. We added 4 shaders in pxContextGL.cpp to support those features:

* fATextureShaderText
* fTextOutlineShaderText
* fTextureBlurShaderText
* fTextureBlurForOutlineShaderText
  • FontFamily name feature. We added this feature to support simple use of font family names that match font file names. For instance , if we put a TTF file, like Arial.ttf and put it in fonts/Arial.ttf, when pixi.js uses the fontFamliy name Arial , pxScene will find it in the local folder first. This is to better match general HTML font specifications like pixi.js might expect.

     // pxFont.cpp line:921
     FILE* fp = nullptr;
     string urlString = string(url);
     string newLocalTTF = string("fonts/") + urlString;
     char* ttf = strchr(url, '.ttf');
     if (ttf == NULL)
     {
       newLocalTTF.append(".ttf");
     }
    
     FontMap::iterator it = mFontMap.find(newLocalTTF.c_str());
     if (it != mFontMap.end())  // local key search in map
     {   
       rtLogDebug("Found pxFont in map for %s\n", url);
       pFont = it->second;
       return pFont;
     }
     else
     {
       fp = fopen(newLocalTTF.c_str(), "rb");
       rtLogInfo("start find local font = %s.", newLocalTTF.c_str());
       if (fp != nullptr) {
         rtLogInfo("found font %s success.", newLocalTTF.c_str());
         url = newLocalTTF.c_str();
         fclose(fp);
       }
       else
       {
         rtLogInfo("Can not find the font = %s.", newLocalTTF.c_str());
       }
     }
    

Font properties

For stroke , bold , italic , shadow , gradient, we added the PX_TEXTURE_ALPHA_88 texture type to store the stroke data - For bold and stroke, first we use freetype generate FT_Face object , then use applyBold to add bold data and stroke data. ```cpp // pxFont.cpp line:366 rtError pxFont::applyBold(uint32_t &offsetX, uint32_t &offsetY) { FT_Pos xBold, yBold;

	  if (mBold)
	  {
	    uint32_t k = (uint32_t)(mPixelSize*BOLD_ADD_RATE+1);
	    k = (k%2)?(k+1):(k); // will add px
	    xBold = k * 64;
	    yBold = xBold * BOLD_ADD_RATE_YX;
	  }
	  else
	  {
	    xBold = 0;
	    yBold = 0;
	  }

	  FT_GlyphSlot g = mFace->glyph;
	  if (g->format == FT_GLYPH_FORMAT_OUTLINE)
	  {
	    FT_BBox oldBox;
	    FT_Outline_Get_CBox(&(g->outline), &oldBox);
	    FT_Outline_EmboldenXY(&(g->outline), xBold, yBold);
	    FT_BBox newBox;
	    FT_Outline_Get_CBox(&(g->outline), &newBox);
	    xBold = (newBox.xMax - newBox.xMin) - (oldBox.xMax - oldBox.xMin);
	    yBold = (newBox.yMax - newBox.yMin) - (oldBox.yMax - oldBox.yMin);
	    offsetX = xBold;
	    offsetY = yBold;
	  }
	  else if (g->format == FT_GLYPH_FORMAT_BITMAP)
	  {
	    FT_Bitmap_Embolden(ft, &(g->bitmap), xBold, yBold);
	    offsetX = xBold;
	    offsetY = yBold;
	  }
	  return RT_OK;
	}
	```
- Italic and stroke  are same as *bold and stroke * , at *pxFont.cpp applyItalic line:405*.
- Shadow is the last step in generate texture flow.  In this step we extend the texture size.
	```cpp
	// pxFont.cpp line:429
	unsigned char* pxFont::applyShadow(GlyphCacheEntry* entry, unsigned char* data, uint32_t &outW, uint32_t &outH)
	{  
	  if ((mShadow or mOutlineSize > 0 ) && outW > 0 && outH > 0 )
	  {
	    uint32_t externPixels = 0;
	    if (mShadow)
	    {
	      externPixels = mShadowBlurRadio;
	    }
	    else
	    {
	      externPixels = 4;
	    }

	    uint32_t realOutW = outW + externPixels * 2;
	    uint32_t realOutH = outH + externPixels * 2;
	    long index, index2;
	    uint32_t bitSize = 1;
	    if (mOutlineSize > 0)
	    {
	      bitSize = 2;
	    }
	    unsigned char* blendImage = (unsigned char *) malloc(sizeof(unsigned char ) * realOutW * realOutH * bitSize);

	    if (!blendImage) {
	      return data;
	    }
	    memset(blendImage, 0, realOutW * realOutH * bitSize);
	    uint32_t px = externPixels;
	    uint32_t py = externPixels;
	    for (long x = 0; x < outW; ++x)
	    {
	      for (long y = 0; y < outH; ++y)
	      {
	        index = px + x + ((py + y) * realOutW);
	        index2 = x + (y * outW);
	        if (bitSize == 2 )
	        {
	          blendImage[2*index] = data[2*index2];
	          blendImage[2*index+1] = data[2*index2+1];
	        }
	        else{
	          blendImage[index] = data[index2];
	        }
	      }
	    }

	    outW = realOutW;
	    outH = realOutH;
	    entry->bitmap_top += externPixels;
	    return blendImage;
	  }
	  return data;
	}
	```
- Gradient :  this feature is supported by the shader. We pass two colors and u_gradientY , u_gradientHeight to calculate the gradient .
	```cpp
	  //shader, pxContextGL.cpp line:191
	  "{"
	  "  vec4 col;"
	  "  float lp = u_gradientY + v_uv.y * u_gradientHeight;"
	  "  col = u_dirColor * (1.0-lp) + a_color*lp;"
	  "  float a = u_alpha *  texture2D(s_texture, v_uv).a;"
	  "  gl_FragColor = col * a;"
	  "}";

	```

Performance enhancement for pixi.js:

This problem was caused by the pixi.js ticker function. The original one used setTimeout as ticker , but it's not accurate in high FPS mode(60 fps), so we now use the pxScene2d update function to driver the ticker function. The final work flow is pxScene2d onUpdate -> pixi.js ticker -> pixi.js update render object transforms -> pxScene2d render objects -> next pxScene2d onUpdate.

  1. We emit onUpdate event to root pxObject.
      // pxScene2d.cpp line:1724
      // send onUpdate event to pixi.js
      rtObjectRef e = new rtMapObject;
      mEmit.send("onUpdate", e);
    
  2. Inject pxScene root to *PXSceneHelper *.
    // autoDetectRenderer.js line:27
    if (utils.isV8())
    {
        const PXSceneHelper = require('./utils/PXSceneHelper').default; // eslint-disable-line global-require
        PXSceneHelper.getInstance().injectPxScene(options.view);
        return new PXSceneRenderer(width, height, options);
    }
  3. Use onUpdate event to driver ticker function.
    	/**
         * request Animation Frame , save callback
         * @param  {Function} callback the ticker callback
         * @return {number}   the animation frame id
         */
        requestAnimationFrame = (callback) =>
        {
            if (typeof callback !== 'function')
            {
                throw new TypeError(`${callback}is not a function`);
            }
            const animationFrameId = frameFunctionIndex++;
    
            frameFunctionMap[animationFrameId] = callback;
    
            return animationFrameId;
        };
    
        /**
         * cancel the animation frame callback
         * @param  {number} id the animation frame id
         */
        cancelAnimationFrame = (id) =>
        {
            frameFunctionMap[id] = null;
        };
    
        // pxscene onUpdate function
        PXSceneHelper.getInstance().getPxScene().on('onUpdate', () =>
        {
            lastTime = new Date().getTime();
            tmpFrameFunctionMap = frameFunctionMap; // save functions
            frameFunctionMap = {};  // clear map use for requestAnimationFrame
            for (const index in tmpFrameFunctionMap)
            {
                if (tmpFrameFunctionMap && tmpFrameFunctionMap[index])
                {
                    tmpFrameFunctionMap[index](lastTime);
                } // ticker
            }
            tmpFrameFunctionMap = null; // release old map
        });