Skip to content

Speed up line rendering with WebGL2 instancing #6162

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

Closed
wants to merge 10 commits into from
Closed
1 change: 1 addition & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import './webgl/light';
import './webgl/loading';
import './webgl/material';
import './webgl/p5.Camera';
import './webgl/p5.DataVector';
import './webgl/p5.Geometry';
import './webgl/p5.Matrix';
import './webgl/p5.RendererGL.Immediate';
Expand Down
52 changes: 52 additions & 0 deletions src/webgl/p5.DataVector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import p5 from '../core/main';

p5.DataVector = class DataVector {
constructor(initialLength = 128) {
this.length = 0;
this.data = new Float32Array(initialLength);
this.initialLength = initialLength;
}

clear() {
this.length = 0;
}

rescale() {
if (this.length < this.data.length / 2) {
// Find the power of 2 size that fits the data
const targetLength = 1 << Math.ceil(Math.log2(this.length));
const newData = new Float32Array(targetLength);
newData.set(this.data.subarray(0, this.length), 0);
this.data = newData;
}
}

reset() {
this.clear();
this.data = new Float32Array(this.initialLength);
}

push(...values) {
this.ensureLength(this.length + values.length);
this.data.set(values, this.length);
this.length += values.length;
}

slice(from, to) {
return this.data.slice(from, Math.min(to, this.length));
}

subArray(from, to) {
return this.data.subarray(from, Math.min(to, this.length));
}

ensureLength(target) {
while (this.data.length < target) {
const newData = new Float32Array(this.data.length * 2);
newData.set(this.data, 0);
this.data = newData;
}
}
};

export default p5.DataVector;
168 changes: 114 additions & 54 deletions src/webgl/p5.Geometry.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,44 @@ p5.Geometry = class {
//@type [p5.Vector]
this.vertices = [];

//an array containing every vertex for stroke drawing
this.lineVertices = [];

// 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 = [];
// In WebGL2 mode, we will use instanced rendering to draw lines, using
// a separate data layout for segments, caps, and joins. In WebGL1 mode,
// we manually duplicate data into one big buffer.
this.lineData = {};
for (const key of ['segments', 'caps', 'joins', 'stroke']) {
const data = {};

data.count = 0;

//an array containing every vertex for stroke drawing
data.lineVertices = new p5.DataVector();

// 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.
data.lineTangentsIn = new p5.DataVector();
data.lineTangentsOut = new p5.DataVector();

// One color per line vertex, generated automatically based on
// vertexStrokeColors in _edgesToVertices()
data.lineVertexColors = new p5.DataVector();

this.lineData[key] = data;
}

// 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 = [];
this.lineData.segments.lineSides = Float32Array.from([1, 3, -1, 3, -3, -1]);
this.lineData.caps.lineSides = Float32Array.from([-1, 2, -2, 2, -1, 1]);
this.lineData.joins.lineSides = Float32Array.from([
-1, -3, -2, -1, 0, -3,
1, 2, 3, 1, 3, 0
]);
this.lineData.stroke.lineSides = [];

//an array containing 1 normal per vertex
//@type [p5.Vector]
Expand All @@ -58,12 +80,10 @@ p5.Geometry = class {
// One color per vertex representing the stroke color at that vertex
this.vertexStrokeColors = [];

// One color per line vertex, generated automatically based on
// vertexStrokeColors in _edgesToVertices()
this.lineVertexColors = [];
this.detailX = detailX !== undefined ? detailX : 1;
this.detailY = detailY !== undefined ? detailY : 1;
this.dirtyFlags = {};
this.webgl1StrokeDataDirty = true;

if (callback instanceof Function) {
callback.call(this);
Expand All @@ -72,22 +92,66 @@ p5.Geometry = class {
}

reset() {
this.lineVertices.length = 0;
this.lineTangentsIn.length = 0;
this.lineTangentsOut.length = 0;
this.lineSides.length = 0;
for (const key in this.lineData) {
this.lineData[key].count = 0;
this.lineData[key].lineVertices.clear();
this.lineData[key].lineTangentsIn.clear();
this.lineData[key].lineTangentsOut.clear();
this.lineData[key].lineVertexColors.clear();
}

this.webgl1StrokeDataDirty = true;
this.lineData.stroke.lineSides = [];
this.vertices.length = 0;
this.edges.length = 0;
this.vertexColors.length = 0;
this.vertexStrokeColors.length = 0;
this.lineVertexColors.length = 0;
this.vertexNormals.length = 0;
this.uvs.length = 0;

this.dirtyFlags = {};
}

/**
* Used in WebGL1 mode when instanced rendering is not available. To draw
* strokes without instanced rendering, we manually duplicate vertices on
* the CPU. This will be slower but will at least work.
*/
_makeStrokeBufferData() {
if (!this.webgl1StrokeDataDirty) return;

const strokeData = this.lineData.stroke;
for (const key of ['joins', 'caps', 'segments']) {
const instanceData = this.lineData[key];
for (const buffer in instanceData) {
if (buffer === 'count') continue;
if (buffer === 'lineSides') {
for (let i = 0; i < instanceData.count; i++) {
strokeData[buffer].push(...instanceData[buffer]);
}
} else {
const valsPerInstance =
(buffer === 'lineVertexColors' || buffer === 'lineVertices')
? 2 : 1;
const size = buffer === 'lineVertexColors' ? 4 : 3;
const stride = valsPerInstance * size;
for (
let i = 0;
i < instanceData[buffer].length;
i += stride
) {
for (let j = 0; j < instanceData.lineSides.length; j++) {
strokeData[buffer].push(
...instanceData[buffer].subArray(i, i + stride)
);
}
}
}
}
}
this.webgl1StrokeDataDirty = false;
}

/**
* computes faces for geometry objects based on the vertices.
* @method computeFaces
Expand Down Expand Up @@ -262,14 +326,17 @@ p5.Geometry = class {
* @chainable
*/
_edgesToVertices() {
this.lineVertices.length = 0;
this.lineTangentsIn.length = 0;
this.lineTangentsOut.length = 0;
this.lineSides.length = 0;
for (const key in this.lineData) {
this.lineData[key].count = 0;
this.lineData[key].lineVertices.clear();
this.lineData[key].lineTangentsIn.clear();
this.lineData[key].lineTangentsOut.clear();
this.lineData[key].lineVertexColors.clear();
}

const closed =
this.edges.length > 1 &&
this.edges[0][0] === this.edges[this.edges.length - 1][1];
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++) {
Expand Down Expand Up @@ -356,11 +423,11 @@ p5.Geometry = class {
* and the side of the centerline each vertex belongs to. Sides follow the
* following scheme:
*
* -1 -1
* -1 -3
* o-------------o
* | |
* o-------------o
* 1 1
* 1 3
*
* @private
* @chainable
Expand All @@ -375,19 +442,14 @@ p5.Geometry = class {
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
this.lineData.segments.lineTangentsIn.push(...dirArr);
this.lineData.segments.lineTangentsOut.push(...dirArr);
this.lineData.segments.lineVertices.push(...a, ...b);
this.lineData.segments.lineVertexColors.push(
...fromColor,
...toColor
);
this.lineData.segments.count++;
return this;
}

Expand All @@ -411,13 +473,13 @@ p5.Geometry = class {
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);
this.lineData.caps.lineVertices.push(...ptArray, ...ptArray);
//this.lineData.caps.lineVertices.push(...ptArray);
this.lineData.caps.lineTangentsIn.push(...tanInArray);
this.lineData.caps.lineTangentsOut.push(...tanOutArray);
this.lineData.caps.lineVertexColors.push(...color, ...color);
//this.lineData.caps.lineVertexColors.push(...color);
this.lineData.caps.count++;
return this;
}

Expand Down Expand Up @@ -453,15 +515,13 @@ p5.Geometry = class {
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.lineData.joins.lineVertices.push(...ptArray, ...ptArray);
//this.lineData.joins.lineVertices.push(...ptArray);
this.lineData.joins.lineTangentsIn.push(...tanInArray);
this.lineData.joins.lineTangentsOut.push(...tanOutArray);
this.lineData.joins.lineVertexColors.push(...color, ...color);
//this.lineData.joins.lineVertexColors.push(...color);
this.lineData.joins.count++;
return this;
}

Expand Down
Loading