Render True Type Font through Bezier curves

Started by broumbroum, August 20, 2009, 20:28:16

Previous topic - Next topic

broumbroum

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 :
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 :
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)

broumbroum

It now uses GLUTesselator from 2.2.1 LWJGL to improve speed :
           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();
                        }
                    };
                }
            };

pd_

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.

broumbroum

Quote from: pd_ on February 23, 2010, 12:50:04...
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.
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)

pd_

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!

broumbroum

updated GLText.zip in the first post (it was no longer available, since the forum crashed)
;)

kappa

ah nice, thx for that, just curious what the licence for the code?


broumbroum