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./);
+ }
+ );
+ })
});