-
Notifications
You must be signed in to change notification settings - Fork 20
Graphics updates
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
- All features are currently only implemented under the OpenGL context.
- The pixi.js changes can be found in this repository https://github.com/topcoderinc/pxPixi
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.
-
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);
-
When graphics objects are created , pxscene2d will invoke its
draw
method, and thosedraw
methods invoke opengl context methods. To support the graphics render , we updated thedrawRect
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); }
-
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.
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 nameArial
, 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()); } }
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;"
"}";
```
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
.
- 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);
- 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); }
- 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 });