TrueType as vector fonts in OpenGL

Started by stephan, December 13, 2005, 20:20:33

Previous topic - Next topic

stephan

Hello,

since a few days, I try to incorporate font outlines in my 3D graphics.  The code is a bit lengthy and pasted below together with a report that allows to double check that enough points are fed into OpenGL commands. Here are the main steps in the code:

1) get a render context and create a glyph vector for a string in a given font
2) get a path iterator from a shape generated by the glyph vector
3) loop along the iterator and emit reports as well as OpenGL-commands to draw

Unfortunately, two problems still remain:

1) GL11.glEvalCoord1f generates straight lines instead of quadratic or cubic curves
2) Is there a way to avoid glu tesselation to render glyphs with holes?

Any help is appreciated, thanks

Stephan


@Override protected void paintGL() {
...
   Font font = new Font("Courier New",Font.PLAIN,2);
   drawString("B", font);
...
}
private void drawString(String text, Font font) {

//AWT part
   Graphics2D gr = (Graphics2D)getGraphics();
   //rendering hints
   gr.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
      RenderingHints.VALUE_ANTIALIAS_ON);
   gr.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
      RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
   gr.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
      RenderingHints.VALUE_FRACTIONALMETRICS_ON);
   gr.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
      RenderingHints.VALUE_INTERPOLATION_BICUBIC);
   gr.setRenderingHint(RenderingHints.KEY_RENDERING,
      RenderingHints.VALUE_RENDER_QUALITY);
   //get shape
   FontRenderContext frc = gr.getFontRenderContext();
   GlyphVector gv = font.createGlyphVector(frc,text);
   Shape shape = gv.getOutline();
   AffineTransform trans = new AffineTransform();//identity matrix
   //PathIterator iter = shape.getPathIterator(trans, 0.25);
   //flatness seems to have no effect
   PathIterator iter = shape.getPathIterator(trans);
   gr.dispose();
      
//OpenGL part
   float[] coords = new float[6];
   FloatBuffer fb2 = BufferUtils.createFloatBuffer(9),
      fb3 = BufferUtils.createFloatBuffer(12);
   float[] last = new float[3], start = new float[3];
   float[] points = new float[9];
   int lineType;
   //attributes for demonstration
   GL11.glPushAttrib(GL11.GL_LINE_BIT);
   GL11.glLineWidth(15.0f);
   //draw
   while(!iter.isDone()) {
      lineType = iter.currentSegment(coords);
      switch(lineType) {
         case PathIterator.SEG_CLOSE:
            report("SEG_CLOSE"); report();
            GL11.glVertex3f(start[0], start[1], 0.0f);
            GL11.glEnd(); break;
         case PathIterator.SEG_CUBICTO:
            report("SEG_CUBICTO: ");
            fb3.clear(); fb3.put(last);
            points[0] = coords[0]; points[1] = coords[1];
            points[3] = coords[2]; points[4] = coords[3];
            points[6] = coords[4]; points[7] = coords[5];
            fb3.put(points); fb3.rewind(); report(fb3);
            GL11.glMap1f(GL11.GL_MAP1_VERTEX_3,
               0.0f, 1.0f, 3, 4, fb3);
            GL11.glEnable(GL11.GL_MAP1_VERTEX_3);
            for(int no = 0; no<10; ++no)
               GL11.glEvalCoord1f((float)(no/10.0));
            GL11.glDisable(GL11.GL_MAP1_VERTEX_3);
            last[0] = coords[4]; last[1] = coords[5];
            break;
         case PathIterator.SEG_LINETO:
            report("SEG_LINETO: ");
            report("{{"+coords[0]+","+coords[1]+", 0.0}}");
             report();
            last[0] = coords[0]; last[1] = coords[1];
            GL11.glVertex3f(last[0], last[1], 0.0f); break;
         case PathIterator.SEG_MOVETO:
            report("SEG_MOVETO: ");
            report("{{"+coords[0]+","+coords[1]+", 0.0}}");
            report();
            start[0] = coords[0]; start[1] = coords[1];
            last[0] = coords[0]; last[1] = coords[1];
            GL11.glBegin(GL11.GL_LINE_STRIP);
            GL11.glVertex3f(last[0], last[1], 0.0f); break;
         case PathIterator.SEG_QUADTO:
            report("SEG_QUADTO: ");
            fb2.clear(); fb2.rewind(); fb2.put(last);
            points[0] = coords[0]; points[1] = coords[1];
            points[3] = coords[2]; points[4] = coords[3];
            fb2.put(points, 0, 6); fb2.rewind(); report(fb2);
            GL11.glMap1f(GL11.GL_MAP1_VERTEX_3,
               0.0f, 1.0f, 3, 3, fb2);
            GL11.glEnable(GL11.GL_MAP1_VERTEX_3);
            for(int no = 0; no<10; ++no)
               GL11.glEvalCoord1f((float)(no/10.0));
            GL11.glDisable(GL11.GL_MAP1_VERTEX_3);
            last[0] = coords[2]; last[1] = coords[3];
            break;
      }
      iter.next();
   }
   //restore
   GL11.glPopAttrib();
}
----
output of report
SEG_MOVETO: {{0.25,-0.078125, 0.0}}
SEG_LINETO: {{0.25,-1.0625, 0.0}}
SEG_LINETO: {{0.140625,-1.0625, 0.0}}
SEG_QUADTO: {{0.140625, -1.0625, 0.0}, {0.109375, -1.0625, 0.0}, {0.09375, -1.0703125, 0.0}
SEG_QUADTO: {{0.09375, -1.0703125, 0.0}, {0.078125, -1.078125, 0.0}, {0.078125, -1.109375, 0.0}
SEG_QUADTO: {{0.078125, -1.109375, 0.0}, {0.078125, -1.125, 0.0}, {0.09375, -1.1328125, 0.0}
SEG_QUADTO: {{0.09375, -1.1328125, 0.0}, {0.109375, -1.140625, 0.0}, {0.140625, -1.140625, 0.0}
SEG_LINETO: {{0.65625,-1.140625, 0.0}}
SEG_QUADTO: {{0.65625, -1.140625, 0.0}, {0.8125, -1.140625, 0.0}, {0.90625, -1.0546875, 0.0}
SEG_QUADTO: {{0.90625, -1.0546875, 0.0}, {1.0, -0.96875, 0.0}, {1.0, -0.84375, 0.0}
SEG_QUADTO: {{1.0, -0.84375, 0.0}, {1.0, -0.703125, 0.0}, {0.84375, -0.609375, 0.0}
SEG_QUADTO: {{0.84375, -0.609375, 0.0}, {0.96875, -0.5625, 0.0}, {1.03125, -0.484375, 0.0}
SEG_QUADTO: {{1.03125, -0.484375, 0.0}, {1.09375, -0.40625, 0.0}, {1.09375, -0.3125, 0.0}
SEG_QUADTO: {{1.09375, -0.3125, 0.0}, {1.09375, -0.234375, 0.0}, {1.0546875, -0.1640625, 0.0}
SEG_QUADTO: {{1.0546875, -0.1640625, 0.0}, {1.015625, -0.09375, 0.0}, {0.9296875, -0.046875, 0.0}
SEG_QUADTO: {{0.9296875, -0.046875, 0.0}, {0.84375, 0.0, 0.0}, {0.75, 0.0, 0.0}
SEG_LINETO: {{0.140625,0.0, 0.0}}
SEG_QUADTO: {{0.140625, 0.0, 0.0}, {0.109375, 0.0, 0.0}, {0.09375, -0.015625, 0.0}
SEG_QUADTO: {{0.09375, -0.015625, 0.0}, {0.078125, -0.03125, 0.0}, {0.078125, -0.046875, 0.0}
SEG_QUADTO: {{0.078125, -0.046875, 0.0}, {0.078125, -0.0625, 0.0}, {0.09375, -0.0703125, 0.0}
SEG_QUADTO: {{0.09375, -0.0703125, 0.0}, {0.109375, -0.078125, 0.0}, {0.140625, -0.078125, 0.0}
SEG_LINETO: {{0.25,-0.078125, 0.0}}
SEG_CLOSE
SEG_MOVETO: {{0.328125,-0.640625, 0.0}}
SEG_LINETO: {{0.625,-0.640625, 0.0}}
SEG_QUADTO: {{0.625, -0.640625, 0.0}, {0.71875, -0.640625, 0.0}, {0.796875, -0.671875, 0.0}
SEG_QUADTO: {{0.796875, -0.671875, 0.0}, {0.859375, -0.703125, 0.0}, {0.890625, -0.75, 0.0}
SEG_QUADTO: {{0.890625, -0.75, 0.0}, {0.921875, -0.796875, 0.0}, {0.921875, -0.859375, 0.0}
SEG_QUADTO: {{0.921875, -0.859375, 0.0}, {0.921875, -0.9375, 0.0}, {0.8515625, -1.0, 0.0}
SEG_QUADTO: {{0.8515625, -1.0, 0.0}, {0.78125, -1.0625, 0.0}, {0.65625, -1.0625, 0.0}
SEG_LINETO: {{0.328125,-1.0625, 0.0}}
SEG_LINETO: {{0.328125,-0.640625, 0.0}}
SEG_CLOSE
SEG_MOVETO: {{0.328125,-0.078125, 0.0}}
SEG_LINETO: {{0.75,-0.078125, 0.0}}
SEG_QUADTO: {{0.75, -0.078125, 0.0}, {0.828125, -0.078125, 0.0}, {0.8828125, -0.1171875, 0.0}
SEG_QUADTO: {{0.8828125, -0.1171875, 0.0}, {0.9375, -0.15625, 0.0}, {0.96875, -0.203125, 0.0}
SEG_QUADTO: {{0.96875, -0.203125, 0.0}, {1.0, -0.25, 0.0}, {1.0, -0.3125, 0.0}
SEG_QUADTO: {{1.0, -0.3125, 0.0}, {1.0, -0.375, 0.0}, {0.9609375, -0.4296875, 0.0}
SEG_QUADTO: {{0.9609375, -0.4296875, 0.0}, {0.921875, -0.484375, 0.0}, {0.84375, -0.515625, 0.0}
SEG_QUADTO: {{0.84375, -0.515625, 0.0}, {0.765625, -0.546875, 0.0}, {0.625, -0.546875, 0.0}
SEG_LINETO: {{0.328125,-0.546875, 0.0}}
SEG_LINETO: {{0.328125,-0.078125, 0.0}}
SEG_CLOSE
----
private void report(float number) {System.out.print(number);}
private void report() {System.out.println();}
private void report(String text) {System.out.print(text);}
private void report(FloatBuffer fb) {
   report("{{");
   for(int no = 0; no < fb.limit()-1; ++no) {
      report(fb.get(no));
      if(no%3==2) report("}, {"); else report(", ");
   }
   report(fb.get(fb.limit()-1)+"}");
   report();
}

baysmith

Are you still working on this? Do you have a complete demo program that I can experiment with?

stephan

Quote from: "baysmith"Are you still working on this? Do you have a complete demo program that I can experiment with?

Hello baysmith,

some missing GLU functions in LWJGL caused my switch to JOGL. Below is a sketch of how I got 3D fonts in JOGL (quick solution, appended, because it is a bit lengthy). I hope that you will find the code helpful to find a solution within the scope of LWJGL (I did not succeed in a reasonable time). The code is incorporated in a larger project. Background can be found in the "redbook": Shreiner/Woo/Neider/Davis, OpenGL Programming Guide, Addison-Wesley, 5th ed 2006 (very good, 800 pages, covers version 2).

Regards   Stephan

1) Get the shape from AWT
types: java.awt.Graphics2D gr, java.awt.Font font, java.awt.font.GlyphVector gv, java.awt.Shape shape, java.awt.geom.AffineTransform trans, java.awt.geom.PathIterator iter
   //get shape
   FontRenderContext frc = gr.getFontRenderContext();
   GlyphVector gv = font.createGlyphVector(frc,text);
   Shape shape = gv.getOutline();
   AffineTransform trans = new AffineTransform();//identity matrix
   //PathIterator iter = shape.getPathIterator(trans, 0.25);
      //flatness seems to have no effect
   PathIterator iter = shape.getPathIterator(trans);
   gr.dispose();
2) Use the shape to tesselate
report(): in essence, same functionality as println, useful for debugging
types (for speed at object level):
   private double[][] quadraticControlPoints = new double[3][3];
   private double[][] cubicControlPoints = new double[4][3];
   private double[] point = new double[3];
   double[] coords = new double[6];
   double[] last = new double[3], start = new double[3];

code:
   gl = drawable.getGL();
      
   int lineType;
   //attributes
   gl.glPushAttrib(GL.GL_LINE_BIT);
   gl.glLineWidth(15.0f);
      
   //tesselator
   glu = drawable.getGLU();
   GLUtesselator tesselator = glu.gluNewTess();
   glu.gluTessCallback(tesselator, GLU.GLU_TESS_BEGIN, this);
   glu.gluTessCallback(tesselator, GLU.GLU_TESS_VERTEX, this);
   glu.gluTessCallback(tesselator, GLU.GLU_TESS_COMBINE, this);         glu.gluTessCallback(tesselator, GLU.GLU_TESS_END, this);         glu.gluTessCallback(tesselator, GLU.GLU_TESS_ERROR, this);         switch(iter.getWindingRule()) {
      case PathIterator.WIND_EVEN_ODD:
         report("Even/odd shape winding");
         glu.gluTessProperty(tesselator,
         GLU.GLU_TESS_WINDING_RULE,
         GLU.GLU_TESS_WINDING_ODD);
         break;
      case PathIterator.WIND_NON_ZERO:
         report("Nonzero shape winding");
         glu.gluTessProperty(tesselator,
            GLU.GLU_TESS_WINDING_RULE,
            GLU.GLU_TESS_WINDING_NONZERO);
         break;
   }
   report();
         
   //draw (points to glu tesselator must be independent objects)
   gl.glNewList(getDisplayList(), GL.GL_COMPILE);
   Object userData = null;
   double[] vertexData = null;
   glu.gluTessBeginPolygon(tesselator, userData);
   while(!iter.isDone()) {
      lineType = iter.currentSegment(coords);
      switch(lineType) {
         case PathIterator.SEG_CLOSE: report("SEG_CLOSE");
      report();
         //vertexData = start;
         //glu.gluTessVertex(tesselator, start, vertexData);
         glu.gluTessEndContour(tesselator);
         break;
      case PathIterator.SEG_CUBICTO: report("SEG_CUBICTO: ");
         cubicControlPoints[0][0] = last[0];
         cubicControlPoints[0][1] = last[1];
         cubicControlPoints[1][0] = coords[0];
         cubicControlPoints[1][1] = coords[1];
         cubicControlPoints[2][0] = coords[2];
         cubicControlPoints[2][1] = coords[3];
         cubicControlPoints[3][0] = coords[4];
         cubicControlPoints[3][1] = coords[5];
         report(cubicControlPoints);
         report(" -> {");
         for(int no = 0; no<10; ++no) {
            point = cubic.point(cubicControlPoints,no/10.0);
            vertexData = point;
            report(point); report(", ");
            glu.gluTessVertex(tesselator, point, vertexData);
         }
         report("}");report();
         last[0] = coords[4]; last[1] = coords[5];
         break;
      case PathIterator.SEG_LINETO: report("SEG_LINETO: ");
         report("{{");report(coords[0]); report(",");
         report(coords[1]); report(", 0.0}}"); report();
         last[0] = coords[0]; last[1] = coords[1];
         vertexData = last.clone();
         glu.gluTessVertex(tesselator, vertexData, vertexData);
         break;
      case PathIterator.SEG_MOVETO: report("SEG_MOVETO: ");
         report("{{");report(coords[0]); report(",");
         report(coords[1]); report(", 0.0}}"); report();
         start[0] = coords[0]; start[1] = coords[1];
         last[0] = coords[0]; last[1] = coords[1];
         vertexData = last.clone();
         glu.gluTessBeginContour(tesselator);
         glu.gluTessVertex(tesselator, vertexData, vertexData);
         break;
      case PathIterator.SEG_QUADTO: report("SEG_QUADTO: ");
         quadraticControlPoints[0][0] = last[0];
         quadraticControlPoints[0][1] = last[1];
         quadraticControlPoints[1][0] = coords[0];
         quadraticControlPoints[1][1] = coords[1];
         quadraticControlPoints[2][0] = coords[2];
         quadraticControlPoints[2][1] = coords[3];
         report(quadraticControlPoints);
         report(" -> {");
         for(int no = 0; no<10; ++no) {
            point = quadratic.point(quadraticControlPoints,
               no/10.0);
            vertexData = point;
            report(point); report(", ");
            glu.gluTessVertex(tesselator, point, vertexData);
         }
         report("}");report();
         last[0] = coords[2]; last[1] = coords[3];
         break;
      }
      iter.next();
   }
   glu.gluTessEndPolygon(tesselator);
   gl.glEndList();
   //restore
   gl.glPopAttrib();

tesselator callbacks:
public void begin(int primitiveType) {
   switch(primitiveType) {
      case GL.GL_TRIANGLE_FAN: report("Triangle fan: "); break;
      case GL.GL_TRIANGLE_STRIP: report("Triangle strip: "); break;
      case GL.GL_TRIANGLES: report("Triangles: "); break;
      case GL.GL_LINE_LOOP: report("Line loop: "); break;
      default: report("Start primitive "+primitiveType+" ");
   }
      
   gl.glBegin(primitiveType);
}
public void beginData(int primitiveType, Object userData) {
   report("Start primitive "+primitiveType+
      " with user data "+userData);
   gl.glBegin(primitiveType);
}
public void edgeFlag(boolean edge) {
   if(edge) report("Draw edge "); else report("Draw no edge ");
   /*only for GL_TRIANGLES*/
}
public void edgeFlagData(boolean edge, Object userData) {
   if(edge) report("Draw edge with user data "+userData+" ");
   else report("Draw no edge with user data "+userData+" ");
   /*only for GL_TRIANGLES*/
}
public void vertex(Object vertexData) {
   double[] coords = (double[])vertexData;
   report("V "); report(coords); report(" ");
   gl.glVertex3dv(coords);
}
public void vertexData(Object vertexData, Object userData) {
   double[] coords = (double[])vertexData;
   report("V "); report(coords); report(" UD "+userData);
      report(" ");
   gl.glVertex3dv(coords);
}
public void end() {
   report("End primitive"); report();
   gl.glEnd();
}
public void endData(Object userData) {
   report("End primitive with user data "+userData); report();
   gl.glEnd();
}
public void combine(
   double[] position3DVertex,
   Object[] dataOfFourNeighbourVertices, //some may be null
   float[] weightsOfFourNeighbourVertices,
   Object[] dataForSplitOrMergeVertex //length 1
) {
   report("Combine at position "); report(position3DVertex);
   double[] vertex = new double[3];
   for(int no = 0; no<3; ++no) vertex[no] = position3DVertex[no];
   dataForSplitOrMergeVertex[0] = vertex;
}
public void combineData(
   double[] position3DVertex,
   Object[] dataOfFourNeighbourVertices, //some may be null
   float[] weightsOfFourNeighbourVertices,
   Object[] dataForSplitOrMergeVertex, //length 1
   Object userData
) {
   report("Combine at position "); report(position3DVertex);
   report(" with user data "+userData);
   double[] vertex = new double[3];
   for(int no = 0; no<3; ++no) vertex[no] = position3DVertex[no];
   dataForSplitOrMergeVertex[0] = vertex;
}
public void error(int errorNumber) {
   System.out.println("Tesselation error "+errorNumber+
      " in Text: "+glu.gluErrorString(errorNumber));
}
public void errorData(int errorNumber, Object userData) {
   System.out.println("Tesselation error "+errorNumber+
      " in Text: "+glu.gluErrorString(errorNumber));
}