Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stroke cap+join support for WebGL #5802

Merged
merged 10 commits into from
Jan 25, 2023
1,655 changes: 751 additions & 904 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"libtess": "^1.2.2",
"lint-staged": "^4.3.0",
"marked": "^4.0.10",
"mocha": "^10.2.0",
"mocha": "^6.2.2",
"np": "^7.6.2",
"omggif": "^1.0.10",
"open": "^7.0.3",
Expand Down
2 changes: 1 addition & 1 deletion src/core/shape/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ p5.prototype.strokeCap = function(cap) {
* Sets the style of the joints which connect line segments. These joints
* are either mitered, beveled, or rounded and specified with the
* corresponding parameters: `MITER`, `BEVEL`, or `ROUND`. The default joint is
* `MITER`.
* `MITER` in 2D mode and `ROUND` in WebGL mode.
*
* The parameter to this method must be written in ALL CAPS because they are
* predefined as constants in ALL CAPS and Javascript is a case-sensitive language.
Expand Down
248 changes: 212 additions & 36 deletions src/webgl/p5.Geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@ p5.Geometry = function(detailX, detailY, callback) {
//an array containing every vertex for stroke drawing
this.lineVertices = [];

//an array 1 normal per lineVertex with
//final position representing which direction to
//displace for strokeWeight
//[[0,0,-1,1], [0,1,0,-1] ...];
this.lineNormals = [];
// The tangents going into or out of a vertex on a line. Along a straight
// line segment, both should be equal. At an endpoint, one or the other
// will not exist and will be all 0. In joins between line segments, they
// may be different, as they will be the tangents on either side of the join.
this.lineTangentsIn = [];
this.lineTangentsOut = [];

// When drawing lines with thickness, entries in this buffer represent which
// side of the centerline the vertex will be placed. The sign of the number
// will represent the side of the centerline, and the absolute value will be
// used as an enum to determine which part of the cap or join each vertex
// represents. See the doc comments for _addCap and _addJoin for diagrams.
this.lineSides = [];

//an array containing 1 normal per vertex
//@type [p5.Vector]
Expand Down Expand Up @@ -64,7 +72,9 @@ p5.Geometry = function(detailX, detailY, callback) {

p5.Geometry.prototype.reset = function() {
this.lineVertices.length = 0;
this.lineNormals.length = 0;
this.lineTangentsIn.length = 0;
this.lineTangentsOut.length = 0;
this.lineSides.length = 0;

this.vertices.length = 0;
this.edges.length = 0;
Expand Down Expand Up @@ -240,55 +250,221 @@ p5.Geometry.prototype._makeTriangleEdges = function() {
};

/**
* Create 4 vertices for each stroke line, two at the beginning position
* and two at the end position. These vertices are displaced relative to
* that line's normal on the GPU
* Converts each line segment into the vertices and vertex attributes needed
* to turn the line into a polygon on screen. This will include:
* - Two triangles line segment to create a rectangle
* - Two triangles per endpoint to create a stroke cap rectangle. A fragment
* shader is responsible for displaying the appropriate cap style within
* that rectangle.
* - Four triangles per join between adjacent line segments, creating a quad on
* either side of the join, perpendicular to the lines. A vertex shader will
* discard the quad in the "elbow" of the join, and a fragment shader will
* display the appropriate join style within the remaining quad.
*
* @private
* @chainable
*/
p5.Geometry.prototype._edgesToVertices = function() {
const lineColorData = [];
this.lineVertices.length = 0;
this.lineNormals.length = 0;

this.lineTangentsIn.length = 0;
this.lineTangentsOut.length = 0;
this.lineSides.length = 0;

const closed =
this.edges.length > 1 &&
this.edges[0][0] === this.edges[this.edges.length - 1][1];
let addedStartingCap = false;
let lastValidDir;
for (let i = 0; i < this.edges.length; i++) {
const endIndex0 = this.edges[i][0];
const endIndex1 = this.edges[i][1];
const begin = this.vertices[endIndex0];
const end = this.vertices[endIndex1];
const prevEdge = this.edges[i - 1];
const currEdge = this.edges[i];
const begin = this.vertices[currEdge[0]];
const end = this.vertices[currEdge[1]];
const fromColor = this.vertexStrokeColors.length > 0
? this.vertexStrokeColors.slice(
endIndex0 * 4,
(endIndex0 + 1) * 4
currEdge[0] * 4,
(currEdge[0] + 1) * 4
)
: [0, 0, 0, 0];
const toColor = this.vertexStrokeColors.length > 0
? this.vertexStrokeColors.slice(
endIndex1 * 4,
(endIndex1 + 1) * 4
currEdge[1] * 4,
(currEdge[1] + 1) * 4
)
: [0, 0, 0, 0];
const dir = end
.copy()
.sub(begin)
.normalize();
const a = begin.array();
const b = begin.array();
const c = end.array();
const d = end.array();
const dirAdd = dir.array();
const dirSub = dir.array();
// below is used to displace the pair of vertices at beginning and end
// in opposite directions
dirAdd.push(1);
dirSub.push(-1);
this.lineNormals.push(dirAdd, dirSub, dirAdd, dirAdd, dirSub, dirSub);
this.lineVertices.push(a, b, c, c, b, d);
lineColorData.push(
fromColor, fromColor, toColor, toColor, fromColor, toColor
);
const dirOK = dir.magSq() > 0;
if (dirOK) {
this._addSegment(begin, end, fromColor, toColor, dir);
}

if (i > 0 && prevEdge[1] === currEdge[0]) {
// Add a join if this segment shares a vertex with the previous. Skip
// actually adding join vertices if either the previous segment or this
// one has a length of 0.
//
// Don't add a join if the tangents point in the same direction, which
// would mean the edges line up exactly, and there is no need for a join.
if (lastValidDir && dirOK && dir.dot(lastValidDir) < 1 - 1e-8) {
this._addJoin(begin, lastValidDir, dir, fromColor);
}
if (dirOK && !addedStartingCap && !closed) {
this._addCap(begin, dir.copy().mult(-1), fromColor);
addedStartingCap = true;
}
} else {
addedStartingCap = false;
// Start a new line
if (dirOK && (!closed || i > 0)) {
this._addCap(begin, dir.copy().mult(-1), fromColor);
addedStartingCap = true;
}
if (lastValidDir && (!closed || i < this.edges.length - 1)) {
// Close off the last segment with a cap
this._addCap(this.vertices[prevEdge[1]], lastValidDir, fromColor);
lastValidDir = undefined;
}
}

if (i === this.edges.length - 1) {
if (closed) {
this._addJoin(
end,
dir,
this.vertices[this.edges[0][1]]
.copy()
.sub(end)
.normalize(),
toColor
);
} else {
this._addCap(end, dir, toColor);
}
}

if (dirOK) {
lastValidDir = dir;
}
}
return this;
};

/**
* Adds the vertices and vertex attributes for two triangles making a rectangle
* for a straight line segment. A vertex shader is responsible for picking
* proper coordinates on the screen given the centerline positions, the tangent,
* and the side of the centerline each vertex belongs to. Sides follow the
* following scheme:
*
* -1 -1
* o-------------o
* | |
* o-------------o
* 1 1
*
* @private
* @chainable
*/
p5.Geometry.prototype._addSegment = function(
begin,
end,
fromColor,
toColor,
dir
) {
const a = begin.array();
const b = end.array();
const dirArr = dir.array();
this.lineSides.push(1, -1, 1, 1, -1, -1);
for (const tangents of [this.lineTangentsIn, this.lineTangentsOut]) {
tangents.push(dirArr, dirArr, dirArr, dirArr, dirArr, dirArr);
}
this.lineVertices.push(a, a, b, b, a, b);
this.lineVertexColors.push(
fromColor,
fromColor,
toColor,
toColor,
fromColor,
toColor
);
return this;
};

/**
* Adds the vertices and vertex attributes for two triangles representing the
* stroke cap of a line. A fragment shader is responsible for displaying the
* appropriate cap style within the rectangle they make.
*
* The lineSides buffer will include the following values for the points on
* the cap rectangle:
*
* -1 -2
* -----------o---o
* | |
* -----------o---o
* 1 2
* @private
* @chainable
*/
p5.Geometry.prototype._addCap = function(point, tangent, color) {
const ptArray = point.array();
const tanInArray = tangent.array();
const tanOutArray = [0, 0, 0];
for (let i = 0; i < 6; i++) {
this.lineVertices.push(ptArray);
this.lineTangentsIn.push(tanInArray);
this.lineTangentsOut.push(tanOutArray);
this.lineVertexColors.push(color);
}
this.lineSides.push(-1, -2, 2, 2, 1, -1);
return this;
};

/**
* Adds the vertices and vertex attributes for four triangles representing a
* join between two adjacent line segments. This creates a quad on either side
* of the shared vertex of the two line segments, with each quad perpendicular
* to the lines. A vertex shader will discard all but the quad in the "elbow" of
* the join, and a fragment shader will display the appropriate join style
* within the remaining quad.
*
* The lineSides buffer will include the following values for the points on
* the join rectangles:
*
* -1 -2
* -------------o----o
* | |
* 1 o----o----o -3
* | | 0 |
* --------o----o |
* 2| 3 |
* | |
* | |
* @private
* @chainable
*/
p5.Geometry.prototype._addJoin = function(
point,
fromTangent,
toTangent,
color
) {
const ptArray = point.array();
const tanInArray = fromTangent.array();
const tanOutArray = toTangent.array();
for (let i = 0; i < 12; i++) {
this.lineVertices.push(ptArray);
this.lineTangentsIn.push(tanInArray);
this.lineTangentsOut.push(tanOutArray);
this.lineVertexColors.push(color);
}
for (const side of [-1, 1]) {
this.lineSides.push(side, 2 * side, 3 * side, side, 3 * side, 0);
}
this.lineVertexColors = lineColorData;
return this;
};

Expand Down
8 changes: 8 additions & 0 deletions src/webgl/p5.RendererGL.Immediate.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@ p5.RendererGL.prototype._drawImmediateFill = function() {
*/
p5.RendererGL.prototype._drawImmediateStroke = function() {
const gl = this.GL;

const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE);
// Prevent strokes from getting removed by culling
gl.disable(gl.CULL_FACE);

const shader = this._getImmediateStrokeShader();
this._useLineColor =
(this.immediateMode.geometry.vertexStrokeColors.length > 0);
Expand All @@ -426,6 +431,9 @@ p5.RendererGL.prototype._drawImmediateStroke = function() {
0,
this.immediateMode.geometry.lineVertices.length
);
if (faceCullingEnabled) {
gl.enable(gl.CULL_FACE);
}
shader.unbindShader();
};

Expand Down
6 changes: 6 additions & 0 deletions src/webgl/p5.RendererGL.Retained.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ p5.RendererGL.prototype.drawBuffers = function(gId) {
const geometry = this.retainedMode.geometry[gId];

if (this._doStroke && geometry.lineVertexCount > 0) {
const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE);
// Prevent strokes from getting removed by culling
gl.disable(gl.CULL_FACE);
const strokeShader = this._getRetainedStrokeShader();
this._useLineColor = (geometry.model.vertexStrokeColors.length > 0);
this._setStrokeUniforms(strokeShader);
Expand All @@ -126,6 +129,9 @@ p5.RendererGL.prototype.drawBuffers = function(gId) {
}
this._applyColorBlend(this.curStrokeColor);
this._drawArrays(gl.TRIANGLES, gId);
if (faceCullingEnabled) {
gl.enable(gl.CULL_FACE);
}
strokeShader.unbindShader();
}

Expand Down
Loading