Stroke attributes in OpenGL
From Processing
| Versions: | 1.0+ |
| Contributors: | tomc, Karl D.D. Willis |
| Started: | 2008-02-08 |
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).
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.
Source Code
/** openglstrokes taken from http://wiki.processing.org/index.php?title=Stroke_attributes_in_OpenGL @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(); } /** LineGraphics.java from http://wiki.processing.org/index.php?title=Stroke_attributes_in_OpenGL @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); } }