diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 8658aad3327..ffc76aeb5d7 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -103,6 +103,9 @@ jobs: - name: Run material and lighting DrawList visual suite run: xvfb-run -a ctest --test-dir cmake_build_dir_rt \ -L visual-material --output-on-failure -VV + - name: Run text visual suite + run: xvfb-run -a ctest --test-dir cmake_build_dir_rt \ + -L visual-text --output-on-failure -VV - name: Upload artifacts uses: actions/upload-artifact@v7 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d25d0df965..58daf8a3887 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -965,6 +965,8 @@ if(COIN_BUILD_VISUAL_TESTS) "unlit_cube_basic,unlit_transformed_cubes,unlit_vertex_colors,unlit_orthographic_basic,unlit_depth_overlap") set(COIN_MATERIAL_VISUAL_SPECS "cube_basic,camera_persp_basic,camera_ortho_basic,material_emissive,two_lights,point_spot_lights_basic,texture_rgb,texture_luminance_alpha,transparent_overlap,depth_buffer_basic,alpha_test_basic,transparency_depth_basic,transparency_order_basic,specular_shininess_basic") + set(COIN_TEXT_VISUAL_SPECS + "text2_layout_basic") add_test(NAME visual_smoke COMMAND CoinVisualTests snapshot --help) set_tests_properties(visual_smoke PROPERTIES LABELS "visual-smoke") if(COIN_BUILD_LEGACY_GL_RENDERER) @@ -974,6 +976,13 @@ if(COIN_BUILD_VISUAL_TESTS) --artifacts-dir ${CMAKE_BINARY_DIR}/render_artifacts ) set_tests_properties(visual_gl PROPERTIES LABELS "visual-legacy") + add_test( + NAME visual_gl_text + COMMAND CoinVisualTests run + --only ${COIN_TEXT_VISUAL_SPECS} + --artifacts-dir ${CMAKE_BINARY_DIR}/render_artifacts + ) + set_tests_properties(visual_gl_text PROPERTIES LABELS "visual-text") endif() add_test( NAME visual_gl_drawlist_compat diff --git a/include/Inventor/nodes/SoShape.h b/include/Inventor/nodes/SoShape.h index 75d1b372012..f3fed82323d 100644 --- a/include/Inventor/nodes/SoShape.h +++ b/include/Inventor/nodes/SoShape.h @@ -166,6 +166,7 @@ class COIN_DLL_API SoShape : public SoNode { void rayPickBoundingBox(SoRayPickAction * action); friend class soshape_primdata; // internal class friend class so_generate_prim_private; // a very private class + friend class SoShapeGLRender; }; #endif // !COIN_SOSHAPE_H diff --git a/src/shapenodes/SoShape.cpp b/src/shapenodes/SoShape.cpp index 3f05acc4c97..6616e9fa108 100644 --- a/src/shapenodes/SoShape.cpp +++ b/src/shapenodes/SoShape.cpp @@ -51,6 +51,7 @@ class SoVBO; #include #include "elements/SoLazyElementP.h" +#include "shapenodes/SoShapeGLRenderP.h" #include #include @@ -882,76 +883,112 @@ SoShape::getComplexityValue(SoAction * action) */ SbBool SoShape::shouldGLRender(SoGLRenderAction * action) +{ + SoShapeGLRenderContext context; + const SoShapeGLRenderDecision decision = + SoShapeGLRender::begin(this, action, context); + if (decision == SoShapeGLRenderDecision::RenderShape) return TRUE; + if (decision == SoShapeGLRenderDecision::Stop) return FALSE; + if (!SoShapeGLRender::sortTriangles(this, action, context)) return FALSE; + return SoShapeGLRender::finish(this, action, context); +} + +SoShapeGLRenderDecision +SoShapeGLRender::begin(SoShape * shape, SoGLRenderAction * action, + SoShapeGLRenderContext & context) { SoState * state = action->getState(); const SoShapeStyleElement * shapestyle = SoShapeStyleElement::get(state); - unsigned int shapestyleflags = shapestyle->getFlags(); + const unsigned int shapestyleflags = shapestyle->getFlags(); + context.shapeStyleFlags = shapestyleflags; + context.transparent = (shapestyleflags & + (SoShapeStyleElement::TRANSP_TEXTURE | + SoShapeStyleElement::TRANSP_MATERIAL)) != 0; if (shapestyleflags & SoShapeStyleElement::INVISIBLE) - return FALSE; - - if (PRIVATE(this)->bboxcache && !state->isCacheOpen() && !SoCullElement::completelyInside(state)) { - if (PRIVATE(this)->bboxcache->isValid(state)) { - if (SoCullElement::cullTest(state, PRIVATE(this)->bboxcache->getProjectedBox())) { - return FALSE; + return SoShapeGLRenderDecision::Stop; + + if (PRIVATE(shape)->bboxcache && !state->isCacheOpen() && + !SoCullElement::completelyInside(state)) { + if (PRIVATE(shape)->bboxcache->isValid(state)) { + if (SoCullElement::cullTest(state, + PRIVATE(shape)->bboxcache->getProjectedBox())) { + return SoShapeGLRenderDecision::Stop; } } } - SbBool transparent = (shapestyleflags & (SoShapeStyleElement::TRANSP_TEXTURE| - SoShapeStyleElement::TRANSP_MATERIAL)) != 0; - if (shapestyleflags & SoShapeStyleElement::SHADOWMAP) { - if (transparent) return FALSE; + if (context.transparent) return SoShapeGLRenderDecision::Stop; int style = SoShadowStyleElement::get(state); - if (style & SoShadowStyleElement::CASTS_SHADOW) return TRUE; - return FALSE; + if (style & SoShadowStyleElement::CASTS_SHADOW) { + return SoShapeGLRenderDecision::RenderShape; + } + return SoShapeGLRenderDecision::Stop; } - if (action->handleTransparency(transparent)) - return FALSE; + if (action->handleTransparency(context.transparent)) + return SoShapeGLRenderDecision::Stop; if (shapestyleflags & SoShapeStyleElement::BBOXCMPLX) { - this->GLRenderBoundingBox(action); - return FALSE; + shape->GLRenderBoundingBox(action); + return SoShapeGLRenderDecision::Stop; } - // test if we should sort triangles before rendering - if (transparent && (shapestyleflags & SoShapeStyleElement::TRANSP_SORTED_TRIANGLES)) { - // lock since pvcache is shared among all threads - PRIVATE(this)->lock(); - this->validatePVCache(action); + return SoShapeGLRenderDecision::Continue; +} - int arrays = SoPrimitiveVertexCache::NORMAL|SoPrimitiveVertexCache::COLOR; - SoGLMultiTextureImageElement::Model model; - SbColor blendcolor; - SoGLImage * glimage = SoGLMultiTextureImageElement::get(state, 0, model, blendcolor); - if (glimage) arrays |= SoPrimitiveVertexCache::TEXCOORD; +SbBool +SoShapeGLRender::sortTriangles(SoShape * shape, SoGLRenderAction * action, + const SoShapeGLRenderContext & context) +{ + if (!context.transparent || + !(context.shapeStyleFlags & SoShapeStyleElement::TRANSP_SORTED_TRIANGLES)) { + return TRUE; + } - SoMaterialBundle mb(action); - mb.sendFirst(); - PRIVATE(this)->setupShapeHints(this, state); - PRIVATE(this)->pvcache->depthSortTriangles(state); - PRIVATE(this)->pvcache->renderTriangles(state, arrays); - if (PRIVATE(this)->pvcache->getNumLineIndices() || - PRIVATE(this)->pvcache->getNumPointIndices()) { - const SoNormalElement * nelem = SoNormalElement::getInstance(state); - if (nelem->getNum() == 0) { - glPushAttrib(GL_LIGHTING_BIT); - glDisable(GL_LIGHTING); - arrays &= SoPrimitiveVertexCache::NORMAL; - } - PRIVATE(this)->pvcache->renderLines(state, arrays); - PRIVATE(this)->pvcache->renderPoints(state, arrays); + SoState * state = action->getState(); + // lock since pvcache is shared among all threads + PRIVATE(shape)->lock(); + shape->validatePVCache(action); - if (nelem->getNum() == 0) { - glPopAttrib(); - } + int arrays = SoPrimitiveVertexCache::NORMAL|SoPrimitiveVertexCache::COLOR; + SoGLMultiTextureImageElement::Model model; + SbColor blendcolor; + SoGLImage * glimage = SoGLMultiTextureImageElement::get(state, 0, model, blendcolor); + if (glimage) arrays |= SoPrimitiveVertexCache::TEXCOORD; + + SoMaterialBundle mb(action); + mb.sendFirst(); + PRIVATE(shape)->setupShapeHints(shape, state); + PRIVATE(shape)->pvcache->depthSortTriangles(state); + PRIVATE(shape)->pvcache->renderTriangles(state, arrays); + if (PRIVATE(shape)->pvcache->getNumLineIndices() || + PRIVATE(shape)->pvcache->getNumPointIndices()) { + const SoNormalElement * nelem = SoNormalElement::getInstance(state); + if (nelem->getNum() == 0) { + glPushAttrib(GL_LIGHTING_BIT); + glDisable(GL_LIGHTING); + arrays &= SoPrimitiveVertexCache::NORMAL; + } + PRIVATE(shape)->pvcache->renderLines(state, arrays); + PRIVATE(shape)->pvcache->renderPoints(state, arrays); + + if (nelem->getNum() == 0) { + glPopAttrib(); } - PRIVATE(this)->unlock(); - return FALSE; // tell shape _not_ to render } + PRIVATE(shape)->unlock(); + return FALSE; +} + +SbBool +SoShapeGLRender::finish(SoShape * shape, SoGLRenderAction * action, + const SoShapeGLRenderContext & context) +{ + SoState * state = action->getState(); + const unsigned int shapestyleflags = context.shapeStyleFlags; if (shapestyleflags & SoShapeStyleElement::BIGIMAGE) { SoGLMultiTextureImageElement::Model model; @@ -983,10 +1020,10 @@ SoShape::shouldGLRender(SoGLRenderAction * action) soshape_bigtexture * bigtex = soshape_get_bigtexture(shapedata, action->getCacheContext()); shapedata->currentbigtexture = bigtex; bigtex->beginShape(big, SoTextureQualityElement::get(state)); - this->generatePrimitives(action); + shape->generatePrimitives(action); // endShape() returns whether more/less detailed textures need to be // fetched. We force a redraw if this is needed. - if (bigtex->endShape(state, this, mb) == FALSE) { + if (bigtex->endShape(state, shape, mb) == FALSE) { action->getCurPath()->getHead()->touch(); } shapedata->rendermode = NORMAL; @@ -1001,13 +1038,13 @@ SoShape::shouldGLRender(SoGLRenderAction * action) const SoNodeList & lights = SoLightElement::getLights(state); if (lights.getLength()) { // lock since bumprender and pvcache is shared among all threads - PRIVATE(this)->lock(); - if (PRIVATE(this)->bumprender == NULL) { - PRIVATE(this)->bumprender = new soshape_bumprender; + PRIVATE(shape)->lock(); + if (PRIVATE(shape)->bumprender == NULL) { + PRIVATE(shape)->bumprender = new soshape_bumprender; } - this->validatePVCache(action); - if (PRIVATE(this)->pvcache->getNumTriangleIndices() == 0) { - PRIVATE(this)->unlock(); + shape->validatePVCache(action); + if (PRIVATE(shape)->pvcache->getNumTriangleIndices() == 0) { + PRIVATE(shape)->unlock(); return TRUE; } SoGLLazyElement::getInstance(state)->send(state, SoLazyElement::ALL_MASK); @@ -1017,7 +1054,7 @@ SoShape::shouldGLRender(SoGLRenderAction * action) glDisable(GL_LIGHTING); glColor3f(1.0f, 1.0f, 1.0f); - PRIVATE(this)->setupShapeHints(this, state); + PRIVATE(shape)->setupShapeHints(shape, state); const int numlights = lights.getLength(); for (int i = 0; i < numlights; i++) { // fetch matrix that convert the light from its object space @@ -1037,7 +1074,7 @@ SoShape::shouldGLRender(SoGLRenderAction * action) // // FIXME: about the above comment; i don't see any locking...? // -mortene. - PRIVATE(this)->bumprender->renderBump(state, PRIVATE(this)->pvcache, + PRIVATE(shape)->bumprender->renderBump(state, PRIVATE(shape)->pvcache, (SoLight*) lights[i], m); if (i == 0) glEnable(GL_BLEND); @@ -1054,8 +1091,8 @@ SoShape::shouldGLRender(SoGLRenderAction * action) SoLazyElement::DIFFUSE_MASK); SoMaterialBundle mb(action); mb.sendFirst(); - PRIVATE(this)->setupShapeHints(this, state); - PRIVATE(this)->bumprender->renderNormal(state, PRIVATE(this)->pvcache); + PRIVATE(shape)->setupShapeHints(shape, state); + PRIVATE(shape)->bumprender->renderNormal(state, PRIVATE(shape)->pvcache); const SbColor spec = SoLazyElement::getSpecular(state); if (spec[0] != 0 || spec[1] != 0 || spec[2] != 0) { // Is the spec. color black? @@ -1075,7 +1112,7 @@ SoShape::shouldGLRender(SoGLRenderAction * action) SoViewingMatrixElement::get(state); m = m.inverse(); m.multLeft(lm); - PRIVATE(this)->bumprender->renderBumpSpecular(state, PRIVATE(this)->pvcache, + PRIVATE(shape)->bumprender->renderBumpSpecular(state, PRIVATE(shape)->pvcache, (SoLight*) lights[i], m); } } @@ -1083,7 +1120,7 @@ SoShape::shouldGLRender(SoGLRenderAction * action) } - PRIVATE(this)->unlock(); + PRIVATE(shape)->unlock(); glPopAttrib(); // we used two units in the bumpmap code @@ -1100,9 +1137,9 @@ SoShape::shouldGLRender(SoGLRenderAction * action) if (shapestyleflags & SoShapeStyleElement::VERTEXARRAY) { // lock since pvcache is shared among all threads - PRIVATE(this)->lock(); - this->validatePVCache(action); - PRIVATE(this)->unlock(); + PRIVATE(shape)->lock(); + shape->validatePVCache(action); + PRIVATE(shape)->unlock(); SoGLCacheContextElement::shouldAutoCache(state, SoGLCacheContextElement::DONT_AUTO_CACHE); @@ -1113,18 +1150,18 @@ SoShape::shouldGLRender(SoGLRenderAction * action) if (glimage) arrays |= SoPrimitiveVertexCache::TEXCOORD; SoMaterialBundle mb(action); mb.sendFirst(); - PRIVATE(this)->setupShapeHints(this, state); - PRIVATE(this)->pvcache->renderTriangles(state, arrays); - if (PRIVATE(this)->pvcache->getNumLineIndices() || - PRIVATE(this)->pvcache->getNumPointIndices()) { + PRIVATE(shape)->setupShapeHints(shape, state); + PRIVATE(shape)->pvcache->renderTriangles(state, arrays); + if (PRIVATE(shape)->pvcache->getNumLineIndices() || + PRIVATE(shape)->pvcache->getNumPointIndices()) { const SoNormalElement * nelem = SoNormalElement::getInstance(state); if (nelem->getNum() == 0) { glPushAttrib(GL_LIGHTING_BIT); glDisable(GL_LIGHTING); arrays &= SoPrimitiveVertexCache::NORMAL; } - PRIVATE(this)->pvcache->renderLines(state, arrays); - PRIVATE(this)->pvcache->renderPoints(state, arrays); + PRIVATE(shape)->pvcache->renderLines(state, arrays); + PRIVATE(shape)->pvcache->renderPoints(state, arrays); if (nelem->getNum() == 0) { glPopAttrib(); @@ -1138,11 +1175,11 @@ SoShape::shouldGLRender(SoGLRenderAction * action) SoMaterialBundle mb(action); mb.sendFirst(); soshape_get_staticdata()->currentbundle = &mb; // needed in the primitive callbacks - this->generatePrimitives(action); + shape->generatePrimitives(action); return FALSE; #else // generatePrimitives() rendering - if (PRIVATE(this)->rendercnt < ((1<rendercnt++; + if (PRIVATE(shape)->rendercnt < ((1<rendercnt++; } return TRUE; // let the shape node render the geometry using OpenGL #endif // ! generatePrimitives() rendering diff --git a/src/shapenodes/SoShapeGLRenderP.h b/src/shapenodes/SoShapeGLRenderP.h new file mode 100644 index 00000000000..f882319d122 --- /dev/null +++ b/src/shapenodes/SoShapeGLRenderP.h @@ -0,0 +1,40 @@ +// src/shapenodes/SoShapeGLRenderP.h + +#ifndef COIN_SOSHAPEGLRENDERP_H +#define COIN_SOSHAPEGLRENDERP_H + +#include + +class SoGLRenderAction; +class SoShape; + +// This is deliberately source-private. It separates the historical +// SoShape GL-render control flow without adding another installed rendering +// policy API for direct-rendered subclasses. The three phases preserve the +// original statement order around primitive-only transparent sorting. +struct SoShapeGLRenderContext { + unsigned int shapeStyleFlags = 0; + SbBool transparent = FALSE; +}; + +enum class SoShapeGLRenderDecision { + Continue, + RenderShape, + Stop +}; + +class SoShapeGLRender { +public: + // begin() performs common visibility/culling/transparency preflight. + // RenderShape and Stop are terminal decisions; only Continue reaches the + // primitive-sorting seam and then finish(). + static SoShapeGLRenderDecision begin( + SoShape * shape, SoGLRenderAction * action, + SoShapeGLRenderContext & context); + static SbBool sortTriangles(SoShape * shape, SoGLRenderAction * action, + const SoShapeGLRenderContext & context); + static SbBool finish(SoShape * shape, SoGLRenderAction * action, + const SoShapeGLRenderContext & context); +}; + +#endif // COIN_SOSHAPEGLRENDERP_H diff --git a/src/shapenodes/SoText2.cpp b/src/shapenodes/SoText2.cpp index b50057f933e..20ac6e91ccf 100644 --- a/src/shapenodes/SoText2.cpp +++ b/src/shapenodes/SoText2.cpp @@ -101,7 +101,9 @@ #include "coindefs.h" #include +#include #include +#include #ifdef HAVE_CONFIG_H #include @@ -143,6 +145,7 @@ #include "nodes/SoSubNodeP.h" #include "caches/SoGlyphCache.h" +#include "shapenodes/SoShapeGLRenderP.h" // The "lean and mean" define is a workaround for a Cygwin bug: when // windows.h is included _after_ one of the X11 or GLX headers above @@ -214,6 +217,36 @@ Default value is SoText2::LEFT. */ +struct SoTextRasterGlyph { + // Coverage for one glyph in the layer's raster coordinate system. The + // bitmap contains binary or antialiased coverage, never material color. + SbVec2s offset; + SbVec2s size; + const unsigned char * bitmap = NULL; +}; + +struct SoTextRasterResult { + // Shared text preparation keeps the historical binary glBitmap layer and + // antialiased pixel-buffer layer separate. LegacyGL and retained rendering + // consume this same result; execution policy stays in their adapters. + SbVec2f binaryOrigin; + SbVec2f antialiasedOrigin; + SbVec2s antialiasedSize; + SbBool hasAntialiasedGlyphs = FALSE; + std::vector binaryGlyphs; + std::vector antialiasedGlyphs; + + void clear() + { + this->binaryOrigin.setValue(0.0f, 0.0f); + this->antialiasedOrigin.setValue(0.0f, 0.0f); + this->antialiasedSize.setValue(0, 0); + this->hasAntialiasedGlyphs = FALSE; + this->binaryGlyphs.clear(); + this->antialiasedGlyphs.clear(); + } +}; + class SoText2P { public: SoText2P(SoText2 * textnode) : maxwidth(0), master(textnode) @@ -225,6 +258,15 @@ class SoText2P { SbVec3f & v2, SbVec3f & v3); void flushGlyphCache(); void buildGlyphCache(SoState * state); + SbBool getRasterResult(SoState * state, SoTextRasterResult & result); + void fillBinaryPixelBuffer(const SoTextRasterResult & result, + unsigned char red, unsigned char green, + unsigned char blue, unsigned int alpha, + unsigned char * pixels) const; + void fillAntialiasedPixelBuffer(const SoTextRasterResult & result, + unsigned char red, unsigned char green, + unsigned char blue, unsigned int alpha, + unsigned char * pixels) const; SbBool shouldBuildGlyphCache(SoState * state); void dumpBuffer(unsigned char * buffer, SbVec2s size, SbVec2s pos, SbBool mono); void computeBBox(SoAction * action, SbBox3f & box, SbVec3f & center); @@ -239,8 +281,7 @@ class SoText2P { SoGlyphCache * cache; SoFieldSensor * spacingsensor; SoFieldSensor * stringsensor; - unsigned char * pixel_buffer; - int pixel_buffer_size; + std::vector pixel_buffer; static void sensor_cb(void * userdata, SoSensor * COIN_UNUSED_ARG(s)) { SoText2P * thisp = (SoText2P*) userdata; @@ -302,8 +343,6 @@ SoText2::SoText2(void) PRIVATE(this)->spacingsensor->attach(&this->spacing); PRIVATE(this)->spacingsensor->setPriority(0); PRIVATE(this)->cache = NULL; - PRIVATE(this)->pixel_buffer = NULL; - PRIVATE(this)->pixel_buffer_size = 0; } /*! @@ -312,7 +351,6 @@ SoText2::SoText2(void) SoText2::~SoText2() { if (PRIVATE(this)->cache) PRIVATE(this)->cache->unref(); - delete[] PRIVATE(this)->pixel_buffer; delete PRIVATE(this)->stringsensor; delete PRIVATE(this)->spacingsensor; @@ -336,7 +374,15 @@ SoText2::initClass(void) void SoText2::GLRender(SoGLRenderAction * action) { - if (!this->shouldGLRender(action)) return; + // SoText2 is a direct pixel renderer, not a primitive-cache triangle + // producer. Keep the common shape gating and late GL setup, but skip only + // the triangle-sorting operation which has no meaning for this node. + SoShapeGLRenderContext renderContext; + const SoShapeGLRenderDecision decision = + SoShapeGLRender::begin(this, action, renderContext); + if (decision == SoShapeGLRenderDecision::Stop) return; + if (decision == SoShapeGLRenderDecision::Continue && + !SoShapeGLRender::finish(this, action, renderContext)) return; SoState * state = action->getState(); @@ -347,13 +393,17 @@ SoText2::GLRender(SoGLRenderAction * action) PRIVATE(this)->buildGlyphCache(state); SoCacheElement::addCacheDependency(state, PRIVATE(this)->cache); - const cc_font_specification * fontspec = PRIVATE(this)->cache->getCachedFontspec(); - // Render only if bbox not outside cull planes. SbBox3f box; SbVec3f center; PRIVATE(this)->computeBBox(action, box, center); if (!SoCullElement::cullTest(state, box, TRUE)) { + SoTextRasterResult raster; + if (!PRIVATE(this)->getRasterResult(state, raster)) { + PRIVATE(this)->unlock(); + state->pop(); + return; + } SoMaterialBundle mb(action); mb.sendFirst(); SbVec3f nilpoint(0.0f, 0.0f, 0.0f); @@ -367,22 +417,6 @@ SoText2::GLRender(SoGLRenderAction * action) nilpoint[0] = (nilpoint[0] + 1.0f) * 0.5f * vpsize[0]; nilpoint[1] = (nilpoint[1] + 1.0f) * 0.5f * vpsize[1]; - SbVec2s bbsize = PRIVATE(this)->bbox.getSize(); - const SbVec2s& bbmin = PRIVATE(this)->bbox.getMin(); - const SbVec2s& bbmax = PRIVATE(this)->bbox.getMax(); - - float textscreenoffsetx = nilpoint[0]+bbmin[0]; - switch (this->justification.getValue()) { - case SoText2::LEFT: - break; - case SoText2::RIGHT: - textscreenoffsetx = nilpoint[0] + bbmin[0] - PRIVATE(this)->maxwidth; - break; - case SoText2::CENTER: - textscreenoffsetx = (nilpoint[0] + bbmin[0] - PRIVATE(this)->maxwidth / 2.0f); - break; - } - // Set new state. glMatrixMode(GL_MODELVIEW); glPushMatrix(); @@ -393,18 +427,6 @@ SoText2::GLRender(SoGLRenderAction * action) glOrtho(0, vpsize[0], 0, vpsize[1], -1.0f, 1.0f); glPixelStorei(GL_UNPACK_ALIGNMENT,1); - float fontsize = SoFontSizeElement::get(state); - int xpos = 0; - int ypos = 0; - int rasterx, rastery; - int ix=0, iy=0; - int bitmappos[2]; - int bitmapsize[2]; - const unsigned char * buffer = NULL; - cc_glyph2d * prevglyph = NULL; - - const int nrlines = this->string.getNum(); - // get the current diffuse color const SbColor & diffuse = SoLazyElement::getDiffuse(state, 0); unsigned char red = (unsigned char) (diffuse[0] * 255.0f); @@ -420,132 +442,35 @@ SoText2::GLRender(SoGLRenderAction * action) glPushAttrib(GL_ENABLE_BIT | GL_PIXEL_MODE_BIT | GL_COLOR_BUFFER_BIT); glPushClientAttrib(GL_CLIENT_PIXEL_STORE_BIT); - SbBool drawPixelBuffer = FALSE; - - for (int i = 0; i < nrlines; i++) { - SbString str = this->string[i]; - switch (this->justification.getValue()) { - case SoText2::LEFT: - xpos = 0; - break; - case SoText2::RIGHT: - xpos = PRIVATE(this)->maxwidth - PRIVATE(this)->stringwidth[i]; - break; - case SoText2::CENTER: - xpos = (PRIVATE(this)->maxwidth - PRIVATE(this)->stringwidth[i]) / 2; - break; - } - - int kerningx = 0; - int kerningy = 0; - int advancex = 0; - int advancey = 0; - - const char * p = str.getString(); - size_t length = cc_string_utf8_validate_length(p); - - for (unsigned int strcharidx = 0; strcharidx < length; strcharidx++) { - uint32_t glyphidx = 0; - - glyphidx = cc_string_utf8_get_char(p); - p = cc_string_utf8_next_char(p); - - cc_glyph2d * glyph = cc_glyph2d_ref(glyphidx, fontspec, 0.0f); - - buffer = cc_glyph2d_getbitmap(glyph, bitmapsize, bitmappos); - - ix = bitmapsize[0]; - iy = bitmapsize[1]; - - // Advance & Kerning - if (strcharidx > 0) - cc_glyph2d_getkerning(prevglyph, glyph, &kerningx, &kerningy); - cc_glyph2d_getadvance(glyph, &advancex, &advancey); - - rasterx = xpos + kerningx + bitmappos[0]; - rastery = ypos + (bitmappos[1] - bitmapsize[1]); - - if (buffer) { - if (cc_glyph2d_getmono(glyph)) { - SoText2P::setRasterPos3f((float)rasterx + textscreenoffsetx, (float)rastery + (int)nilpoint[1], -nilpoint[2]); - glBitmap(ix,iy,0,0,0,0,(const GLubyte *)buffer); - } - else { - if (!drawPixelBuffer) { - int numpixels = bbsize[0] * bbsize[1]; - if (numpixels > PRIVATE(this)->pixel_buffer_size) { - delete[] PRIVATE(this)->pixel_buffer; - PRIVATE(this)->pixel_buffer = new unsigned char[numpixels*4]; - PRIVATE(this)->pixel_buffer_size = numpixels; - } - memset(PRIVATE(this)->pixel_buffer, 0, numpixels * 4); - drawPixelBuffer = TRUE; - } - - int memx = rasterx - bbmin[0]; - int memy = bbsize[1] - (bbmax[1] - rastery - 1) - 1; - - if (memx >= 0 && memx + bitmapsize[0] <= bbsize[0] && - memy >= 0 && memy + bitmapsize[1] <= bbsize[1]) { - - unsigned char * dst = PRIVATE(this)->pixel_buffer + (memy * bbsize[0] + memx) * 4; - const unsigned char * src = buffer; - int nextlineoffset = (bbsize[0] - bitmapsize[0]) * 4; - - // Ouch. This must lead to pretty slow rendering - for (int y = 0; y < iy; y++) { - for (int x = 0; x < ix; x++) { - *dst++ = red; *dst++ = green; *dst++ = blue; - // alpha from the gray level pixel value, blended with current value (because glyph bitmaps can overlap) - int srcval = *src; - int oldval = *dst; - *dst = ((oldval * (256 - srcval) + alpha * srcval) >> 8); - src++; dst++; - } - dst += nextlineoffset; - } - } else { - static SbBool once = TRUE; - if (once) { - SoDebugError::post("SoText2::GLRender", - "Unable to copy glyph to memory buffer. Position [%d,%d], size [%d,%d], buffer size [%d,%d]", - memx, memy, bitmapsize[0], bitmapsize[1], bbsize[0], bbsize[1]); - once = FALSE; - } - } - } - } - - xpos += (advancex + kerningx); - - if (prevglyph) { - // should be safe to unref here. SoGlyphCache will have a - // ref'ed instance - cc_glyph2d_unref(prevglyph); - } - prevglyph = glyph; - } - - ypos -= (int)(((int) fontsize) * this->spacing.getValue()); + for (const SoTextRasterGlyph & glyph : raster.binaryGlyphs) { + SoText2P::setRasterPos3f( + raster.binaryOrigin[0] + glyph.offset[0], + raster.binaryOrigin[1] + glyph.offset[1], + -nilpoint[2]); + glBitmap(glyph.size[0], glyph.size[1], 0, 0, 0, 0, + static_cast(glyph.bitmap)); } - if (prevglyph) { - // should be safe to unref here. SoGlyphCache will have a ref'ed - // instance - cc_glyph2d_unref(prevglyph); - } - - if (drawPixelBuffer) { + if (raster.hasAntialiasedGlyphs) { + const size_t pixelBytes = + static_cast(raster.antialiasedSize[0]) * + static_cast(raster.antialiasedSize[1]) * 4; + PRIVATE(this)->pixel_buffer.resize(pixelBytes); + PRIVATE(this)->fillAntialiasedPixelBuffer( + raster, red, green, blue, alpha, + PRIVATE(this)->pixel_buffer.data()); glEnable(GL_ALPHA_TEST); glAlphaFunc(GL_GREATER, 0.3f); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - - rastery = (int)floor(nilpoint[1]+0.5) - bbsize[1] + bbmax[1]; - - SoText2P::setRasterPos3f((GLfloat)floor(textscreenoffsetx+0.5), (GLfloat)rastery, -nilpoint[2]); - glDrawPixels(bbsize[0], bbsize[1], GL_RGBA, GL_UNSIGNED_BYTE, (const GLubyte *)PRIVATE(this)->pixel_buffer); + SoText2P::setRasterPos3f(raster.antialiasedOrigin[0], + raster.antialiasedOrigin[1], + -nilpoint[2]); + glDrawPixels(raster.antialiasedSize[0], raster.antialiasedSize[1], GL_RGBA, + GL_UNSIGNED_BYTE, + static_cast( + PRIVATE(this)->pixel_buffer.data())); } // pop old state @@ -797,6 +722,199 @@ SoText2P::getQuad(SoState * state, SbVec3f & v0, SbVec3f & v1, return TRUE; } +SbBool +SoText2P::getRasterResult(SoState * state, SoTextRasterResult & result) +{ + result.clear(); + if (!state) return FALSE; + + this->buildGlyphCache(state); + + const SbVec2s size = this->bbox.getSize(); + if (size[0] <= 0 || size[1] <= 0) return FALSE; + + SbVec3f nilpoint(0.0f, 0.0f, 0.0f); + const SbMatrix & model = SoModelMatrixElement::get(state); + const SbMatrix projection = model * SoViewingMatrixElement::get(state) * + SoProjectionMatrixElement::get(state); + projection.multVecMatrix(nilpoint, nilpoint); + + const SbVec2s viewportSize = + SoViewportRegionElement::get(state).getViewportSizePixels(); + const float screenx = (nilpoint[0] + 1.0f) * 0.5f * viewportSize[0]; + const float screeny = (nilpoint[1] + 1.0f) * 0.5f * viewportSize[1]; + const SbVec2s & bbmin = this->bbox.getMin(); + const SbVec2s & bbmax = this->bbox.getMax(); + + float textscreenoffsetx = screenx + bbmin[0]; + switch (PUBLIC(this)->justification.getValue()) { + case SoText2::RIGHT: + textscreenoffsetx = screenx + bbmin[0] - this->maxwidth; + break; + case SoText2::CENTER: + textscreenoffsetx = screenx + bbmin[0] - this->maxwidth / 2.0f; + break; + case SoText2::LEFT: + default: + break; + } + + result.binaryOrigin.setValue( + textscreenoffsetx + static_cast(bbmin[0]), + static_cast(static_cast(screeny)) - size[1] + bbmax[1]); + result.antialiasedOrigin.setValue( + std::floor(textscreenoffsetx + 0.5f), + std::floor(screeny + 0.5f) - size[1] + bbmax[1]); + result.antialiasedSize = size; + const cc_font_specification * fontspec = this->cache->getCachedFontspec(); + const int lineCount = PUBLIC(this)->string.getNum(); + for (int line = 0; line < lineCount; ++line) { + int lineOffset = 0; + switch (PUBLIC(this)->justification.getValue()) { + case SoText2::RIGHT: + lineOffset = this->maxwidth - this->stringwidth[line]; + break; + case SoText2::CENTER: + lineOffset = (this->maxwidth - this->stringwidth[line]) / 2; + break; + case SoText2::LEFT: + default: + break; + } + + const char * p = PUBLIC(this)->string[line].getString(); + const size_t length = cc_string_utf8_validate_length(p); + const int positionCount = this->positions[line].getLength(); + for (size_t character = 0; + character < length && character < static_cast(positionCount); + ++character) { + const uint32_t glyphIndex = cc_string_utf8_get_char(p); + p = cc_string_utf8_next_char(p); + cc_glyph2d * glyph = cc_glyph2d_ref(glyphIndex, fontspec, 0.0f); + if (!glyph) continue; + + int bitmapSize[2]; + int bitmapOffset[2]; + const unsigned char * bitmap = cc_glyph2d_getbitmap( + glyph, bitmapSize, bitmapOffset); + if (bitmap) { + const SbVec2s & position = + this->positions[line][static_cast(character)]; + const int rasterX = lineOffset + position[0]; + const int offsetX = rasterX - bbmin[0]; + const int offsetY = size[1] - bbmax[1] + position[1]; + const bool mono = cc_glyph2d_getmono(glyph) != 0; + if (mono) { + SoTextRasterGlyph rasterGlyph; + rasterGlyph.offset.setValue(static_cast(offsetX), + static_cast(offsetY)); + rasterGlyph.size.setValue(static_cast(bitmapSize[0]), + static_cast(bitmapSize[1])); + rasterGlyph.bitmap = bitmap; + result.binaryGlyphs.push_back(rasterGlyph); + } + else { + result.hasAntialiasedGlyphs = TRUE; + if (offsetX >= 0 && offsetY >= 0 && + offsetX + bitmapSize[0] <= size[0] && + offsetY + bitmapSize[1] <= size[1]) { + SoTextRasterGlyph rasterGlyph; + rasterGlyph.offset.setValue(static_cast(offsetX), + static_cast(offsetY)); + rasterGlyph.size.setValue(static_cast(bitmapSize[0]), + static_cast(bitmapSize[1])); + rasterGlyph.bitmap = bitmap; + result.antialiasedGlyphs.push_back(rasterGlyph); + } + else { + static SbBool once = TRUE; + if (once) { + SoDebugError::post( + "SoText2::GLRender", + "Unable to copy glyph to memory buffer. Position [%d,%d], size [%d,%d], buffer size [%d,%d]", + offsetX, offsetY, bitmapSize[0], bitmapSize[1], + size[0], size[1]); + once = FALSE; + } + } + } + } + cc_glyph2d_unref(glyph); + } + } + return TRUE; +} + +void +SoText2P::fillBinaryPixelBuffer(const SoTextRasterResult & result, + const unsigned char red, + const unsigned char green, + const unsigned char blue, + const unsigned int alpha, + unsigned char * pixels) const +{ + if (!pixels || result.antialiasedSize[0] <= 0 || result.antialiasedSize[1] <= 0) return; + const size_t pixelCount = static_cast(result.antialiasedSize[0]) * + static_cast(result.antialiasedSize[1]); + std::memset(pixels, 0, pixelCount * 4); + + for (const SoTextRasterGlyph & glyph : result.binaryGlyphs) { + const size_t rowBytes = static_cast(glyph.size[0] + 7) / 8; + for (int y = 0; y < glyph.size[1]; ++y) { + for (int x = 0; x < glyph.size[0]; ++x) { + if (!glyph.bitmap || + !(glyph.bitmap[static_cast(y) * rowBytes + x / 8] & + (0x80u >> (x & 7)))) continue; + const int dstx = glyph.offset[0] + x; + const int dsty = glyph.offset[1] + y; + if (dstx < 0 || dstx >= result.antialiasedSize[0] || + dsty < 0 || dsty >= result.antialiasedSize[1]) continue; + unsigned char * pixel = pixels + + (static_cast(dsty) * result.antialiasedSize[0] + dstx) * 4; + pixel[0] = red; + pixel[1] = green; + pixel[2] = blue; + pixel[3] = static_cast((alpha * 255u) >> 8); + } + } + } +} + +void +SoText2P::fillAntialiasedPixelBuffer(const SoTextRasterResult & result, + const unsigned char red, + const unsigned char green, + const unsigned char blue, + const unsigned int alpha, + unsigned char * pixels) const +{ + if (!pixels || result.antialiasedSize[0] <= 0 || result.antialiasedSize[1] <= 0) return; + const size_t pixelCount = static_cast(result.antialiasedSize[0]) * + static_cast(result.antialiasedSize[1]); + std::memset(pixels, 0, pixelCount * 4); + for (const SoTextRasterGlyph & glyph : result.antialiasedGlyphs) { + for (int y = 0; y < glyph.size[1]; ++y) { + for (int x = 0; x < glyph.size[0]; ++x) { + const int dstx = glyph.offset[0] + x; + const int dsty = glyph.offset[1] + y; + if (dstx < 0 || dstx >= result.antialiasedSize[0] || + dsty < 0 || dsty >= result.antialiasedSize[1]) continue; + unsigned char * pixel = pixels + + (static_cast(dsty) * result.antialiasedSize[0] + dstx) * 4; + pixel[0] = red; + pixel[1] = green; + pixel[2] = blue; + if (!glyph.bitmap) continue; + const unsigned int sourceAlpha = glyph.bitmap[ + static_cast(y) * glyph.size[0] + x]; + const unsigned int oldAlpha = pixel[3]; + pixel[3] = static_cast( + (oldAlpha * (256u - sourceAlpha) + alpha * sourceAlpha) >> 8); + } + } + } +} + // Debug convenience method. void SoText2P::dumpBuffer(unsigned char * buffer, SbVec2s size, SbVec2s pos, SbBool mono) diff --git a/testsuite/CMakeLists.txt b/testsuite/CMakeLists.txt index 88d125d8cde..8e717c05337 100644 --- a/testsuite/CMakeLists.txt +++ b/testsuite/CMakeLists.txt @@ -268,6 +268,14 @@ if(COIN_BUILD_GL_TESTS_EFFECTIVE) coin_add_gl_test(GLSLRuntimeTest GLSLRuntimeTest.cpp) coin_add_gl_test(GLSLDiagnosticsTest GLSLDiagnosticsTest.cpp) coin_add_gl_test(OffscreenReadbackTest OffscreenReadbackTest.cpp) + if(COIN_BUILD_LEGACY_GL_RENDERER) + coin_add_gl_test(SoText2RasterLayoutTest + SoText2RasterLayoutTest.cpp) + coin_add_gl_test(LegacyDirectRasterTransparencyTest + LegacyDirectRasterTransparencyTest.cpp) + coin_add_gl_test(LegacyShapeGatingOrderTest + LegacyShapeGatingOrderTest.cpp) + endif() endif() # Many warnings are generated from test macros on macOS with Xcode. diff --git a/testsuite/LegacyDirectRasterTransparencyTest.cpp b/testsuite/LegacyDirectRasterTransparencyTest.cpp new file mode 100644 index 00000000000..d03e4be3c00 --- /dev/null +++ b/testsuite/LegacyDirectRasterTransparencyTest.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include + +#include "support/GLTestContext.h" + +#include +#include +#include + +namespace { + +int +skip(const char * reason) +{ + std::cout << "SKIP: " << reason << std::endl; + return 77; +} + +} + +static int +runTest(GLTestContext & context) +{ + SoSeparator * root = new SoSeparator; + root->ref(); + + SoOrthographicCamera * camera = new SoOrthographicCamera; + camera->position = SbVec3f(0.0f, 0.0f, 1.0f); + camera->height = 2.0f; + root->addChild(camera); + + SoMaterial * material = new SoMaterial; + material->diffuseColor = SbColor(1.0f, 1.0f, 1.0f); + material->transparency = 0.5f; + root->addChild(material); + + SoText2 * text = new SoText2; + text->string = "Coin"; + root->addChild(text); + + context.bindFramebuffer(); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + SoGLRenderAction action(SbViewportRegion(64, 64)); + action.setCacheContext(context.contextId()); + action.setTransparencyType( + SoGLRenderAction::SORTED_OBJECT_SORTED_TRIANGLE_BLEND); + action.apply(root); + const std::vector pixels = context.readPixels(); + bool sawText = false; + for (size_t i = 0; i < pixels.size(); i += 4) { + if (pixels[i] > 20 || pixels[i + 1] > 20 || pixels[i + 2] > 20) { + sawText = true; + break; + } + } + + root->unref(); + + if (!sawText) { + std::cerr << "FAIL: direct-rendered transparent SoText2 was suppressed by " + << "sorted primitive transparency" << std::endl; + return 1; + } + return 0; +} + +int +main() +{ + SoDB::init(); + GLTestContextConfig config; + config.profile = GLTestProfile::Compatibility; + config.major = 3; + config.minor = 3; + config.width = 64; + config.height = 64; + GLTestContext context; + if (!context.initialize(config)) { + SoDB::finish(); + return skip("LegacyGL compatibility context is unavailable"); + } + const int result = runTest(context); + SoDB::finish(); + return result; +} diff --git a/testsuite/LegacyShapeGatingOrderTest.cpp b/testsuite/LegacyShapeGatingOrderTest.cpp new file mode 100644 index 00000000000..0c9bd47086b --- /dev/null +++ b/testsuite/LegacyShapeGatingOrderTest.cpp @@ -0,0 +1,178 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "support/GLTestContext.h" + +#include +#include +#include + +namespace { + +class ShadowMapGatingShape : public SoShape { + SO_NODE_HEADER(ShadowMapGatingShape); + +public: + static void initClass(void); + ShadowMapGatingShape(void); + + SbBool rendered = FALSE; + + void GLRender(SoGLRenderAction * action) override { + SoShapeStyleElement::setShadowMapRendering(action->getState(), TRUE); + SoShadowStyleElement::set(action->getState(), + SoShadowStyleElement::CASTS_SHADOW); + this->rendered = this->shouldGLRender(action); + } + +protected: + ~ShadowMapGatingShape() override {} + void generatePrimitives(SoAction *) override {} + void computeBBox(SoAction *, + SbBox3f & box, SbVec3f & center) override { + box.makeEmpty(); + center.setValue(0.0f, 0.0f, 0.0f); + } +}; + +SO_NODE_SOURCE(ShadowMapGatingShape); + +void +ShadowMapGatingShape::initClass(void) +{ + SO_NODE_INIT_CLASS(ShadowMapGatingShape, SoShape, "SoShape"); +} + +ShadowMapGatingShape::ShadowMapGatingShape(void) +{ + SO_NODE_CONSTRUCTOR(ShadowMapGatingShape); +} + +int +skip(const char * reason) +{ + std::cout << "SKIP: " << reason << std::endl; + return 77; +} + +int +testShadowMapDecision(GLTestContext & context) +{ + ShadowMapGatingShape::initClass(); + + SoSeparator * root = new SoSeparator; + root->ref(); + ShadowMapGatingShape * shape = new ShadowMapGatingShape; + root->addChild(shape); + + context.bindFramebuffer(); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + SoGLRenderAction action(SbViewportRegion(8, 8)); + action.setCacheContext(context.contextId()); + action.apply(root); + + const SbBool rendered = shape->rendered; + root->unref(); + + if (!rendered) { + std::cerr << "FAIL: shadow-map casting shape was stopped before rendering" + << std::endl; + return 1; + } + return 0; +} + +} + +static int +runTest(GLTestContext & context) +{ + const int shadowResult = testShadowMapDecision(context); + + SoSeparator * root = new SoSeparator; + root->ref(); + + SoOrthographicCamera * camera = new SoOrthographicCamera; + camera->position = SbVec3f(0.0f, 0.0f, 1.0f); + camera->height = 2.0f; + root->addChild(camera); + + SoMaterial * material = new SoMaterial; + material->diffuseColor = SbColor(1.0f, 0.0f, 0.0f); + material->transparency = 0.5f; + root->addChild(material); + + SoLightModel * lightModel = new SoLightModel; + lightModel->model = SoLightModel::BASE_COLOR; + root->addChild(lightModel); + + SoCoordinate3 * coordinates = new SoCoordinate3; + coordinates->point.set1Value(0, SbVec3f(-0.7f, -0.7f, 0.0f)); + coordinates->point.set1Value(1, SbVec3f(0.7f, -0.7f, 0.0f)); + coordinates->point.set1Value(2, SbVec3f(0.0f, 0.7f, 0.0f)); + root->addChild(coordinates); + SoFaceSet * faceSet = new SoFaceSet; + faceSet->numVertices.set1Value(0, 3); + root->addChild(faceSet); + + context.bindFramebuffer(); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + SoGLRenderAction action(SbViewportRegion(64, 64)); + action.setCacheContext(context.contextId()); + action.setTransparencyType( + SoGLRenderAction::SORTED_OBJECT_SORTED_TRIANGLE_BLEND); + action.apply(root); + const std::vector pixels = context.readPixels(); + bool sawTransparentShape = false; + for (size_t i = 0; i < pixels.size(); i += 4) { + if (pixels[i] > 20 && pixels[i] > pixels[i + 1] + 10 && + pixels[i] > pixels[i + 2] + 10) { + sawTransparentShape = true; + break; + } + } + + root->unref(); + + if (!sawTransparentShape) { + std::cerr << "FAIL: transparent ordinary geometry did not survive the " + << "sorted-triangle gating seam" << std::endl; + return 1; + } + return shadowResult; +} + +int +main() +{ + SoDB::init(); + GLTestContextConfig config; + config.profile = GLTestProfile::Compatibility; + config.major = 3; + config.minor = 3; + config.width = 64; + config.height = 64; + GLTestContext context; + if (!context.initialize(config)) { + SoDB::finish(); + return skip("LegacyGL compatibility context is unavailable"); + } + const int result = runTest(context); + SoDB::finish(); + return result; +} diff --git a/testsuite/SoText2RasterLayoutTest.cpp b/testsuite/SoText2RasterLayoutTest.cpp new file mode 100644 index 00000000000..cc1e72b6939 --- /dev/null +++ b/testsuite/SoText2RasterLayoutTest.cpp @@ -0,0 +1,120 @@ +#include +#include "support/GLTestContext.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +struct Bounds { + int minx; + int maxx; +}; + +bool +renderText(GLTestContext & context, SoText2::Justification justification, + Bounds & bounds) +{ + SoSeparator * root = new SoSeparator; + root->ref(); + + SoOrthographicCamera * camera = new SoOrthographicCamera; + camera->position = SbVec3f(0.0f, 0.0f, 1.0f); + camera->height = 2.0f; + root->addChild(camera); + + SoMaterial * material = new SoMaterial; + material->diffuseColor = SbColor(1.0f, 1.0f, 1.0f); + root->addChild(material); + + SoText2 * text = new SoText2; + text->string.set1Value(0, "WIDE LINE"); + text->string.set1Value(1, "i"); + text->justification = justification; + root->addChild(text); + + context.bindFramebuffer(); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + SoGLRenderAction action(SbViewportRegion(128, 64)); + action.setCacheContext(context.contextId()); + action.apply(root); + const std::vector pixels = context.readPixels(); + bounds.minx = 128; + bounds.maxx = -1; + for (int y = 0; y < 64; ++y) { + for (int x = 0; x < 128; ++x) { + const uint8_t * pixel = &pixels[static_cast(y * 128 + x) * 4]; + if (pixel[0] > 20 || pixel[1] > 20 || pixel[2] > 20) { + bounds.minx = std::min(bounds.minx, x); + bounds.maxx = std::max(bounds.maxx, x); + } + } + } + + root->unref(); + return bounds.maxx >= bounds.minx; +} + +} + +int +main() +{ + SoDB::init(); + + GLTestContextConfig config; + config.profile = GLTestProfile::Compatibility; + config.major = 3; + config.minor = 3; + config.width = 128; + config.height = 64; + GLTestContext context; + if (!context.initialize(config)) { + SoDB::finish(); + return 77; + } + + Bounds left; + Bounds center; + Bounds right; + const bool rendered = + renderText(context, SoText2::LEFT, left) && + renderText(context, SoText2::CENTER, center) && + renderText(context, SoText2::RIGHT, right); + + if (!rendered) { + std::cout << "SKIP: LegacyGL compatibility rendering is unavailable" << std::endl; + SoDB::finish(); + return 77; + } + + const int leftWidth = left.maxx - left.minx; + const int centerWidth = center.maxx - center.minx; + const int rightWidth = right.maxx - right.minx; + const bool sameWidth = + std::abs(leftWidth - centerWidth) <= 2 && + std::abs(centerWidth - rightWidth) <= 2; + const bool aligned = + left.minx > center.minx + 5 && + center.minx > right.minx + 5; + + if (!sameWidth || !aligned) { + std::cerr << "FAIL: multiline SoText2 justification is inconsistent" + << " (left=" << left.minx << ", center=" << center.minx + << ", right=" << right.minx << ")" << std::endl; + SoDB::finish(); + return 1; + } + + SoDB::finish(); + return 0; +} diff --git a/testsuite/render/baselines/gl/text2_layout_basic.png b/testsuite/render/baselines/gl/text2_layout_basic.png new file mode 100644 index 00000000000..926cbd2b9fb Binary files /dev/null and b/testsuite/render/baselines/gl/text2_layout_basic.png differ diff --git a/testsuite/render/scenes/text2_layout_basic.iv b/testsuite/render/scenes/text2_layout_basic.iv new file mode 100644 index 00000000000..8604943c511 --- /dev/null +++ b/testsuite/render/scenes/text2_layout_basic.iv @@ -0,0 +1,38 @@ +#Inventor V2.1 ascii + +Separator { + OrthographicCamera { + position 0 0 5 + height 4.5 + nearDistance 0.1 + farDistance 20 + } + LightModel { model BASE_COLOR } + + Separator { + Translation { translation -1.6 1.2 0 } + Material { diffuseColor 0.95 0.25 0.15 } + Text2 { + string [ "LEFT ALIGNED", "short" ] + justification LEFT + } + } + + Separator { + Translation { translation 0.0 0.0 0 } + Material { diffuseColor 0.15 0.85 0.35 } + Text2 { + string [ "CENTER ALIGNED", "short" ] + justification CENTER + } + } + + Separator { + Translation { translation 1.6 -1.2 0 } + Material { diffuseColor 0.2 0.45 0.95 } + Text2 { + string [ "RIGHT ALIGNED", "short" ] + justification RIGHT + } + } +} diff --git a/testsuite/render/specs/text2_layout_basic.yml b/testsuite/render/specs/text2_layout_basic.yml new file mode 100644 index 00000000000..a54b599f2e9 --- /dev/null +++ b/testsuite/render/specs/text2_layout_basic.yml @@ -0,0 +1,8 @@ +id: text2_layout_basic +scene: ../scenes/text2_layout_basic.iv + +viewport: + background: [0.08, 0.08, 0.08, 1.0] + +baseline: ../baselines/gl/text2_layout_basic.png +compare: relaxed