| versions | 0135+ |
|---|---|
| contributors | tomc |
| started on | 2008-02-08 00:19 |
Since the introduction of a renderer based on Java's Graphics2D class, Processing has had the ability to do high quality line rendering, complete with choices of line cap and join style.
Unfortunately, such control is not available using OpenGL. It's not just a Processing/Java issue, OpenGL leaves it out (and offers it as an exercise for the reader), as documented in the FAQ:
“OpenGL doesn't provide a mechanism to cleanly join lines that share common vertices nor to cleanly cap the endpoints.
…
Another solution is for the application to handle the capping and mitering. Instead of rendering lines, the application needs to render face-on polygons. The application will need to perform the necessary math to calculate the vertex locations to provide the desired capping and joining styles.”
– 14.100 How do I turn on wide−line endpoint capping or mitering?
Even if you're jumping straight to the hardware-accelerated OpenGL functionality with beginGL and endGL, one useful thing about continuing to use Processing is that it puts the entire Java libraries at your finger tips.
The AWT classes in Java provide functions for generating shapes and iterating over the path of their outlines. This includes the BasicStroke class which can generate the outline of a wide-stroke line over a General Path shape. This is the key to getting line caps and joins working in Processing's OpenGL mode.
The only issue with a path that you get back from BasicStroke is that it won't always render cleanly using beginShape and endShape to draw a POLYGON. That's where OpenGL's GLU tesselation functions come into play, dividing the shape into triangles for us. This can be a bit complicated to get set up, but thankfully the Processing source code contains an example of how to interface the GLU tesselators with AWT's rendering classes in the PGraphicsOpenGL.java file's text functions.
I've written a wrapper around the required functions, so that you can draw stylish line caps and joins in OpenGL using Processing-like syntax:
/** openglstrokes taken from http://processinghacks.com/hacks:openglstrokes @author Tom Carden */ import processing.opengl.*; LineGraphics lg; // create 1000 empty points float[][] points = new float[1000][2]; // but don't draw any yet int numPoints = 0; float sw = 50.0; void setup() { // only works with OPENGL size(640,480,OPENGL); noLoop(); // make a line graphics help for this applet lg = new LineGraphics(this); } void draw() { background(0); // render our points using a white line with alpha blending lg.stroke(255, 40); lg.strokeWeight(50); lg.strokeCap(PROJECT); // SQUARE, ROUND lg.strokeJoin(BEVEL); // ROUND, MITER lg.line(points, numPoints); // render our points in red the regular Processing way noFill(); stroke(255,0,0); strokeWeight(1); beginShape(); for (int i = 0; i < numPoints; i++) { vertex(points[i][0], points[i][1]); } endShape(); } // click to draw void mousePressed() { addPoint(); redraw(); } // drag to draw void mouseDragged() { addPoint(); redraw(); } void addPoint() { if (numPoints < points.length) { points[numPoints][0] = mouseX; points[numPoints][1] = mouseY; numPoints++; } } // clear points on space void keyPressed() { if (key == ' ') { numPoints = 0; } redraw(); }
Here's the code for the LineGraphics.java, which you should put in another tab alongside the above Processing code. I've not tested it extensively, so if you find bad bugs in it please fix them here or let me know.
/** LineGraphics.java from http://processinghacks.com/hacks:openglstrokes @author Tom Carden */ import processing.core.*; import processing.opengl.*; import java.awt.*; import java.awt.geom.*; import javax.media.opengl.*; import javax.media.opengl.glu.*; import com.sun.opengl.util.*; // this is hacked up from Ben Fry's PGraphicsOpenGL text rendering code // // as such, it's released under LGPL // // it's not as good as Ben's code because I removed a lot of the comments // I wouldn't write comments that useful // // he's right that this is slow though - // if you need it, you'll doubtless want to mess with it quite a bit public class LineGraphics extends GLUtessellatorCallbackAdapter implements PConstants { public int strokeColor; public float strokeWeight; public int strokeCap; public int strokeJoin; PApplet p; GL gl; GLU glu; GLUtessellator tobj; PGraphicsOpenGL g; public LineGraphics(PApplet p) { this.p = p; g = (PGraphicsOpenGL)p.g; gl = ((PGraphicsOpenGL)g).gl; glu = ((PGraphicsOpenGL)g).glu; tobj = glu.gluNewTess(); glu.gluTessCallback(tobj, GLU.GLU_TESS_BEGIN, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_END, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_VERTEX, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_COMBINE, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_ERROR, this); strokeColor = g.strokeColor; strokeWeight = g.strokeWeight; strokeCap = g.strokeCap; strokeJoin = g.strokeJoin; } public void line(float[][] points, int numPoints) { if (numPoints < 2) return; int bsCap = strokeCap == ROUND ? BasicStroke.CAP_ROUND : strokeCap == PROJECT ? BasicStroke.CAP_SQUARE : BasicStroke.CAP_BUTT; int bsJoin = strokeJoin == ROUND ? BasicStroke.JOIN_ROUND : strokeJoin == BEVEL ? BasicStroke.JOIN_BEVEL : BasicStroke.JOIN_MITER; BasicStroke bs = new BasicStroke(strokeWeight, bsCap, bsJoin); GeneralPath path = new GeneralPath(GeneralPath.WIND_NON_ZERO); path.moveTo(points[0][0], points[0][1]); for (int i = 1; i < numPoints; i++) { path.lineTo(points[i][0], points[i][1]); } Shape sh = bs.createStrokedShape(path); glu.gluTessBeginPolygon(tobj, null); // second param to gluTessVertex is for a user defined object that contains // additional info about this point, but that's not needed for anything float lastX = 0; float lastY = 0; // "unfortunately the tesselator won't work properly unless a // new array of doubles is allocated for each point. that bites ass, // but also just reaffirms that in order to make things fast, // display lists will be the way to go." -- Ben Fry :) double vertex[]; float coords[] = new float[6]; PathIterator iter = sh.getPathIterator(null); // ,5) add a number on here to simplify verts int rule = iter.getWindingRule(); switch(rule) { case PathIterator.WIND_EVEN_ODD: glu.gluTessProperty(tobj, GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_ODD); break; case PathIterator.WIND_NON_ZERO: glu.gluTessProperty(tobj, GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_NONZERO); break; } g.beginGL(); float ir = strokeColor >> 16 & 0xff; float ig = strokeColor >> 8 & 0xff; float ib = strokeColor & 0xff; float ia = strokeColor >> 24 & 0xff; float cr = (float)ir/255.0f; float cg = (float)ig/255.0f; float cb = (float)ib/255.0f; float ca = (float)ia/255.0f; gl.glColor4f(cr,cg,cb,ca); while (!iter.isDone()) { switch (iter.currentSegment(coords)) { case PathIterator.SEG_MOVETO: // 1 point (2 vars) in coords glu.gluTessBeginContour(tobj); case PathIterator.SEG_LINETO: // 1 point vertex = new double[] { coords[0], coords[1], 0 }; glu.gluTessVertex(tobj, vertex, 0, vertex); lastX = coords[0]; lastY = coords[1]; break; case PathIterator.SEG_QUADTO: // 2 points for (int i = 1; i < g.bezierDetail; i++) { float t = (float)i / (float)g.bezierDetail; vertex = new double[] { g.bezierPoint(lastX, coords[0], coords[2], coords[2], t), g.bezierPoint(lastY, coords[1], coords[3], coords[3], t), 0 }; glu.gluTessVertex(tobj, vertex, 0, vertex); } lastX = coords[2]; lastY = coords[3]; break; case PathIterator.SEG_CUBICTO: // 3 points for (int i = 1; i < g.bezierDetail; i++) { float t = (float)i / (float)g.bezierDetail; vertex = new double[] { g.bezierPoint(lastX, coords[0], coords[2], coords[4], t), g.bezierPoint(lastY, coords[1], coords[3], coords[5], t), 0 }; glu.gluTessVertex(tobj, vertex, 0, vertex); } lastX = coords[4]; lastY = coords[5]; break; case PathIterator.SEG_CLOSE: glu.gluTessEndContour(tobj); break; } iter.next(); } glu.gluTessEndPolygon(tobj); g.endGL(); } ////// GLUtessellatorCallbackAdapter functions public void begin(int type) { gl.glBegin(type); } public void end() { gl.glEnd(); } public void vertex(Object data) { if (data instanceof double[]) { double[] d = (double[]) data; if (d.length != 3) { throw new RuntimeException("TessCallback vertex() data isn't length 3"); } gl.glVertex3d(d[0], d[1], d[2]); } else { throw new RuntimeException("TessCallback vertex() data not understood"); } } public void error(int errnum) { throw new RuntimeException("Tessellation Error: " + glu.gluErrorString(errnum)); } public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) { double[] vertex = new double[coords.length]; vertex[0] = coords[0]; vertex[1] = coords[1]; vertex[2] = coords[2]; outData[0] = vertex; } ////// end GLUtessellatorCallbackAdapter functions ////// Processing style stroke settings public void stroke(int gr) { strokeColor = p.color(gr); } public void stroke(int gr, int a) { strokeColor = p.color(gr, a); } public void stroke(int r, int gr, int b) { strokeColor = p.color(r, gr, b); } public void stroke(int r, int gr, int b, int a) { strokeColor = p.color(r, gr, b, a); } public void stroke(float gr) { strokeColor = p.color(gr); } public void stroke(float gr, float a) { strokeColor = p.color(gr, a); } public void stroke(float r, float gr, float b) { strokeColor = p.color(r, gr, b); } public void stroke(float r, float gr, float b, float a) { strokeColor = p.color(r, gr, b, a); } public void strokeWeight(float sw) { strokeWeight = sw; } public void strokeCap(int cap) { strokeCap = cap; } public void strokeJoin(int join) { strokeJoin = join; } }
Here is a slightly different version of Toms code. This version keeps the General Path, and just adds to it each time rather than creating it from scratch each redraw. This may be slightly faster but the tessellation is still slow, as Fry originally mentioned - the next step would be to look at opengl display lists to cache shapes that don't change. Here is a somewhat dated library which does that. The code for the library itself is here.
/** openglstrokes taken from http://processinghacks.com/hacks:openglstrokes @author Tom Carden @edited Karl D.D. Willis */ import processing.opengl.*; LineGraphics lg; void setup() { // only works with OPENGL size(640,480,OPENGL); //smooth(); noLoop(); // make a line graphics help for this applet lg = new LineGraphics(this); lg.stroke(255,0,0, 150); lg.strokeWeight(50); lg.strokeCap(SQUARE); // PROJECT, SQUARE, ROUND lg.strokeJoin(BEVEL); // BEVEL, ROUND, MITER } void draw() { background(0); lg.draw(); } // click to draw void mousePressed() { lg.moveTo(mouseX, mouseY); redraw(); } // drag to draw void mouseDragged() { lg.lineTo(mouseX, mouseY); redraw(); } void mouseReleased(){ //lg.closePath(); //redraw(); } // clear void keyPressed() { if (key == ' ') { lg.clearPath(); } redraw(); }
And here is the updated LineGraphics.java file
/** LineGraphics.java from http://processinghacks.com/hacks:openglstrokes @author Tom Carden @edited Karl D.D. Willis */ import processing.core.*; import processing.opengl.*; import java.awt.*; import java.awt.geom.*; import javax.media.opengl.*; import javax.media.opengl.glu.*; import com.sun.opengl.util.*; // this is hacked up from Ben Fry's PGraphicsOpenGL text rendering code // // as such, it's released under LGPL // // it's not as good as Ben's code because I removed a lot of the comments // I wouldn't write comments that useful // // he's right that this is slow though - // if you need it, you'll doubtless want to mess with it quite a bit public class LineGraphics extends GLUtessellatorCallbackAdapter implements PConstants { public int strokeColor; public float strokeWeight; PApplet p; GL gl; GLU glu; GLUtessellator tobj; PGraphicsOpenGL g; BasicStroke bs; GeneralPath path; int strokeCap, strokeJoin; int count = 0; public LineGraphics(PApplet p) { this.p = p; g = (PGraphicsOpenGL)p.g; gl = ((PGraphicsOpenGL)g).gl; glu = ((PGraphicsOpenGL)g).glu; tobj = glu.gluNewTess(); glu.gluTessCallback(tobj, GLU.GLU_TESS_BEGIN, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_END, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_VERTEX, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_COMBINE, this); glu.gluTessCallback(tobj, GLU.GLU_TESS_ERROR, this); strokeCap = g.strokeCap == ROUND ? BasicStroke.CAP_ROUND : g.strokeCap == PROJECT ? BasicStroke.CAP_SQUARE : BasicStroke.CAP_BUTT; strokeJoin = g.strokeJoin == ROUND ? BasicStroke.JOIN_ROUND : g.strokeJoin == BEVEL ? BasicStroke.JOIN_BEVEL : BasicStroke.JOIN_MITER; bs = new BasicStroke(strokeWeight, strokeCap, strokeJoin); path = new GeneralPath(GeneralPath.WIND_NON_ZERO); } public void moveTo(float x, float y){ path.moveTo(x, y); } public void lineTo(float x, float y){ path.lineTo(x, y); } public void closePath(){ path.closePath(); } public void clearPath(){ path = new GeneralPath(GeneralPath.WIND_NON_ZERO); } public void draw() { // Make the outline of the stroke from the path Shape sh = bs.createStrokedShape(path); glu.gluTessBeginPolygon(tobj, null); // second param to gluTessVertex is for a user defined object that contains // additional info about this point, but that's not needed for anything float lastX = 0; float lastY = 0; // "unfortunately the tesselator won't work properly unless a // new array of doubles is allocated for each point. that bites ass, // but also just reaffirms that in order to make things fast, // display lists will be the way to go." -- Ben Fry :) double vertex[]; float coords[] = new float[6]; PathIterator iter = sh.getPathIterator(null); // ,5) add a number on here to simplify verts int rule = iter.getWindingRule(); switch(rule) { case PathIterator.WIND_EVEN_ODD: glu.gluTessProperty(tobj, GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_ODD); break; case PathIterator.WIND_NON_ZERO: glu.gluTessProperty(tobj, GLU.GLU_TESS_WINDING_RULE, GLU.GLU_TESS_WINDING_NONZERO); break; } g.beginGL(); float ir = strokeColor >> 16 & 0xff; float ig = strokeColor >> 8 & 0xff; float ib = strokeColor & 0xff; float ia = strokeColor >> 24 & 0xff; float cr = (float)ir/255.0f; float cg = (float)ig/255.0f; float cb = (float)ib/255.0f; float ca = (float)ia/255.0f; gl.glColor4f(cr,cg,cb,ca); while (!iter.isDone()) { switch (iter.currentSegment(coords)) { case PathIterator.SEG_MOVETO: // 1 point (2 vars) in coords glu.gluTessBeginContour(tobj); case PathIterator.SEG_LINETO: // 1 point vertex = new double[] { coords[0], coords[1], 0 }; glu.gluTessVertex(tobj, vertex, 0, vertex); lastX = coords[0]; lastY = coords[1]; break; case PathIterator.SEG_QUADTO: // 2 points for (int i = 1; i < g.bezierDetail; i++) { float t = (float)i / (float)g.bezierDetail; vertex = new double[] { g.bezierPoint(lastX, coords[0], coords[2], coords[2], t), g.bezierPoint(lastY, coords[1], coords[3], coords[3], t), 0 }; glu.gluTessVertex(tobj, vertex, 0, vertex); } lastX = coords[2]; lastY = coords[3]; break; case PathIterator.SEG_CUBICTO: // 3 points for (int i = 1; i < g.bezierDetail; i++) { float t = (float)i / (float)g.bezierDetail; vertex = new double[] { g.bezierPoint(lastX, coords[0], coords[2], coords[4], t), g.bezierPoint(lastY, coords[1], coords[3], coords[5], t), 0 }; glu.gluTessVertex(tobj, vertex, 0, vertex); } lastX = coords[4]; lastY = coords[5]; break; case PathIterator.SEG_CLOSE: glu.gluTessEndContour(tobj); break; } iter.next(); } glu.gluTessEndPolygon(tobj); g.endGL(); } ////// GLUtessellatorCallbackAdapter functions public void begin(int type) { gl.glBegin(type); } public void end() { gl.glEnd(); } public void vertex(Object data) { if (data instanceof double[]) { double[] d = (double[]) data; if (d.length != 3) { throw new RuntimeException("TessCallback vertex() data isn't length 3"); } gl.glVertex3d(d[0], d[1], d[2]); } else { throw new RuntimeException("TessCallback vertex() data not understood"); } } public void error(int errnum) { throw new RuntimeException("Tessellation Error: " + glu.gluErrorString(errnum)); } public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) { double[] vertex = new double[coords.length]; vertex[0] = coords[0]; vertex[1] = coords[1]; vertex[2] = coords[2]; outData[0] = vertex; } ////// end GLUtessellatorCallbackAdapter functions ////// Processing style stroke settings public void stroke(int gr) { strokeColor = p.color(gr); } public void stroke(int gr, int a) { strokeColor = p.color(gr, a); } public void stroke(int r, int gr, int b) { strokeColor = p.color(r, gr, b); } public void stroke(int r, int gr, int b, int a) { strokeColor = p.color(r, gr, b, a); } public void stroke(float gr) { strokeColor = p.color(gr); } public void stroke(float gr, float a) { strokeColor = p.color(gr, a); } public void stroke(float r, float gr, float b) { strokeColor = p.color(r, gr, b); } public void stroke(float r, float gr, float b, float a) { strokeColor = p.color(r, gr, b, a); } public void strokeWeight(float sw) { strokeWeight = sw; bs = new BasicStroke(strokeWeight, strokeCap, strokeJoin); } public void strokeCap(int cap) { strokeCap = cap == ROUND ? BasicStroke.CAP_ROUND : cap == PROJECT ? BasicStroke.CAP_SQUARE : BasicStroke.CAP_BUTT; bs = new BasicStroke(strokeWeight, strokeCap, strokeJoin); } public void strokeJoin(int join) { strokeJoin = join == ROUND ? BasicStroke.JOIN_ROUND : join == BEVEL ? BasicStroke.JOIN_BEVEL : BasicStroke.JOIN_MITER; bs = new BasicStroke(strokeWeight, strokeCap, strokeJoin); } }