diff --git a/.gitignore b/.gitignore index 7883f080bb..42f1e447dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,25 @@ -*.DS_Store -.project -node_modules/* -experiments/* -lib_old/* -lib/p5.* -lib/modules -docs/reference/* -!*.gitkeep -examples/3d/ -.idea -dist/ -p5.zip -bower-repo/ -p5-website/ -.vscode/settings.json -.nyc_output/* -coverage/ -lib/p5-test.js -release/ -yarn.lock -docs/data.json -analyzer/ -preview/ +*.DS_Store +.project +node_modules/* +experiments/* +lib_old/* +lib/p5.* +lib/modules +docs/reference/* +!*.gitkeep +examples/3d/ +.idea +dist/ +p5.zip +bower-repo/ +p5-website/ +.vscode/settings.json +.nyc_output/* +coverage/ +lib/p5-test.js +release/ +yarn.lock +docs/data.json +analyzer/ +preview/ __screenshots__/ \ No newline at end of file diff --git a/src/core/shape/vertex.js b/src/core/shape/vertex.js index 60c2cf1489..f1dd9ff214 100644 --- a/src/core/shape/vertex.js +++ b/src/core/shape/vertex.js @@ -2091,7 +2091,7 @@ p5.prototype.vertex = function(x, y, moveTo, u, v) { * `normal()` will affect all following vertices until `normal()` is called * again: * - * + * ```javascript * beginShape(); * * // Set the vertex normal. @@ -2114,7 +2114,7 @@ p5.prototype.vertex = function(x, y, moveTo, u, v) { * vertex(-30, 30, 0); * * endShape(); - * + * ``` * * @method normal * @param {p5.Vector} vector vertex normal as a p5.Vector object. @@ -2253,4 +2253,176 @@ p5.prototype.normal = function(x, y, z) { return this; }; +/** Sets the shader's vertex property or attribute variables. + * + * An vertex property or vertex attribute is a variable belonging to a vertex in a shader. p5.js provides some + * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are + * set using vertex(), normal() + * and fill() respectively. Custom properties can also + * be defined within beginShape() and + * endShape(). + * + * The first parameter, `propertyName`, is a string with the property's name. + * This is the same variable name which should be declared in the shader, such as + * `in vec3 aProperty`, similar to .`setUniform()`. + * + * The second parameter, `data`, is the value assigned to the shader variable. This + * value will be applied to subsequent vertices created with + * vertex(). It can be a Number or an array of numbers, + * and in the shader program the type can be declared according to the WebGL + * specification. Common types include `float`, `vec2`, `vec3`, `vec4` or matrices. + * + * See also the vertexProperty() method on + * Geometry objects. + * + * @example + *
+ * + * const vertSrc = `#version 300 es + * precision mediump float; + * uniform mat4 uModelViewMatrix; + * uniform mat4 uProjectionMatrix; + * + * in vec3 aPosition; + * in vec2 aOffset; + * + * void main(){ + * vec4 positionVec4 = vec4(aPosition.xyz, 1.0); + * positionVec4.xy += aOffset; + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * const fragSrc = `#version 300 es + * precision mediump float; + * out vec4 outColor; + * void main(){ + * outColor = vec4(0.0, 1.0, 1.0, 1.0); + * } + * `; + * + * function setup(){ + * createCanvas(100, 100, WEBGL); + * + * // Create and use the custom shader. + * const myShader = createShader(vertSrc, fragSrc); + * shader(myShader); + * + * describe('A wobbly, cyan circle on a gray background.'); + * } + * + * function draw(){ + * // Set the styles + * background(125); + * noStroke(); + * + * // Draw the circle. + * beginShape(); + * for (let i = 0; i < 30; i++){ + * const x = 40 * cos(i/30 * TWO_PI); + * const y = 40 * sin(i/30 * TWO_PI); + * + * // Apply some noise to the coordinates. + * const xOff = 10 * noise(x + millis()/1000) - 5; + * const yOff = 10 * noise(y + millis()/1000) - 5; + * + * // Apply these noise values to the following vertex. + * vertexProperty('aOffset', [xOff, yOff]); + * vertex(x, y); + * } + * endShape(CLOSE); + * } + * + *
+ * + *
+ * + * let myShader; + * const cols = 10; + * const rows = 10; + * const cellSize = 6; + * + * const vertSrc = `#version 300 es + * precision mediump float; + * uniform mat4 uProjectionMatrix; + * uniform mat4 uModelViewMatrix; + * + * in vec3 aPosition; + * in vec3 aNormal; + * in vec3 aVertexColor; + * in float aDistance; + * + * out vec3 vVertexColor; + * + * void main(){ + * vec4 positionVec4 = vec4(aPosition, 1.0); + * positionVec4.xyz += aDistance * aNormal * 2.0;; + * vVertexColor = aVertexColor; + * gl_Position = uProjectionMatrix * uModelViewMatrix * positionVec4; + * } + * `; + * + * const fragSrc = `#version 300 es + * precision mediump float; + * + * in vec3 vVertexColor; + * out vec4 outColor; + * + * void main(){ + * outColor = vec4(vVertexColor, 1.0); + * } + * `; + * + * function setup(){ + * createCanvas(100, 100, WEBGL); + * + * // Create and apply the custom shader. + * myShader = createShader(vertSrc, fragSrc); + * shader(myShader); + * noStroke(); + * describe('A blue grid, which moves away from the mouse position, on a gray background.'); + * } + * + * function draw(){ + * background(200); + * + * // Draw the grid in the middle of the screen. + * translate(-cols*cellSize/2, -rows*cellSize/2); + * beginShape(QUADS); + * for (let i = 0; i < cols; i++) { + * for (let j = 0; j < rows; j++) { + * + * // Calculate the cell position. + * let x = i * cellSize; + * let y = j * cellSize; + * + * fill(j/rows*255, j/cols*255, 255); + * + * // Calculate the distance from the corner of each cell to the mouse. + * let distance = dist(x1,y1, mouseX, mouseY); + * + * // Send the distance to the shader. + * vertexProperty('aDistance', min(distance, 100)); + * + * vertex(x, y); + * vertex(x + cellSize, y); + * vertex(x + cellSize, y + cellSize); + * vertex(x, y + cellSize); + * } + * } + * endShape(); + * } + * + *
+ * + * @method vertexProperty + * @param {String} attributeName the name of the vertex attribute. + * @param {Number|Number[]} data the data tied to the vertex attribute. + */ +p5.prototype.vertexProperty = function(attributeName, data){ + // this._assert3d('vertexProperty'); + // p5._validateParameters('vertexProperty', arguments); + this._renderer.vertexProperty(attributeName, data); +}; + export default p5; diff --git a/src/webgl/3d_primitives.js b/src/webgl/3d_primitives.js index 9dc5d65622..eea99ee2d8 100644 --- a/src/webgl/3d_primitives.js +++ b/src/webgl/3d_primitives.js @@ -3003,21 +3003,33 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { } const LUTLength = this._lookUpTableBezier.length; + const immediateGeometry = this.immediateMode.geometry; // fillColors[0]: start point color // fillColors[1],[2]: control point color // fillColors[3]: end point color const fillColors = []; for (m = 0; m < 4; m++) fillColors.push([]); - fillColors[0] = this.immediateMode.geometry.vertexColors.slice(-4); + fillColors[0] = immediateGeometry.vertexColors.slice(-4); fillColors[3] = this.curFillColor.slice(); // Do the same for strokeColor. const strokeColors = []; for (m = 0; m < 4; m++) strokeColors.push([]); - strokeColors[0] = this.immediateMode.geometry.vertexStrokeColors.slice(-4); + strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); strokeColors[3] = this.curStrokeColor.slice(); + // Do the same for custom vertex properties + const userVertexProperties = {}; + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + userVertexProperties[propName] = []; + for (m = 0; m < 4; m++) userVertexProperties[propName].push([]); + userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); + userVertexProperties[propName][3] = prop.getCurrentData(); + } + if (argLength === 6) { this.isBezier = true; @@ -3045,14 +3057,25 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } + for (const propName in immediateGeometry.userVertexProperties){ + const size = immediateGeometry.userVertexProperties[propName].getDataSize(); + for (k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 + ); + userVertexProperties[propName][2].push( + userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 + ); + } + } - for (i = 0; i < LUTLength; i++) { + for (let i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; this.curStrokeColor = [0, 0, 0, 0]; _x = _y = 0; - for (m = 0; m < 4; m++) { - for (k = 0; k < 4; k++) { + for (let m = 0; m < 4; m++) { + for (let k = 0; k < 4; k++) { this.curFillColor[k] += this._lookUpTableBezier[i][m] * fillColors[m][k]; this.curStrokeColor[k] += @@ -3061,11 +3084,26 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { _x += w_x[m] * this._lookUpTableBezier[i][m]; _y += w_y[m] * this._lookUpTableBezier[i][m]; } + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 4; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } this.vertex(_x, _y); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[3]; this.curStrokeColor = strokeColors[3]; + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + prop.setCurrentData(userVertexProperties[propName][2]); + } this.immediateMode._bezierVertex[0] = args[4]; this.immediateMode._bezierVertex[1] = args[5]; } else if (argLength === 9) { @@ -3082,7 +3120,7 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { const totalLength = d0 + d1 + d2; d0 /= totalLength; d2 /= totalLength; - for (k = 0; k < 4; k++) { + for (let k = 0; k < 4; k++) { fillColors[1].push( fillColors[0][k] * (1-d0) + fillColors[3][k] * d0 ); @@ -3096,7 +3134,18 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { strokeColors[0][k] * d2 + strokeColors[3][k] * (1-d2) ); } - for (i = 0; i < LUTLength; i++) { + for (const propName in immediateGeometry.userVertexProperties){ + const size = immediateGeometry.userVertexProperties[propName].getDataSize(); + for (k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][3][k] * d0 + ); + userVertexProperties[propName][2].push( + userVertexProperties[propName][0][k] * (1-d2) + userVertexProperties[propName][3][k] * d2 + ); + } + } + for (let i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; this.curStrokeColor = [0, 0, 0, 0]; @@ -3112,11 +3161,26 @@ p5.RendererGL.prototype.bezierVertex = function(...args) { _y += w_y[m] * this._lookUpTableBezier[i][m]; _z += w_z[m] * this._lookUpTableBezier[i][m]; } + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 4; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableBezier[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } this.vertex(_x, _y, _z); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[3]; this.curStrokeColor = strokeColors[3]; + for (const propName in immediateGeometry.userVertexProperties) { + 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]; @@ -3163,21 +3227,33 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { } const LUTLength = this._lookUpTableQuadratic.length; + const immediateGeometry = this.immediateMode.geometry; // fillColors[0]: start point color // fillColors[1]: control point color // fillColors[2]: end point color const fillColors = []; for (m = 0; m < 3; m++) fillColors.push([]); - fillColors[0] = this.immediateMode.geometry.vertexColors.slice(-4); + fillColors[0] = immediateGeometry.vertexColors.slice(-4); fillColors[2] = this.curFillColor.slice(); // Do the same for strokeColor. const strokeColors = []; for (m = 0; m < 3; m++) strokeColors.push([]); - strokeColors[0] = this.immediateMode.geometry.vertexStrokeColors.slice(-4); + strokeColors[0] = immediateGeometry.vertexStrokeColors.slice(-4); strokeColors[2] = this.curStrokeColor.slice(); + // Do the same for user defined vertex properties + const userVertexProperties = {}; + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + userVertexProperties[propName] = []; + for (m = 0; m < 3; m++) userVertexProperties[propName].push([]); + userVertexProperties[propName][0] = prop.getSrcArray().slice(-size); + userVertexProperties[propName][2] = prop.getCurrentData(); + } + if (argLength === 4) { this.isQuadratic = true; @@ -3190,7 +3266,7 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { let d1 = Math.hypot(w_x[1]-w_x[2], w_y[1]-w_y[2]); const totalLength = d0 + d1; d0 /= totalLength; - for (k = 0; k < 4; k++) { + for (let k = 0; k < 4; k++) { fillColors[1].push( fillColors[0][k] * (1-d0) + fillColors[2][k] * d0 ); @@ -3198,14 +3274,23 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { strokeColors[0][k] * (1-d0) + strokeColors[2][k] * d0 ); } + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + for (let k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 + ); + } + } - for (i = 0; i < LUTLength; i++) { + for (let i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; this.curStrokeColor = [0, 0, 0, 0]; _x = _y = 0; - for (m = 0; m < 3; m++) { - for (k = 0; k < 4; k++) { + for (let m = 0; m < 3; m++) { + for (let k = 0; k < 4; k++) { this.curFillColor[k] += this._lookUpTableQuadratic[i][m] * fillColors[m][k]; this.curStrokeColor[k] += @@ -3214,12 +3299,28 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { _x += w_x[m] * this._lookUpTableQuadratic[i][m]; _y += w_y[m] * this._lookUpTableQuadratic[i][m]; } + + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 3; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } this.vertex(_x, _y); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[2]; this.curStrokeColor = strokeColors[2]; + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + prop.setCurrentData(userVertexProperties[propName][2]); + } this.immediateMode._quadraticVertex[0] = args[2]; this.immediateMode._quadraticVertex[1] = args[3]; } else if (argLength === 6) { @@ -3244,6 +3345,16 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { ); } + for (const propName in immediateGeometry.userVertexProperties){ + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + for (let k = 0; k < size; k++){ + userVertexProperties[propName][1].push( + userVertexProperties[propName][0][k] * (1-d0) + userVertexProperties[propName][2][k] * d0 + ); + } + } + for (i = 0; i < LUTLength; i++) { // Interpolate colors using control points this.curFillColor = [0, 0, 0, 0]; @@ -3260,12 +3371,27 @@ p5.RendererGL.prototype.quadraticVertex = function(...args) { _y += w_y[m] * this._lookUpTableQuadratic[i][m]; _z += w_z[m] * this._lookUpTableQuadratic[i][m]; } + for (const propName in immediateGeometry.userVertexProperties) { + const prop = immediateGeometry.userVertexProperties[propName]; + const size = prop.getDataSize(); + let newValues = Array(size).fill(0); + for (let m = 0; m < 3; m++){ + for (let k = 0; k < size; k++){ + newValues[k] += this._lookUpTableQuadratic[i][m] * userVertexProperties[propName][m][k]; + } + } + prop.setCurrentData(newValues); + } this.vertex(_x, _y, _z); } // so that we leave currentColor with the last value the user set it to this.curFillColor = fillColors[2]; this.curStrokeColor = strokeColors[2]; + for (const propName in immediateGeometry.userVertexProperties) { + 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]; diff --git a/src/webgl/GeometryBuilder.js b/src/webgl/GeometryBuilder.js index ac78ec7e94..195da6b181 100644 --- a/src/webgl/GeometryBuilder.js +++ b/src/webgl/GeometryBuilder.js @@ -60,6 +60,32 @@ class GeometryBuilder { ); this.geometry.uvs.push(...input.uvs); + const inputUserVertexProps = input.userVertexProperties; + const builtUserVertexProps = this.geometry.userVertexProperties; + const numPreviousVertices = this.geometry.vertices.length - input.vertices.length; + + for (const propName in builtUserVertexProps){ + if (propName in inputUserVertexProps){ + continue; + } + const prop = builtUserVertexProps[propName] + const size = prop.getDataSize(); + const numMissingValues = size * input.vertices.length; + const missingValues = Array(numMissingValues).fill(0); + prop.pushDirect(missingValues); + } + for (const propName in inputUserVertexProps){ + const prop = inputUserVertexProps[propName]; + const data = prop.getSrcArray(); + const size = prop.getDataSize(); + if (numPreviousVertices > 0 && !(propName in builtUserVertexProps)){ + const numMissingValues = size * numPreviousVertices; + const missingValues = Array(numMissingValues).fill(0); + this.geometry.vertexProperty(propName, missingValues, size); + } + this.geometry.vertexProperty(propName, data, size); + } + if (this.renderer._doFill) { this.geometry.faces.push( ...input.faces.map(f => f.map(idx => idx + startIdx)) diff --git a/src/webgl/p5.Geometry.js b/src/webgl/p5.Geometry.js index 8ee028ea67..12689acc86 100644 --- a/src/webgl/p5.Geometry.js +++ b/src/webgl/p5.Geometry.js @@ -283,6 +283,8 @@ p5.Geometry = class Geometry { // One color per vertex representing the stroke color at that vertex this.vertexStrokeColors = []; + this.userVertexProperties = {}; + // One color per line vertex, generated automatically based on // vertexStrokeColors in _edgesToVertices() this.lineVertexColors = new p5.DataArray(); @@ -450,6 +452,11 @@ p5.Geometry = class Geometry { this.vertexNormals.length = 0; this.uvs.length = 0; + for (const propName in this.userVertexProperties){ + this.userVertexProperties[propName].delete(); + } + this.userVertexProperties = {}; + this.dirtyFlags = {}; } @@ -1910,6 +1917,186 @@ p5.Geometry = class Geometry { } return this; } + +/** Sets the shader's vertex property or attribute variables. + * + * An vertex property or vertex attribute is a variable belonging to a vertex in a shader. p5.js provides some + * default properties, such as `aPosition`, `aNormal`, `aVertexColor`, etc. These are + * set using vertex(), normal() + * and fill() respectively. Custom properties can also + * be defined within beginShape() and + * endShape(). + * + * The first parameter, `propertyName`, is a string with the property's name. + * This is the same variable name which should be declared in the shader, as in + * `in vec3 aProperty`, similar to .`setUniform()`. + * + * The second parameter, `data`, is the value assigned to the shader variable. This value + * will be pushed directly onto the Geometry object. There should be the same number + * of custom property values as vertices, this method should be invoked once for each + * vertex. + * + * The `data` can be a Number or an array of numbers. Tn the shader program the type + * can be declared according to the WebGL specification. Common types include `float`, + * `vec2`, `vec3`, `vec4` or matrices. + * + * See also the global vertexProperty() function. + * + * @example + *
+ * + * let geo; + * + * function cartesianToSpherical(x, y, z) { + * let r = sqrt(pow(x, x) + pow(y, y) + pow(z, z)); + * let theta = acos(z / r); + * let phi = atan2(y, x); + * return { theta, phi }; + * } + * + * function setup() { + * createCanvas(100, 100, WEBGL); + * + * // Modify the material shader to display roughness. + * const myShader = materialShader().modify({ + * vertexDeclarations:`in float aRoughness; + * out float vRoughness;`, + * fragmentDeclarations: 'in float vRoughness;', + * 'void afterVertex': `() { + * vRoughness = aRoughness; + * }`, + * 'vec4 combineColors': `(ColorComponents components) { + * vec4 color = vec4(0.); + * color.rgb += components.diffuse * components.baseColor * (1.0-vRoughness); + * color.rgb += components.ambient * components.ambientColor; + * color.rgb += components.specular * components.specularColor * (1.0-vRoughness); + * color.a = components.opacity; + * return color; + * }` + * }); + * + * // Create the Geometry object. + * beginGeometry(); + * fill('hotpink'); + * sphere(45, 50, 50); + * geo = endGeometry(); + * + * // Set the roughness value for every vertex. + * for (let v of geo.vertices){ + * + * // convert coordinates to spherical coordinates + * let spherical = cartesianToSpherical(v.x, v.y, v.z); + * + * // Set the custom roughness vertex property. + * let roughness = noise(spherical.theta*5, spherical.phi*5); + * geo.vertexProperty('aRoughness', roughness); + * } + * + * // Use the custom shader. + * shader(myShader); + * + * describe('A rough pink sphere rotating on a blue background.'); + * } + * + * function draw() { + * // Set some styles and lighting + * background('lightblue'); + * noStroke(); + * + * specularMaterial(255,125,100); + * shininess(2); + * + * directionalLight('white', -1, 1, -1); + * ambientLight(320); + * + * rotateY(millis()*0.001); + * + * // Draw the geometry + * model(geo); + * } + * + *
+ * + * @method vertexProperty + * @param {String} propertyName the name of the vertex property. + * @param {Number|Number[]} data the data tied to the vertex property. + * @param {Number} [size] optional size of each unit of data. + */ + vertexProperty(propertyName, data, size){ + let prop; + if (!this.userVertexProperties[propertyName]){ + prop = this.userVertexProperties[propertyName] = + this._userVertexPropertyHelper(propertyName, data, size); + } + prop = this.userVertexProperties[propertyName]; + if (size){ + prop.pushDirect(data); + } else{ + prop.setCurrentData(data); + prop.pushCurrentData(); + } + } + + _userVertexPropertyHelper(propertyName, data, size){ + const geometryInstance = this; + const prop = this.userVertexProperties[propertyName] = { + name: propertyName, + dataSize: size ? size : data.length ? data.length : 1, + geometry: geometryInstance, + // Getters + getName(){ + return this.name; + }, + getCurrentData(){ + return this.currentData; + }, + getDataSize() { + return this.dataSize; + }, + getSrcName() { + const src = this.name.concat('Src'); + return src; + }, + getDstName() { + const dst = this.name.concat('Buffer'); + return dst; + }, + getSrcArray() { + const srcName = this.getSrcName(); + return this.geometry[srcName]; + }, + //Setters + setCurrentData(data) { + const size = data.length ? data.length : 1; + if (size != this.getDataSize()){ + p5._friendlyError(`Custom vertex property '${this.name}' has been set with various data sizes. You can change it's name, or if it was an accident, set '${this.name}' to have the same number of inputs each time!`, 'vertexProperty()'); + } + this.currentData = data; + }, + // Utilities + pushCurrentData(){ + const data = this.getCurrentData(); + this.pushDirect(data); + }, + pushDirect(data) { + if (data.length){ + this.getSrcArray().push(...data); + } else{ + this.getSrcArray().push(data); + } + }, + resetSrcArray(){ + this.geometry[this.getSrcName()] = []; + }, + delete() { + const srcName = this.getSrcName(); + delete this.geometry[srcName]; + delete this; + } + }; + this[prop.getSrcName()] = []; + return this.userVertexProperties[propertyName]; + } }; /** diff --git a/src/webgl/p5.RenderBuffer.js b/src/webgl/p5.RenderBuffer.js index 79851e485f..96fd2fc5e9 100644 --- a/src/webgl/p5.RenderBuffer.js +++ b/src/webgl/p5.RenderBuffer.js @@ -32,10 +32,12 @@ p5.RenderBuffer = class { 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 const createBuffer = !buffer; @@ -53,7 +55,6 @@ p5.RenderBuffer = class { 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; } diff --git a/src/webgl/p5.RendererGL.Immediate.js b/src/webgl/p5.RendererGL.Immediate.js index 47a656fa83..357c1c5527 100644 --- a/src/webgl/p5.RendererGL.Immediate.js +++ b/src/webgl/p5.RendererGL.Immediate.js @@ -33,12 +33,15 @@ import './p5.RenderBuffer'; p5.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; }; -const immediateBufferStrides = { +p5.RendererGL.prototype.immediateBufferStrides = { vertices: 1, vertexNormals: 1, vertexColors: 4, @@ -78,8 +81,8 @@ p5.RendererGL.prototype.vertex = function(x, y) { // 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 immediateBufferStrides) { - const stride = immediateBufferStrides[key]; + for (const key in this.immediateBufferStrides) { + const stride = this.immediateBufferStrides[key]; const buffer = this.immediateMode.geometry[key]; buffer.push( ...buffer.slice( @@ -113,6 +116,19 @@ p5.RendererGL.prototype.vertex = function(x, y) { const vert = new p5.Vector(x, y, z); this.immediateMode.geometry.vertices.push(vert); this.immediateMode.geometry.vertexNormals.push(this._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.curFillColor || [0.5, 0.5, 0.5, 1.0]; this.immediateMode.geometry.vertexColors.push( vertexColor[0], @@ -165,6 +181,40 @@ p5.RendererGL.prototype.vertex = function(x, y) { return this; }; +p5.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 p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), propertyName, this) + ); + } + prop.setCurrentData(data); +}; + +p5.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 @@ -268,6 +318,7 @@ p5.RendererGL.prototype.endShape = function( this.immediateMode._bezierVertex.length = 0; this.immediateMode._quadraticVertex.length = 0; this.immediateMode._curveVertex.length = 0; + return this; }; @@ -282,7 +333,6 @@ p5.RendererGL.prototype.endShape = function( */ p5.RendererGL.prototype._processVertices = function(mode) { if (this.immediateMode.geometry.vertices.length === 0) return; - const calculateStroke = this._doStroke; const shouldClose = mode === constants.CLOSE; if (calculateStroke) { @@ -435,20 +485,42 @@ p5.RendererGL.prototype._tesselateShape = function() { 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 + p5.RendererGL.prototype.tessyVertexSize + 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) { @@ -519,6 +591,9 @@ p5.RendererGL.prototype._drawImmediateFill = function(count = 1) { 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( @@ -565,6 +640,9 @@ p5.RendererGL.prototype._drawImmediateStroke = function() { 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.curStrokeColor, diff --git a/src/webgl/p5.RendererGL.Retained.js b/src/webgl/p5.RendererGL.Retained.js index 49f2dd772b..57ce2a9d15 100644 --- a/src/webgl/p5.RendererGL.Retained.js +++ b/src/webgl/p5.RendererGL.Retained.js @@ -62,6 +62,8 @@ p5.RendererGL.prototype._freeBuffers = function(gId) { // free all the buffers freeBuffers(this.retainedMode.buffers.stroke); freeBuffers(this.retainedMode.buffers.fill); + freeBuffers(this.retainedMode.buffers.user); + this.retainedMode.buffers.user = []; }; /** @@ -114,6 +116,12 @@ p5.RendererGL.prototype.createBuffers = function(gId, model) { ? model.lineVertices.length / 3 : 0; + for (const propName in model.userVertexProperties){ + const prop = model.userVertexProperties[propName]; + this.retainedMode.buffers.user.push( + new p5.RenderBuffer(prop.getDataSize(), prop.getSrcName(), prop.getDstName(), prop.getName(), this) + ); + } return buffers; }; @@ -130,7 +138,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { if ( !this.geometryBuilder && this._doFill && - this.retainedMode.geometry[gId].vertexCount > 0 + geometry.vertexCount > 0 ) { this._useVertexColor = (geometry.model.vertexColors.length > 0); const fillShader = this._getRetainedFillShader(); @@ -138,6 +146,16 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { 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 @@ -158,6 +176,16 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { 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.curStrokeColor, @@ -170,7 +198,7 @@ p5.RendererGL.prototype.drawBuffers = function(gId) { if (this.geometryBuilder) { this.geometryBuilder.addRetained(geometry); } - + return this; }; diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index f712787233..65375fa48e 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -575,7 +575,7 @@ p5.RendererGL = class RendererGL extends Renderer { this.userFillShader = undefined; this.userStrokeShader = undefined; this.userPointShader = undefined; - + this._useUserAttributes = undefined; // Default drawing is done in Retained Mode // Geometry and Material hashes stored here this.retainedMode = { @@ -599,7 +599,8 @@ p5.RendererGL = class RendererGL extends Renderer { text: [ new p5.RenderBuffer(3, 'vertices', 'vertexBuffer', 'aPosition', this, this._vToNArray), new p5.RenderBuffer(2, 'uvs', 'uvBuffer', 'aTexCoord', this, this._flatten) - ] + ], + user:[] } }; @@ -627,7 +628,8 @@ p5.RendererGL = class RendererGL extends Renderer { new p5.RenderBuffer(3, 'lineTangentsOut', 'lineTangentsOutBuffer', 'aTangentOut', this), new p5.RenderBuffer(1, 'lineSides', 'lineSidesBuffer', 'aSide', this) ], - point: this.GL.createBuffer() + point: this.GL.createBuffer(), + user:[] } }; @@ -2416,6 +2418,7 @@ p5.RendererGL = class RendererGL extends Renderer { return p; } _initTessy() { + this.tessyVertexSize = 12; // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { for (const element of data) { @@ -2434,8 +2437,8 @@ p5.RendererGL = class RendererGL extends Renderer { console.log(`error number: ${errno}`); } // callback for when segments intersect and must be split - function combinecallback(coords, data, weight) { - const result = new Array(p5.RendererGL.prototype.tessyVertexSize).fill(0); + 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; @@ -2443,7 +2446,7 @@ p5.RendererGL = class RendererGL extends Renderer { } } return result; - } + }; function edgeCallback(flag) { // don't really care about the flag, but need no-strip/no-fan behavior @@ -2475,7 +2478,7 @@ p5.RendererGL = class RendererGL extends Renderer { for ( let j = 0; j < contour.length; - j += p5.RendererGL.prototype.tessyVertexSize + j += this.tessyVertexSize ) { if (contour[j + 2] !== z) { allSameZ = false; @@ -2498,11 +2501,11 @@ p5.RendererGL = class RendererGL extends Renderer { for ( let j = 0; j < contour.length; - j += p5.RendererGL.prototype.tessyVertexSize + j += this.tessyVertexSize ) { const coords = contour.slice( j, - j + p5.RendererGL.prototype.tessyVertexSize + j + this.tessyVertexSize ); this._tessy.gluTessVertex(coords, coords); } @@ -2525,8 +2528,4 @@ p5.prototype._assert3d = function (name) { ); }; -// function to initialize GLU Tesselator - -p5.RendererGL.prototype.tessyVertexSize = 12; - -export default p5.RendererGL; +export default p5.RendererGL; \ No newline at end of file diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index e5e8a9be7a..105a49047f 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -130,4 +130,100 @@ visualSuite('WebGL', function() { } ); }); + + visualSuite('vertexProperty', function(){ + const vertSrc = `#version 300 es + precision mediump float; + uniform mat4 uProjectionMatrix; + uniform mat4 uModelViewMatrix; + in vec3 aPosition; + in vec3 aCol; + out vec3 vCol; + void main(){ + vCol = aCol; + gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0); + }`; + const fragSrc = `#version 300 es + precision mediump float; + in vec3 vCol; + out vec4 outColor; + void main(){ + outColor = vec4(vCol, 1.0); + }`; + visualTest( + 'on TESS shape mode', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.background('white'); + const myShader = p5.createShader(vertSrc, fragSrc); + p5.shader(myShader); + p5.beginShape(p5.TESS); + p5.noStroke(); + for (let i = 0; i < 20; i++){ + let x = 20 * p5.sin(i/20*p5.TWO_PI); + let y = 20 * p5.cos(i/20*p5.TWO_PI); + p5.vertexProperty('aCol', [x/20, -y/20, 0]); + p5.vertex(x, y); + } + p5.endShape(); + screenshot(); + } + ); + visualTest( + 'on QUADS shape mode', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.background('white'); + const myShader = p5.createShader(vertSrc, fragSrc); + p5.shader(myShader) + p5.beginShape(p5.QUADS); + p5.noStroke(); + p5.translate(-25,-25); + for (let i = 0; i < 5; i++){ + for (let j = 0; j < 5; j++){ + let x1 = i * 10; + let x2 = x1 + 10; + let y1 = j * 10; + let y2 = y1 + 10; + p5.vertexProperty('aCol', [1, 0, 0]); + p5.vertex(x1, y1); + p5.vertexProperty('aCol', [0, 0, 1]); + p5.vertex(x2, y1); + p5.vertexProperty('aCol', [0, 1, 1]); + p5.vertex(x2, y2); + p5.vertexProperty('aCol', [1, 1, 1]); + p5.vertex(x1, y2); + } + } + p5.endShape(); + screenshot(); + } + ); + visualTest( + 'on buildGeometry outputs containing 3D primitives', function(p5, screenshot) { + p5.createCanvas(50, 50, p5.WEBGL); + p5.background('white'); + const myShader = p5.createShader(vertSrc, fragSrc); + p5.shader(myShader); + const shape = p5.buildGeometry(() => { + p5.push(); + p5.translate(15,-10,0); + p5.sphere(5); + p5.pop(); + p5.beginShape(p5.TRIANGLES); + p5.vertexProperty('aCol', [1,0,0]) + p5.vertex(-5, 5, 0); + p5.vertexProperty('aCol', [0,1,0]) + p5.vertex(5, 5, 0); + p5.vertexProperty('aCol', [0,0,1]) + p5.vertex(0, -5, 0); + p5.endShape(p5.CLOSE); + p5.push(); + p5.translate(-15,10,0); + p5.box(10); + p5.pop(); + }) + p5.model(shape); + screenshot(); + } + ); + }); }); diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png new file mode 100644 index 0000000000..75018122a4 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/setAttribute/on QUADS shape mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png new file mode 100644 index 0000000000..5a5164da2f Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/setAttribute/on TESS shape mode/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png new file mode 100644 index 0000000000..596ec54c90 Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/setAttribute/on buildGeometry outputs containing 3D primitives/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/webgl/p5.RendererGL.js b/test/unit/webgl/p5.RendererGL.js index 66e9008713..6bfae7eb93 100644 --- a/test/unit/webgl/p5.RendererGL.js +++ b/test/unit/webgl/p5.RendererGL.js @@ -1577,15 +1577,19 @@ suite('p5.RendererGL', function() { renderer.beginShape(myp5.TESS); renderer.fill(255, 255, 255); renderer.normal(-1, -1, 1); + renderer.vertexProperty('aCustom', [1, 1, 1]) renderer.vertex(-10, -10, 0, 0); renderer.fill(255, 0, 0); renderer.normal(1, -1, 1); + renderer.vertexProperty('aCustom', [1, 0, 0]) renderer.vertex(10, -10, 1, 0); renderer.fill(0, 255, 0); renderer.normal(1, 1, 1); + renderer.vertexProperty('aCustom', [0, 1, 0]) renderer.vertex(10, 10, 1, 1); renderer.fill(0, 0, 255); renderer.normal(-1, 1, 1); + renderer.vertexProperty('aCustom', [0, 0, 1]) renderer.vertex(-10, 10, 0, 1); renderer.endShape(myp5.CLOSE); @@ -1641,6 +1645,15 @@ suite('p5.RendererGL', function() { [1, 1, 1] ); + assert.deepEqual(renderer.immediateMode.geometry.aCustomSrc, [ + 1, 0, 0, + 0, 0, 1, + 1, 1, 1, + 0, 0, 1, + 1, 0, 0, + 0, 1, 0 + ]); + assert.deepEqual(renderer.immediateMode.geometry.vertexColors, [ 1, 0, 0, 1, 0, 0, 1, 1, @@ -2490,4 +2503,171 @@ suite('p5.RendererGL', function() { } ); }); + + suite('vertexProperty()', function() { + test('Immediate mode data and buffers created in beginShape', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + + myp5.beginShape(); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1, 2, 3]); + myp5.vertex(0,0,0); + expect(myp5._renderer.immediateMode.geometry.userVertexProperties.aCustom).to.containSubset({ + name: 'aCustom', + currentData: 1, + dataSize: 1 + }); + expect(myp5._renderer.immediateMode.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([ + { + size: 1, + src: 'aCustomSrc', + dst: 'aCustomBuffer', + attr: 'aCustom', + }, + { + size: 3, + src: 'aCustomVec3Src', + dst: 'aCustomVec3Buffer', + attr: 'aCustomVec3', + } + ]); + myp5.endShape(); + } + ); + test('Immediate mode data and buffers deleted after beginShape', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + + myp5.beginShape(); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); + myp5.vertex(0,0,0); + 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, []); + myp5.endShape(); + } + ); + test('Data copied over from beginGeometry', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + myp5.beginGeometry(); + myp5.beginShape(); + myp5.vertexProperty('aCustom', 1); + myp5.vertexProperty('aCustomVec3', [1,2,3]); + myp5.vertex(0,1,0); + myp5.vertex(-1,0,0); + myp5.vertex(1,0,0); + const immediateCopy = myp5._renderer.immediateMode.geometry; + myp5.endShape(); + const myGeo = myp5.endGeometry(); + assert.deepEqual(immediateCopy.aCustomSrc, myGeo.aCustomSrc); + assert.deepEqual(immediateCopy.aCustomVec3Src, myGeo.aCustomVec3Src); + } + ); + 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.gId, myGeo); + expect(myp5._renderer.retainedMode.buffers.user).to.containSubset([ + { + size: 1, + src: 'aCustomSrc', + dst: 'aCustomBuffer', + attr: 'aCustom', + }, + { + size: 3, + src: 'aCustomVec3Src', + dst: 'aCustomVec3Buffer', + attr: 'aCustomVec3', + } + ]); + } + ); + test('Retained mode buffers deleted after 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.model(myGeo); + assert.equal(myp5._renderer.retainedMode.buffers.user.length, 0); + } + ); + test('Friendly error if different sizes used', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + const logs = []; + const myLog = (...data) => logs.push(data.join(', ')); + const oldLog = console.log; + console.log = myLog; + myp5.beginShape(); + myp5.vertexProperty('aCustom', [1,2,3]); + myp5.vertex(0,0,0); + myp5.vertexProperty('aCustom', [1,2]); + myp5.vertex(1,0,0); + myp5.endShape(); + console.log = oldLog; + expect(logs.join('\n')).to.match(/Custom vertex property 'aCustom' has been set with various data sizes/); + } + ); + test('Friendly error too many values set', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + const logs = []; + const myLog = (...data) => logs.push(data.join(', ')); + const oldLog = console.log; + console.log = myLog; + let myGeo = new p5.Geometry(); + myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.vertexProperty('aCustom', 1); + myGeo.vertexProperty('aCustom', 2); + 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./); + } + ); + test('Friendly error if too few values set', + function() { + myp5.createCanvas(50, 50, myp5.WEBGL); + const logs = []; + const myLog = (...data) => logs.push(data.join(', ')); + const oldLog = console.log; + console.log = myLog; + let myGeo = new p5.Geometry(); + myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.vertices.push(new p5.Vector(0,0,0)); + myGeo.vertexProperty('aCustom', 1); + myp5.model(myGeo); + console.log = oldLog; + expect(logs.join('\n')).to.match(/One of the geometries has a custom vertex property 'aCustom' with fewer values than vertices./); + } + ); + }) });