From 42628aa58abeb821c79533b6b664aee990be9215 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 10:18:36 -0500 Subject: [PATCH 1/9] Consolidate immediate mode into RendererGL + shape builder --- src/webgl/3d_primitives.js | 208 +++++---- src/webgl/GeometryBuilder.js | 6 +- src/webgl/ShapeBuilder.js | 610 ++++++++++++++++++++++++ src/webgl/index.js | 2 - src/webgl/loading.js | 6 +- src/webgl/p5.RenderBuffer.js | 56 +-- src/webgl/p5.RendererGL.Immediate.js | 669 --------------------------- src/webgl/p5.RendererGL.Retained.js | 196 ++------ src/webgl/p5.RendererGL.js | 453 ++++++++++-------- test/unit/core/rendering.js | 4 +- test/unit/webgl/p5.Framebuffer.js | 4 + test/unit/webgl/p5.RendererGL.js | 143 +++--- test/unit/webgl/p5.Texture.js | 4 + 13 files changed, 1142 insertions(+), 1219 deletions(-) create mode 100644 src/webgl/ShapeBuilder.js delete mode 100644 src/webgl/p5.RendererGL.Immediate.js diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 98b64726cf..7092ef58fc 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -2084,7 +2084,7 @@ function primitives3D(p5, fn){ const _vertex = []; _vertex.push(new Vector(x, y, z)); - this._drawPoints(_vertex, this.immediateMode.buffers.point); + this._drawPoints(_vertex, this.buffers.point); return this; }; @@ -2097,8 +2097,8 @@ function primitives3D(p5, fn){ const x3 = args[4], y3 = args[5]; - const gId = 'tri'; - if (!this.geometryInHash(gId)) { + const gid = 'tri'; + if (!this.geometryInHash(gid)) { const _triangle = function() { const vertices = []; vertices.push(new Vector(0, 0, 0)); @@ -2112,7 +2112,8 @@ function primitives3D(p5, fn){ const triGeom = new Geometry(1, 1, _triangle); triGeom._edgesToVertices(); triGeom.computeNormals(); - this.createBuffers(gId, triGeom); + triGeom.gid = gid; + this.createBuffers(triGeom); } // only one triangle is cached, one point is at the origin, and the @@ -2134,7 +2135,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix = mult; - this.drawBuffers(gId); + this._drawGeometry(this.geometryBufferCache[gid].model); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2166,18 +2167,18 @@ function primitives3D(p5, fn){ const detail = args[7] || 25; let shape; - let gId; + let gid; // check if it is an ellipse or an arc if (Math.abs(stop - start) >= constants.TWO_PI) { shape = 'ellipse'; - gId = `${shape}|${detail}|`; + gid = `${shape}|${detail}|`; } else { shape = 'arc'; - gId = `${shape}|${start}|${stop}|${mode}|${detail}|`; + gid = `${shape}|${start}|${stop}|${mode}|${detail}|`; } - if (!this.geometryInHash(gId)) { + if (!this.geometryInHash(gid)) { const _arc = function() { // if the start and stop angles are not the same, push vertices to the array @@ -2255,7 +2256,8 @@ function primitives3D(p5, fn){ ); } - this.createBuffers(gId, arcGeom); + arcGeom.gid = gid; + this.createBuffers(arcGeom); } const uModelMatrix = this.states.uModelMatrix.copy(); @@ -2264,7 +2266,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); - this.drawBuffers(gId); + this._drawGeometry(this.geometryBufferCache[gid].model); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2284,8 +2286,8 @@ function primitives3D(p5, fn){ const perPixelLighting = this._pInst._glAttributes.perPixelLighting; const detailX = args[4] || (perPixelLighting ? 1 : 24); const detailY = args[5] || (perPixelLighting ? 1 : 16); - const gId = `rect|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { + const gid = `rect|${detailX}|${detailY}`; + if (!this.geometryInHash(gid)) { const _rect = function() { for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; @@ -2311,7 +2313,8 @@ function primitives3D(p5, fn){ .computeFaces() .computeNormals() ._edgesToVertices(); - this.createBuffers(gId, rectGeom); + rectGeom.gid = gid; + this.createBuffers(rectGeom); } // only a single rectangle (of a given detail) is cached: a square with @@ -2323,7 +2326,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); - this.drawBuffers(gId); + this._drawGeometry(this.geometryBufferCache[gid].model); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2392,11 +2395,11 @@ function primitives3D(p5, fn){ this.vertex(x1, y1); } - this.immediateMode.geometry.uvs.length = 0; - for (const vert of this.immediateMode.geometry.vertices) { + this.shapeBuilder.geometry.uvs.length = 0; + for (const vert of this.shapeBuilder.geometry.vertices) { const u = (vert.x - x1) / width; const v = (vert.y - y1) / height; - this.immediateMode.geometry.uvs.push(u, v); + this.shapeBuilder.geometry.uvs.push(u, v); } this.endShape(constants.CLOSE); @@ -2408,10 +2411,10 @@ function primitives3D(p5, fn){ RendererGL.prototype.quad = function(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4, detailX=2, detailY=2) { /* eslint-enable max-len */ - const gId = + const gid = `quad|${x1}|${y1}|${z1}|${x2}|${y2}|${z2}|${x3}|${y3}|${z3}|${x4}|${y4}|${z4}|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { + if (!this.geometryInHash(gid)) { const quadGeom = new Geometry(detailX, detailY, function() { //algorithm adapted from c++ to js //https://stackoverflow.com/questions/16989181/whats-the-correct-way-to-draw-a-distorted-plane-in-opengl/16993202#16993202 @@ -2459,9 +2462,10 @@ function primitives3D(p5, fn){ quadGeom.edges.push([startVertex, endVertex]); } quadGeom._edgesToVertices(); - this.createBuffers(gId, quadGeom); + quadGeom.gid = gid; + this.createBuffers(quadGeom); } - this.drawBuffers(gId); + this._drawGeometry(this.geometryBufferCache[gid].model); return this; }; @@ -2605,7 +2609,7 @@ function primitives3D(p5, fn){ }; RendererGL.prototype.bezierVertex = function(...args) { - if (this.immediateMode._bezierVertex.length === 0) { + if (this.shapeBuilder._bezierVertex.length === 0) { throw Error('vertex() must be used once before calling bezierVertex()'); } else { let w_x = []; @@ -2643,7 +2647,7 @@ function primitives3D(p5, fn){ } const LUTLength = this._lookUpTableBezier.length; - const immediateGeometry = this.immediateMode.geometry; + const immediateGeometry = this.shapeBuilder.geometry; // fillColors[0]: start point color // fillColors[1],[2]: control point color @@ -2673,8 +2677,8 @@ function primitives3D(p5, fn){ if (argLength === 6) { this.isBezier = true; - w_x = [this.immediateMode._bezierVertex[0], args[0], args[2], args[4]]; - w_y = [this.immediateMode._bezierVertex[1], args[1], args[3], args[5]]; + w_x = [this.shapeBuilder._bezierVertex[0], args[0], args[2], args[4]]; + w_y = [this.shapeBuilder._bezierVertex[1], args[1], args[3], args[5]]; // The ratio of the distance between the start point, the two control- // points, and the end point determines the intermediate color. let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1]); @@ -2744,14 +2748,14 @@ function primitives3D(p5, fn){ const prop = immediateGeometry.userVertexProperties[propName]; prop.setCurrentData(userVertexProperties[propName][2]); } - this.immediateMode._bezierVertex[0] = args[4]; - this.immediateMode._bezierVertex[1] = args[5]; + this.shapeBuilder._bezierVertex[0] = args[4]; + this.shapeBuilder._bezierVertex[1] = args[5]; } else if (argLength === 9) { this.isBezier = true; - w_x = [this.immediateMode._bezierVertex[0], args[0], args[3], args[6]]; - w_y = [this.immediateMode._bezierVertex[1], args[1], args[4], args[7]]; - w_z = [this.immediateMode._bezierVertex[2], args[2], args[5], args[8]]; + w_x = [this.shapeBuilder._bezierVertex[0], args[0], args[3], args[6]]; + w_y = [this.shapeBuilder._bezierVertex[1], args[1], args[4], args[7]]; + w_z = [this.shapeBuilder._bezierVertex[2], args[2], args[5], args[8]]; // The ratio of the distance between the start point, the two control- // points, and the end point determines the intermediate color. let d0 = Math.hypot(w_x[0]-w_x[1], w_y[0]-w_y[1], w_z[0]-w_z[1]); @@ -2821,15 +2825,15 @@ function primitives3D(p5, fn){ const prop = immediateGeometry.userVertexProperties[propName]; prop.setCurrentData(userVertexProperties[propName][2]); } - this.immediateMode._bezierVertex[0] = args[6]; - this.immediateMode._bezierVertex[1] = args[7]; - this.immediateMode._bezierVertex[2] = args[8]; + this.shapeBuilder._bezierVertex[0] = args[6]; + this.shapeBuilder._bezierVertex[1] = args[7]; + this.shapeBuilder._bezierVertex[2] = args[8]; } } }; RendererGL.prototype.quadraticVertex = function(...args) { - if (this.immediateMode._quadraticVertex.length === 0) { + if (this.shapeBuilder._quadraticVertex.length === 0) { throw Error('vertex() must be used once before calling quadraticVertex()'); } else { let w_x = []; @@ -2867,7 +2871,7 @@ function primitives3D(p5, fn){ } const LUTLength = this._lookUpTableQuadratic.length; - const immediateGeometry = this.immediateMode.geometry; + const immediateGeometry = this.shapeBuilder.geometry; // fillColors[0]: start point color // fillColors[1]: control point color @@ -2897,8 +2901,8 @@ function primitives3D(p5, fn){ if (argLength === 4) { this.isQuadratic = true; - w_x = [this.immediateMode._quadraticVertex[0], args[0], args[2]]; - w_y = [this.immediateMode._quadraticVertex[1], args[1], args[3]]; + w_x = [this.shapeBuilder._quadraticVertex[0], args[0], args[2]]; + w_y = [this.shapeBuilder._quadraticVertex[1], args[1], args[3]]; // The ratio of the distance between the start point, the control- // point, and the end point determines the intermediate color. @@ -2961,14 +2965,14 @@ function primitives3D(p5, fn){ const prop = immediateGeometry.userVertexProperties[propName]; prop.setCurrentData(userVertexProperties[propName][2]); } - this.immediateMode._quadraticVertex[0] = args[2]; - this.immediateMode._quadraticVertex[1] = args[3]; + this.shapeBuilder._quadraticVertex[0] = args[2]; + this.shapeBuilder._quadraticVertex[1] = args[3]; } else if (argLength === 6) { this.isQuadratic = true; - w_x = [this.immediateMode._quadraticVertex[0], args[0], args[3]]; - w_y = [this.immediateMode._quadraticVertex[1], args[1], args[4]]; - w_z = [this.immediateMode._quadraticVertex[2], args[2], args[5]]; + w_x = [this.shapeBuilder._quadraticVertex[0], args[0], args[3]]; + w_y = [this.shapeBuilder._quadraticVertex[1], args[1], args[4]]; + w_z = [this.shapeBuilder._quadraticVertex[2], args[2], args[5]]; // The ratio of the distance between the start point, the control- // point, and the end point determines the intermediate color. @@ -3032,9 +3036,9 @@ function primitives3D(p5, fn){ const prop = immediateGeometry.userVertexProperties[propName]; prop.setCurrentData(userVertexProperties[propName][2]); } - this.immediateMode._quadraticVertex[0] = args[3]; - this.immediateMode._quadraticVertex[1] = args[4]; - this.immediateMode._quadraticVertex[2] = args[5]; + this.shapeBuilder._quadraticVertex[0] = args[3]; + this.shapeBuilder._quadraticVertex[1] = args[4]; + this.shapeBuilder._quadraticVertex[2] = args[5]; } } }; @@ -3075,21 +3079,21 @@ function primitives3D(p5, fn){ const LUTLength = this._lookUpTableBezier.length; if (argLength === 2) { - this.immediateMode._curveVertex.push(args[0]); - this.immediateMode._curveVertex.push(args[1]); - if (this.immediateMode._curveVertex.length === 8) { + this.shapeBuilder._curveVertex.push(args[0]); + this.shapeBuilder._curveVertex.push(args[1]); + if (this.shapeBuilder._curveVertex.length === 8) { this.isCurve = true; w_x = this._bezierToCatmull([ - this.immediateMode._curveVertex[0], - this.immediateMode._curveVertex[2], - this.immediateMode._curveVertex[4], - this.immediateMode._curveVertex[6] + this.shapeBuilder._curveVertex[0], + this.shapeBuilder._curveVertex[2], + this.shapeBuilder._curveVertex[4], + this.shapeBuilder._curveVertex[6] ]); w_y = this._bezierToCatmull([ - this.immediateMode._curveVertex[1], - this.immediateMode._curveVertex[3], - this.immediateMode._curveVertex[5], - this.immediateMode._curveVertex[7] + this.shapeBuilder._curveVertex[1], + this.shapeBuilder._curveVertex[3], + this.shapeBuilder._curveVertex[5], + this.shapeBuilder._curveVertex[7] ]); for (i = 0; i < LUTLength; i++) { _x = @@ -3105,32 +3109,32 @@ function primitives3D(p5, fn){ this.vertex(_x, _y); } for (i = 0; i < argLength; i++) { - this.immediateMode._curveVertex.shift(); + this.shapeBuilder._curveVertex.shift(); } } } else if (argLength === 3) { - this.immediateMode._curveVertex.push(args[0]); - this.immediateMode._curveVertex.push(args[1]); - this.immediateMode._curveVertex.push(args[2]); - if (this.immediateMode._curveVertex.length === 12) { + this.shapeBuilder._curveVertex.push(args[0]); + this.shapeBuilder._curveVertex.push(args[1]); + this.shapeBuilder._curveVertex.push(args[2]); + if (this.shapeBuilder._curveVertex.length === 12) { this.isCurve = true; w_x = this._bezierToCatmull([ - this.immediateMode._curveVertex[0], - this.immediateMode._curveVertex[3], - this.immediateMode._curveVertex[6], - this.immediateMode._curveVertex[9] + this.shapeBuilder._curveVertex[0], + this.shapeBuilder._curveVertex[3], + this.shapeBuilder._curveVertex[6], + this.shapeBuilder._curveVertex[9] ]); w_y = this._bezierToCatmull([ - this.immediateMode._curveVertex[1], - this.immediateMode._curveVertex[4], - this.immediateMode._curveVertex[7], - this.immediateMode._curveVertex[10] + this.shapeBuilder._curveVertex[1], + this.shapeBuilder._curveVertex[4], + this.shapeBuilder._curveVertex[7], + this.shapeBuilder._curveVertex[10] ]); w_z = this._bezierToCatmull([ - this.immediateMode._curveVertex[2], - this.immediateMode._curveVertex[5], - this.immediateMode._curveVertex[8], - this.immediateMode._curveVertex[11] + this.shapeBuilder._curveVertex[2], + this.shapeBuilder._curveVertex[5], + this.shapeBuilder._curveVertex[8], + this.shapeBuilder._curveVertex[11] ]); for (i = 0; i < LUTLength; i++) { _x = @@ -3151,7 +3155,7 @@ function primitives3D(p5, fn){ this.vertex(_x, _y, _z); } for (i = 0; i < argLength; i++) { - this.immediateMode._curveVertex.shift(); + this.shapeBuilder._curveVertex.shift(); } } } @@ -3343,9 +3347,9 @@ function primitives3D(p5, fn){ detailX = 1, detailY = 1 ) { - const gId = `plane|${detailX}|${detailY}`; + const gid = `plane|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { + if (!this.geometryInHash(gid)) { const _plane = function() { let u, v, p; for (let i = 0; i <= this.detailY; i++) { @@ -3368,10 +3372,11 @@ function primitives3D(p5, fn){ ' than 1 detailX or 1 detailY' ); } - this.createBuffers(gId, planeGeom); + planeGeom.gid = gid; + this.createBuffers(planeGeom); } - this.drawBuffersScaled(gId, width, height, 1); + this.drawBuffersScaled(this.geometryBufferCache[gid].model, width, height, 1); } RendererGL.prototype.box = function( @@ -3390,8 +3395,8 @@ function primitives3D(p5, fn){ detailY = perPixelLighting ? 1 : 4; } - const gId = `box|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { + const gid = `box|${detailX}|${detailY}`; + if (!this.geometryInHash(gid)) { const _box = function() { const cubeIndices = [ [0, 4, 2, 6], // -1, 0, 0],// -x @@ -3450,9 +3455,10 @@ function primitives3D(p5, fn){ //initialize our geometry buffer with //the key val pair: //geometry Id, Geom object - this.createBuffers(gId, boxGeom); + boxGeom.gid = gid; + this.createBuffers(boxGeom); } - this.drawBuffersScaled(gId, width, height, depth); + this.drawBuffersScaled(this.geometryBufferCache[gid].model, width, height, depth); } RendererGL.prototype.sphere = function( @@ -3470,9 +3476,9 @@ function primitives3D(p5, fn){ detailX = 24, detailY = 16 ) { - const gId = `ellipsoid|${detailX}|${detailY}`; + const gid = `ellipsoid|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { + if (!this.geometryInHash(gid)) { const _ellipsoid = function() { for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; @@ -3502,10 +3508,11 @@ function primitives3D(p5, fn){ ' than 24 detailX or 24 detailY' ); } - this.createBuffers(gId, ellipsoidGeom); + ellipsoidGeom.gid = gid; + this.createBuffers(ellipsoidGeom); } - this.drawBuffersScaled(gId, radiusX, radiusY, radiusZ); + this.drawBuffersScaled(this.geometryBufferCache[gid].model, radiusX, radiusY, radiusZ); } RendererGL.prototype.cylinder = function( @@ -3516,8 +3523,8 @@ function primitives3D(p5, fn){ bottomCap = true, topCap = true ) { - const gId = `cylinder|${detailX}|${detailY}|${bottomCap}|${topCap}`; - if (!this.geometryInHash(gId)) { + const gid = `cylinder|${detailX}|${detailY}|${bottomCap}|${topCap}`; + if (!this.geometryInHash(gid)) { const cylinderGeom = new p5.Geometry(detailX, detailY); _truncatedCone.call( cylinderGeom, @@ -3538,10 +3545,11 @@ function primitives3D(p5, fn){ ' than 24 detailX or 16 detailY' ); } - this.createBuffers(gId, cylinderGeom); + cylinderGeom.gid = gid; + this.createBuffers(cylinderGeom); } - this.drawBuffersScaled(gId, radius, height, radius); + this.drawBuffersScaled(this.geometryBufferCache[gid].model, radius, height, radius); } RendererGL.prototype.cone = function( @@ -3551,8 +3559,8 @@ function primitives3D(p5, fn){ detailY = 1, cap = true ) { - const gId = `cone|${detailX}|${detailY}|${cap}`; - if (!this.geometryInHash(gId)) { + const gid = `cone|${detailX}|${detailY}|${cap}`; + if (!this.geometryInHash(gid)) { const coneGeom = new Geometry(detailX, detailY); _truncatedCone.call(coneGeom, 1, 0, 1, detailX, detailY, cap, false); if (detailX <= 24 && detailY <= 16) { @@ -3563,10 +3571,11 @@ function primitives3D(p5, fn){ ' than 24 detailX or 16 detailY' ); } - this.createBuffers(gId, coneGeom); + coneGeom.gid = gid; + this.createBuffers(coneGeom); } - this.drawBuffersScaled(gId, radius, height, radius); + this.drawBuffersScaled(this.geometryBufferCache[gid].model, radius, height, radius); } RendererGL.prototype.torus = function( @@ -3584,9 +3593,9 @@ function primitives3D(p5, fn){ } const tubeRatio = (tubeRadius / radius).toPrecision(4); - const gId = `torus|${tubeRatio}|${detailX}|${detailY}`; + const gid = `torus|${tubeRatio}|${detailX}|${detailY}`; - if (!this.geometryInHash(gId)) { + if (!this.geometryInHash(gid)) { const _torus = function() { for (let i = 0; i <= this.detailY; i++) { const v = i / this.detailY; @@ -3625,9 +3634,10 @@ function primitives3D(p5, fn){ ' than 24 detailX or 16 detailY' ); } - this.createBuffers(gId, torusGeom); + torusGeom.gid = gid; + this.createBuffers(torusGeom); } - this.drawBuffersScaled(gId, radius, radius, radius); + this.drawBuffersScaled(this.geometryBufferCache[gid].model, radius, radius, radius); } } diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index a6889ba457..a5691db31e 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -108,9 +108,7 @@ class GeometryBuilder { * Adds geometry from the renderer's immediate mode into the builder's * combined geometry. */ - addImmediate() { - const geometry = this.renderer.immediateMode.geometry; - const shapeMode = this.renderer.immediateMode.shapeMode; + addImmediate(geometry, shapeMode) { const faces = []; if (this.renderer.states.doFill) { @@ -143,7 +141,7 @@ class GeometryBuilder { * combined geometry. */ addRetained(geometry) { - this.addGeometry(geometry.model); + this.addGeometry(geometry); } /** diff --git a/src/webgl/ShapeBuilder.js b/src/webgl/ShapeBuilder.js new file mode 100644 index 0000000000..258f8c07e8 --- /dev/null +++ b/src/webgl/ShapeBuilder.js @@ -0,0 +1,610 @@ +import * as constants from '../core/constants'; +import { Geometry } from './p5.Geometry'; +import libtess from 'libtess'; // Fixed with exporting module from libtess +import { Vector } from '../math/p5.Vector'; +import { RenderBuffer } from './p5.RenderBuffer'; + +const INITIAL_BUFFER_STRIDES = { + vertices: 1, + vertexNormals: 1, + vertexColors: 4, + vertexStrokeColors: 4, + uvs: 2 +}; + +// The total number of properties per vertex, before additional +// user attributes are added. +const INITIAL_VERTEX_SIZE = + Object.values(INITIAL_BUFFER_STRIDES).reduce((acc, next) => acc + next); + +export class ShapeBuilder { + constructor(renderer) { + this.renderer = renderer; + this.shapeMode = constants.TESS; + this.geometry = new Geometry(); + this.geometry.gid = '__IMMEDIATE_MODE_GEOMETRY__'; + + this.contourIndices = []; + this._useUserVertexProperties = undefined; + + this._bezierVertex = []; + this._quadraticVertex = []; + this._curveVertex = []; + + // Used to distinguish between user calls to vertex() and internal calls + this.isProcessingVertices = false; + + // Used for converting shape outlines into triangles for rendering + this._tessy = this._initTessy(); + this.tessyVertexSize = INITIAL_VERTEX_SIZE; + this.bufferStrides = { ...INITIAL_BUFFER_STRIDES }; + } + + beginShape(mode = constants.TESS) { + this.shapeMode = mode; + if (this._useUserVertexProperties === true){ + this._resetUserVertexProperties(); + } + this.geometry.reset(); + this.contourIndices = []; + } + + endShape = function( + mode, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind, + count = 1 + ) { + if (this.shapeMode === constants.POINTS) { + // @TODO(dave) move to renderer directly + this.renderer._drawPoints( + this.geometry.vertices, + this.renderer.buffers.point + ); + return this; + } + // When we are drawing a shape then the shape mode is TESS, + // but in case of triangle we can skip the breaking into small triangle + // this can optimize performance by skipping the step of breaking it into triangles + if (this.geometry.vertices.length === 3 && + this.shapeMode === constants.TESS + ) { + this.shapeMode === constants.TRIANGLES; + } + + this.isProcessingVertices = true; + this._processVertices(...arguments); + this.isProcessingVertices = false; + + // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we + // need to convert them to a supported format. In `vertex()`, we reformat + // the input data into the formats specified below. + if (this.shapeMode === constants.QUADS) { + this.shapeMode = constants.TRIANGLES; + } else if (this.shapeMode === constants.QUAD_STRIP) { + this.shapeMode = constants.TRIANGLE_STRIP; + } + + this.isBezier = false; + this.isQuadratic = false; + this.isCurve = false; + this._bezierVertex.length = 0; + this._quadraticVertex.length = 0; + this._curveVertex.length = 0; + } + + beginContour() { + if (this.shapeMode !== constants.TESS) { + throw new Error('WebGL mode can only use contours with beginShape(TESS).'); + } + this.contourIndices.push( + this.geometry.vertices.length + ); + } + + vertex(x, y) { + // WebGL doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn + // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra + // work to convert QUAD_STRIP here, since the only difference is in how edges + // are rendered.) + if (this.shapeMode === constants.QUADS) { + // A finished quad turned into triangles should leave 6 vertices in the + // buffer: + // 0--3 0 3--5 + // | | --> | \ \ | + // 1--2 1--2 4 + // When vertex index 3 is being added, add the necessary duplicates. + if (this.geometry.vertices.length % 6 === 3) { + for (const key in this.bufferStrides) { + const stride = this.bufferStrides[key]; + const buffer = this.geometry[key]; + buffer.push( + ...buffer.slice( + buffer.length - 3 * stride, + buffer.length - 2 * stride + ), + ...buffer.slice(buffer.length - stride, buffer.length) + ); + } + } + } + + let z, u, v; + + // default to (x, y) mode: all other arguments assumed to be 0. + z = u = v = 0; + + if (arguments.length === 3) { + // (x, y, z) mode: (u, v) assumed to be 0. + z = arguments[2]; + } else if (arguments.length === 4) { + // (x, y, u, v) mode: z assumed to be 0. + u = arguments[2]; + v = arguments[3]; + } else if (arguments.length === 5) { + // (x, y, z, u, v) mode + z = arguments[2]; + u = arguments[3]; + v = arguments[4]; + } + const vert = new Vector(x, y, z); + this.geometry.vertices.push(vert); + this.geometry.vertexNormals.push(this.renderer.states._currentNormal); + + for (const propName in this.geometry.userVertexProperties){ + const geom = this.geometry; + const prop = geom.userVertexProperties[propName]; + const verts = geom.vertices; + if (prop.getSrcArray().length === 0 && verts.length > 1) { + const numMissingValues = prop.getDataSize() * (verts.length - 1); + const missingValues = Array(numMissingValues).fill(0); + prop.pushDirect(missingValues); + } + prop.pushCurrentData(); + } + + const vertexColor = this.renderer.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; + this.geometry.vertexColors.push( + vertexColor[0], + vertexColor[1], + vertexColor[2], + vertexColor[3] + ); + const lineVertexColor = this.renderer.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; + this.geometry.vertexStrokeColors.push( + lineVertexColor[0], + lineVertexColor[1], + lineVertexColor[2], + lineVertexColor[3] + ); + + if (this.renderer.states.textureMode === constants.IMAGE && !this.isProcessingVertices) { + if (this.renderer.states._tex !== null) { + if (this.renderer.states._tex.width > 0 && this.renderer.states._tex.height > 0) { + u /= this.renderer.states._tex.width; + v /= this.renderer.states._tex.height; + } + } else if ( + this.renderer.states.userFillShader !== undefined || + this.renderer.states.userStrokeShader !== undefined || + this.renderer.states.userPointShader !== undefined || + this.renderer.states.userImageShader !== undefined + ) { + // Do nothing if user-defined shaders are present + } else if ( + this.renderer.states._tex === null && + arguments.length >= 4 + ) { + // Only throw this warning if custom uv's have been provided + console.warn( + 'You must first call texture() before using' + + ' vertex() with image based u and v coordinates' + ); + } + } + + this.geometry.uvs.push(u, v); + + this._bezierVertex[0] = x; + this._bezierVertex[1] = y; + this._bezierVertex[2] = z; + + this._quadraticVertex[0] = x; + this._quadraticVertex[1] = y; + this._quadraticVertex[2] = z; + + return this; + } + + vertexProperty(propertyName, data) { + if (!this._useUserVertexProperties) { + this._useUserVertexProperties = true; + this.geometry.userVertexProperties = {}; + } + const propertyExists = this.geometry.userVertexProperties[propertyName]; + let prop; + if (propertyExists){ + prop = this.geometry.userVertexProperties[propertyName]; + } else { + prop = this.geometry._userVertexPropertyHelper(propertyName, data); + this.tessyVertexSize += prop.getDataSize(); + this.bufferStrides[prop.getSrcName()] = prop.getDataSize(); + this.renderer.buffers.user.push( + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this.renderer) + ); + } + prop.setCurrentData(data); + } + + _resetUserVertexProperties() { + const properties = this.geometry.userVertexProperties; + for (const propName in properties){ + const prop = properties[propName]; + delete this.bufferStrides[propName]; + prop.delete(); + } + this._useUserVertexProperties = false; + this.tessyVertexSize = INITIAL_VERTEX_SIZE; + this.geometry.userVertexProperties = {}; + } + + /** + * Interpret the vertices of the current geometry according to + * the current shape mode, and convert them to something renderable (either + * triangles or lines.) + * @private + */ + _processVertices(mode) { + if (this.geometry.vertices.length === 0) return; + + const calculateStroke = this.renderer.states.doStroke; + const shouldClose = mode === constants.CLOSE; + if (calculateStroke) { + this.geometry.edges = this._calculateEdges( + this.shapeMode, + this.geometry.vertices, + shouldClose + ); + if (!this.renderer.geometryBuilder) { + this.geometry._edgesToVertices(); + } + } + + // For hollow shapes, user must set mode to TESS + const convexShape = this.shapeMode === constants.TESS; + // If the shape has a contour, we have to re-triangulate to cut out the + // contour region + const hasContour = this.contourIndices.length > 0; + // We tesselate when drawing curves or convex shapes + const shouldTess = + this.renderer.states.doFill && + ( + this.isBezier || + this.isQuadratic || + this.isCurve || + convexShape || + hasContour + ) && + this.shapeMode !== constants.LINES; + + if (shouldTess) { + this._tesselateShape(); + } + } + + /** + * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and + * tesselates shapes when applicable. + * @private + * @returns {Number[]} indices for custom shape vertices indicating edges. + */ + _calculateEdges( + shapeMode, + verts, + shouldClose + ) { + const res = []; + let i = 0; + const contourIndices = this.contourIndices.slice(); + let contourStart = 0; + switch (shapeMode) { + case constants.TRIANGLE_STRIP: + for (i = 0; i < verts.length - 2; i++) { + res.push([i, i + 1]); + res.push([i, i + 2]); + } + res.push([i, i + 1]); + break; + case constants.TRIANGLE_FAN: + for (i = 1; i < verts.length - 1; i++) { + res.push([0, i]); + res.push([i, i + 1]); + } + res.push([0, verts.length - 1]); + break; + case constants.TRIANGLES: + for (i = 0; i < verts.length - 2; i = i + 3) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 2, i]); + } + break; + case constants.LINES: + for (i = 0; i < verts.length - 1; i = i + 2) { + res.push([i, i + 1]); + } + break; + case constants.QUADS: + // Quads have been broken up into two triangles by `vertex()`: + // 0 3--5 + // | \ \ | + // 1--2 4 + for (i = 0; i < verts.length - 5; i += 6) { + res.push([i, i + 1]); + res.push([i + 1, i + 2]); + res.push([i + 3, i + 5]); + res.push([i + 4, i + 5]); + } + break; + case constants.QUAD_STRIP: + // 0---2---4 + // | | | + // 1---3---5 + for (i = 0; i < verts.length - 2; i += 2) { + res.push([i, i + 1]); + res.push([i, i + 2]); + res.push([i + 1, i + 3]); + } + res.push([i, i + 1]); + break; + default: + // TODO: handle contours in other modes too + for (i = 0; i < verts.length; i++) { + // Handle breaks between contours + if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { + res.push([i, i + 1]); + } else { + if (shouldClose || contourStart) { + res.push([i, contourStart]); + } + if (contourIndices.length > 0) { + contourStart = contourIndices.shift(); + } + } + } + break; + } + if (shapeMode !== constants.TESS && shouldClose) { + res.push([verts.length - 1, 0]); + } + return res; + } + + /** + * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. + * @private + */ + _tesselateShape() { + // TODO: handle non-TESS shape modes that have contours + this.shapeMode = constants.TRIANGLES; + const contours = [[]]; + for (let i = 0; i < this.geometry.vertices.length; i++) { + if ( + this.contourIndices.length > 0 && + this.contourIndices[0] === i + ) { + this.contourIndices.shift(); + contours.push([]); + } + contours[contours.length-1].push( + this.geometry.vertices[i].x, + this.geometry.vertices[i].y, + this.geometry.vertices[i].z, + this.geometry.uvs[i * 2], + this.geometry.uvs[i * 2 + 1], + this.geometry.vertexColors[i * 4], + this.geometry.vertexColors[i * 4 + 1], + this.geometry.vertexColors[i * 4 + 2], + this.geometry.vertexColors[i * 4 + 3], + this.geometry.vertexNormals[i].x, + this.geometry.vertexNormals[i].y, + this.geometry.vertexNormals[i].z + ); + for (const propName in this.geometry.userVertexProperties) { + const prop = this.geometry.userVertexProperties[propName]; + const start = i * prop.getDataSize(); + const end = start + prop.getDataSize(); + const vals = prop.getSrcArray().slice(start, end); + contours[contours.length-1].push(...vals); + } + } + + const polyTriangles = this._triangulate(contours); + const originalVertices = this.geometry.vertices; + this.geometry.vertices = []; + this.geometry.vertexNormals = []; + this.geometry.uvs = []; + for (const propName in this.geometry.userVertexProperties){ + const prop = this.geometry.userVertexProperties[propName]; + prop.resetSrcArray(); + } + const colors = []; + for ( + let j = 0, polyTriLength = polyTriangles.length; + j < polyTriLength; + j = j + this.tessyVertexSize + ) { + colors.push(...polyTriangles.slice(j + 5, j + 9)); + this.renderer.normal(...polyTriangles.slice(j + 9, j + 12)); + { + let offset = 12; + for (const propName in this.geometry.userVertexProperties){ + const prop = this.geometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + const start = j + offset; + const end = start + size; + prop.setCurrentData(polyTriangles.slice(start, end)); + offset += size; + } + } + this.vertex(...polyTriangles.slice(j, j + 5)); + } + if (this.renderer.geometryBuilder) { + // Tesselating the face causes the indices of edge vertices to stop being + // correct. When rendering, this is not a problem, since _edgesToVertices + // will have been called before this, and edge vertex indices are no longer + // needed. However, the geometry builder still needs this information, so + // when one is active, we need to update the indices. + // + // We record index mappings in a Map so that once we have found a + // corresponding vertex, we don't need to loop to find it again. + const newIndex = new Map(); + this.geometry.edges = + this.geometry.edges.map(edge => edge.map(origIdx => { + if (!newIndex.has(origIdx)) { + const orig = originalVertices[origIdx]; + let newVertIndex = this.geometry.vertices.findIndex( + v => + orig.x === v.x && + orig.y === v.y && + orig.z === v.z + ); + if (newVertIndex === -1) { + // The tesselation process didn't output a vertex with the exact + // coordinate as before, potentially due to numerical issues. This + // doesn't happen often, but in this case, pick the closest point + let closestDist = Infinity; + let closestIndex = 0; + for ( + let i = 0; + i < this.geometry.vertices.length; + i++ + ) { + const vert = this.geometry.vertices[i]; + const dX = orig.x - vert.x; + const dY = orig.y - vert.y; + const dZ = orig.z - vert.z; + const dist = dX*dX + dY*dY + dZ*dZ; + if (dist < closestDist) { + closestDist = dist; + closestIndex = i; + } + } + newVertIndex = closestIndex; + } + newIndex.set(origIdx, newVertIndex); + } + return newIndex.get(origIdx); + })); + } + this.geometry.vertexColors = colors; + } + + _initTessy() { + // function called for each vertex of tesselator output + function vertexCallback(data, polyVertArray) { + for (const element of data) { + polyVertArray.push(element); + } + } + + function begincallback(type) { + if (type !== libtess.primitiveType.GL_TRIANGLES) { + console.log(`expected TRIANGLES but got type: ${type}`); + } + } + + function errorcallback(errno) { + console.log('error callback'); + console.log(`error number: ${errno}`); + } + + // callback for when segments intersect and must be split + const combinecallback = (coords, data, weight) => { + const result = new Array(this.tessyVertexSize).fill(0); + for (let i = 0; i < weight.length; i++) { + for (let j = 0; j < result.length; j++) { + if (weight[i] === 0 || !data[i]) continue; + result[j] += data[i][j] * weight[i]; + } + } + return result; + }; + + function edgeCallback(flag) { + // don't really care about the flag, but need no-strip/no-fan behavior + } + + const tessy = new libtess.GluTesselator(); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); + tessy.gluTessProperty( + libtess.gluEnum.GLU_TESS_WINDING_RULE, + libtess.windingRule.GLU_TESS_WINDING_NONZERO + ); + + return tessy; + } + + /** + * Runs vertices through libtess to convert them into triangles + * @private + */ + _triangulate(contours) { + // libtess will take 3d verts and flatten to a plane for tesselation. + // libtess is capable of calculating a plane to tesselate on, but + // if all of the vertices have the same z values, we'll just + // assume the face is facing the camera, letting us skip any performance + // issues or bugs in libtess's automatic calculation. + const z = contours[0] ? contours[0][2] : undefined; + let allSameZ = true; + for (const contour of contours) { + for ( + let j = 0; + j < contour.length; + j += this.tessyVertexSize + ) { + if (contour[j + 2] !== z) { + allSameZ = false; + break; + } + } + } + if (allSameZ) { + this._tessy.gluTessNormal(0, 0, 1); + } else { + // Let libtess pick a plane for us + this._tessy.gluTessNormal(0, 0, 0); + } + + const triangleVerts = []; + this._tessy.gluTessBeginPolygon(triangleVerts); + + for (const contour of contours) { + this._tessy.gluTessBeginContour(); + for ( + let j = 0; + j < contour.length; + j += this.tessyVertexSize + ) { + const coords = contour.slice( + j, + j + this.tessyVertexSize + ); + this._tessy.gluTessVertex(coords, coords); + } + this._tessy.gluTessEndContour(); + } + + // finish polygon + this._tessy.gluTessEndPolygon(); + + return triangleVerts; + } +}; diff --git a/src/webgl/index.js b/src/webgl/index.js index 74ed708fad..d66c4b0acc 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -14,7 +14,6 @@ import shader from './p5.Shader'; import camera from './p5.Camera'; import texture from './p5.Texture'; import rendererGL from './p5.RendererGL'; -import rendererGLImmediate from './p5.RendererGL.Immediate'; import rendererGLRetained from './p5.RendererGL.Retained'; export default function(p5){ @@ -34,6 +33,5 @@ export default function(p5){ dataArray(p5, p5.prototype); shader(p5, p5.prototype); texture(p5, p5.prototype); - rendererGLImmediate(p5, p5.prototype); rendererGLRetained(p5, p5.prototype); } diff --git a/src/webgl/loading.js b/src/webgl/loading.js index 3fadd77c5d..4163cb04c7 100755 --- a/src/webgl/loading.js +++ b/src/webgl/loading.js @@ -1112,7 +1112,7 @@ function loading(p5, fn){ * * */ - fn.model = function (model) { + fn.model = function (model, count = 1) { this._assert3d('model'); p5._validateParameters('model', arguments); if (model.vertices.length > 0) { @@ -1123,10 +1123,10 @@ function loading(p5, fn){ } model._edgesToVertices(); - this._renderer.createBuffers(model.gid, model); + this._renderer._getOrMakeCachedBuffers(model); } - this._renderer.drawBuffers(model.gid); + this._renderer._drawGeometry(model, { count }); } }; } diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index e2f597a832..87c80ce45b 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -1,5 +1,5 @@ class RenderBuffer { - constructor(size, src, dst, attr, renderer, map){ + constructor(size, src, dst, attr, renderer, map) { this.size = size; // the number of FLOATs in each vertex this.src = src; // the name of the model's source array this.dst = dst; // the name of the geometry's buffer @@ -9,73 +9,67 @@ class RenderBuffer { } /** - * Enables and binds the buffers used by shader when the appropriate data exists in geometry. - * Must always be done prior to drawing geometry in WebGL. - * @param {p5.Geometry} geometry Geometry that is going to be drawn - * @param {p5.Shader} shader Active shader - * @private - */ + * Enables and binds the buffers used by shader when the appropriate data exists in geometry. + * Must always be done prior to drawing geometry in WebGL. + * @param {p5.Geometry} geometry Geometry that is going to be drawn + * @param {p5.Shader} shader Active shader + * @private + */ _prepareBuffer(geometry, shader) { const attributes = shader.attributes; const gl = this._renderer.GL; - let model; - if (geometry.model) { - model = geometry.model; - } else { - model = geometry; - } + const glBuffers = this._renderer._getOrMakeCachedBuffers(geometry); // loop through each of the buffer definitions const attr = attributes[this.attr]; if (!attr) { return; } - // check if the model has the appropriate source array - let buffer = geometry[this.dst]; - const src = model[this.src]; - if (!src){ - return; - } - if (src.length > 0) { - // check if we need to create the GL buffer + // check if the geometry has the appropriate source array + let buffer = glBuffers[this.dst]; + const src = geometry[this.src]; + if (src && 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(); + // create and remember the buffer + glBuffers[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) { + if (createBuffer || geometry.dirtyFlags[this.src] !== false) { const map = this.map; - // get the values from the model, possibly transformed + // get the values from the geometry, possibly transformed const values = map ? map(src) : src; // fill the buffer with the values this._renderer._bindBuffer(buffer, gl.ARRAY_BUFFER, values); - // mark the model's source array as clean - model.dirtyFlags[this.src] = false; + // mark the geometry's source array as clean + geometry.dirtyFlags[this.src] = false; } // enable the attribute shader.enableAttrib(attr, this.size); } else { const loc = attr.location; - if (loc === -1 || !this._renderer.registerEnabled.has(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.delete(loc); } } -}; +} -function renderBuffer(p5, fn){ +function renderBuffer(p5, fn) { p5.RenderBuffer = RenderBuffer; } export default renderBuffer; export { RenderBuffer }; -if(typeof p5 !== 'undefined'){ +if (typeof p5 !== "undefined") { renderBuffer(p5, p5.prototype); } diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js deleted file mode 100644 index c8454b6b8e..0000000000 --- a/src/webgl/p5.RendererGL.Immediate.js +++ /dev/null @@ -1,669 +0,0 @@ -/** - * Welcome to RendererGL Immediate Mode. - * Immediate mode is used for drawing custom shapes - * from a set of vertices. Immediate Mode is activated - * when you call beginShape() & de-activated when you call endShape(). - * Immediate mode is a style of programming borrowed - * from OpenGL's (now-deprecated) immediate mode. - * It differs from p5.js' default, Retained Mode, which caches - * geometries and buffers on the CPU to reduce the number of webgl - * draw calls. Retained mode is more efficient & performative, - * however, Immediate Mode is useful for sketching quick - * geometric ideas. - */ -import * as constants from '../core/constants'; -import { RendererGL } from './p5.RendererGL'; -import { Vector } from '../math/p5.Vector'; -import { RenderBuffer } from './p5.RenderBuffer'; - -function rendererGLImmediate(p5, fn){ - /** - * Begin shape drawing. This is a helpful way of generating - * custom shapes quickly. However in WEBGL mode, application - * performance will likely drop as a result of too many calls to - * beginShape() / endShape(). As a high performance alternative, - * please use p5.js geometry primitives. - * @private - * @method beginShape - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN, QUADS, QUAD_STRIP, - * and TESS(WEBGL only) - * @chainable - */ - RendererGL.prototype.beginShape = function(mode) { - this.immediateMode.shapeMode = - mode !== undefined ? mode : constants.TESS; - if (this._useUserVertexProperties === true){ - this._resetUserVertexProperties(); - } - this.immediateMode.geometry.reset(); - this.immediateMode.contourIndices = []; - return this; - }; - - RendererGL.prototype.immediateBufferStrides = { - vertices: 1, - vertexNormals: 1, - vertexColors: 4, - vertexStrokeColors: 4, - uvs: 2 - }; - - RendererGL.prototype.beginContour = function() { - if (this.immediateMode.shapeMode !== constants.TESS) { - throw new Error('WebGL mode can only use contours with beginShape(TESS).'); - } - this.immediateMode.contourIndices.push( - this.immediateMode.geometry.vertices.length - ); - }; - - /** - * adds a vertex to be drawn in a custom Shape. - * @private - * @method vertex - * @param {Number} x x-coordinate of vertex - * @param {Number} y y-coordinate of vertex - * @param {Number} z z-coordinate of vertex - * @chainable - * @TODO implement handling of p5.Vector args - */ - RendererGL.prototype.vertex = function(x, y) { - // WebGL 1 doesn't support QUADS or QUAD_STRIP, so we duplicate data to turn - // QUADS into TRIANGLES and QUAD_STRIP into TRIANGLE_STRIP. (There is no extra - // work to convert QUAD_STRIP here, since the only difference is in how edges - // are rendered.) - if (this.immediateMode.shapeMode === constants.QUADS) { - // A finished quad turned into triangles should leave 6 vertices in the - // buffer: - // 0--3 0 3--5 - // | | --> | \ \ | - // 1--2 1--2 4 - // When vertex index 3 is being added, add the necessary duplicates. - if (this.immediateMode.geometry.vertices.length % 6 === 3) { - for (const key in this.immediateBufferStrides) { - const stride = this.immediateBufferStrides[key]; - const buffer = this.immediateMode.geometry[key]; - buffer.push( - ...buffer.slice( - buffer.length - 3 * stride, - buffer.length - 2 * stride - ), - ...buffer.slice(buffer.length - stride, buffer.length) - ); - } - } - } - - let z, u, v; - - // default to (x, y) mode: all other arguments assumed to be 0. - z = u = v = 0; - - if (arguments.length === 3) { - // (x, y, z) mode: (u, v) assumed to be 0. - z = arguments[2]; - } else if (arguments.length === 4) { - // (x, y, u, v) mode: z assumed to be 0. - u = arguments[2]; - v = arguments[3]; - } else if (arguments.length === 5) { - // (x, y, z, u, v) mode - z = arguments[2]; - u = arguments[3]; - v = arguments[4]; - } - const vert = new Vector(x, y, z); - this.immediateMode.geometry.vertices.push(vert); - this.immediateMode.geometry.vertexNormals.push(this.states._currentNormal); - - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const geom = this.immediateMode.geometry; - const prop = geom.userVertexProperties[propName]; - const verts = geom.vertices; - if (prop.getSrcArray().length === 0 && verts.length > 1) { - const numMissingValues = prop.getDataSize() * (verts.length - 1); - const missingValues = Array(numMissingValues).fill(0); - prop.pushDirect(missingValues); - } - prop.pushCurrentData(); - } - - const vertexColor = this.states.curFillColor || [0.5, 0.5, 0.5, 1.0]; - this.immediateMode.geometry.vertexColors.push( - vertexColor[0], - vertexColor[1], - vertexColor[2], - vertexColor[3] - ); - const lineVertexColor = this.states.curStrokeColor || [0.5, 0.5, 0.5, 1]; - this.immediateMode.geometry.vertexStrokeColors.push( - lineVertexColor[0], - lineVertexColor[1], - lineVertexColor[2], - lineVertexColor[3] - ); - - if (this.states.textureMode === constants.IMAGE && !this.isProcessingVertices) { - if (this.states._tex !== null) { - if (this.states._tex.width > 0 && this.states._tex.height > 0) { - u /= this.states._tex.width; - v /= this.states._tex.height; - } - } else if ( - this.states.userFillShader !== undefined || - this.states.userStrokeShader !== undefined || - this.states.userPointShader !== undefined || - this.states.userImageShader !== undefined - ) { - // Do nothing if user-defined shaders are present - } else if ( - this.states._tex === null && - arguments.length >= 4 - ) { - // Only throw this warning if custom uv's have been provided - console.warn( - 'You must first call texture() before using' + - ' vertex() with image based u and v coordinates' - ); - } - } - - this.immediateMode.geometry.uvs.push(u, v); - - this.immediateMode._bezierVertex[0] = x; - this.immediateMode._bezierVertex[1] = y; - this.immediateMode._bezierVertex[2] = z; - - this.immediateMode._quadraticVertex[0] = x; - this.immediateMode._quadraticVertex[1] = y; - this.immediateMode._quadraticVertex[2] = z; - - return this; - }; - - RendererGL.prototype.vertexProperty = function(propertyName, data){ - if(!this._useUserVertexProperties){ - this._useUserVertexProperties = true; - this.immediateMode.geometry.userVertexProperties = {}; - } - const propertyExists = this.immediateMode.geometry.userVertexProperties[propertyName]; - let prop; - if (propertyExists){ - prop = this.immediateMode.geometry.userVertexProperties[propertyName]; - } - else { - prop = this.immediateMode.geometry._userVertexPropertyHelper(propertyName, data); - this.tessyVertexSize += prop.getDataSize(); - this.immediateBufferStrides[prop.getSrcName()] = prop.getDataSize(); - this.immediateMode.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) - ); - } - prop.setCurrentData(data); - }; - - RendererGL.prototype._resetUserVertexProperties = function(){ - const properties = this.immediateMode.geometry.userVertexProperties; - for (const propName in properties){ - const prop = properties[propName]; - delete this.immediateBufferStrides[propName]; - prop.delete(); - } - this._useUserVertexProperties = false; - this.tessyVertexSize = 12; - this.immediateMode.geometry.userVertexProperties = {}; - this.immediateMode.buffers.user = []; - }; - - /** - * Sets the normal to use for subsequent vertices. - * @private - * @method normal - * @param {Number} x - * @param {Number} y - * @param {Number} z - * @chainable - * - * @method normal - * @param {Vector} v - * @chainable - */ - RendererGL.prototype.normal = function(xorv, y, z) { - if (xorv instanceof Vector) { - this.states._currentNormal = xorv; - } else { - this.states._currentNormal = new Vector(xorv, y, z); - } - - return this; - }; - - /** - * End shape drawing and render vertices to screen. - * @chainable - */ - RendererGL.prototype.endShape = function( - mode, - isCurve, - isBezier, - isQuadratic, - isContour, - shapeKind, - count = 1 - ) { - if (this.immediateMode.shapeMode === constants.POINTS) { - this._drawPoints( - this.immediateMode.geometry.vertices, - this.immediateMode.buffers.point - ); - return this; - } - // When we are drawing a shape then the shape mode is TESS, - // but in case of triangle we can skip the breaking into small triangle - // this can optimize performance by skipping the step of breaking it into triangles - if (this.immediateMode.geometry.vertices.length === 3 && - this.immediateMode.shapeMode === constants.TESS - ) { - this.immediateMode.shapeMode === constants.TRIANGLES; - } - - this.isProcessingVertices = true; - this._processVertices(...arguments); - this.isProcessingVertices = false; - - // LINE_STRIP and LINES are not used for rendering, instead - // they only indicate a way to modify vertices during the _processVertices() step - let is_line = false; - if ( - this.immediateMode.shapeMode === constants.LINE_STRIP || - this.immediateMode.shapeMode === constants.LINES - ) { - this.immediateMode.shapeMode = constants.TRIANGLE_FAN; - is_line = true; - } - - // WebGL doesn't support the QUADS and QUAD_STRIP modes, so we - // need to convert them to a supported format. In `vertex()`, we reformat - // the input data into the formats specified below. - if (this.immediateMode.shapeMode === constants.QUADS) { - this.immediateMode.shapeMode = constants.TRIANGLES; - } else if (this.immediateMode.shapeMode === constants.QUAD_STRIP) { - this.immediateMode.shapeMode = constants.TRIANGLE_STRIP; - } - - if (this.states.doFill && !is_line) { - if ( - !this.geometryBuilder && - this.immediateMode.geometry.vertices.length >= 3 - ) { - this._drawImmediateFill(count); - } - } - if (this.states.doStroke) { - if ( - !this.geometryBuilder && - this.immediateMode.geometry.lineVertices.length >= 1 - ) { - this._drawImmediateStroke(); - } - } - - if (this.geometryBuilder) { - this.geometryBuilder.addImmediate(); - } - - this.isBezier = false; - this.isQuadratic = false; - this.isCurve = false; - this.immediateMode._bezierVertex.length = 0; - this.immediateMode._quadraticVertex.length = 0; - this.immediateMode._curveVertex.length = 0; - - return this; - }; - - /** - * Called from endShape(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @param {Number} mode webgl primitives mode. beginShape supports the - * following modes: - * POINTS,LINES,LINE_STRIP,LINE_LOOP,TRIANGLES, - * TRIANGLE_STRIP, TRIANGLE_FAN and TESS(WEBGL only) - */ - RendererGL.prototype._processVertices = function(mode) { - if (this.immediateMode.geometry.vertices.length === 0) return; - - const calculateStroke = this.states.doStroke; - const shouldClose = mode === constants.CLOSE; - if (calculateStroke) { - this.immediateMode.geometry.edges = this._calculateEdges( - this.immediateMode.shapeMode, - this.immediateMode.geometry.vertices, - shouldClose - ); - if (!this.geometryBuilder) { - this.immediateMode.geometry._edgesToVertices(); - } - } - // For hollow shapes, user must set mode to TESS - const convexShape = this.immediateMode.shapeMode === constants.TESS; - // If the shape has a contour, we have to re-triangulate to cut out the - // contour region - const hasContour = this.immediateMode.contourIndices.length > 0; - // We tesselate when drawing curves or convex shapes - const shouldTess = - this.states.doFill && - ( - this.isBezier || - this.isQuadratic || - this.isCurve || - convexShape || - hasContour - ) && - this.immediateMode.shapeMode !== constants.LINES; - - if (shouldTess) { - this._tesselateShape(); - } - }; - - /** - * Called from _processVertices(). This function calculates the stroke vertices for custom shapes and - * tesselates shapes when applicable. - * @private - * @returns {Number[]} indices for custom shape vertices indicating edges. - */ - RendererGL.prototype._calculateEdges = function( - shapeMode, - verts, - shouldClose - ) { - const res = []; - let i = 0; - const contourIndices = this.immediateMode.contourIndices.slice(); - let contourStart = 0; - switch (shapeMode) { - case constants.TRIANGLE_STRIP: - for (i = 0; i < verts.length - 2; i++) { - res.push([i, i + 1]); - res.push([i, i + 2]); - } - res.push([i, i + 1]); - break; - case constants.TRIANGLE_FAN: - for (i = 1; i < verts.length - 1; i++) { - res.push([0, i]); - res.push([i, i + 1]); - } - res.push([0, verts.length - 1]); - break; - case constants.TRIANGLES: - for (i = 0; i < verts.length - 2; i = i + 3) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 2, i]); - } - break; - case constants.LINES: - for (i = 0; i < verts.length - 1; i = i + 2) { - res.push([i, i + 1]); - } - break; - case constants.QUADS: - // Quads have been broken up into two triangles by `vertex()`: - // 0 3--5 - // | \ \ | - // 1--2 4 - for (i = 0; i < verts.length - 5; i += 6) { - res.push([i, i + 1]); - res.push([i + 1, i + 2]); - res.push([i + 3, i + 5]); - res.push([i + 4, i + 5]); - } - break; - case constants.QUAD_STRIP: - // 0---2---4 - // | | | - // 1---3---5 - for (i = 0; i < verts.length - 2; i += 2) { - res.push([i, i + 1]); - res.push([i, i + 2]); - res.push([i + 1, i + 3]); - } - res.push([i, i + 1]); - break; - default: - // TODO: handle contours in other modes too - for (i = 0; i < verts.length; i++) { - // Handle breaks between contours - if (i + 1 < verts.length && i + 1 !== contourIndices[0]) { - res.push([i, i + 1]); - } else { - if (shouldClose || contourStart) { - res.push([i, contourStart]); - } - if (contourIndices.length > 0) { - contourStart = contourIndices.shift(); - } - } - } - break; - } - if (shapeMode !== constants.TESS && shouldClose) { - res.push([verts.length - 1, 0]); - } - return res; - }; - - /** - * Called from _processVertices() when applicable. This function tesselates immediateMode.geometry. - * @private - */ - RendererGL.prototype._tesselateShape = function() { - // TODO: handle non-TESS shape modes that have contours - this.immediateMode.shapeMode = constants.TRIANGLES; - const contours = [[]]; - for (let i = 0; i < this.immediateMode.geometry.vertices.length; i++) { - if ( - this.immediateMode.contourIndices.length > 0 && - this.immediateMode.contourIndices[0] === i - ) { - this.immediateMode.contourIndices.shift(); - contours.push([]); - } - contours[contours.length-1].push( - this.immediateMode.geometry.vertices[i].x, - this.immediateMode.geometry.vertices[i].y, - this.immediateMode.geometry.vertices[i].z, - this.immediateMode.geometry.uvs[i * 2], - this.immediateMode.geometry.uvs[i * 2 + 1], - this.immediateMode.geometry.vertexColors[i * 4], - this.immediateMode.geometry.vertexColors[i * 4 + 1], - this.immediateMode.geometry.vertexColors[i * 4 + 2], - this.immediateMode.geometry.vertexColors[i * 4 + 3], - this.immediateMode.geometry.vertexNormals[i].x, - this.immediateMode.geometry.vertexNormals[i].y, - this.immediateMode.geometry.vertexNormals[i].z - ); - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const start = i * prop.getDataSize(); - const end = start + prop.getDataSize(); - const vals = prop.getSrcArray().slice(start, end); - contours[contours.length-1].push(...vals); - } - } - const polyTriangles = this._triangulate(contours); - const originalVertices = this.immediateMode.geometry.vertices; - this.immediateMode.geometry.vertices = []; - this.immediateMode.geometry.vertexNormals = []; - this.immediateMode.geometry.uvs = []; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - prop.resetSrcArray(); - } - const colors = []; - for ( - let j = 0, polyTriLength = polyTriangles.length; - j < polyTriLength; - j = j + this.tessyVertexSize - ) { - colors.push(...polyTriangles.slice(j + 5, j + 9)); - this.normal(...polyTriangles.slice(j + 9, j + 12)); - { - let offset = 12; - for (const propName in this.immediateMode.geometry.userVertexProperties){ - const prop = this.immediateMode.geometry.userVertexProperties[propName]; - const size = prop.getDataSize(); - const start = j + offset; - const end = start + size; - prop.setCurrentData(polyTriangles.slice(start, end)); - offset += size; - } - } - this.vertex(...polyTriangles.slice(j, j + 5)); - } - if (this.geometryBuilder) { - // Tesselating the face causes the indices of edge vertices to stop being - // correct. When rendering, this is not a problem, since _edgesToVertices - // will have been called before this, and edge vertex indices are no longer - // needed. However, the geometry builder still needs this information, so - // when one is active, we need to update the indices. - // - // We record index mappings in a Map so that once we have found a - // corresponding vertex, we don't need to loop to find it again. - const newIndex = new Map(); - this.immediateMode.geometry.edges = - this.immediateMode.geometry.edges.map(edge => edge.map(origIdx => { - if (!newIndex.has(origIdx)) { - const orig = originalVertices[origIdx]; - let newVertIndex = this.immediateMode.geometry.vertices.findIndex( - v => - orig.x === v.x && - orig.y === v.y && - orig.z === v.z - ); - if (newVertIndex === -1) { - // The tesselation process didn't output a vertex with the exact - // coordinate as before, potentially due to numerical issues. This - // doesn't happen often, but in this case, pick the closest point - let closestDist = Infinity; - let closestIndex = 0; - for ( - let i = 0; - i < this.immediateMode.geometry.vertices.length; - i++ - ) { - const vert = this.immediateMode.geometry.vertices[i]; - const dX = orig.x - vert.x; - const dY = orig.y - vert.y; - const dZ = orig.z - vert.z; - const dist = dX*dX + dY*dY + dZ*dZ; - if (dist < closestDist) { - closestDist = dist; - closestIndex = i; - } - } - newVertIndex = closestIndex; - } - newIndex.set(origIdx, newVertIndex); - } - return newIndex.get(origIdx); - })); - } - this.immediateMode.geometry.vertexColors = colors; - }; - - /** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the fill geometry. - * @private - */ - RendererGL.prototype._drawImmediateFill = function(count = 1) { - const gl = this.GL; - this._useVertexColor = (this.immediateMode.geometry.vertexColors.length > 0); - - let shader; - shader = this._getFillShader(); - - this._setFillUniforms(shader); - - for (const buff of this.immediateMode.buffers.fill) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - shader.disableRemainingAttributes(); - - this._applyColorBlend( - this.states.curFillColor, - this.immediateMode.geometry.hasFillTransparency() - ); - - if (count === 1) { - gl.drawArrays( - this.immediateMode.shapeMode, - 0, - this.immediateMode.geometry.vertices.length - ); - } - else { - try { - gl.drawArraysInstanced( - this.immediateMode.shapeMode, - 0, - this.immediateMode.geometry.vertices.length, - count - ); - } - catch (e) { - console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); - } - } - shader.unbindShader(); - }; - - /** - * Called from endShape(). Responsible for calculating normals, setting shader uniforms, - * enabling all appropriate buffers, applying color blend, and drawing the stroke geometry. - * @private - */ - RendererGL.prototype._drawImmediateStroke = function() { - const gl = this.GL; - - this._useLineColor = - (this.immediateMode.geometry.vertexStrokeColors.length > 0); - - const shader = this._getImmediateStrokeShader(); - this._setStrokeUniforms(shader); - for (const buff of this.immediateMode.buffers.stroke) { - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - for (const buff of this.immediateMode.buffers.user){ - buff._prepareBuffer(this.immediateMode.geometry, shader); - } - shader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - this.immediateMode.geometry.hasFillTransparency() - ); - - gl.drawArrays( - gl.TRIANGLES, - 0, - this.immediateMode.geometry.lineVertices.length / 3 - ); - shader.unbindShader(); - }; -} - -export default rendererGLImmediate; - -if(typeof p5 !== 'undefined'){ - rendererGLImmediate(p5, p5.prototype); -} diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 780e4183cc..1d6ad770b0 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -16,34 +16,13 @@ function rendererGLRetained(p5, fn){ this._freeBuffers(geometry.gid); }; - /** - * _initBufferDefaults - * @private - * @description initializes buffer defaults. runs each time a new geometry is - * registered - * @param {String} gId key of the geometry object - * @returns {Object} a new buffer object - */ - RendererGL.prototype._initBufferDefaults = function(gId) { - this._freeBuffers(gId); - - //@TODO remove this limit on hashes in retainedMode.geometry - if (Object.keys(this.retainedMode.geometry).length > 1000) { - const key = Object.keys(this.retainedMode.geometry)[0]; - this._freeBuffers(key); - } - - //create a new entry in our retainedMode.geometry - return (this.retainedMode.geometry[gId] = {}); - }; - - RendererGL.prototype._freeBuffers = function(gId) { - const buffers = this.retainedMode.geometry[gId]; + RendererGL.prototype._freeBuffers = function(gid) { + const buffers = this.geometryBufferCache[gid]; if (!buffers) { return; } - delete this.retainedMode.geometry[gId]; + delete this.geometryBufferCache[gid]; const gl = this.GL; if (buffers.indexBuffer) { @@ -60,23 +39,38 @@ function rendererGLRetained(p5, fn){ } // free all the buffers - freeBuffers(this.retainedMode.buffers.stroke); - freeBuffers(this.retainedMode.buffers.fill); - freeBuffers(this.retainedMode.buffers.user); - this.retainedMode.buffers.user = []; + freeBuffers(this.buffers.stroke); + freeBuffers(this.buffers.fill); + freeBuffers(this.buffers.user); }; /** - * creates a buffers object that holds the WebGL render buffers + * Creates a buffers object that holds the WebGL render buffers * for a geometry. * @private - * @param {String} gId key of the geometry object * @param {p5.Geometry} model contains geometry data */ - RendererGL.prototype.createBuffers = function(gId, model) { + RendererGL.prototype.createBuffers = function(model) { const gl = this.GL; + + const gid = model.gid; + if (!gid) { + throw new Error('The p5.Geometry you passed in has no gid property!'); + } + //initialize the gl buffers for our geom groups - const buffers = this._initBufferDefaults(gId); + this._freeBuffers(gid); + + //@TODO remove this limit on hashes in geometryBufferCache + if (Object.keys(this.geometryBufferCache).length > 1000) { + const key = Object.keys(this.geometryBufferCache)[0]; + this._freeBuffers(key); + } + + //create a new entry in our geometryBufferCache + const buffers = {}; + this.geometryBufferCache[gid] = buffers; + buffers.model = model; let indexBuffer = buffers.indexBuffer; @@ -108,107 +102,25 @@ function rendererGLRetained(p5, fn){ gl.deleteBuffer(indexBuffer); buffers.indexBuffer = null; } + // TODO: delete? // the vertex count comes directly from the model buffers.vertexCount = model.vertices ? model.vertices.length : 0; } + // TODO: delete? buffers.lineVertexCount = model.lineVertices ? model.lineVertices.length / 3 : 0; - for (const propName in model.userVertexProperties){ + for (const propName in model.userVertexProperties) { const prop = model.userVertexProperties[propName]; - this.retainedMode.buffers.user.push( + this.buffers.user.push( new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) ); } return buffers; }; - /** - * Draws buffers given a geometry key ID - * @private - * @param {String} gId ID in our geom hash - * @chainable - */ - RendererGL.prototype.drawBuffers = function(gId) { - const gl = this.GL; - const geometry = this.retainedMode.geometry[gId]; - - if ( - !this.geometryBuilder && - this.states.doFill && - geometry.vertexCount > 0 - ) { - this._useVertexColor = (geometry.model.vertexColors.length > 0); - - let fillShader; - if (this._drawingFilter && this.states.userFillShader) { - fillShader = this.states.userFillShader; - } else { - fillShader = this._getFillShader(); - } - this._setFillUniforms(fillShader); - - for (const buff of this.retainedMode.buffers.fill) { - buff._prepareBuffer(geometry, fillShader); - } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } - buff._prepareBuffer(geometry, fillShader); - } - fillShader.disableRemainingAttributes(); - if (geometry.indexBuffer) { - //vertex index buffer - this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER); - } - this._applyColorBlend( - this.states.curFillColor, - geometry.model.hasFillTransparency() - ); - this._drawElements(gl.TRIANGLES, gId); - fillShader.unbindShader(); - } - - if (!this.geometryBuilder && this.states.doStroke && geometry.lineVertexCount > 0) { - this._useLineColor = (geometry.model.vertexStrokeColors.length > 0); - const strokeShader = this._getRetainedStrokeShader(); - this._setStrokeUniforms(strokeShader); - for (const buff of this.retainedMode.buffers.stroke) { - buff._prepareBuffer(geometry, strokeShader); - } - for (const buff of this.retainedMode.buffers.user){ - const prop = geometry.model.userVertexProperties[buff.attr]; - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if(adjustedLength > geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if(adjustedLength < geometry.model.vertices.length){ - p5._friendlyError(`One of the geometries has a custom vertex property ${prop.name} with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } - buff._prepareBuffer(geometry, strokeShader); - } - strokeShader.disableRemainingAttributes(); - this._applyColorBlend( - this.states.curStrokeColor, - geometry.model.hasStrokeTransparency() - ); - this._drawArrays(gl.TRIANGLES, gId); - strokeShader.unbindShader(); - } - - if (this.geometryBuilder) { - this.geometryBuilder.addRetained(geometry); - } - - return this; - }; - /** * Calls drawBuffers() with a scaled model/view matrix. * @@ -219,13 +131,13 @@ function rendererGLRetained(p5, fn){ * * @private * @method drawBuffersScaled - * @param {String} gId ID in our geom hash + * @param {String} gid ID in our geom hash * @param {Number} scaleX the amount to scale in the X direction * @param {Number} scaleY the amount to scale in the Y direction * @param {Number} scaleZ the amount to scale in the Z direction */ RendererGL.prototype.drawBuffersScaled = function( - gId, + model, scaleX, scaleY, scaleZ @@ -234,50 +146,16 @@ function rendererGLRetained(p5, fn){ try { this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); - this.drawBuffers(gId); + if (this.geometryBuilder) { + this.geometryBuilder.addRetained(model); + } else { + this._drawGeometry(model); + } } finally { this.states.uModelMatrix = originalModelMatrix; } }; - RendererGL.prototype._drawArrays = function(drawMode, gId) { - this.GL.drawArrays( - drawMode, - 0, - this.retainedMode.geometry[gId].lineVertexCount - ); - return this; - }; - - RendererGL.prototype._drawElements = function(drawMode, gId) { - const buffers = this.retainedMode.geometry[gId]; - const gl = this.GL; - // render the fill - if (buffers.indexBuffer) { - // If this model is using a Uint32Array we need to ensure the - // OES_element_index_uint WebGL extension is enabled. - if ( - this._pInst.webglVersion !== constants.WEBGL2 && - buffers.indexBufferType === gl.UNSIGNED_INT - ) { - if (!gl.getExtension('OES_element_index_uint')) { - throw new Error( - 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' - ); - } - } - // we're drawing faces - gl.drawElements( - gl.TRIANGLES, - buffers.vertexCount, - buffers.indexBufferType, - 0 - ); - } else { - // drawing vertices - gl.drawArrays(drawMode || gl.TRIANGLES, 0, buffers.vertexCount); - } - }; RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { const gl = this.GL; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 2ba2e45cee..219d8c2d51 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1,6 +1,5 @@ import * as constants from '../core/constants'; import GeometryBuilder from './GeometryBuilder'; -import libtess from 'libtess'; // Fixed with exporting module from libtess import { Renderer } from '../core/p5.Renderer'; import { Matrix } from './p5.Matrix'; import { Camera } from './p5.Camera'; @@ -14,6 +13,7 @@ import { Texture, MipmapTexture } from './p5.Texture'; import { Framebuffer } from './p5.Framebuffer'; import { Graphics } from '../core/p5.Graphics'; import { Element } from '../core/p5.Element'; +import { ShapeBuilder } from './ShapeBuilder'; import lightingShader from './shaders/lighting.glsl'; import webgl2CompatibilityShader from './shaders/webgl2Compatibility.glsl'; @@ -307,64 +307,33 @@ class RendererGL extends Renderer { this.states.userPointShader = undefined; this.states.userImageShader = undefined; - this._useUserVertexProperties = undefined; - - // Default drawing is done in Retained Mode - // Geometry and Material hashes stored here - this.retainedMode = { - geometry: {}, - buffers: { - stroke: [ - new RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - fill: [ - new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - //new BufferDef(3, 'vertexSpeculars', 'specularBuffer', 'aSpecularColor'), - new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - text: [ - new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - user:[] - } - }; - - // Immediate Mode - // Geometry and Material hashes stored here - this.immediateMode = { - geometry: new Geometry(), - shapeMode: constants.TRIANGLE_FAN, - contourIndices: [], - _bezierVertex: [], - _quadraticVertex: [], - _curveVertex: [], - buffers: { - fill: [ - new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), - new RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), - new RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ], - stroke: [ - new RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), - new RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), - new RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), - new RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), - new RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) - ], - point: this.GL.createBuffer(), - user:[] - } - }; + // Used by beginShape/endShape functions to construct a p5.Geometry + this.shapeBuilder = new ShapeBuilder(this); + + this.buffers = { + fill: [ + new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), + new RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), + new RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), + new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + stroke: [ + new RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), + new RenderBuffer(3, 'lineVertices', 'lineVerticesBuffer', 'aPosition', this), + new RenderBuffer(3, 'lineTangentsIn', 'lineTangentsInBuffer', 'aTangentIn', this), + new RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), + new RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) + ], + text: [ + new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), + new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + ], + point: this.GL.createBuffer(), + user:[] + } + + this.geometryBufferCache = {}; this.pointSize = 5.0; //default point size this.curStrokeWeight = 1; @@ -397,15 +366,16 @@ class RendererGL extends Renderer { // current curveDetail in the Quadratic lookUpTable this._lutQuadraticDetail = 0; - // Used to distinguish between user calls to vertex() and internal calls - this.isProcessingVertices = false; - this._tessy = this._initTessy(); this.fontInfos = {}; this._curShader = undefined; } + ////////////////////////////////////////////// + // Geometry Building + ////////////////////////////////////////////// + /** * Starts creating a new p5.Geometry. Subsequent shapes drawn will be added * to the geometry and then returned when @@ -466,6 +436,234 @@ class RendererGL extends Renderer { return this.endGeometry(); } + + ////////////////////////////////////////////// + // Shape drawing + ////////////////////////////////////////////// + + beginShape(...args) { + this.shapeBuilder.beginShape(...args); + } + + endShape( + mode, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind, + count = 1 + ) { + this.shapeBuilder.endShape( + mode, + isCurve, + isBezier, + isQuadratic, + isContour, + shapeKind + ); + + if (this.geometryBuilder) { + this.geometryBuilder.addImmediate( + this.shapeBuilder.geometry, + this.shapeBuilder.shapeMode + ); + } else if (this.states.doFill || this.states.doStroke) { + this._drawGeometry( + this.shapeBuilder.geometry, + { mode: this.shapeBuilder.shapeMode, count } + ); + } + } + + beginContour(...args) { + this.shapeBuilder.beginContour(...args); + } + + vertex(...args) { + this.shapeBuilder.vertex(...args); + } + + vertexProperty(...args) { + this.shapeBuilder.vertexProperty(...args); + } + + normal(xorv, y, z) { + if (xorv instanceof Vector) { + this.states._currentNormal = xorv; + } else { + this.states._currentNormal = new Vector(xorv, y, z); + } + } + + ////////////////////////////////////////////// + // Rendering + ////////////////////////////////////////////// + + _drawGeometry(geometry, { mode = constants.TRIANGLES, count = 1 } = {}) { + if ( + this.states.doFill && + geometry.vertices.length >= 3 && + ![constants.LINES, constants.POINTS].includes(mode) + ) { + this._drawFills(geometry, { mode, count }); + } + + if (this.states.doStroke && geometry.lineVertices.length >= 1) { + this._drawStrokes(geometry, { count }); + } + + this.buffers.user = []; + } + + _drawFills(geometry, { count, mode } = {}) { + this._useVertexColor = geometry.vertexColors.length > 0; + + const shader = this._drawingFilter && this.states.userFillShader + ? this.states.userFillShader + : this._getFillShader(); + this._setFillUniforms(shader); + + for (const buff of this.buffers.fill) { + buff._prepareBuffer(geometry, shader); + } + this._prepareUserAttributes(geometry, shader); + shader.disableRemainingAttributes(); + + this._applyColorBlend( + this.states.curFillColor, + geometry.hasFillTransparency() + ); + + this._drawBuffers(geometry, { mode, count }); + + shader.unbindShader(); + } + + _drawStrokes(geometry, { count } = {}) { + const gl = this.GL; + + this._useLineColor = geometry.vertexStrokeColors.length > 0; + + const shader = this._getStrokeShader(); + this._setStrokeUniforms(shader); + + for (const buff of this.buffers.stroke) { + buff._prepareBuffer(geometry, shader); + } + this._prepareUserAttributes(geometry, shader); + shader.disableRemainingAttributes(); + + this._applyColorBlend( + this.states.curStrokeColor, + geometry.hasStrokeTransparency() + ); + + if (count === 1) { + gl.drawArrays( + gl.TRIANGLES, + 0, + geometry.lineVertices.length / 3 + ); + } else { + try { + gl.drawArraysInstanced( + gl.TRIANGLES, + 0, + geometry.lineVertices.length / 3, + count + ); + } catch (e) { + console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + } + } + + shader.unbindShader(); + } + + _prepareUserAttributes(geometry, shader) { + for (const buff of this.buffers.user) { + // Check for the right data size + const prop = geometry.userVertexProperties[buff.attr]; + if (prop) { + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if (adjustedLength > geometry.vertices.length) { + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if (adjustedLength < geometry.vertices.length) { + p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } + } + buff._prepareBuffer(geometry, shader); + } + } + + _drawBuffers(geometry, { mode = this.GL.TRIANGLES, count }) { + const gl = this.GL; + const glBuffers = this.geometryBufferCache[geometry.gid]; + + if (glBuffers?.indexBuffer) { + // If this model is using a Uint32Array we need to ensure the + // OES_element_index_uint WebGL extension is enabled. + if ( + this._pInst.webglVersion !== constants.WEBGL2 && + glBuffers.indexBufferType === gl.UNSIGNED_INT + ) { + if (!gl.getExtension('OES_element_index_uint')) { + throw new Error( + 'Unable to render a 3d model with > 65535 triangles. Your web browser does not support the WebGL Extension OES_element_index_uint.' + ); + } + } + + if (count === 1) { + gl.drawElements( + gl.TRIANGLES, + glBuffers.vertexCount, + glBuffers.indexBufferType, + 0 + ); + } else { + try { + gl.drawElementsInstanced( + gl.TRIANGLES, + glBuffers.vertexCount, + glBuffers.indexBufferType, + 0, + count + ); + } catch (e) { + console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + } + } + } else { + if (count === 1) { + gl.drawArrays( + mode, + 0, + geometry.vertices.length + ); + } else { + try { + gl.drawArraysInstanced( + mode, + 0, + geometry.vertices.length, + count + ); + } catch (e) { + console.log('🌸 p5.js says: Instancing is only supported in WebGL2 mode'); + } + } + } + } + + _getOrMakeCachedBuffers(geometry) { + if (!this.geometryInHash(geometry.gid)) { + this.createBuffers(geometry); + } + return this.geometryBufferCache[geometry.gid] + } + ////////////////////////////////////////////// // Setting ////////////////////////////////////////////// @@ -1167,8 +1365,8 @@ class RendererGL extends Renderer { // HASH | for geometry ////////////////////////////////////////////// - geometryInHash(gId) { - return this.retainedMode.geometry[gId] !== undefined; + geometryInHash(gid) { + return this.geometryBufferCache[gid] !== undefined; } viewport(w, h) { @@ -1390,7 +1588,7 @@ class RendererGL extends Renderer { * and the shader must be valid in that context. */ - _getImmediateStrokeShader() { + _getStrokeShader() { // select the stroke shader to use const stroke = this.states.userStrokeShader; if (stroke) { @@ -1400,10 +1598,6 @@ class RendererGL extends Renderer { } - _getRetainedStrokeShader() { - return this._getImmediateStrokeShader(); - } - _getSphereMapping(img) { if (!this.sphereMapping) { this.sphereMapping = this._pInst.createFilterShader( @@ -1460,10 +1654,6 @@ class RendererGL extends Renderer { return point; } - _getRetainedLineShader() { - return this._getImmediateLineShader(); - } - baseMaterialShader() { if (!this._pInst._glAttributes.perPixelLighting) { throw new Error( @@ -1858,15 +2048,15 @@ class RendererGL extends Renderer { return new Framebuffer(this, options); } - _setStrokeUniforms(baseStrokeShader) { - baseStrokeShader.bindShader(); + _setStrokeUniforms(strokeShader) { + strokeShader.bindShader(); // set the uniform values - baseStrokeShader.setUniform('uUseLineColor', this._useLineColor); - baseStrokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); - baseStrokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); - baseStrokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); - baseStrokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); + strokeShader.setUniform('uUseLineColor', this._useLineColor); + strokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); + strokeShader.setUniform('uStrokeWeight', this.curStrokeWeight); + strokeShader.setUniform('uStrokeCap', STROKE_CAP_ENUM[this.curStrokeCap]); + strokeShader.setUniform('uStrokeJoin', STROKE_JOIN_ENUM[this.curStrokeJoin]); } _setFillUniforms(fillShader) { @@ -2081,106 +2271,7 @@ class RendererGL extends Renderer { const p = [p1, p2, p3, p4]; return p; } - _initTessy() { - this.tessyVertexSize = 12; - // function called for each vertex of tesselator output - function vertexCallback(data, polyVertArray) { - for (const element of data) { - polyVertArray.push(element); - } - } - - function begincallback(type) { - if (type !== libtess.primitiveType.GL_TRIANGLES) { - console.log(`expected TRIANGLES but got type: ${type}`); - } - } - function errorcallback(errno) { - console.log('error callback'); - console.log(`error number: ${errno}`); - } - // callback for when segments intersect and must be split - const combinecallback = (coords, data, weight) => { - const result = new Array(this.tessyVertexSize).fill(0); - for (let i = 0; i < weight.length; i++) { - for (let j = 0; j < result.length; j++) { - if (weight[i] === 0 || !data[i]) continue; - result[j] += data[i][j] * weight[i]; - } - } - return result; - }; - - function edgeCallback(flag) { - // don't really care about the flag, but need no-strip/no-fan behavior - } - - const tessy = new libtess.GluTesselator(); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); - tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); - tessy.gluTessProperty( - libtess.gluEnum.GLU_TESS_WINDING_RULE, - libtess.windingRule.GLU_TESS_WINDING_NONZERO - ); - - return tessy; - } - - _triangulate(contours) { - // libtess will take 3d verts and flatten to a plane for tesselation. - // libtess is capable of calculating a plane to tesselate on, but - // if all of the vertices have the same z values, we'll just - // assume the face is facing the camera, letting us skip any performance - // issues or bugs in libtess's automatic calculation. - const z = contours[0] ? contours[0][2] : undefined; - let allSameZ = true; - for (const contour of contours) { - for ( - let j = 0; - j < contour.length; - j += this.tessyVertexSize - ) { - if (contour[j + 2] !== z) { - allSameZ = false; - break; - } - } - } - if (allSameZ) { - this._tessy.gluTessNormal(0, 0, 1); - } else { - // Let libtess pick a plane for us - this._tessy.gluTessNormal(0, 0, 0); - } - - const triangleVerts = []; - this._tessy.gluTessBeginPolygon(triangleVerts); - - for (const contour of contours) { - this._tessy.gluTessBeginContour(); - for ( - let j = 0; - j < contour.length; - j += this.tessyVertexSize - ) { - const coords = contour.slice( - j, - j + this.tessyVertexSize - ); - this._tessy.gluTessVertex(coords, coords); - } - this._tessy.gluTessEndContour(); - } - - // finish polygon - this._tessy.gluTessEndPolygon(); - - return triangleVerts; - } }; function rendererGL(p5, fn){ @@ -2372,8 +2463,8 @@ function rendererGL(p5, fn){ } if (!this._setupDone) { - for (const x in this._renderer.retainedMode.geometry) { - if (this._renderer.retainedMode.geometry.hasOwnProperty(x)) { + for (const x in this._renderer.geometryBufferCache) { + if (this._renderer.geometryBufferCache.hasOwnProperty(x)) { p5._friendlyError( 'Sorry, Could not set the attributes, you need to call setAttributes() ' + 'before calling the other drawing methods in setup()' diff --git a/test/unit/core/rendering.js b/test/unit/core/rendering.js index b2c9b81834..9f0fa44762 100644 --- a/test/unit/core/rendering.js +++ b/test/unit/core/rendering.js @@ -116,8 +116,10 @@ suite('Rendering', function() { glStub = vi.spyOn(p5.RendererGL.prototype, '_getMaxTextureSize'); const fakeMaxTextureSize = 100; glStub.mockReturnValue(fakeMaxTextureSize); + const prevRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; myp5.createCanvas(200, 200, myp5.WEBGL); - myp5.pixelDensity(1); + window.devicePixelRatio = prevRatio; assert.equal(myp5.width, 100); assert.equal(myp5.height, 100); }); diff --git a/test/unit/webgl/p5.Framebuffer.js b/test/unit/webgl/p5.Framebuffer.js index cbf78d07f4..9dfe186ff1 100644 --- a/test/unit/webgl/p5.Framebuffer.js +++ b/test/unit/webgl/p5.Framebuffer.js @@ -3,8 +3,11 @@ import { vi } from 'vitest'; suite('p5.Framebuffer', function() { let myp5; + let prevPixelRatio; beforeAll(function() { + prevPixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; myp5 = new p5(function(p) { p.setup = function() {}; p.draw = function() {}; @@ -13,6 +16,7 @@ suite('p5.Framebuffer', function() { afterAll(function() { myp5.remove(); + window.devicePixelRatio = prevPixelRatio; }); suite('formats and channels', function() { diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index bb3c39e115..9ae68a70eb 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1320,7 +1320,7 @@ suite('p5.RendererGL', function() { myp5.stroke(255); myp5.triangle(0, 0, 1, 0, 0, 1); - var buffers = renderer.retainedMode.geometry['tri']; + var buffers = renderer.geometryBufferCache['tri']; assert.isObject(buffers); assert.isDefined(buffers.indexBuffer); @@ -1442,13 +1442,13 @@ suite('p5.RendererGL', function() { [3, 1, 7] ]; assert.equal( - renderer.immediateMode.geometry.vertices.length, + renderer.shapeBuilder.geometry.vertices.length, expectedVerts.length ); expectedVerts.forEach(function([x, y, z], i) { - assert.equal(renderer.immediateMode.geometry.vertices[i].x, x); - assert.equal(renderer.immediateMode.geometry.vertices[i].y, y); - assert.equal(renderer.immediateMode.geometry.vertices[i].z, z); + assert.equal(renderer.shapeBuilder.geometry.vertices[i].x, x); + assert.equal(renderer.shapeBuilder.geometry.vertices[i].y, y); + assert.equal(renderer.shapeBuilder.geometry.vertices[i].z, z); }); const expectedUVs = [ @@ -1468,7 +1468,7 @@ suite('p5.RendererGL', function() { [1, 0], [1, 1] ].flat(); - assert.deepEqual(renderer.immediateMode.geometry.uvs, expectedUVs); + assert.deepEqual(renderer.shapeBuilder.geometry.uvs, expectedUVs); const expectedColors = [ [1, 0, 0, 1], @@ -1488,7 +1488,7 @@ suite('p5.RendererGL', function() { [1, 0, 1, 1] ].flat(); assert.deepEqual( - renderer.immediateMode.geometry.vertexColors, + renderer.shapeBuilder.geometry.vertexColors, expectedColors ); @@ -1510,13 +1510,13 @@ suite('p5.RendererGL', function() { [21, 22, 23] ]; assert.equal( - renderer.immediateMode.geometry.vertexNormals.length, + renderer.shapeBuilder.geometry.vertexNormals.length, expectedNormals.length ); expectedNormals.forEach(function([x, y, z], i) { - assert.equal(renderer.immediateMode.geometry.vertexNormals[i].x, x); - assert.equal(renderer.immediateMode.geometry.vertexNormals[i].y, y); - assert.equal(renderer.immediateMode.geometry.vertexNormals[i].z, z); + assert.equal(renderer.shapeBuilder.geometry.vertexNormals[i].x, x); + assert.equal(renderer.shapeBuilder.geometry.vertexNormals[i].y, y); + assert.equal(renderer.shapeBuilder.geometry.vertexNormals[i].z, z); }); }); @@ -1534,7 +1534,7 @@ suite('p5.RendererGL', function() { renderer.vertex(3, 1); renderer.endShape(); - assert.equal(renderer.immediateMode.geometry.edges.length, 8); + assert.equal(renderer.shapeBuilder.geometry.edges.length, 8); }); test('QUAD_STRIP mode makes edges for strip outlines', function() { @@ -1551,7 +1551,7 @@ suite('p5.RendererGL', function() { renderer.endShape(); // Two full quads (2 * 4) plus two edges connecting them - assert.equal(renderer.immediateMode.geometry.edges.length, 10); + assert.equal(renderer.shapeBuilder.geometry.edges.length, 10); }); test('TRIANGLE_FAN mode makes edges for each triangle', function() { @@ -1569,7 +1569,7 @@ suite('p5.RendererGL', function() { renderer.vertex(-5, 0); renderer.endShape(); - assert.equal(renderer.immediateMode.geometry.edges.length, 7); + assert.equal(renderer.shapeBuilder.geometry.edges.length, 7); }); test('TESS preserves vertex data', function() { @@ -1595,59 +1595,59 @@ suite('p5.RendererGL', function() { renderer.vertex(-10, 10, 0, 1); renderer.endShape(myp5.CLOSE); - assert.equal(renderer.immediateMode.geometry.vertices.length, 6); + assert.equal(renderer.shapeBuilder.geometry.vertices.length, 6); assert.deepEqual( - renderer.immediateMode.geometry.vertices[0].array(), + renderer.shapeBuilder.geometry.vertices[0].array(), [10, -10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[1].array(), + renderer.shapeBuilder.geometry.vertices[1].array(), [-10, 10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[2].array(), + renderer.shapeBuilder.geometry.vertices[2].array(), [-10, -10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[3].array(), + renderer.shapeBuilder.geometry.vertices[3].array(), [-10, 10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[4].array(), + renderer.shapeBuilder.geometry.vertices[4].array(), [10, -10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[5].array(), + renderer.shapeBuilder.geometry.vertices[5].array(), [10, 10, 0] ); - assert.equal(renderer.immediateMode.geometry.vertexNormals.length, 6); + assert.equal(renderer.shapeBuilder.geometry.vertexNormals.length, 6); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[0].array(), + renderer.shapeBuilder.geometry.vertexNormals[0].array(), [1, -1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[1].array(), + renderer.shapeBuilder.geometry.vertexNormals[1].array(), [-1, 1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[2].array(), + renderer.shapeBuilder.geometry.vertexNormals[2].array(), [-1, -1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[3].array(), + renderer.shapeBuilder.geometry.vertexNormals[3].array(), [-1, 1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[4].array(), + renderer.shapeBuilder.geometry.vertexNormals[4].array(), [1, -1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[5].array(), + renderer.shapeBuilder.geometry.vertexNormals[5].array(), [1, 1, 1] ); - assert.deepEqual(renderer.immediateMode.geometry.aCustomSrc, [ + assert.deepEqual(renderer.shapeBuilder.geometry.aCustomSrc, [ 1, 0, 0, 0, 0, 1, 1, 1, 1, @@ -1656,7 +1656,7 @@ suite('p5.RendererGL', function() { 0, 1, 0 ]); - assert.deepEqual(renderer.immediateMode.geometry.vertexColors, [ + assert.deepEqual(renderer.shapeBuilder.geometry.vertexColors, [ 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, @@ -1665,7 +1665,7 @@ suite('p5.RendererGL', function() { 0, 1, 0, 1 ]); - assert.deepEqual(renderer.immediateMode.geometry.uvs, [ + assert.deepEqual(renderer.shapeBuilder.geometry.uvs, [ 1, 0, 0, 1, 0, 0, @@ -1692,7 +1692,7 @@ suite('p5.RendererGL', function() { renderer.endShape(myp5.CLOSE); // Vertex colors are not run through tessy - assert.deepEqual(renderer.immediateMode.geometry.vertexStrokeColors, [ + assert.deepEqual(renderer.shapeBuilder.geometry.vertexStrokeColors, [ 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, @@ -1715,7 +1715,7 @@ suite('p5.RendererGL', function() { renderer.endShape(myp5.CLOSE); // UVs are correctly translated through tessy - assert.deepEqual(renderer.immediateMode.geometry.uvs, [ + assert.deepEqual(renderer.shapeBuilder.geometry.uvs, [ 0, 0, 1, 0, 1, 1, @@ -1752,59 +1752,59 @@ suite('p5.RendererGL', function() { renderer.vertex(-10, 10, 0, 1); renderer.endShape(myp5.CLOSE); - assert.equal(renderer.immediateMode.geometry.vertices.length, 6); + assert.equal(renderer.shapeBuilder.geometry.vertices.length, 6); assert.deepEqual( - renderer.immediateMode.geometry.vertices[0].array(), + renderer.shapeBuilder.geometry.vertices[0].array(), [0, 0, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[1].array(), + renderer.shapeBuilder.geometry.vertices[1].array(), [-10, 10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[2].array(), + renderer.shapeBuilder.geometry.vertices[2].array(), [-10, -10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[3].array(), + renderer.shapeBuilder.geometry.vertices[3].array(), [10, 10, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[4].array(), + renderer.shapeBuilder.geometry.vertices[4].array(), [0, 0, 0] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[5].array(), + renderer.shapeBuilder.geometry.vertices[5].array(), [10, -10, 0] ); - assert.equal(renderer.immediateMode.geometry.vertexNormals.length, 6); + assert.equal(renderer.shapeBuilder.geometry.vertexNormals.length, 6); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[0].array(), + renderer.shapeBuilder.geometry.vertexNormals[0].array(), [0, 0, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[1].array(), + renderer.shapeBuilder.geometry.vertexNormals[1].array(), [-1, 1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[2].array(), + renderer.shapeBuilder.geometry.vertexNormals[2].array(), [-1, -1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[3].array(), + renderer.shapeBuilder.geometry.vertexNormals[3].array(), [1, 1, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[4].array(), + renderer.shapeBuilder.geometry.vertexNormals[4].array(), [0, 0, 1] ); assert.deepEqual( - renderer.immediateMode.geometry.vertexNormals[5].array(), + renderer.shapeBuilder.geometry.vertexNormals[5].array(), [1, -1, 1] ); - assert.deepEqual(renderer.immediateMode.geometry.vertexColors, [ + assert.deepEqual(renderer.shapeBuilder.geometry.vertexColors, [ 0.5, 0.5, 0.5, 1, 0, 0, 1, 1, 1, 1, 1, 1, @@ -1813,7 +1813,7 @@ suite('p5.RendererGL', function() { 1, 0, 0, 1 ]); - assert.deepEqual(renderer.immediateMode.geometry.uvs, [ + assert.deepEqual(renderer.shapeBuilder.geometry.uvs, [ 0.5, 0.5, 0, 1, 0, 0, @@ -1834,29 +1834,29 @@ suite('p5.RendererGL', function() { renderer.vertex(-10, 0, 10); renderer.endShape(myp5.CLOSE); - assert.equal(renderer.immediateMode.geometry.vertices.length, 6); + assert.equal(renderer.shapeBuilder.geometry.vertices.length, 6); assert.deepEqual( - renderer.immediateMode.geometry.vertices[0].array(), + renderer.shapeBuilder.geometry.vertices[0].array(), [10, 0, 10] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[1].array(), + renderer.shapeBuilder.geometry.vertices[1].array(), [-10, 0, -10] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[2].array(), + renderer.shapeBuilder.geometry.vertices[2].array(), [10, 0, -10] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[3].array(), + renderer.shapeBuilder.geometry.vertices[3].array(), [-10, 0, -10] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[4].array(), + renderer.shapeBuilder.geometry.vertices[4].array(), [10, 0, 10] ); assert.deepEqual( - renderer.immediateMode.geometry.vertices[5].array(), + renderer.shapeBuilder.geometry.vertices[5].array(), [-10, 0, 10] ); }); @@ -2515,19 +2515,19 @@ suite('p5.RendererGL', function() { myp5.vertexProperty('aCustom', 1); myp5.vertexProperty('aCustomVec3', [1, 2, 3]); myp5.vertex(0,0,0); - expect(myp5._renderer.immediateMode.geometry.userVertexProperties.aCustom).to.containSubset({ + expect(myp5._renderer.shapeBuilder.geometry.userVertexProperties.aCustom).to.containSubset({ name: 'aCustom', currentData: 1, dataSize: 1 }); - expect(myp5._renderer.immediateMode.geometry.userVertexProperties.aCustomVec3).to.containSubset({ + expect(myp5._renderer.shapeBuilder.geometry.userVertexProperties.aCustomVec3).to.containSubset({ name: 'aCustomVec3', currentData: [1, 2, 3], dataSize: 3 }); - assert.deepEqual(myp5._renderer.immediateMode.geometry.aCustomSrc, [1]); - assert.deepEqual(myp5._renderer.immediateMode.geometry.aCustomVec3Src, [1,2,3]); - expect(myp5._renderer.immediateMode.buffers.user).to.containSubset([ + assert.deepEqual(myp5._renderer.shapeBuilder.geometry.aCustomSrc, [1]); + assert.deepEqual(myp5._renderer.shapeBuilder.geometry.aCustomVec3Src, [1,2,3]); + expect(myp5._renderer.buffers.user).to.containSubset([ { size: 1, src: 'aCustomSrc', @@ -2555,10 +2555,10 @@ suite('p5.RendererGL', function() { myp5.endShape(); myp5.beginShape(); - assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomSrc); - assert.isUndefined(myp5._renderer.immediateMode.geometry.aCustomVec3Src); - assert.deepEqual(myp5._renderer.immediateMode.geometry.userVertexProperties, {}); - assert.deepEqual(myp5._renderer.immediateMode.buffers.user, []); + assert.isUndefined(myp5._renderer.shapeBuilder.geometry.aCustomSrc); + assert.isUndefined(myp5._renderer.shapeBuilder.geometry.aCustomVec3Src); + assert.deepEqual(myp5._renderer.shapeBuilder.geometry.userVertexProperties, {}); + assert.deepEqual(myp5._renderer.buffers.user, []); myp5.endShape(); } ); @@ -2572,7 +2572,7 @@ suite('p5.RendererGL', function() { myp5.vertex(0,1,0); myp5.vertex(-1,0,0); myp5.vertex(1,0,0); - const immediateCopy = myp5._renderer.immediateMode.geometry; + const immediateCopy = myp5._renderer.shapeBuilder.geometry; myp5.endShape(); const myGeo = myp5.endGeometry(); assert.deepEqual(immediateCopy.aCustomSrc, myGeo.aCustomSrc); @@ -2590,8 +2590,8 @@ suite('p5.RendererGL', function() { myp5.vertex(1,0,0); myp5.endShape(); const myGeo = myp5.endGeometry(); - myp5._renderer.createBuffers(myGeo.gId, myGeo); - expect(myp5._renderer.retainedMode.buffers.user).to.containSubset([ + myp5._renderer.createBuffers(myGeo); + expect(myp5._renderer.buffers.user).to.containSubset([ { size: 1, src: 'aCustomSrc', @@ -2607,7 +2607,7 @@ suite('p5.RendererGL', function() { ]); } ); - test('Retained mode buffers deleted after rendering', + test.only('Retained mode buffers deleted after rendering', function() { myp5.createCanvas(50, 50, myp5.WEBGL); myp5.beginGeometry(); @@ -2616,10 +2616,11 @@ suite('p5.RendererGL', function() { myp5.vertexProperty('aCustomVec3', [1,2,3]); myp5.vertex(0,0,0); myp5.vertex(1,0,0); + myp5.vertex(1,1,0); myp5.endShape(); const myGeo = myp5.endGeometry(); myp5.model(myGeo); - assert.equal(myp5._renderer.retainedMode.buffers.user.length, 0); + assert.equal(myp5._renderer.buffers.user.length, 0); } ); test.skip('Friendly error if different sizes used', @@ -2647,6 +2648,7 @@ suite('p5.RendererGL', function() { const oldLog = console.log; console.log = myLog; let myGeo = new p5.Geometry(); + myGeo.gid = 'myGeo'; myGeo.vertices.push(new p5.Vector(0,0,0)); myGeo.vertexProperty('aCustom', 1); myGeo.vertexProperty('aCustom', 2); @@ -2663,6 +2665,7 @@ suite('p5.RendererGL', function() { const oldLog = console.log; console.log = myLog; let myGeo = new p5.Geometry(); + myGeo.gid = 'myGeo'; myGeo.vertices.push(new p5.Vector(0,0,0)); myGeo.vertices.push(new p5.Vector(0,0,0)); myGeo.vertexProperty('aCustom', 1); diff --git a/test/unit/webgl/p5.Texture.js b/test/unit/webgl/p5.Texture.js index 2c0ba6f36d..4d05ec280c 100644 --- a/test/unit/webgl/p5.Texture.js +++ b/test/unit/webgl/p5.Texture.js @@ -8,8 +8,11 @@ suite('p5.Texture', function() { var imgElementNotPowerOfTwo; var imgElementPowerOfTwo; var canvas; + let prevPixelRatio; beforeEach(function() { + prevPixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; return new Promise(done => { myp5 = new p5(function(p) { p.setup = async function() { @@ -42,6 +45,7 @@ suite('p5.Texture', function() { }); afterEach(function() { + window.devicePixelRatio = prevPixelRatio; myp5.remove(); }); From 3a7774897f94d701bfc69d1a5ee9a491fce17bdb Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 10:41:43 -0500 Subject: [PATCH 2/9] Fix how FES is referenced --- src/webgl/p5.RendererGL.js | 18 ++++++++++-------- test/unit/webgl/p5.RendererGL.js | 13 +++++++++---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 219d8c2d51..fc82c40722 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -583,14 +583,16 @@ class RendererGL extends Renderer { _prepareUserAttributes(geometry, shader) { for (const buff of this.buffers.user) { - // Check for the right data size - const prop = geometry.userVertexProperties[buff.attr]; - if (prop) { - const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); - if (adjustedLength > geometry.vertices.length) { - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); - } else if (adjustedLength < geometry.vertices.length) { - p5._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + if (!this._pInst.constructor.disableFriendleErrors) { + // Check for the right data size + const prop = geometry.userVertexProperties[buff.attr]; + if (prop) { + const adjustedLength = prop.getSrcArray().length / prop.getDataSize(); + if (adjustedLength > geometry.vertices.length) { + this._pInst.constructor._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with more values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } else if (adjustedLength < geometry.vertices.length) { + this._pInst.constructor._friendlyError(`One of the geometries has a custom vertex property '${prop.getName()}' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method.`, 'vertexProperty()'); + } } } buff._prepareBuffer(geometry, shader); diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 9ae68a70eb..9302f67895 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -2607,7 +2607,7 @@ suite('p5.RendererGL', function() { ]); } ); - test.only('Retained mode buffers deleted after rendering', + test('Retained mode buffers deleted after rendering', function() { myp5.createCanvas(50, 50, myp5.WEBGL); myp5.beginGeometry(); @@ -2649,9 +2649,13 @@ suite('p5.RendererGL', function() { console.log = myLog; let myGeo = new p5.Geometry(); myGeo.gid = 'myGeo'; - myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.vertices.push(new myp5.createVector(0,0,0)); + myGeo.vertices.push(new myp5.createVector(1,0,0)); + myGeo.vertices.push(new myp5.createVector(1,1,0)); myGeo.vertexProperty('aCustom', 1); myGeo.vertexProperty('aCustom', 2); + myGeo.vertexProperty('aCustom', 3); + myGeo.vertexProperty('aCustom', 4); myp5.model(myGeo); console.log = oldLog; expect(logs.join('\n')).to.match(/One of the geometries has a custom vertex property 'aCustom' with more values than vertices./); @@ -2666,8 +2670,9 @@ suite('p5.RendererGL', function() { console.log = myLog; let myGeo = new p5.Geometry(); myGeo.gid = 'myGeo'; - myGeo.vertices.push(new p5.Vector(0,0,0)); - myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.vertices.push(new myp5.createVector(0,0,0)); + myGeo.vertices.push(new myp5.createVector(1,0,0)); + myGeo.vertices.push(new myp5.createVector(1,1,0)); myGeo.vertexProperty('aCustom', 1); myp5.model(myGeo); console.log = oldLog; From 9b6146420791c8f6aa9e8a68ea3468993893d7ca Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 11:25:14 -0500 Subject: [PATCH 3/9] Factor retained mode out into a geometry cache --- src/webgl/3d_primitives.js | 42 +++---- src/webgl/GeometryBufferCache.js | 118 ++++++++++++++++++ src/webgl/index.js | 2 - src/webgl/p5.RendererGL.Retained.js | 187 ---------------------------- src/webgl/p5.RendererGL.js | 92 +++++++++----- src/webgl/text.js | 2 +- test/unit/webgl/p5.RendererGL.js | 63 ++++++---- 7 files changed, 243 insertions(+), 263 deletions(-) create mode 100644 src/webgl/GeometryBufferCache.js delete mode 100644 src/webgl/p5.RendererGL.Retained.js diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 7092ef58fc..dd97148a16 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -863,7 +863,7 @@ function primitives3D(p5, fn){ * */ fn.freeGeometry = function(geometry) { - this._renderer._freeBuffers(geometry.gid); + this._renderer.geometryBufferCache.freeBuffers(geometry.gid); }; /** @@ -2113,7 +2113,7 @@ function primitives3D(p5, fn){ triGeom._edgesToVertices(); triGeom.computeNormals(); triGeom.gid = gid; - this.createBuffers(triGeom); + this.geometryBufferCache.ensureCached(triGeom); } // only one triangle is cached, one point is at the origin, and the @@ -2135,7 +2135,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix = mult; - this._drawGeometry(this.geometryBufferCache[gid].model); + this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2257,7 +2257,7 @@ function primitives3D(p5, fn){ } arcGeom.gid = gid; - this.createBuffers(arcGeom); + this.geometryBufferCache.ensureCached(arcGeom); } const uModelMatrix = this.states.uModelMatrix.copy(); @@ -2266,7 +2266,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); - this._drawGeometry(this.geometryBufferCache[gid].model); + this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2314,7 +2314,7 @@ function primitives3D(p5, fn){ .computeNormals() ._edgesToVertices(); rectGeom.gid = gid; - this.createBuffers(rectGeom); + this.geometryBufferCache.ensureCached(rectGeom); } // only a single rectangle (of a given detail) is cached: a square with @@ -2326,7 +2326,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); - this._drawGeometry(this.geometryBufferCache[gid].model); + this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2463,9 +2463,9 @@ function primitives3D(p5, fn){ } quadGeom._edgesToVertices(); quadGeom.gid = gid; - this.createBuffers(quadGeom); + this.geometryBufferCache.ensureCached(quadGeom); } - this._drawGeometry(this.geometryBufferCache[gid].model); + this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); return this; }; @@ -3373,10 +3373,10 @@ function primitives3D(p5, fn){ ); } planeGeom.gid = gid; - this.createBuffers(planeGeom); + this.geometryBufferCache.ensureCached(planeGeom); } - this.drawBuffersScaled(this.geometryBufferCache[gid].model, width, height, 1); + this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), width, height, 1); } RendererGL.prototype.box = function( @@ -3456,9 +3456,9 @@ function primitives3D(p5, fn){ //the key val pair: //geometry Id, Geom object boxGeom.gid = gid; - this.createBuffers(boxGeom); + this.geometryBufferCache.ensureCached(boxGeom); } - this.drawBuffersScaled(this.geometryBufferCache[gid].model, width, height, depth); + this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), width, height, depth); } RendererGL.prototype.sphere = function( @@ -3509,10 +3509,10 @@ function primitives3D(p5, fn){ ); } ellipsoidGeom.gid = gid; - this.createBuffers(ellipsoidGeom); + this.geometryBufferCache.ensureCached(ellipsoidGeom); } - this.drawBuffersScaled(this.geometryBufferCache[gid].model, radiusX, radiusY, radiusZ); + this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radiusX, radiusY, radiusZ); } RendererGL.prototype.cylinder = function( @@ -3546,10 +3546,10 @@ function primitives3D(p5, fn){ ); } cylinderGeom.gid = gid; - this.createBuffers(cylinderGeom); + this.geometryBufferCache.ensureCached(cylinderGeom); } - this.drawBuffersScaled(this.geometryBufferCache[gid].model, radius, height, radius); + this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radius, height, radius); } RendererGL.prototype.cone = function( @@ -3572,10 +3572,10 @@ function primitives3D(p5, fn){ ); } coneGeom.gid = gid; - this.createBuffers(coneGeom); + this.geometryBufferCache.ensureCached(coneGeom); } - this.drawBuffersScaled(this.geometryBufferCache[gid].model, radius, height, radius); + this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radius, height, radius); } RendererGL.prototype.torus = function( @@ -3635,9 +3635,9 @@ function primitives3D(p5, fn){ ); } torusGeom.gid = gid; - this.createBuffers(torusGeom); + this.geometryBufferCache.ensureCached(torusGeom); } - this.drawBuffersScaled(this.geometryBufferCache[gid].model, radius, radius, radius); + this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radius, radius, radius); } } diff --git a/src/webgl/GeometryBufferCache.js b/src/webgl/GeometryBufferCache.js new file mode 100644 index 0000000000..28209d6b99 --- /dev/null +++ b/src/webgl/GeometryBufferCache.js @@ -0,0 +1,118 @@ +export class GeometryBufferCache { + constructor(renderer) { + this.renderer = renderer; + this.cache = {}; + } + + numCached() { + return Object.keys(this.cache).length; + } + + isCached(gid) { + return this.cache[gid] !== undefined; + } + + getModelByID(gid) { + return this.cache[gid]?.geometry; + } + + getCached(model) { + return this.getCachedID(model.gid); + } + + getCachedID(gid) { + return this.cache[gid]; + } + + ensureCached(geometry) { + const gid = geometry.gid; + if (!gid) { + throw new Error('The p5.Geometry you passed in has no gid property!'); + } + + if (this.isCached(geometry.gid)) return this.getCached(geometry); + + const gl = this.renderer.GL; + + //initialize the gl buffers for our geom groups + this.freeBuffers(gid); + + if (Object.keys(this.cache).length > 1000) { + const key = Object.keys(this.cache)[0]; + this.freeBuffers(key); + } + + //create a new entry in our cache + const buffers = {}; + this.cache[gid] = buffers; + + buffers.geometry = geometry; + + let indexBuffer = buffers.indexBuffer; + + if (geometry.faces.length) { + // allocate space for faces + if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); + const vals = geometry.faces.flat(); + + // If any face references a vertex with an index greater than the maximum + // un-singed 16 bit integer, then we need to use a Uint32Array instead of a + // Uint16Array + const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); + let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; + this.renderer._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); + + // If we're using a Uint32Array for our indexBuffer we will need to pass a + // different enum value to WebGL draw triangles. This happens in + // the _drawElements function. + buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 + ? gl.UNSIGNED_INT + : gl.UNSIGNED_SHORT; + + // the vertex count is based on the number of faces + buffers.vertexCount = geometry.faces.length * 3; + } else { + // the index buffer is unused, remove it + if (indexBuffer) { + gl.deleteBuffer(indexBuffer); + buffers.indexBuffer = null; + } + // the vertex count comes directly from the geometry + buffers.vertexCount = geometry.vertices ? geometry.vertices.length : 0; + } + + buffers.lineVertexCount = geometry.lineVertices + ? geometry.lineVertices.length / 3 + : 0; + + return buffers; + } + + freeBuffers(gid) { + const buffers = this.cache[gid]; + if (!buffers) { + return; + } + + delete this.cache[gid]; + + const gl = this.renderer.GL; + if (buffers.indexBuffer) { + gl.deleteBuffer(buffers.indexBuffer); + } + + function freeBuffers(defs) { + for (const def of defs) { + if (buffers[def.dst]) { + gl.deleteBuffer(buffers[def.dst]); + buffers[def.dst] = null; + } + } + } + + // free all the buffers + freeBuffers(this.renderer.buffers.stroke); + freeBuffers(this.renderer.buffers.fill); + freeBuffers(this.renderer.buffers.user); + } +} diff --git a/src/webgl/index.js b/src/webgl/index.js index d66c4b0acc..adcf04631c 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -14,7 +14,6 @@ import shader from './p5.Shader'; import camera from './p5.Camera'; import texture from './p5.Texture'; import rendererGL from './p5.RendererGL'; -import rendererGLRetained from './p5.RendererGL.Retained'; export default function(p5){ rendererGL(p5, p5.prototype); @@ -33,5 +32,4 @@ export default function(p5){ dataArray(p5, p5.prototype); shader(p5, p5.prototype); texture(p5, p5.prototype); - rendererGLRetained(p5, p5.prototype); } diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js deleted file mode 100644 index 1d6ad770b0..0000000000 --- a/src/webgl/p5.RendererGL.Retained.js +++ /dev/null @@ -1,187 +0,0 @@ -//Retained Mode. The default mode for rendering 3D primitives -//in WEBGL. -import * as constants from '../core/constants'; -import { RendererGL } from './p5.RendererGL'; -import { RenderBuffer } from './p5.RenderBuffer'; - -function rendererGLRetained(p5, fn){ - /** - * @param {p5.Geometry} geometry The model whose resources will be freed - */ - RendererGL.prototype.freeGeometry = function(geometry) { - if (!geometry.gid) { - console.warn('The model you passed to freeGeometry does not have an id!'); - return; - } - this._freeBuffers(geometry.gid); - }; - - RendererGL.prototype._freeBuffers = function(gid) { - const buffers = this.geometryBufferCache[gid]; - if (!buffers) { - return; - } - - delete this.geometryBufferCache[gid]; - - const gl = this.GL; - if (buffers.indexBuffer) { - gl.deleteBuffer(buffers.indexBuffer); - } - - function freeBuffers(defs) { - for (const def of defs) { - if (buffers[def.dst]) { - gl.deleteBuffer(buffers[def.dst]); - buffers[def.dst] = null; - } - } - } - - // free all the buffers - freeBuffers(this.buffers.stroke); - freeBuffers(this.buffers.fill); - freeBuffers(this.buffers.user); - }; - - /** - * Creates a buffers object that holds the WebGL render buffers - * for a geometry. - * @private - * @param {p5.Geometry} model contains geometry data - */ - RendererGL.prototype.createBuffers = function(model) { - const gl = this.GL; - - const gid = model.gid; - if (!gid) { - throw new Error('The p5.Geometry you passed in has no gid property!'); - } - - //initialize the gl buffers for our geom groups - this._freeBuffers(gid); - - //@TODO remove this limit on hashes in geometryBufferCache - if (Object.keys(this.geometryBufferCache).length > 1000) { - const key = Object.keys(this.geometryBufferCache)[0]; - this._freeBuffers(key); - } - - //create a new entry in our geometryBufferCache - const buffers = {}; - this.geometryBufferCache[gid] = buffers; - - buffers.model = model; - - let indexBuffer = buffers.indexBuffer; - - if (model.faces.length) { - // allocate space for faces - if (!indexBuffer) indexBuffer = buffers.indexBuffer = gl.createBuffer(); - const vals = RendererGL.prototype._flatten(model.faces); - - // If any face references a vertex with an index greater than the maximum - // un-singed 16 bit integer, then we need to use a Uint32Array instead of a - // Uint16Array - const hasVertexIndicesOverMaxUInt16 = vals.some(v => v > 65535); - let type = hasVertexIndicesOverMaxUInt16 ? Uint32Array : Uint16Array; - this._bindBuffer(indexBuffer, gl.ELEMENT_ARRAY_BUFFER, vals, type); - - // If we're using a Uint32Array for our indexBuffer we will need to pass a - // different enum value to WebGL draw triangles. This happens in - // the _drawElements function. - buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 - ? gl.UNSIGNED_INT - : gl.UNSIGNED_SHORT; - - // the vertex count is based on the number of faces - buffers.vertexCount = model.faces.length * 3; - } else { - // the index buffer is unused, remove it - if (indexBuffer) { - gl.deleteBuffer(indexBuffer); - buffers.indexBuffer = null; - } - // TODO: delete? - // the vertex count comes directly from the model - buffers.vertexCount = model.vertices ? model.vertices.length : 0; - } - - // TODO: delete? - buffers.lineVertexCount = model.lineVertices - ? model.lineVertices.length / 3 - : 0; - - for (const propName in model.userVertexProperties) { - const prop = model.userVertexProperties[propName]; - this.buffers.user.push( - new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) - ); - } - return buffers; - }; - - /** - * Calls drawBuffers() with a scaled model/view matrix. - * - * This is used by various 3d primitive methods (in primitives.js, eg. plane, - * box, torus, etc...) to allow caching of un-scaled geometries. Those - * geometries are generally created with unit-length dimensions, cached as - * such, and then scaled appropriately in this method prior to rendering. - * - * @private - * @method drawBuffersScaled - * @param {String} gid ID in our geom hash - * @param {Number} scaleX the amount to scale in the X direction - * @param {Number} scaleY the amount to scale in the Y direction - * @param {Number} scaleZ the amount to scale in the Z direction - */ - RendererGL.prototype.drawBuffersScaled = function( - model, - scaleX, - scaleY, - scaleZ - ) { - let originalModelMatrix = this.states.uModelMatrix.copy(); - try { - this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); - - if (this.geometryBuilder) { - this.geometryBuilder.addRetained(model); - } else { - this._drawGeometry(model); - } - } finally { - - this.states.uModelMatrix = originalModelMatrix; - } - }; - - RendererGL.prototype._drawPoints = function(vertices, vertexBuffer) { - const gl = this.GL; - const pointShader = this._getImmediatePointShader(); - this._setPointUniforms(pointShader); - - this._bindBuffer( - vertexBuffer, - gl.ARRAY_BUFFER, - this._vToNArray(vertices), - Float32Array, - gl.STATIC_DRAW - ); - - pointShader.enableAttrib(pointShader.attributes.aPosition, 3); - - this._applyColorBlend(this.states.curStrokeColor); - - gl.drawArrays(gl.Points, 0, vertices.length); - - pointShader.unbindShader(); - }; -} - -export default rendererGLRetained; - -if(typeof p5 !== 'undefined'){ - rendererGLRetained(p5, p5.prototype); -} diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index fc82c40722..30ed581bbb 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -14,6 +14,7 @@ import { Framebuffer } from './p5.Framebuffer'; import { Graphics } from '../core/p5.Graphics'; import { Element } from '../core/p5.Element'; import { ShapeBuilder } from './ShapeBuilder'; +import { GeometryBufferCache } from './GeometryBufferCache'; import lightingShader from './shaders/lighting.glsl'; import webgl2CompatibilityShader from './shaders/webgl2Compatibility.glsl'; @@ -316,7 +317,7 @@ class RendererGL extends Renderer { new RenderBuffer(3, 'vertexNormals', 'normalBuffer', 'aNormal', this, this._vToNArray), new RenderBuffer(4, 'vertexColors', 'colorBuffer', 'aVertexColor', this), new RenderBuffer(3, 'vertexAmbients', 'ambientBuffer', 'aAmbientColor', this), - new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, (arr) => arr.flat()) ], stroke: [ new RenderBuffer(4, 'lineVertexColors', 'lineColorBuffer', 'aVertexColor', this), @@ -327,13 +328,13 @@ class RendererGL extends Renderer { ], text: [ new RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), - new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) + new RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, (arr) => arr.flat()) ], point: this.GL.createBuffer(), user:[] } - this.geometryBufferCache = {}; + this.geometryBufferCache = new GeometryBufferCache(this); this.pointSize = 5.0; //default point size this.curStrokeWeight = 1; @@ -501,6 +502,13 @@ class RendererGL extends Renderer { ////////////////////////////////////////////// _drawGeometry(geometry, { mode = constants.TRIANGLES, count = 1 } = {}) { + for (const propName in geometry.userVertexProperties) { + const prop = geometry.userVertexProperties[propName]; + this.buffers.user.push( + new RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) + ); + } + if ( this.states.doFill && geometry.vertices.length >= 3 && @@ -516,6 +524,27 @@ class RendererGL extends Renderer { this.buffers.user = []; } + _drawGeometryScaled( + model, + scaleX, + scaleY, + scaleZ + ) { + let originalModelMatrix = this.states.uModelMatrix.copy(); + try { + this.states.uModelMatrix.scale(scaleX, scaleY, scaleZ); + + if (this.geometryBuilder) { + this.geometryBuilder.addRetained(model); + } else { + this._drawGeometry(model); + } + } finally { + + this.states.uModelMatrix = originalModelMatrix; + } + } + _drawFills(geometry, { count, mode } = {}) { this._useVertexColor = geometry.vertexColors.length > 0; @@ -581,6 +610,28 @@ class RendererGL extends Renderer { shader.unbindShader(); } + _drawPoints(vertices, vertexBuffer) { + const gl = this.GL; + const pointShader = this._getPointShader(); + this._setPointUniforms(pointShader); + + this._bindBuffer( + vertexBuffer, + gl.ARRAY_BUFFER, + this._vToNArray(vertices), + Float32Array, + gl.STATIC_DRAW + ); + + pointShader.enableAttrib(pointShader.attributes.aPosition, 3); + + this._applyColorBlend(this.states.curStrokeColor); + + gl.drawArrays(gl.Points, 0, vertices.length); + + pointShader.unbindShader(); + } + _prepareUserAttributes(geometry, shader) { for (const buff of this.buffers.user) { if (!this._pInst.constructor.disableFriendleErrors) { @@ -601,7 +652,7 @@ class RendererGL extends Renderer { _drawBuffers(geometry, { mode = this.GL.TRIANGLES, count }) { const gl = this.GL; - const glBuffers = this.geometryBufferCache[geometry.gid]; + const glBuffers = this.geometryBufferCache.getCached(geometry); if (glBuffers?.indexBuffer) { // If this model is using a Uint32Array we need to ensure the @@ -660,10 +711,7 @@ class RendererGL extends Renderer { } _getOrMakeCachedBuffers(geometry) { - if (!this.geometryInHash(geometry.gid)) { - this.createBuffers(geometry); - } - return this.geometryBufferCache[geometry.gid] + return this.geometryBufferCache.ensureCached(geometry); } ////////////////////////////////////////////// @@ -1368,7 +1416,7 @@ class RendererGL extends Renderer { ////////////////////////////////////////////// geometryInHash(gid) { - return this.geometryBufferCache[gid] !== undefined; + return this.geometryBufferCache.isCached(gid); } viewport(w, h) { @@ -1647,7 +1695,7 @@ class RendererGL extends Renderer { } - _getImmediatePointShader() { + _getPointShader() { // select the point shader to use const point = this.states.userPointShader; if (!point || !point.isPointShader()) { @@ -2223,16 +2271,6 @@ class RendererGL extends Renderer { Uint32Array ].some(x => arr instanceof x); } - /** - * turn a two dimensional array into one dimensional array - * @private - * @param {Array} arr 2-dimensional array - * @return {Array} 1-dimensional array - * [[1, 2, 3],[4, 5, 6]] -> [1, 2, 3, 4, 5, 6] - */ - _flatten(arr) { - return arr.flat(); - } /** * turn a p5.Vector Array into a one dimensional number array @@ -2465,14 +2503,12 @@ function rendererGL(p5, fn){ } if (!this._setupDone) { - for (const x in this._renderer.geometryBufferCache) { - if (this._renderer.geometryBufferCache.hasOwnProperty(x)) { - p5._friendlyError( - 'Sorry, Could not set the attributes, you need to call setAttributes() ' + - 'before calling the other drawing methods in setup()' - ); - return; - } + if (this._renderer.geometryBufferCache.numCached() > 0) { + p5._friendlyError( + 'Sorry, Could not set the attributes, you need to call setAttributes() ' + + 'before calling the other drawing methods in setup()' + ); + return; } } diff --git a/src/webgl/text.js b/src/webgl/text.js index ebc474478e..b8355f0c9a 100644 --- a/src/webgl/text.js +++ b/src/webgl/text.js @@ -704,7 +704,7 @@ function text(p5, fn){ } })); geom.computeFaces().computeNormals(); - g = this.createBuffers('glyph', geom); + g = this.geometryBufferCache.ensureCached(geom); } // bind the shader buffers diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 9302f67895..c869073811 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1320,7 +1320,7 @@ suite('p5.RendererGL', function() { myp5.stroke(255); myp5.triangle(0, 0, 1, 0, 0, 1); - var buffers = renderer.geometryBufferCache['tri']; + var buffers = renderer.geometryBufferCache.getCachedID('tri'); assert.isObject(buffers); assert.isDefined(buffers.indexBuffer); @@ -2582,29 +2582,44 @@ suite('p5.RendererGL', function() { test('Retained mode buffers are created for rendering', function() { myp5.createCanvas(50, 50, myp5.WEBGL); - myp5.beginGeometry(); - myp5.beginShape(); - myp5.vertexProperty('aCustom', 1); - myp5.vertexProperty('aCustomVec3', [1,2,3]); - myp5.vertex(0,0,0); - myp5.vertex(1,0,0); - myp5.endShape(); - const myGeo = myp5.endGeometry(); - myp5._renderer.createBuffers(myGeo); - expect(myp5._renderer.buffers.user).to.containSubset([ - { - size: 1, - src: 'aCustomSrc', - dst: 'aCustomBuffer', - attr: 'aCustom', - }, - { - size: 3, - src: 'aCustomVec3Src', - dst: 'aCustomVec3Buffer', - attr: 'aCustomVec3', - } - ]); + + const prevDrawFills = myp5._renderer._drawFills; + let called = false; + myp5._renderer._drawFills = function(...args) { + called = true; + expect(myp5._renderer.buffers.user).to.containSubset([ + { + size: 1, + src: 'aCustomSrc', + dst: 'aCustomBuffer', + attr: 'aCustom', + }, + { + size: 3, + src: 'aCustomVec3Src', + dst: 'aCustomVec3Buffer', + attr: 'aCustomVec3', + } + ]); + + prevDrawFills.apply(this, args); + } + + try { + myp5.beginGeometry(); + myp5.beginShape(); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + myp5.vertex(1,0,0); + myp5.vertex(1,1,0); + myp5.endShape(); + const myGeo = myp5.endGeometry(); + myp5.model(myGeo); + expect(called).to.equal(true); + } finally { + myp5._renderer._drawFills = prevDrawFills; + } } ); test('Retained mode buffers deleted after rendering', From 8558837dd4be42a830f56e12e55f0432007e326c Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 11:38:04 -0500 Subject: [PATCH 4/9] Prevent download tests from actually downloading a file in the test runner --- test/js/p5_helpers.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/js/p5_helpers.js b/test/js/p5_helpers.js index 5eee86ac6a..a8a72daf24 100644 --- a/test/js/p5_helpers.js +++ b/test/js/p5_helpers.js @@ -30,6 +30,17 @@ export function testWithDownload(name, fn, asyncFn = false) { return new Promise((resolve, reject) => { let blobContainer = {}; + const prevClick = HTMLAnchorElement.prototype.click; + const prevDispatchEvent = HTMLAnchorElement.prototype.dispatchEvent; + const blockDownloads = () => { + HTMLAnchorElement.prototype.click = () => {}; + HTMLAnchorElement.prototype.dispatchEvent = () => {}; + } + const unblockDownloads = () => { + HTMLAnchorElement.prototype.click = prevClick; + HTMLAnchorElement.prototype.dispatchEvent = prevDispatchEvent; + } + // create a backup of createObjectURL let couBackup = window.URL.createObjectURL; @@ -40,6 +51,7 @@ export function testWithDownload(name, fn, asyncFn = false) { blobContainer.blob = blob; return couBackup(blob); }; + blockDownloads(); let error; if (asyncFn) { @@ -54,6 +66,7 @@ export function testWithDownload(name, fn, asyncFn = false) { // restore createObjectURL to the original one window.URL.createObjectURL = couBackup; error ? reject(error) : resolve(); + unblockDownloads(); }); } else { try { @@ -64,6 +77,7 @@ export function testWithDownload(name, fn, asyncFn = false) { // restore createObjectURL to the original one window.URL.createObjectURL = couBackup; error ? reject(error) : resolve(); + unblockDownloads(); } }); }; From fcb8abe219f214443b4e2e3127a3431470ffa630 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 11:52:57 -0500 Subject: [PATCH 5/9] Don't cache vertex counts in the geometry cache --- src/webgl/3d_primitives.js | 20 ++++++++++---------- src/webgl/GeometryBufferCache.js | 11 +---------- src/webgl/p5.RendererGL.js | 8 +++++--- test/unit/webgl/p5.RendererGL.js | 7 ++++--- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index dd97148a16..869b7ab240 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -2135,7 +2135,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix = mult; - this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); + this._drawGeometry(this.geometryBufferCache.getGeometryByID(gid)); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2266,7 +2266,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); - this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); + this._drawGeometry(this.geometryBufferCache.getGeometryByID(gid)); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2326,7 +2326,7 @@ function primitives3D(p5, fn){ this.states.uModelMatrix.translate([x, y, 0]); this.states.uModelMatrix.scale(width, height, 1); - this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); + this._drawGeometry(this.geometryBufferCache.getGeometryByID(gid)); } finally { this.states.uModelMatrix = uModelMatrix; } @@ -2465,7 +2465,7 @@ function primitives3D(p5, fn){ quadGeom.gid = gid; this.geometryBufferCache.ensureCached(quadGeom); } - this._drawGeometry(this.geometryBufferCache.getModelByID(gid)); + this._drawGeometry(this.geometryBufferCache.getGeometryByID(gid)); return this; }; @@ -3376,7 +3376,7 @@ function primitives3D(p5, fn){ this.geometryBufferCache.ensureCached(planeGeom); } - this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), width, height, 1); + this._drawGeometryScaled(this.geometryBufferCache.getGeometryByID(gid), width, height, 1); } RendererGL.prototype.box = function( @@ -3458,7 +3458,7 @@ function primitives3D(p5, fn){ boxGeom.gid = gid; this.geometryBufferCache.ensureCached(boxGeom); } - this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), width, height, depth); + this._drawGeometryScaled(this.geometryBufferCache.getGeometryByID(gid), width, height, depth); } RendererGL.prototype.sphere = function( @@ -3512,7 +3512,7 @@ function primitives3D(p5, fn){ this.geometryBufferCache.ensureCached(ellipsoidGeom); } - this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radiusX, radiusY, radiusZ); + this._drawGeometryScaled(this.geometryBufferCache.getGeometryByID(gid), radiusX, radiusY, radiusZ); } RendererGL.prototype.cylinder = function( @@ -3549,7 +3549,7 @@ function primitives3D(p5, fn){ this.geometryBufferCache.ensureCached(cylinderGeom); } - this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radius, height, radius); + this._drawGeometryScaled(this.geometryBufferCache.getGeometryByID(gid), radius, height, radius); } RendererGL.prototype.cone = function( @@ -3575,7 +3575,7 @@ function primitives3D(p5, fn){ this.geometryBufferCache.ensureCached(coneGeom); } - this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radius, height, radius); + this._drawGeometryScaled(this.geometryBufferCache.getGeometryByID(gid), radius, height, radius); } RendererGL.prototype.torus = function( @@ -3637,7 +3637,7 @@ function primitives3D(p5, fn){ torusGeom.gid = gid; this.geometryBufferCache.ensureCached(torusGeom); } - this._drawGeometryScaled(this.geometryBufferCache.getModelByID(gid), radius, radius, radius); + this._drawGeometryScaled(this.geometryBufferCache.getGeometryByID(gid), radius, radius, radius); } } diff --git a/src/webgl/GeometryBufferCache.js b/src/webgl/GeometryBufferCache.js index 28209d6b99..289a1daaff 100644 --- a/src/webgl/GeometryBufferCache.js +++ b/src/webgl/GeometryBufferCache.js @@ -12,7 +12,7 @@ export class GeometryBufferCache { return this.cache[gid] !== undefined; } - getModelByID(gid) { + getGeometryByID(gid) { return this.cache[gid]?.geometry; } @@ -68,23 +68,14 @@ export class GeometryBufferCache { buffers.indexBufferType = hasVertexIndicesOverMaxUInt16 ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT; - - // the vertex count is based on the number of faces - buffers.vertexCount = geometry.faces.length * 3; } else { // the index buffer is unused, remove it if (indexBuffer) { gl.deleteBuffer(indexBuffer); buffers.indexBuffer = null; } - // the vertex count comes directly from the geometry - buffers.vertexCount = geometry.vertices ? geometry.vertices.length : 0; } - buffers.lineVertexCount = geometry.lineVertices - ? geometry.lineVertices.length / 3 - : 0; - return buffers; } diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 30ed581bbb..15b3f23097 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -654,7 +654,9 @@ class RendererGL extends Renderer { const gl = this.GL; const glBuffers = this.geometryBufferCache.getCached(geometry); - if (glBuffers?.indexBuffer) { + if (!glBuffers) return; + + if (glBuffers.indexBuffer) { // If this model is using a Uint32Array we need to ensure the // OES_element_index_uint WebGL extension is enabled. if ( @@ -671,7 +673,7 @@ class RendererGL extends Renderer { if (count === 1) { gl.drawElements( gl.TRIANGLES, - glBuffers.vertexCount, + geometry.faces.length * 3, glBuffers.indexBufferType, 0 ); @@ -679,7 +681,7 @@ class RendererGL extends Renderer { try { gl.drawElementsInstanced( gl.TRIANGLES, - glBuffers.vertexCount, + geometry.faces.length * 3, glBuffers.indexBufferType, 0, count diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index c869073811..ae0b100511 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1320,7 +1320,8 @@ suite('p5.RendererGL', function() { myp5.stroke(255); myp5.triangle(0, 0, 1, 0, 0, 1); - var buffers = renderer.geometryBufferCache.getCachedID('tri'); + const buffers = renderer.geometryBufferCache.getCachedID('tri'); + const geom = renderer.geometryBufferCache.getGeometryByID('tri'); assert.isObject(buffers); assert.isDefined(buffers.indexBuffer); @@ -1332,13 +1333,13 @@ suite('p5.RendererGL', function() { assert.isDefined(buffers.lineTangentsOutBuffer); assert.isDefined(buffers.vertexBuffer); - assert.equal(buffers.vertexCount, 3); + assert.equal(geom.faces.length, 1); // 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(geom.lineVertices.length, 54 * 3); }); }); From e9f400d002cea5401ca12ebe9c056a0a39c0e090 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 12:17:32 -0500 Subject: [PATCH 6/9] Move all uniform setting outside of p5.Shader --- src/webgl/p5.RendererGL.js | 38 ++++++++++++++++++++++++++++++++++++++ src/webgl/p5.Shader.js | 36 ------------------------------------ 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 15b3f23097..1a0ce2d748 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -551,6 +551,7 @@ class RendererGL extends Renderer { const shader = this._drawingFilter && this.states.userFillShader ? this.states.userFillShader : this._getFillShader(); + this._setGlobalUniforms(shader); this._setFillUniforms(shader); for (const buff of this.buffers.fill) { @@ -575,6 +576,7 @@ class RendererGL extends Renderer { this._useLineColor = geometry.vertexStrokeColors.length > 0; const shader = this._getStrokeShader(); + this._setGlobalUniforms(shader); this._setStrokeUniforms(shader); for (const buff of this.buffers.stroke) { @@ -613,6 +615,7 @@ class RendererGL extends Renderer { _drawPoints(vertices, vertexBuffer) { const gl = this.GL; const pointShader = this._getPointShader(); + this._setGlobalUniforms(pointShader); this._setPointUniforms(pointShader); this._bindBuffer( @@ -2100,6 +2103,41 @@ class RendererGL extends Renderer { return new Framebuffer(this, options); } + _setGlobalUniforms(shader) { + shader.bindShader(); + + const modelMatrix = this.states.uModelMatrix; + const viewMatrix = this.states.uViewMatrix; + const projectionMatrix = this.states.uPMatrix; + const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); + this.states.uMVMatrix = this.calculateCombinedMatrix(); + + const modelViewProjectionMatrix = modelViewMatrix.copy(); + modelViewProjectionMatrix.mult(projectionMatrix); + + shader.setUniform( + 'uPerspective', + this.states.curCamera.useLinePerspective ? 1 : 0 + ); + shader.setUniform('uViewMatrix', viewMatrix.mat4); + shader.setUniform('uProjectionMatrix', projectionMatrix.mat4); + shader.setUniform('uModelMatrix', modelMatrix.mat4); + shader.setUniform('uModelViewMatrix', modelViewMatrix.mat4); + shader.setUniform( + 'uModelViewProjectionMatrix', + modelViewProjectionMatrix.mat4 + ); + if (shader.uniforms.uNormalMatrix) { + this.states.uNMatrix.inverseTranspose(this.states.uMVMatrix); + shader.setUniform('uNormalMatrix', this.states.uNMatrix.mat3); + } + if (shader.uniforms.uCameraRotation) { + this.states.curMatrix.inverseTranspose(this.states.uViewMatrix); + shader.setUniform('uCameraRotation', this.states.curMatrix.mat3); + } + shader.setUniform('uViewport', this._viewport); + } + _setStrokeUniforms(strokeShader) { strokeShader.bindShader(); diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 5620d08a40..5b79df3ebc 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -737,10 +737,6 @@ class Shader { if (!this._bound) { this.useProgram(); this._bound = true; - - this._setMatrixUniforms(); - - this.setUniform('uViewport', this._renderer._viewport); } } @@ -790,38 +786,6 @@ class Shader { } } - _setMatrixUniforms() { - const modelMatrix = this._renderer.states.uModelMatrix; - const viewMatrix = this._renderer.states.uViewMatrix; - const projectionMatrix = this._renderer.states.uPMatrix; - const modelViewMatrix = (modelMatrix.copy()).mult(viewMatrix); - this._renderer.states.uMVMatrix = this._renderer.calculateCombinedMatrix(); - - const modelViewProjectionMatrix = modelViewMatrix.copy(); - modelViewProjectionMatrix.mult(projectionMatrix); - - this.setUniform( - 'uPerspective', - this._renderer.states.curCamera.useLinePerspective ? 1 : 0 - ); - this.setUniform('uViewMatrix', viewMatrix.mat4); - this.setUniform('uProjectionMatrix', projectionMatrix.mat4); - this.setUniform('uModelMatrix', modelMatrix.mat4); - this.setUniform('uModelViewMatrix', modelViewMatrix.mat4); - this.setUniform( - 'uModelViewProjectionMatrix', - modelViewProjectionMatrix.mat4 - ); - if (this.uniforms.uNormalMatrix) { - this._renderer.states.uNMatrix.inverseTranspose(this._renderer.states.uMVMatrix); - this.setUniform('uNormalMatrix', this._renderer.states.uNMatrix.mat3); - } - if (this.uniforms.uCameraRotation) { - this._renderer.states.curMatrix.inverseTranspose(this._renderer.states.uViewMatrix); - this.setUniform('uCameraRotation', this._renderer.states.curMatrix.mat3); - } - } - /** * @chainable * @private From 2d106783d792dfddfeb0e72d8ba559d2f447a854 Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 13:08:14 -0500 Subject: [PATCH 7/9] Fix #7030 --- preview/index.html | 40 ++++++++++++++++++++++++-------- src/webgl/p5.RendererGL.js | 16 +++++-------- src/webgl/p5.Shader.js | 7 ++++-- test/unit/webgl/p5.RendererGL.js | 39 +++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 22 deletions(-) diff --git a/preview/index.html b/preview/index.html index deb1e21e11..2d5aacf310 100644 --- a/preview/index.html +++ b/preview/index.html @@ -20,17 +20,37 @@ import p5 from '../src/app.js'; const sketch = function (p) { + let myShader + let tex p.setup = function () { - p.createCanvas(200, 200); - }; - - p.draw = function () { - p.background(0, 50, 50); - p.circle(100, 100, 50); - - p.fill('white'); - p.textSize(30); - p.text('hello', 10, 30); + p.createCanvas(20, 10, p.WEBGL); + p.background(255); + + myShader = p.baseMaterialShader().modify({ + uniforms: { + 'sampler2D myTex': undefined, + }, + 'Inputs getPixelInputs': `(Inputs inputs) { + inputs.color = texture(myTex, inputs.texCoord); + return inputs; + }` + }) + + // Make a red texture + tex = p.createFramebuffer(); + tex.draw(() => p.background('red')); + + p.translate(-p.width/2, -p.height/2) + p.shader(myShader); + p.noStroke(); + myShader.setUniform('myTex', tex); + + // Draw once to the left + p.rect(0, 0, 10, 10); + + // Draw once to the right + p.rect(10, 0, 10, 10); + console.log(p.canvas.toDataURL()) }; }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 1a0ce2d748..75b91a16a8 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -551,8 +551,10 @@ class RendererGL extends Renderer { const shader = this._drawingFilter && this.states.userFillShader ? this.states.userFillShader : this._getFillShader(); + shader.bindShader(); this._setGlobalUniforms(shader); this._setFillUniforms(shader); + shader.bindTextures(); for (const buff of this.buffers.fill) { buff._prepareBuffer(geometry, shader); @@ -576,8 +578,10 @@ class RendererGL extends Renderer { this._useLineColor = geometry.vertexStrokeColors.length > 0; const shader = this._getStrokeShader(); + shader.bindShader(); this._setGlobalUniforms(shader); this._setStrokeUniforms(shader); + shader.bindTextures(); for (const buff of this.buffers.stroke) { buff._prepareBuffer(geometry, shader); @@ -615,8 +619,10 @@ class RendererGL extends Renderer { _drawPoints(vertices, vertexBuffer) { const gl = this.GL; const pointShader = this._getPointShader(); + pointShader.bindShader(); this._setGlobalUniforms(pointShader); this._setPointUniforms(pointShader); + pointShader.bindTextures(); this._bindBuffer( vertexBuffer, @@ -2104,8 +2110,6 @@ class RendererGL extends Renderer { } _setGlobalUniforms(shader) { - shader.bindShader(); - const modelMatrix = this.states.uModelMatrix; const viewMatrix = this.states.uViewMatrix; const projectionMatrix = this.states.uPMatrix; @@ -2139,8 +2143,6 @@ class RendererGL extends Renderer { } _setStrokeUniforms(strokeShader) { - strokeShader.bindShader(); - // set the uniform values strokeShader.setUniform('uUseLineColor', this._useLineColor); strokeShader.setUniform('uMaterialColor', this.states.curStrokeColor); @@ -2150,8 +2152,6 @@ class RendererGL extends Renderer { } _setFillUniforms(fillShader) { - fillShader.bindShader(); - this.mixedSpecularColor = [...this.states.curSpecularColor]; if (this.states._useMetalness > 0) { @@ -2236,8 +2236,6 @@ class RendererGL extends Renderer { fillShader.setUniform('uConstantAttenuation', this.states.constantAttenuation); fillShader.setUniform('uLinearAttenuation', this.states.linearAttenuation); fillShader.setUniform('uQuadraticAttenuation', this.states.quadraticAttenuation); - - fillShader.bindTextures(); } // getting called from _setFillUniforms @@ -2257,8 +2255,6 @@ class RendererGL extends Renderer { } _setPointUniforms(pointShader) { - pointShader.bindShader(); - // set the uniform values pointShader.setUniform('uMaterialColor', this.states.curStrokeColor); // @todo is there an instance where this isn't stroke weight? diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 5b79df3ebc..b05b43d344 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -747,7 +747,6 @@ class Shader { unbindShader() { if (this._bound) { this.unbindTextures(); - //this._renderer.GL.useProgram(0); ?? this._bound = false; } return this; @@ -781,8 +780,12 @@ class Shader { } unbindTextures() { + const gl = this._renderer.GL; + const empty = this._renderer._getEmptyTexture(); for (const uniform of this.samplers) { - this.setUniform(uniform.name, this._renderer._getEmptyTexture()); + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + empty.bindTexture(); + gl.uniform1i(uniform.location, uniform.samplerIndex); } } diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index ae0b100511..aa1ea754dc 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -69,6 +69,45 @@ suite('p5.RendererGL', function() { }); }); + suite('texture binding', function() { + test('textures remain bound after each draw call', function() { + myp5.createCanvas(20, 10, myp5.WEBGL); + myp5.background(255); + + const myShader = myp5.baseMaterialShader().modify({ + uniforms: { + 'sampler2D myTex': undefined, + }, + 'Inputs getPixelInputs': `(Inputs inputs) { + inputs.color = texture(myTex, inputs.texCoord); + return inputs; + }` + }) + + // Make a red texture + const tex = myp5.createFramebuffer(); + tex.draw(() => myp5.background('red')); + + myp5.shader(myShader); + // myp5.fill('red'); + myp5.noStroke(); + myShader.setUniform('myTex', tex); + + myp5.translate(-myp5.width/2, -myp5.height/2); + myp5.rectMode(myp5.CORNER); + + // Draw once to the left + myp5.rect(0, 0, 10, 10); + + // Draw once to the right + myp5.rect(10, 0, 10, 10); + + // Both rectangles should be red + assert.deepEqual(myp5.get(5, 5), [255, 0, 0, 255]); + assert.deepEqual(myp5.get(15, 5), [255, 0, 0, 255]); + }) + }); + suite('default stroke shader', function() { test('check activate and deactivating fill and stroke', function() { myp5.noStroke(); From 3b2cac2286caeb839892c10c118712b2d8aaddbc Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 14:09:53 -0500 Subject: [PATCH 8/9] Fix sneaky bug where emptyTexture() is changing the texture binding --- src/webgl/p5.RendererGL.js | 6 ++++++ src/webgl/p5.Shader.js | 14 ++++++++++---- test/unit/webgl/p5.RendererGL.js | 31 ++++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index 75b91a16a8..a11c9d6359 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -1974,6 +1974,12 @@ class RendererGL extends Renderer { return code; } + /** + * @private + * Note: DO NOT CALL THIS while in the middle of binding another texture, + * since it will change the texture binding in order to allocate the empty + * texture! Grab its value beforehand! + */ _getEmptyTexture() { if (!this._emptyTexture) { // a plain white texture RGBA, full alpha, single pixel. diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index b05b43d344..2e3330ff0a 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -755,13 +755,15 @@ class Shader { bindTextures() { const gl = this._renderer.GL; + const empty = this._renderer._getEmptyTexture(); + for (const uniform of this.samplers) { let tex = uniform.texture; if (tex === undefined) { // user hasn't yet supplied a texture for this slot. // (or there may not be one--maybe just lighting), // so we supply a default texture instead. - tex = this._renderer._getEmptyTexture(); + uniform.texture = tex = empty; } gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); tex.bindTexture(); @@ -783,9 +785,11 @@ class Shader { const gl = this._renderer.GL; const empty = this._renderer._getEmptyTexture(); for (const uniform of this.samplers) { - gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); - empty.bindTexture(); - gl.uniform1i(uniform.location, uniform.samplerIndex); + if (uniform.texture?.isFramebufferTexture) { + gl.activeTexture(gl.TEXTURE0 + uniform.samplerIndex); + empty.bindTexture(); + gl.uniform1i(uniform.location, uniform.samplerIndex); + } } } @@ -1037,6 +1041,8 @@ class Shader { * */ setUniform(uniformName, data) { + this.init(); + const uniform = this.uniforms[uniformName]; if (!uniform) { return; diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index aa1ea754dc..931aa0ef24 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -70,6 +70,36 @@ suite('p5.RendererGL', function() { }); suite('texture binding', function() { + test('setting a custom texture works', function() { + myp5.createCanvas(10, 10, myp5.WEBGL); + myp5.background(255); + + const myShader = myp5.baseMaterialShader().modify({ + uniforms: { + 'sampler2D myTex': undefined, + }, + 'Inputs getPixelInputs': `(Inputs inputs) { + inputs.color = texture(myTex, inputs.texCoord); + return inputs; + }` + }) + + // Make a red texture + const tex = myp5.createFramebuffer(); + tex.draw(() => myp5.background('red')); + console.log(tex.get().canvas.toDataURL()); + + myp5.shader(myShader); + myp5.fill('red') + myp5.noStroke(); + myShader.setUniform('myTex', tex); + + myp5.rectMode(myp5.CENTER) + myp5.rect(0, 0, myp5.width, myp5.height); + + // It should be red + assert.deepEqual(myp5.get(5, 5), [255, 0, 0, 255]); + }) test('textures remain bound after each draw call', function() { myp5.createCanvas(20, 10, myp5.WEBGL); myp5.background(255); @@ -89,7 +119,6 @@ suite('p5.RendererGL', function() { tex.draw(() => myp5.background('red')); myp5.shader(myShader); - // myp5.fill('red'); myp5.noStroke(); myShader.setUniform('myTex', tex); From d5d73fbc4edb1f4d74b1c40e9f727cef62314f4e Mon Sep 17 00:00:00 2001 From: Dave Pagurek Date: Sun, 3 Nov 2024 14:14:07 -0500 Subject: [PATCH 9/9] Put back index.html --- preview/index.html | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/preview/index.html b/preview/index.html index 2d5aacf310..deb1e21e11 100644 --- a/preview/index.html +++ b/preview/index.html @@ -20,37 +20,17 @@ import p5 from '../src/app.js'; const sketch = function (p) { - let myShader - let tex p.setup = function () { - p.createCanvas(20, 10, p.WEBGL); - p.background(255); - - myShader = p.baseMaterialShader().modify({ - uniforms: { - 'sampler2D myTex': undefined, - }, - 'Inputs getPixelInputs': `(Inputs inputs) { - inputs.color = texture(myTex, inputs.texCoord); - return inputs; - }` - }) - - // Make a red texture - tex = p.createFramebuffer(); - tex.draw(() => p.background('red')); - - p.translate(-p.width/2, -p.height/2) - p.shader(myShader); - p.noStroke(); - myShader.setUniform('myTex', tex); - - // Draw once to the left - p.rect(0, 0, 10, 10); - - // Draw once to the right - p.rect(10, 0, 10, 10); - console.log(p.canvas.toDataURL()) + p.createCanvas(200, 200); + }; + + p.draw = function () { + p.background(0, 50, 50); + p.circle(100, 100, 50); + + p.fill('white'); + p.textSize(30); + p.text('hello', 10, 30); }; };