From 710adfd6989c2895ad205d794e67630d163026ad Mon Sep 17 00:00:00 2001 From: Robert Osfield Date: Tue, 27 Jun 2006 13:09:00 +0000 Subject: [PATCH] From Eric Wing, added support for outline/shadow and colour gradient effects. --- include/osgText/Text | 141 +++++++- src/osgText/Text.cpp | 834 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 956 insertions(+), 19 deletions(-) diff --git a/include/osgText/Text b/include/osgText/Text index 1ffeae8ea..10d177aa1 100644 --- a/include/osgText/Text +++ b/include/osgText/Text @@ -211,13 +211,124 @@ public: unsigned int getDrawMode() const { return _drawMode; } + + enum BackdropType + { + DROP_SHADOW_BOTTOM_RIGHT = 0, // usually the type of shadow you see + DROP_SHADOW_CENTER_RIGHT, + DROP_SHADOW_TOP_RIGHT, + DROP_SHADOW_BOTTOM_CENTER, + DROP_SHADOW_TOP_CENTER, + DROP_SHADOW_BOTTOM_LEFT, + DROP_SHADOW_CENTER_LEFT, + DROP_SHADOW_TOP_LEFT, + OUTLINE, + NONE + }; + + /** + * BackdropType gives you a background shadow text behind your regular + * text. This helps give text extra contrast which can be useful when + * placing text against noisy backgrounds. + * The color of the background shadow text is specified by setBackdropColor(). + * DROP_SHADOW_BOTTOM_RIGHT will draw backdrop text to the right and down of + * the normal text. Other DROW_SHADOW_* modes do the same for their repective directions. + * OUTLINE will draw backdrop text so that it appears the text has an outline + * or border around the normal text. This mode is particularly useful against + * really noisy backgrounds that may put text on top of things that have + * all types of colors which you don't have control over. + * Some real world examples of this general technique in use that I know of + * are Google Earth, Sid Meier's Pirates (2004 Remake), and Star Control 2 (PC 1993). + * The default is NONE. + */ + void setBackdropType(BackdropType type); + + BackdropType getBackdropType() const { return _backdropType; } + + /** + * Sets the amount text is offset to create the backdrop/shadow effect. + * Set the value too high and for example, in OUTLINE mode you will get a "Brady Bunch" + * effect where you see duplicates of the text in a 3x3 grid. + * Set the value too small and you won't see anything. + * The values represent percentages. 1.0 means 100% so a value of 1.0 + * in DROW_SHADOW_LEFT_CENTER mode would cause each glyph to be echoed + * next to it self. So the letter 'e' might look like 'ee'. + * Good values tend to be in the 0.03 to 0.10 range (but will be subject + * to your specific font and display characteristics). + * Note that the text bounding boxes are updated to include backdrop offsets. + * However, other metric information such as getCharacterHeight() are unaffected + * by this. This means that individual glyph spacing (kerning?) are unchanged + * even when this mode is used. + * The default is 0.07 (7% offset). + */ + void setBackdropOffset(float offset = 0.07f); + /** + * This overloaded version lets you specify the offset for the horizontal + * and vertical components separately. + */ + void setBackdropOffset(float horizontal, float vertical); + + float getBackdropHorizontalOffet() const { return _backdropHorizontalOffset; } + + float getBackdropVerticalOffset() const { return _backdropVerticalOffset; } + + /** + * This specifies the color of the backdrop text. + * The default is black. + */ + void setBackdropColor(const osg::Vec4& color); + + const osg::Vec4& getBackdropColor() const { return _backdropColor; } + + + enum ColorGradientMode + { + SOLID = 0, // a.k.a. ColorGradients off + PER_CHARACTER, + OVERALL + }; + + /** + * This sets different types of text coloring modes. + * When the coloring mode is not set to SOLID, the + * colors specified in setColorGradientCorners() determine + * the colors for the text. + * When the gradient mode is OVERALL, the coloring scheme + * attempts to approximate the effect as if the entire text box/region + * were a single polygon and you had applied colors to each of the four + * corners with GL_SMOOTH enabled. In this mode, OpenGL interpolates + * the colors across the polygon, and this is what OVERALL tries to + * emulate. This can be used to give nice embellishments on things + * like logos and names. + * PER_CHARACTER is similar to OVERALL except that it applies the + * color interpolation to the four corners of each character instead + * of across the overall text box. + * The default is SOLID (a.k.a. off). + */ + void setColorGradientMode(ColorGradientMode mode); + + ColorGradientMode getColorGradientMode() const { return _colorGradientMode; } + + /** + * Used only for gradient mode, let's you specify the colors of the 4 corners. + * If ColorGradients are off, these values are ignored (and the value from setColor() + * is the only one that is relevant. + */ + void setColorGradientCorners(const osg::Vec4& topLeft, const osg::Vec4& bottomLeft, const osg::Vec4& bottomRight, const osg::Vec4& topRight); + + const osg::Vec4& getColorGradientTopLeft() const { return _colorGradientTopLeft; } + const osg::Vec4& getColorGradientBottomLeft() const { return _colorGradientBottomLeft; } + const osg::Vec4& getColorGradientBottomRight() const { return _colorGradientBottomRight; } + const osg::Vec4& getColorGradientTopRight() const { return _colorGradientTopRight; } + + void setKerningType(KerningType kerningType) { _kerningType = kerningType; } KerningType getKerningType() const { return _kerningType; } /** Get the number of wrapped lines - only valid after computeGlyphRepresentation() has been called, returns 0 otherwise */ unsigned int getLineCount() const { return _lineCount; } - + /** Draw the text.*/ virtual void drawImplementation(osg::State& state) const; @@ -254,6 +365,7 @@ public: typedef std::vector Coords2; typedef std::vector Coords3; typedef std::vector TexCoords; + typedef std::vector ColorCoords; Glyphs _glyphs; Coords2 _coords; @@ -261,6 +373,9 @@ public: TexCoords _texcoords; LineNumbers _lineNumbers; + osg::buffered_object _transformedBackdropCoords[8]; + ColorCoords _colorCoords; + Glyphs getGlyphs() { return _glyphs; } const Glyphs getGlyphs() const { return _glyphs; } @@ -272,11 +387,11 @@ public: TexCoords& getTexCoords() { return _texcoords; } const TexCoords& getTexCoords() const { return _texcoords; } - + LineNumbers& getLineNumbers() { return _lineNumbers; } const LineNumbers& getLineNumbers() const { return _lineNumbers; } }; - + typedef std::map,GlyphQuads> TextureGlyphQuadMap; /** Direct Access to GlyphQuads */ @@ -355,7 +470,27 @@ protected: void computePositions(); void computePositions(unsigned int contextID) const; + + void computeBackdropPositions(unsigned int contextID) const; + void computeColorGradients() const; + void computeColorGradientsOverall() const; + void computeColorGradientsPerCharacter() const; + BackdropType _backdropType; + float _backdropHorizontalOffset; + float _backdropVerticalOffset; + osg::Vec4 _backdropColor; + + ColorGradientMode _colorGradientMode; + osg::Vec4 _colorGradientTopLeft; + osg::Vec4 _colorGradientBottomLeft; + osg::Vec4 _colorGradientBottomRight; + osg::Vec4 _colorGradientTopRight; + + // Helper functions for color interpolation + float bilinearInterpolate(float x1, float x2, float y1, float y2, float x, float y, float q11, float q12, float q21, float q22) const; + void convertHsvToRgb( float hsv[], float rgb[] ) const; + void convertRgbToHsv( float rgb[], float hsv[] ) const; }; } diff --git a/src/osgText/Text.cpp b/src/osgText/Text.cpp index b85622b9f..043f1bc56 100644 --- a/src/osgText/Text.cpp +++ b/src/osgText/Text.cpp @@ -42,7 +42,16 @@ Text::Text(): _color(1.0f,1.0f,1.0f,1.0f), _drawMode(TEXT), _kerningType(KERNING_DEFAULT), - _lineCount(0) + _lineCount(0), + _backdropType(NONE), + _backdropHorizontalOffset(0.07f), + _backdropVerticalOffset(0.07f), + _backdropColor(0.0f, 0.0f, 0.0f, 1.0f), + _colorGradientMode(SOLID), + _colorGradientTopLeft(1.0f, 0.0f, 0.0f, 1.0f), + _colorGradientBottomLeft(0.0f, 1.0f, 0.0f, 1.0f), + _colorGradientBottomRight(0.0f, 0.0f, 1.0f, 1.0f), + _colorGradientTopRight(1.0f, 1.0f, 1.0f, 1.0f) { setUseDisplayList(false); setSupportsDisplayList(false); @@ -693,6 +702,7 @@ void Text::computeGlyphRepresentation() setStateSet(const_cast((*_textureGlyphQuadMap.begin()).first.get())); computePositions(); + computeColorGradients(); } void Text::computePositions() @@ -706,7 +716,6 @@ void Text::computePositions() void Text::computePositions(unsigned int contextID) const { - switch(_alignment) { case LEFT_TOP: _offset.set(_textBB.xMin(),_textBB.yMax(),_textBB.zMin()); break; @@ -730,8 +739,6 @@ void Text::computePositions(unsigned int contextID) const case RIGHT_BOTTOM_BASE_LINE: _offset.set(_textBB.xMax(),-_characterHeight*(_lineCount-1),0.0f); break; } - - AutoTransformCache& atc = _autoTransformCache[contextID]; osg::Matrix& matrix = atc._matrix; @@ -809,11 +816,9 @@ void Text::computePositions(unsigned int contextID) const } - if (_autoRotateToScreen) { matrix.postMult(rotate_matrix); - } if (!_rotation.zeroRotation() ) @@ -834,8 +839,6 @@ void Text::computePositions(unsigned int contextID) const matrix.makeTranslate(_position-_offset); } - - // now apply matrix to the glyphs. for(TextureGlyphQuadMap::iterator titr=_textureGlyphQuadMap.begin(); titr!=_textureGlyphQuadMap.end(); @@ -857,12 +860,508 @@ void Text::computePositions(unsigned int contextID) const } } + computeBackdropPositions(contextID); + _normal = osg::Matrix::transform3x3(osg::Vec3(0.0f,0.0f,1.0f),matrix); _normal.normalize(); const_cast(this)->dirtyBound(); } +// Presumes the atc matrix is already up-to-date +void Text::computeBackdropPositions(unsigned int contextID) const +{ + if(_backdropType == NONE) + { + return; + } + + float width = 0.0f; + float height = 0.0f; + float running_width = 0.0f; + float running_height = 0.0f; + float avg_width = 0.0f; + float avg_height = 0.0f; + int counter = 0; + unsigned int i; + AutoTransformCache& atc = _autoTransformCache[contextID]; + osg::Matrix& matrix = atc._matrix; + + // This section is going to try to compute the average width and height + // for a character among the text. The reason I shift by an + // average amount per-character instead of shifting each character + // by its per-instance amount is because it may look strange to see + // the individual backdrop text letters not space themselves the same + // way the foreground text does. Using one value gives uniformity. + for(TextureGlyphQuadMap::const_iterator const_titr=_textureGlyphQuadMap.begin(); + const_titr!=_textureGlyphQuadMap.end(); + ++const_titr) + { + const GlyphQuads& glyphquad = const_titr->second; + const GlyphQuads::Coords2& coords2 = glyphquad._coords; + for(i = 0; i < coords2.size(); i+=4) + { + width = coords2[i+2].x() - coords2[i].x(); + height = coords2[i].y() - coords2[i+1].y(); + + running_width += width; + running_height += height; + counter++; + } + } + avg_width = running_width/counter; + avg_height = running_height/counter; + + // now apply matrix to the glyphs. + for(TextureGlyphQuadMap::iterator titr=_textureGlyphQuadMap.begin(); + titr!=_textureGlyphQuadMap.end(); + ++titr) + { + GlyphQuads& glyphquad = titr->second; + GlyphQuads::Coords2& coords2 = glyphquad._coords; + + unsigned int backdrop_index; + unsigned int max_backdrop_index; + if(_backdropType == OUTLINE) + { + // For outline, we want to draw the in every direction + backdrop_index = 0; + max_backdrop_index = 8; + } + else + { + // Yes, this may seem a little strange, + // but since the code is using references, + // I would have to duplicate the following code twice + // for each part of the if/else because I can't + // declare a reference without setting it immediately + // and it wouldn't survive the scope. + // So it happens that the _backdropType value matches + // the index in the array I want to store the coordinates + // in. So I'll just setup the for-loop so it only does + // the one direction I'm interested in. + backdrop_index = _backdropType; + max_backdrop_index = _backdropType+1; + } + for( ; backdrop_index < max_backdrop_index; backdrop_index++) + { + GlyphQuads::Coords3& transformedCoords = glyphquad._transformedBackdropCoords[backdrop_index][contextID]; + unsigned int numCoords = coords2.size(); + if (numCoords!=transformedCoords.size()) + { + transformedCoords.resize(numCoords); + } + + for(i=0;isecond; + const GlyphQuads::Coords2& coords2 = glyphquad._coords; + + for(i=0;i max_x) + { + max_x = coords2[i].x(); + } + if(coords2[i].x() < min_x) + { + min_x = coords2[i].x(); + } + if(coords2[i].y() > max_y) + { + max_y = coords2[i].y(); + } + if(coords2[i].y() < min_y) + { + min_y = coords2[i].y(); + } + + } + } + + rgb_q11[0] = _colorGradientBottomLeft[0]; + rgb_q11[1] = _colorGradientBottomLeft[1]; + rgb_q11[2] = _colorGradientBottomLeft[2]; + + rgb_q12[0] = _colorGradientTopLeft[0]; + rgb_q12[1] = _colorGradientTopLeft[1]; + rgb_q12[2] = _colorGradientTopLeft[2]; + + rgb_q21[0] = _colorGradientBottomRight[0]; + rgb_q21[1] = _colorGradientBottomRight[1]; + rgb_q21[2] = _colorGradientBottomRight[2]; + + rgb_q22[0] = _colorGradientTopRight[0]; + rgb_q22[1] = _colorGradientTopRight[1]; + rgb_q22[2] = _colorGradientTopRight[2]; + + // for linear interpolation to look correct + // for colors and imitate what OpenGL does, + // we need to convert over to Hue-Saturation-Value + // and linear interpolate in that space. + // HSV will interpolate through the color spectrum. + // Now that I think about this, perhaps we could + // extend this to use function pointers or something + // so users may specify their own color interpolation + // scales such as Intensity, or Heated Metal, etc. + convertRgbToHsv(rgb_q11, hsv_q11); + convertRgbToHsv(rgb_q12, hsv_q12); + convertRgbToHsv(rgb_q21, hsv_q21); + convertRgbToHsv(rgb_q22, hsv_q22); + + for(TextureGlyphQuadMap::iterator titr=_textureGlyphQuadMap.begin(); + titr!=_textureGlyphQuadMap.end(); + ++titr) + { + GlyphQuads& glyphquad = titr->second; + GlyphQuads::Coords2& coords2 = glyphquad._coords; + GlyphQuads::ColorCoords& colorCoords = glyphquad._colorCoords; + + unsigned int numCoords = coords2.size(); + if (numCoords!=colorCoords.size()) + { + colorCoords.resize(numCoords); + } + + for(i=0;isecond; + GlyphQuads::Coords2& coords2 = glyphquad._coords; + GlyphQuads::ColorCoords& colorCoords = glyphquad._colorCoords; + + unsigned int numCoords = coords2.size(); + if (numCoords!=colorCoords.size()) + { + colorCoords.resize(numCoords); + } + + for(unsigned int i=0;isecond; + // For backdrop text + if(_backdropType != NONE) + { + unsigned int backdrop_index; + unsigned int max_backdrop_index; + if(_backdropType == OUTLINE) + { + backdrop_index = 0; + max_backdrop_index = 8; + } + else + { + backdrop_index = _backdropType; + max_backdrop_index = _backdropType+1; + } + + state.setTexCoordPointer( 0, 2, GL_FLOAT, 0, &(glyphquad._texcoords.front())); + state.disableColorPointer(); + glColor4fv(_backdropColor.ptr()); + + for( ; backdrop_index < max_backdrop_index; backdrop_index++) + { + const GlyphQuads::Coords3& transformedBackdropCoords = glyphquad._transformedBackdropCoords[backdrop_index][contextID]; + if (!transformedBackdropCoords.empty()) + { + state.setVertexPointer( 3, GL_FLOAT, 0, &(transformedBackdropCoords.front())); + glPolygonOffset(-1.0f, -1.0f * (backdrop_index+1) ); + glDrawArrays(GL_QUADS,0,transformedBackdropCoords.size()); + } + } + } // end of backdrop text + const GlyphQuads::Coords3& transformedCoords = glyphquad._transformedCoords[contextID]; if (!transformedCoords.empty()) { state.setVertexPointer( 3, GL_FLOAT, 0, &(transformedCoords.front())); state.setTexCoordPointer( 0, 2, GL_FLOAT, 0, &(glyphquad._texcoords.front())); + if(_colorGradientMode == SOLID) + { + state.disableColorPointer(); + glColor4fv(_color.ptr()); + } + else + { + state.setColorPointer( 4, GL_FLOAT, 0, &(glyphquad._colorCoords.front())); + } + + if(_backdropType != NONE) + { + // Make sure that the main (foreground) text is on top + glPolygonOffset(-10, -10); + } glDrawArrays(GL_QUADS,0,transformedCoords.size()); + glPolygonOffset(0,0); } } + + if(_backdropType != NONE) + { + glPopAttrib(); + } } if (_drawMode & BOUNDINGBOX) { - + if (_textBB.valid()) { state.applyTextureMode(0,GL_TEXTURE_2D,osg::StateAttribute::OFF); - + const osg::Matrix& matrix = _autoTransformCache[contextID]._matrix; osg::Vec3 c00(osg::Vec3(_textBB.xMin(),_textBB.yMin(),_textBB.zMin())*matrix); @@ -1007,9 +1564,6 @@ void Text::drawImplementation(osg::State& state) const glEnd(); } - - -// glPopMatrix(); } void Text::accept(osg::Drawable::ConstAttributeFunctor& af) const @@ -1047,3 +1601,251 @@ void Text::releaseGLObjects(osg::State* state) const Drawable::releaseGLObjects(state); if (_font.valid()) _font->releaseGLObjects(state); } + + + +void Text::setBackdropType(BackdropType type) +{ + if (_backdropType==type) return; + + _backdropType = type; + computeGlyphRepresentation(); +} + +void Text::setBackdropOffset(float offset) +{ + _backdropHorizontalOffset = offset; + _backdropVerticalOffset = offset; + computeGlyphRepresentation(); +} + +void Text::setBackdropOffset(float horizontal, float vertical) +{ + _backdropHorizontalOffset = horizontal; + _backdropVerticalOffset = vertical; + computeGlyphRepresentation(); +} + +void Text::setBackdropColor(const osg::Vec4& color) +{ + _backdropColor = color; + computeGlyphRepresentation(); +} + +void Text::setColorGradientMode(ColorGradientMode mode) +{ + if (_colorGradientMode==mode) return; + + _colorGradientMode = mode; + computeGlyphRepresentation(); +} + +void Text::setColorGradientCorners(const osg::Vec4& topLeft, const osg::Vec4& bottomLeft, const osg::Vec4& bottomRight, const osg::Vec4& topRight) +{ + _colorGradientTopLeft = topLeft; + _colorGradientBottomLeft = bottomLeft; + _colorGradientBottomRight = bottomRight; + _colorGradientTopRight = topRight; + computeGlyphRepresentation(); +} + +// Formula for f(x,y) from Wikipedia "Bilinear interpolation", 2006-06-18 +float Text::bilinearInterpolate(float x1, float x2, float y1, float y2, float x, float y, float q11, float q12, float q21, float q22) const +{ + return ( + ((q11 / ((x2-x1)*(y2-y1))) * (x2-x)*(y2-y)) + + ((q21 / ((x2-x1)*(y2-y1))) * (x-x1)*(y2-y)) + + ((q12 / ((x2-x1)*(y2-y1))) * (x2-x)*(y-y1)) + + ((q22 / ((x2-x1)*(y2-y1))) * (x-x1)*(y-y1)) + ); +} + + +/** + ** routines to convert between RGB and HSV + ** + ** Reference: Foley, van Dam, Feiner, Hughes, + ** "Computer Graphics Principles and Practices," + ** Additon-Wesley, 1990, pp592-593. + **/ +/* + * FUNCTION + * HsvRgb( hsv, rgb ) + * + * DESCRIPTION + * convert a hue-saturation-value into a red-green-blue value + * + * NOTE + * Array sizes are 3 + * Values are between 0.0 and 1.0 + */ + +void Text::convertHsvToRgb( float hsv[], float rgb[] ) const +{ + float h, s, v; /* hue, sat, value */ + /* double delta; */ /* change in color value */ + float r, g, b; /* red, green, blue */ + float i, f, p, q, t; /* interim values */ + + + /* guarantee valid input: */ + + h = hsv[0] / 60.f; + while( h >= 6.f ) h -= 6.f; + while( h < 0.f ) h += 6.f; + + s = hsv[1]; + if( s < 0.f ) + s = 0.f; + if( s > 1.f ) + s = 1.f; + + v = hsv[2]; + if( v < 0.f ) + v = 0.f; + if( v > 1.f ) + v = 1.f; + + + /* if sat==0, then is a gray: */ + + if( s == 0.0f ) + { + rgb[0] = rgb[1] = rgb[2] = v; + return; + } + + + /* get an rgb from the hue itself: */ + + i = floor( h ); + f = h - i; + p = v * ( 1.f - s ); + q = v * ( 1.f - s*f ); + t = v * ( 1.f - ( s * (1.f-f) ) ); + + switch( (int) i ) + { + case 0: + r = v; g = t; b = p; + break; + + case 1: + r = q; g = v; b = p; + break; + + case 2: + r = p; g = v; b = t; + break; + + case 3: + r = p; g = q; b = v; + break; + + case 4: + r = t; g = p; b = v; + break; + + case 5: + r = v; g = p; b = q; + break; + + default: + /* never happens? */ + r = 0; g = 0; b = 0; + break; + } + + + rgb[0] = r; + rgb[1] = g; + rgb[2] = b; + +} + +/* + * FUNCTION + * RgbHsv + * + * DESCRIPTION + * convert a red-green-blue value into hue-saturation-value + * + * NOTE + * Array sizes are 3 + * Values are between 0.0 and 1.0 + */ + +void Text::convertRgbToHsv( float rgb[], float hsv[] ) const +{ + float r, g, b; /* red, green, blue */ + float min, max; /* min and max rgb values */ + float fmin, fmax, diff; /* min, max, and range of rgb vals */ + float hue, sat, value; /* h s v */ + float cr, cg, cb; /* coefficients for computing hue */ + + + /* determine min and max color primary values: */ + + r = rgb[0]; g = rgb[1]; b = rgb[2]; + min = r; max = r; + if( g < min ) min = g; + if( g > max ) max = g; + if( b < min ) min = b; + if( b > max ) max = b; + + fmin = min; + fmax = max; + diff = fmax - fmin; + + + /* get value and saturation: */ + + value = fmax; + if( max == 0.f ) + sat = 0.0f; + else + sat = diff/fmax; + + + + /* compute hue: */ + + if( sat == 0.0f ) + hue = 0.0f; + else + { + float inv_diff = 1.0f / diff; + cr = ( fmax-r ) * inv_diff; + cg = ( fmax-g ) * inv_diff; + cb = ( fmax-b ) * inv_diff; + + if( max == r ) + hue = (g-b) * inv_diff; + else if( max == g ) + hue = 2.f + (b-r) * inv_diff; + else if( max == b ) + hue = 4.f + (r-g) * inv_diff; + else + hue = 0.0f; + } + + + hue *= 60.0f; + if( hue < 0.0f ) + hue += 360.0f; + if( hue > 360.0f ) + hue -= 360.0f; + + + /* store output values: */ + + hsv[0] = hue; + hsv[1] = sat; + hsv[2] = value; + +} + + + + +