diff --git a/src/app.js b/src/app.js index 763ed82a15..4cd7b6fc07 100644 --- a/src/app.js +++ b/src/app.js @@ -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'; diff --git a/src/webgl/p5.DataVector.js b/src/webgl/p5.DataVector.js new file mode 100644 index 0000000000..a259160857 --- /dev/null +++ b/src/webgl/p5.DataVector.js @@ -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; diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 34eee396e6..61b3fd8e61 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -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] @@ -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); @@ -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 @@ -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++) { @@ -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 @@ -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; } @@ -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; } @@ -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; } diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index 246bc8fa29..c2b85c642f 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -8,6 +8,28 @@ p5.RenderBuffer = class { this.attr = attr; // the name of the vertex attribute this._renderer = renderer; this.map = map; // optional, a transformation function to apply to src + this._namespace = undefined; + this._stride = 0; + this._offset = 0; + this._divisor = undefined; + } + namespace(namespace) { + // A namespace within a p5.Geometry in which to find the source buffer and + // to store the destination buffer + this._namespace = namespace.split('.'); + return this; + } + stride(stride) { + this._stride = stride; // how many bytes between the starts of adjacent values + return this; + } + offset(offset) { + this._offset = offset; // how many bytes to start at + return this; + } + divisor(divisor) { + this._divisor = divisor; // how many instances it stays the same for + return this; } /** @@ -27,6 +49,25 @@ p5.RenderBuffer = class { model = geometry; } + let geometryData = geometry; + if (this._namespace) { + for (const prefix of this._namespace) { + if (!geometryData[prefix]) { + geometryData[prefix] = {}; + } + geometryData = geometryData[prefix]; + } + } + let modelData = model; + if (this._namespace) { + for (const prefix of this._namespace) { + if (!modelData[prefix]) { + modelData[prefix] = {}; + } + modelData = modelData[prefix]; + } + } + // loop through each of the buffer definitions const attr = attributes[this.attr]; if (!attr) { @@ -34,20 +75,24 @@ p5.RenderBuffer = class { } // check if the model has the appropriate source array - let buffer = geometry[this.dst]; - const src = model[this.src]; + let buffer = geometryData[this.dst]; + const src = modelData[this.src]; if (src.length > 0) { // check if we need to create the GL buffer const createBuffer = !buffer; if (createBuffer) { // create and remember the buffer - geometry[this.dst] = buffer = gl.createBuffer(); + geometryData[this.dst] = buffer = gl.createBuffer(); } // bind the buffer gl.bindBuffer(gl.ARRAY_BUFFER, buffer); // check if we need to fill the buffer with data - if (createBuffer || model.dirtyFlags[this.src] !== false) { + let key = this.dst; + if (this._namespace) { + key = this._namespace.join('.') + '.' + key; + } + if (createBuffer || model.dirtyFlags[key] !== false) { const map = this.map; // get the values from the model, possibly transformed const values = map ? map(src) : src; @@ -55,17 +100,26 @@ p5.RenderBuffer = class { this._renderer._bindBuffer(buffer, gl.ARRAY_BUFFER, values); // mark the model's source array as clean - model.dirtyFlags[this.src] = false; + model.dirtyFlags[key] = false; } + // enable the attribute - shader.enableAttrib(attr, this.size); + shader.enableAttrib( + attr, // location + this.size, // size + gl.FLOAT, // type + false, // normalized + this._stride, + this._offset, + this._divisor + ); } else { const loc = attr.location; - if (loc === -1 || !this._renderer.registerEnabled[loc]) { return; } + if (loc === -1 || !this._renderer.registerEnabled.has(loc)) { return; } // Disable register corresponding to unused attribute gl.disableVertexAttribArray(loc); // Record register availability - this._renderer.registerEnabled[loc] = false; + this._renderer.registerEnabled.delete(loc); } } }; diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 714d29f982..8e45742e67 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -195,12 +195,12 @@ p5.RendererGL.prototype.endShape = function( this._processVertices(...arguments); this.isProcessingVertices = false; if (this._doFill) { - if (this.immediateMode.geometry.vertices.length > 1) { + if (this.immediateMode.geometry.vertices.length >= 3) { this._drawImmediateFill(); } } if (this._doStroke) { - if (this.immediateMode.geometry.lineVertices.length > 1) { + if (this.immediateMode.geometry.edges.length > 0) { this._drawImmediateStroke(); } } @@ -375,6 +375,7 @@ p5.RendererGL.prototype._drawImmediateFill = function() { this._setFillUniforms(shader); + //this.disableAttributes(); for (const buff of this.immediateMode.buffers.fill) { buff._prepareBuffer(this.immediateMode.geometry, shader); } @@ -415,25 +416,37 @@ 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); this._setStrokeUniforms(shader); - for (const buff of this.immediateMode.buffers.stroke) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } this._applyColorBlend(this.curStrokeColor); - gl.drawArrays( - gl.TRIANGLES, - 0, - this.immediateMode.geometry.lineVertices.length - ); - if (faceCullingEnabled) { - gl.enable(gl.CULL_FACE); + if (this.webglVersion === constants.WEBGL2) { + for (const key of ['joins', 'caps', 'segments']) { + const count = + this.immediateMode.geometry.lineData[key].count; + if (count === 0) continue; + //this.disableAttributes(); + for (const buff of this.immediateMode.buffers[key]) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + gl.drawArraysInstanced( + gl.TRIANGLES, + 0, + this.immediateMode.geometry.lineData[key].lineSides.length, + count + ); + } + } else { + this.immediateMode.geometry._makeStrokeBufferData(); + for (const buff of this.immediateMode.buffers.stroke) { + buff._prepareBuffer(this.immediateMode.geometry, shader); + } + gl.drawArrays( + gl.TRIANGLES, + 0, + this.immediateMode.geometry.lineData.stroke.lineSides.length + ); } shader.unbindShader(); }; diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index a6f9931333..e246f6f24a 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -103,8 +103,6 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { buffers.vertexCount = model.vertices ? model.vertices.length : 0; } - buffers.lineVertexCount = model.lineVertices ? model.lineVertices.length : 0; - return buffers; }; @@ -118,10 +116,11 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { const gl = this.GL; const geometry = this.retainedMode.geometry[gId]; - if (this._doFill) { + if (this._doFill && this.retainedMode.geometry[gId].vertexCount > 0) { this._useVertexColor = (geometry.model.vertexColors.length > 0); const fillShader = this._getRetainedFillShader(); this._setFillUniforms(fillShader); + //this.disableAttributes(); for (const buff of this.retainedMode.buffers.fill) { buff._prepareBuffer(geometry, fillShader); } @@ -134,20 +133,27 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { fillShader.unbindShader(); } - 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); + if (this._doStroke && geometry.model.edges.length > 0) { const strokeShader = this._getRetainedStrokeShader(); this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); this._setStrokeUniforms(strokeShader); - for (const buff of this.retainedMode.buffers.stroke) { - buff._prepareBuffer(geometry, strokeShader); - } this._applyColorBlend(this.curStrokeColor); - this._drawArrays(gl.TRIANGLES, gId); - if (faceCullingEnabled) { - gl.enable(gl.CULL_FACE); + if (this.webglVersion === constants.WEBGL2) { + for (const key of ['joins', 'caps', 'segments']) { + const { count } = this.retainedMode.geometry[gId].model.lineData[key]; + if (count === 0) continue; + //this.disableAttributes(); + for (const buff of this.retainedMode.buffers[key]) { + buff._prepareBuffer(geometry, strokeShader); + } + this._drawArraysInstanced(gl.TRIANGLES, gId, key); + } + } else { + geometry.model._makeStrokeBufferData(); + for (const buff of this.retainedMode.buffers.stroke) { + buff._prepareBuffer(geometry, strokeShader); + } + this._drawArrays(gl.TRIANGLES, gId); } strokeShader.unbindShader(); } @@ -189,7 +195,19 @@ p5.RendererGL.prototype._drawArrays = function(drawMode, gId) { this.GL.drawArrays( drawMode, 0, - this.retainedMode.geometry[gId].lineVertexCount + this.retainedMode.geometry[gId].model.lineData.stroke.lineSides.length + ); + return this; +}; + +p5.RendererGL.prototype._drawArraysInstanced = function(drawMode, gId, key) { + const count = + this.retainedMode.geometry[gId].model.lineData[key].count; + this.GL.drawArraysInstanced( + drawMode, + 0, + this.retainedMode.geometry[gId].model.lineData[key].lineSides.length, + count ); return this; }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index f5f28e60f7..af49577c52 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -148,7 +148,7 @@ p5.RendererGL = function (elt, pInst, isMainCanvas, attr) { this._useLineColor = false; this._useVertexColor = false; - this.registerEnabled = []; + this.registerEnabled = new Set(); this._tint = [255, 255, 255, 255]; @@ -194,18 +194,132 @@ p5.RendererGL = function (elt, pInst, isMainCanvas, attr) { this.userStrokeShader = undefined; this.userPointShader = undefined; + this._prevAttributeParams = {}; + + const makeStrokeBuffer = prefix => [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineFromColorBuffer', 'aFromVertexColor', this) + .namespace(`lineData.${prefix}`) + .divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 4 * 2) + .offset(0), + new p5.RenderBuffer(4, 'lineVertexColors', 'lineToColorBuffer', 'aToVertexColor', this) + .namespace(`lineData.${prefix}`) + .divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 4 * 2) + .offset(Float32Array.BYTES_PER_ELEMENT * 4), + new p5.RenderBuffer(3, 'lineVertices', 'lineFromVerticesBuffer', 'aFromPosition', this) + .namespace(`lineData.${prefix}`) + .divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 3 * 2) + .offset(0), + new p5.RenderBuffer(3, 'lineVertices', 'lineToVerticesBuffer', 'aToPosition', this) + .namespace(`lineData.${prefix}`).divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 3 * 2) + .offset(Float32Array.BYTES_PER_ELEMENT * 3), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this) + .namespace(`lineData.${prefix}`).divisor(1), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this) + .namespace(`lineData.${prefix}`).divisor(1), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + .namespace(`lineData.${prefix}`).divisor(0) + ]; + + const makeStrokeBuffers = () => { + return { + stroke: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineFromColorBuffer', 'aFromVertexColor', this) + .namespace('lineData.stroke') + .stride(Float32Array.BYTES_PER_ELEMENT * 4 * 2) + .offset(0), + new p5.RenderBuffer(4, 'lineVertexColors', 'lineToColorBuffer', 'aToVertexColor', this) + .namespace('lineData.stroke') + .stride(Float32Array.BYTES_PER_ELEMENT * 4 * 2) + .offset(Float32Array.BYTES_PER_ELEMENT * 4), + new p5.RenderBuffer(3, 'lineVertices', 'lineFromVerticesBuffer', 'aFromPosition', this) + .namespace('lineData.stroke') + .stride(Float32Array.BYTES_PER_ELEMENT * 3 * 2) + .offset(0), + new p5.RenderBuffer(3, 'lineVertices', 'lineToVerticesBuffer', 'aToPosition', this) + .namespace('lineData.stroke') + .stride(Float32Array.BYTES_PER_ELEMENT * 3 * 2) + .offset(Float32Array.BYTES_PER_ELEMENT * 3), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this) + .namespace('lineData.stroke'), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this) + .namespace('lineData.stroke'), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + .namespace('lineData.stroke') + ], + segments: makeStrokeBuffer('segments'), + caps: makeStrokeBuffer('caps'), + joins: makeStrokeBuffer('joins') + /*segments: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineFromColorBuffer', 'aFromVertexColor', this) + .namespace('lineData.segments') + .divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 4 * 2) + .offset(0), + new p5.RenderBuffer(4, 'lineVertexColors', 'lineToColorBuffer', 'aToVertexColor', this) + .namespace('lineData.segments') + .divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 4 * 2) + .offset(Float32Array.BYTES_PER_ELEMENT * 4), + new p5.RenderBuffer(3, 'lineVertices', 'lineFromVerticesBuffer', 'aFromPosition', this) + .namespace('lineData.segments') + .divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 3 * 2) + .offset(0), + new p5.RenderBuffer(3, 'lineVertices', 'lineToVerticesBuffer', 'aToPosition', this) + .namespace('lineData.segments').divisor(1) + .stride(Float32Array.BYTES_PER_ELEMENT * 3 * 2) + .offset(Float32Array.BYTES_PER_ELEMENT * 3), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this) + .namespace('lineData.segments').divisor(1), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this) + .namespace('lineData.segments').divisor(1), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + .namespace('lineData.segments').divisor(0) + ], + caps: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineFromColorBuffer', 'aFromVertexColor', this) + .namespace('lineData.caps').divisor(1), + new p5.RenderBuffer(4, 'lineVertexColors', 'lineToColorBuffer', 'aToVertexColor', this) + .namespace('lineData.caps').divisor(1), + new p5.RenderBuffer(3, 'lineVertices', 'lineFromVerticesBuffer', 'aFromPosition', this) + .namespace('lineData.caps').divisor(1), + new p5.RenderBuffer(3, 'lineVertices', 'lineToVerticesBuffer', 'aToPosition', this) + .namespace('lineData.caps').divisor(1), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this) + .namespace('lineData.caps').divisor(1), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this) + .namespace('lineData.caps').divisor(1), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + .namespace('lineData.caps').divisor(0) + ], + joins: [ + new p5.RenderBuffer(4, 'lineVertexColors', 'lineFromColorBuffer', 'aFromVertexColor', this) + .namespace('lineData.joins').divisor(1), + new p5.RenderBuffer(4, 'lineVertexColors', 'lineToColorBuffer', 'aToVertexColor', this) + .namespace('lineData.joins').divisor(1), + new p5.RenderBuffer(3, 'lineVertices', 'lineFromVerticesBuffer', 'aFromPosition', this) + .namespace('lineData.joins').divisor(1), + new p5.RenderBuffer(3, 'lineVertices', 'lineToVerticesBuffer', 'aToPosition', this) + .namespace('lineData.joins').divisor(1), + new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this) + .namespace('lineData.joins').divisor(1), + new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this) + .namespace('lineData.joins').divisor(1), + new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + .namespace('lineData.joins').divisor(0) + ]*/ + }; + }; + // Default drawing is done in Retained Mode // Geometry and Material hashes stored here this.retainedMode = { geometry: {}, - buffers: { - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this, this._flatten), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this, this._flatten), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], + buffers: Object.assign({ fill: [ new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), @@ -218,7 +332,7 @@ p5.RendererGL = function (elt, pInst, isMainCanvas, attr) { new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) ] - } + }, makeStrokeBuffers()) }; // Immediate Mode @@ -229,7 +343,7 @@ p5.RendererGL = function (elt, pInst, isMainCanvas, attr) { _bezierVertex: [], _quadraticVertex: [], _curveVertex: [], - buffers: { + buffers: Object.assign({ fill: [ new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), new p5.RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), @@ -237,15 +351,8 @@ p5.RendererGL = function (elt, pInst, isMainCanvas, attr) { new p5.RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) ], - stroke: [ - new p5.RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this, this._flatten), - new p5.RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this, this._flatten), - new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this, this._flatten), - new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], point: this.GL.createBuffer() - } + }, makeStrokeBuffers()) }; this.pointSize = 5.0; //default point size @@ -614,6 +721,13 @@ p5.prototype.setAttributes = function (key, value) { * @class p5.RendererGL */ +p5.RendererGL.prototype.disableAttributes = function() { + for (const loc of this.registerEnabled.values()) { + this.GL.disableVertexAttribArray(loc); + this.registerEnabled.delete(loc); + } +}; + p5.RendererGL.prototype._update = function () { // reset model view and apply initial camera transform // (containing only look at info; no projection). @@ -1616,13 +1730,26 @@ p5.RendererGL.prototype._bindBuffer = function ( target, values, type, - usage + { + usage = this.GL.STATIC_DRAW, + count = values && values.length, + offset = 0 + } = {} ) { if (!target) target = this.GL.ARRAY_BUFFER; this.GL.bindBuffer(target, buffer); if (values !== undefined) { - const data = new (type || Float32Array)(values); - this.GL.bufferData(target, data, usage || this.GL.STATIC_DRAW); + if (values.data) { + // bufferData seems to send the whole Float32Array to the GPU even if + // it only uses a small part of it, so if we have a DataVector whose size is + // way larger than its data, we need to scale it back down + values.rescale(); + } + let data = values.data || values; + if (!(data instanceof (type || Float32Array))) { + data = new (type || Float32Array)(data); + } + this.GL.bufferData(target, data, usage, offset, count); } }; diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index ac4bf11bf2..9411b81578 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -7,6 +7,7 @@ */ import p5 from '../core/main'; +import * as constants from '../core/constants'; /** * Shader class for WEBGL Mode @@ -228,7 +229,6 @@ p5.Shader = class { unbindShader() { if (this._bound) { this.unbindTextures(); - //this._renderer.GL.useProgram(0); ?? this._bound = false; } return this; @@ -551,7 +551,7 @@ p5.Shader = class { * @chainable * @private */ - enableAttrib(attr, size, type, normalized, stride, offset) { + enableAttrib(attr, size, type, normalized, stride, offset, divisor) { if (attr) { if ( typeof IS_MINIFIED === 'undefined' && @@ -565,10 +565,28 @@ p5.Shader = class { if (loc !== -1) { const gl = this._renderer.GL; // Enable register even if it is disabled - if (!this._renderer.registerEnabled[loc]) { + if (!this._renderer.registerEnabled.has(loc)) { gl.enableVertexAttribArray(loc); // Record register availability - this._renderer.registerEnabled[loc] = true; + this._renderer.registerEnabled.add(loc); + } + + /*const params = { size, type, normalized, stride, offset, divisor }; + const prevParams = this._renderer._prevAttributeParams[attr.name] || {}; + let changed = false; + for (const key in params) { + if (params[key] !== prevParams[key]) { + changed = true; + break; + } + } + if (!changed) return this; + this._renderer._prevAttributeParams[attr.name] = params;*/ + + if (divisor !== undefined) { + this._renderer.GL.vertexAttribDivisor(loc, divisor); + } else if (this._renderer.webglVersion === constants.WEBGL2) { + this._renderer.GL.vertexAttribDivisor(loc, 0); } this._renderer.GL.vertexAttribPointer( loc, diff --git a/src/webgl/shaders/line.vert b/src/webgl/shaders/line.vert index 968e4824a1..68ca07ea75 100644 --- a/src/webgl/shaders/line.vert +++ b/src/webgl/shaders/line.vert @@ -32,11 +32,13 @@ uniform vec4 uViewport; uniform int uPerspective; uniform int uStrokeJoin; -attribute vec4 aPosition; +attribute vec4 aFromPosition; +attribute vec4 aToPosition; attribute vec3 aTangentIn; attribute vec3 aTangentOut; attribute float aSide; -attribute vec4 aVertexColor; +attribute vec4 aFromVertexColor; +attribute vec4 aToVertexColor; varying vec4 vColor; varying vec2 vTangent; @@ -76,9 +78,11 @@ void main() { aTangentIn != aTangentOut ) ? 1. : 0.; - vec4 posp = uModelViewMatrix * aPosition; - vec4 posqIn = uModelViewMatrix * (aPosition + vec4(aTangentIn, 0)); - vec4 posqOut = uModelViewMatrix * (aPosition + vec4(aTangentOut, 0)); + vec4 position = abs(aSide) == 1. ? aFromPosition : aToPosition; + vec4 vertexColor = abs(aSide) == 1. ? aFromVertexColor : aToVertexColor; + vec4 posp = uModelViewMatrix * position; + vec4 posqIn = uModelViewMatrix * (position + vec4(aTangentIn, 0)); + vec4 posqOut = uModelViewMatrix * (position + vec4(aTangentOut, 0)); float facingCamera = pow( // The word space tangent's z value is 0 if it's facing the camera @@ -204,7 +208,7 @@ void main() { float normalOffset = sign(aSide); // Caps will have side values of -2 or 2 on the edge of the cap that // extends out from the line - float tangentOffset = abs(aSide) - 1.; + float tangentOffset = abs(aSide) == 2. ? 1. : 0.; offset = (normal * normalOffset + tangent * tangentOffset) * uStrokeWeight * 0.5 * curPerspScale; vMaxDist = uStrokeWeight / 2.; @@ -214,5 +218,5 @@ void main() { gl_Position.xy = p.xy + offset.xy; gl_Position.zw = p.zw; - vColor = (uUseLineColor ? aVertexColor : uMaterialColor); + vColor = (uUseLineColor ? vertexColor : uMaterialColor); } diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 0201eb440f..327dc05a93 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -798,27 +798,28 @@ suite('p5.RendererGL', function() { myp5.fill(255); myp5.stroke(255); - myp5.triangle(0, 0, 1, 0, 0, 1); + myp5.box(100); - var buffers = renderer.retainedMode.geometry['tri']; + var buffers = renderer.retainedMode.geometry['box|4|4']; assert.isObject(buffers); assert.isDefined(buffers.indexBuffer); assert.isDefined(buffers.indexBufferType); assert.isDefined(buffers.vertexBuffer); - assert.isDefined(buffers.lineVerticesBuffer); - assert.isDefined(buffers.lineSidesBuffer); - assert.isDefined(buffers.lineTangentsInBuffer); - assert.isDefined(buffers.lineTangentsOutBuffer); + for (const key of ['segments', 'joins', 'caps']) { + assert.isDefined(buffers.lineData[key].lineFromVerticesBuffer); + assert.isDefined(buffers.lineData[key].lineToVerticesBuffer); + assert.isDefined(buffers.lineData[key].lineSidesBuffer); + assert.isDefined(buffers.lineData[key].lineTangentsInBuffer); + assert.isDefined(buffers.lineData[key].lineTangentsOutBuffer); + } assert.isDefined(buffers.vertexBuffer); - assert.equal(buffers.vertexCount, 3); + assert.equal(buffers.vertexCount, 36); - // 6 verts per line segment x3 (each is a quad made of 2 triangles) - // + 12 verts per join x3 (2 quads each, 1 is discarded in the shader) - // + 6 verts per line cap x0 (1 quad each) - // = 54 - assert.equal(buffers.lineVertexCount, 54); + assert.equal(buffers.model.lineData.segments.count, 12); + assert.equal(buffers.model.lineData.joins.count, 4); + assert.equal(buffers.model.lineData.caps.count, 16); done(); }); @@ -1660,13 +1661,13 @@ suite('p5.RendererGL', function() { const attributes = renderer._curShader.attributes; const loc = attributes.aTexCoord.location; - assert.equal(renderer.registerEnabled[loc], true); + assert.equal(renderer.registerEnabled.has(loc), true); myp5.model(myGeom); - assert.equal(renderer.registerEnabled[loc], false); + assert.equal(renderer.registerEnabled.has(loc), false); myp5.triangle(-8, -8, 8, 8, -8, 8); - assert.equal(renderer.registerEnabled[loc], true); + assert.equal(renderer.registerEnabled.has(loc), true); done(); });