LWJGL Forum

Programming => General Java Game Development => Topic started by: broumbroum on August 20, 2009, 20:28:16

Title: Render True Type Font through Bezier curves
Post by: broumbroum on August 20, 2009, 20:28:16
Something requested as an alternative to TrueTypeFont class was a way to render cyrillic characters.
The GLText class can (hopefully) do this by using bezier-curves.
It's basically using AWTGLCanvas or any other component where we could get a Font :
Code: [Select]
final Font font = gld.getFont();
        final GlyphVector gv = gld.getFont().createGlyphVector(new FontRenderContext(font.getTransform(), true, false), text);
        
Then comes the tricky bezier computation that will be stored in a GL list using a Shape PathIterator :
Code: [Select]
GLList gllist = new GLGlyph(glyphChar, font.getSize()) {

                @Override
                public Runnable getList() {
                    return new Runnable() {

                        public void run() {
                            Shape glyph = gv.getGlyphOutline(indexChar, -(float) glyphBounds.getX(), -(float) glyphBounds.getY());
                            PathIterator pi = glyph.getPathIterator(null);
                            Point2D.Float current = new Point2D.Float();
                            while (!pi.isDone()) {
                                float[] coords = new float[6];
                                int path = pi.currentSegment(coords);
                                switch (path) {
                                    case PathIterator.SEG_MOVETO:
                                        /*System.err.println("PaIt begin move"); */
                                        GL11.glBegin(GL11.GL_LINE_LOOP);
                                        GL11.glVertex2f(coords[0], coords[1]);
                                        current.x = coords[0];
                                        current.y = coords[1];
                                        break;
                                    case PathIterator.SEG_CLOSE:
                                        /*System.err.println("PaIt close");*/
                                        GL11.glEnd();
                                        break;
                                    case PathIterator.SEG_LINETO:
                                        /*System.err.println("PaIt line");*/
                                        GL11.glVertex2f(coords[0], coords[1]);
                                        current.x = coords[0];
                                        current.y = coords[1];
                                        break;
                                    case PathIterator.SEG_CUBICTO:
                                        /*System.err.println("PaIt cubic");*/
                                        for (Point2D.Float p : _computeBezierCurve(new Point2D.Float[]{current, new Point2D.Float(coords[0], coords[1]), new Point.Float(coords[2], coords[3]), new Point.Float(coords[4], coords[5])}, font.getSize())) {
                                            GL11.glVertex2f(p.x, p.y);
                                        }
                                        current.x = coords[4];
                                        current.y = coords[5];
                                        break;
                                    case PathIterator.SEG_QUADTO:
                                        /*System.err.println("PaIt quad");*/
                                        for (Point2D.Float p : _computeBezierCurve(new Point2D.Float[]{current, new Point2D.Float(coords[0], coords[1]), new Point.Float(coords[2], coords[3])}, font.getSize())) {
                                            GL11.glVertex2f(p.x, p.y);
                                        }
                                        current.x = coords[2];
                                        current.y = coords[3];
                                        break;
                                    default:
                                        break;
                                }
                                pi.next();
                            }
                        }
                    };
                }
            };
This way seems faster on rendering and is totally dependent of the current Font used when the method is invoked. That's if Font.size changes, then the renderer will increase the size too; as well as the Font.name changes then everything appear in the new font character (that case hasn't been tested yet).
 :D

EDIT : GLList added, and LICENSE + NOTICE file (Apache 2 License + BSD LWJGL notice)
Title: Re: Render True Type Font through Bezier curves
Post by: broumbroum on December 26, 2009, 23:45:05
It now uses GLUTesselator from 2.2.1 LWJGL to improve speed :
Code: [Select]
           final char glyphChar = text.charAt(i);
            final Rectangle2D glyphBounds = gv.getGlyphOutline(i).getBounds2D();
            final int indexChar = i;
            GL11.glPushMatrix();
            GL11.glTranslated(glyphBounds.getX(), glyphBounds.getY(), 0);
            GLList gllist = new GLGlyph(glyphChar, font.getSize()) {

                @Override
                public Runnable getList() {
                    return new Runnable() {

                        public void run() {
                            Shape glyph = gv.getGlyphOutline(indexChar, -(float) glyphBounds.getX(), -(float) glyphBounds.getY());
                            PathIterator pi = glyph.getPathIterator(null);
                            GLUtessellator tess = GLUtessellatorImpl.gluNewTess();
                            GLUtessellatorCallbackAdapter tessCb = new GLUtessellatorCallbackAdapter() {

                                @Override
                                public void begin(int type) {
                                    GL11.glBegin(type);
                                }

                                @Override
                                public void vertex(Object vertexData) {
                                    float[] vert = (float[]) vertexData;
                                    GL11.glVertex2f(vert[0], vert[1]);
                                }

                                public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) {
                                    for (int i = 0; i < outData.length; i++) {
                                        float[] combined = new float[6];
                                        combined[0] = (float) coords[0];
                                        combined[1] = (float) coords[1];
                                        /*combined[2] = (float) coords[2];
                                        for (int j = 3; j < 6; j++) {
                                        for(int d = 0; d < data.length; d++)
                                        combined[j] = weight[d] * data[d][j];
                                        }*/
                                        outData[i] = combined;
                                    }
                                }

                                @Override
                                public void end() {
                                    GL11.glEnd();
                                }
                            };
                            tess.gluTessCallback(GLU.GLU_TESS_BEGIN, tessCb);
                            tess.gluTessCallback(GLU.GLU_TESS_VERTEX, tessCb);
                            tess.gluTessCallback(GLU.GLU_TESS_COMBINE, tessCb);
                            tess.gluTessCallback(GLU.GLU_TESS_END, tessCb);
                            Point2D.Float current = new Point2D.Float();
                            tess.gluTessBeginPolygon(null);
                            while (!pi.isDone()) {
                                float[] coords = new float[6];
                                int path = pi.currentSegment(coords);
                                switch (pi.getWindingRule()) {
                                    case PathIterator.WIND_EVEN_ODD:
                                        tess.gluTessProperty(GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_ODD);
                                        break;
                                    case PathIterator.WIND_NON_ZERO:
                                        tess.gluTessProperty(GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_NONZERO);
                                        break;
                                    default:
                                        if (JXAenvUtils._debug) {
                                            System.err.println(JXAenvUtils._JXAEnvOutput("no winding rule", JXAenvUtils.APP_WARNING));
                                        }
                                        break;
                                }

                                switch (path) {
                                    case PathIterator.SEG_MOVETO:
                                        /*System.err.println("PaIt begin move"); */
                                        /*GL11.glBegin(GL11.GL_LINE_LOOP);*/
                                        tess.gluTessBeginContour();
                                        /*GL11.glVertex2f(coords[0], coords[1]);*/
                                        tess.gluTessVertex(new double[]{coords[0], coords[1], 0.}, 0, new float[]{coords[0], coords[1], 0f});
                                        current.x = coords[0];
                                        current.y = coords[1];
                                        break;
                                    case PathIterator.SEG_CLOSE:
                                        /*System.err.println("PaIt close");*/
                                        /*GL11.glEnd();*/
                                        tess.gluTessEndContour();
                                        break;
                                    case PathIterator.SEG_LINETO:
                                        /*System.err.println("PaIt line");*/
                                        /*GL11.glVertex2f(coords[0], coords[1]);*/
                                        tess.gluTessVertex(new double[]{coords[0], coords[1], 0.}, 0, new float[]{coords[0], coords[1], 0f});
                                        current.x = coords[0];
                                        current.y = coords[1];
                                        break;
                                    case PathIterator.SEG_CUBICTO:
                                        /*System.err.println("PaIt cubic");*/
                                        for (Point2D.Float p : GLGeom._computeBezierCurve(new Point2D.Float[]{current, new Point2D.Float(coords[0], coords[1]), new Point.Float(coords[2], coords[3]), new Point.Float(coords[4], coords[5])}, font.getSize())) {
                                            /*GL11.glVertex2f(p.x, p.y);*/
                                            tess.gluTessVertex(new double[]{p.x, p.y, 0.}, 0, new float[]{p.x, p.y, 0f});
                                        }
                                        current.x = coords[4];
                                        current.y = coords[5];
                                        break;
                                    case PathIterator.SEG_QUADTO:
                                        /*System.err.println("PaIt quad");*/
                                        for (Point2D.Float p : GLGeom._computeBezierCurve(new Point2D.Float[]{current, new Point2D.Float(coords[0], coords[1]), new Point.Float(coords[2], coords[3])}, font.getSize())) {
                                            /*GL11.glVertex2f(p.x, p.y);*/
                                            tess.gluTessVertex(new double[]{p.x, p.y, 0.}, 0, new float[]{p.x, p.y, 0f});
                                        }
                                        current.x = coords[2];
                                        current.y = coords[3];
                                        break;
                                    default:
                                        break;
                                }
                                pi.next();
                            }
                            tess.gluTessEndPolygon();
                            tess.gluDeleteTess();
                        }
                    };
                }
            };
Title: Re: Render True Type Font through Bezier curves
Post by: pd_ on February 23, 2010, 12:50:04
Thanks for this, it works pretty well! :)

One question, though: how do I get it to render smoothly?
I have GL_LINE_SMOOTH and blending enabled (and I tried it with lines), but it seems that tesselated polygons are not affected by that, so texts rendered in smaller sizes are practially unreadable.
Title: Re: Render True Type Font through Bezier curves
Post by: broumbroum on February 24, 2010, 20:44:34
...
 so texts rendered in smaller sizes are practially unreadable.

the GLGeom compute bezier curve has its last parameter as a "fine tuning". It's set on the font size, that is if the font is small, near 1, the bezier curves would  be computed as with 1 control point (see bezier curves on wikipedia) and therefore it may be unsufficient for high-definition overlays.
Code: [Select]
for (Point2D.Float p :
GLGeom._computeBezierCurve(new Point2D.Float[]{current, new Point2D.Float(coords[0], coords[1]), new Point.Float(coords[2], coords[3])},
font.getSize())) {} // in cubic and quadratic cases
I suggest you to get the last parameter throsholded to a value of ~3-5 control points when font is smaller than 5. adjust it to your requirements/tests ! 8)
Title: Re: Render True Type Font through Bezier curves
Post by: pd_ on February 25, 2010, 08:23:07
Sorry, I guess I put it a little misunderstandably.
I didn't really refer to the smoothness of the bezier curve (thanks for that hint though, it might be a nice toy!) but to the smoothness of the rendering as in "anti-aliasing".

I believe that due to aliasing, at some places, some pixels go missing (e.g. on the "e") and some are off (e.g. on the "C"). See the attachment for an example (that's "Lucida Console", size 11 and bold, regular has similar problems).
The question is how can I enable anti-aliasing for tesselated polygons? GL_BLEND is enabled, but that doesn't seem to do it.

Thanks for your help!
Title: Re: Render True Type Font through Bezier curves
Post by: broumbroum on November 26, 2010, 01:32:07
updated GLText.zip in the first post (it was no longer available, since the forum crashed)
;)
Title: Re: Render True Type Font through Bezier curves
Post by: kappa on November 26, 2010, 09:35:18
ah nice, thx for that, just curious what the licence for the code?
Title: Re: Render True Type Font through Bezier curves
Post by: broumbroum on November 26, 2010, 13:49:37
Apache (http://www.apache.org/licenses/LICENSE-2.0.html)
Title: Re: Render True Type Font through Bezier curves
Post by: broumbroum on January 12, 2012, 10:22:09
It's a bit slower than bitmap rendering, though PBO can solve part of this issue (work in progress for HUD), but a lot easier to change font size and face, to change the default Font.
GLText.java (full) http://sf3jswing.hg.sourceforge.net/hgweb/sf3jswing/sf3jswing/file/4bdc82814598/sf3jswing-jigaxtended/src/all/net/sf/jiga/xtended/impl/game/gl/GLText.java
GLGeom.java (tess and renderShape) http://sf3jswing.hg.sourceforge.net/hgweb/sf3jswing/sf3jswing/file/4bdc82814598/sf3jswing-jigaxtended/src/all/net/sf/jiga/xtended/impl/game/gl/GLGeom.java