Skip to content

Commit ed20ca3

Browse files
authored
Merge pull request #6109 from davepagurek/fix/fbo-pixels
Implement load/updatePixels() and get() for framebuffers
2 parents 3b9dad6 + ec3fd55 commit ed20ca3

File tree

7 files changed

+690
-53
lines changed

7 files changed

+690
-53
lines changed

src/image/pixels.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,8 @@ p5.prototype.filter = function(operation, value) {
510510
* @method get
511511
* @param {Number} x x-coordinate of the pixel
512512
* @param {Number} y y-coordinate of the pixel
513-
* @param {Number} w width
514-
* @param {Number} h height
513+
* @param {Number} w width of the section to be returned
514+
* @param {Number} h height of the section to be returned
515515
* @return {p5.Image} the rectangle <a href="#/p5.Image">p5.Image</a>
516516
* @example
517517
* <div>

src/webgl/p5.Framebuffer.js

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import p5 from '../core/main';
77
import * as constants from '../core/constants';
88
import { checkWebGLCapabilities } from './p5.Texture';
9+
import { readPixelsWebGL, readPixelWebGL } from './p5.RendererGL';
910

1011
class FramebufferCamera extends p5.Camera {
1112
/**
@@ -109,6 +110,24 @@ class Framebuffer {
109110
this.target = target;
110111
this.target._renderer.framebuffers.add(this);
111112

113+
/**
114+
* A <a href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
115+
* /Global_Objects/Uint8ClampedArray' target='_blank'>Uint8ClampedArray</a>
116+
* containing the values for all the pixels in the Framebuffer.
117+
*
118+
* Like the <a href="#/p5/pixels">main canvas pixels property</a>, call
119+
* <a href="#/p5.Framebuffer/loadPixels">loadPixels()</a> before reading
120+
* it, and call <a href="#/p5.Framebuffer.updatePixels">updatePixels()</a>
121+
* afterwards to update its data.
122+
*
123+
* Note that updating pixels via this property will be slower than
124+
* <a href="#/p5.Framebuffer/begin">drawing to the framebuffer directly.</a>
125+
* Consider using a shader instead of looping over pixels.
126+
*
127+
* @property {Number[]} pixels
128+
*/
129+
this.pixels = [];
130+
112131
this.format = settings.format || constants.UNSIGNED_BYTE;
113132
this.channels = settings.channels || (
114133
target._renderer._pInst._glAttributes.alpha
@@ -931,6 +950,276 @@ class Framebuffer {
931950
callback();
932951
this.end();
933952
}
953+
954+
/**
955+
* Call this befpre updating <a href="#/p5.Framebuffer/pixels">pixels</a>
956+
* and calling <a href="#/p5.Framebuffer/updatePixels">updatePixels</a>
957+
* to replace the content of the framebuffer with the data in the pixels
958+
* array.
959+
*/
960+
loadPixels() {
961+
const gl = this.gl;
962+
const prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
963+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
964+
const colorFormat = this._glColorFormat();
965+
this.pixels = readPixelsWebGL(
966+
this.pixels,
967+
gl,
968+
this.framebuffer,
969+
0,
970+
0,
971+
this.width * this.density,
972+
this.height * this.density,
973+
colorFormat.format,
974+
colorFormat.type
975+
);
976+
gl.bindFramebuffer(gl.FRAMEBUFFER, prevFramebuffer);
977+
}
978+
979+
/**
980+
* Get a region of pixels from the canvas in the form of a
981+
* <a href="#/p5.Image">p5.Image</a>, or a single pixel as an array of
982+
* numbers.
983+
*
984+
* Returns an array of [R,G,B,A] values for any pixel or grabs a section of
985+
* an image. If the Framebuffer has been set up to not store alpha values, then
986+
* only [R,G,B] will be returned. If no parameters are specified, the entire
987+
* image is returned.
988+
* Use the x and y parameters to get the value of one pixel. Get a section of
989+
* the display window by specifying additional w and h parameters. When
990+
* getting an image, the x and y parameters define the coordinates for the
991+
* upper-left corner of the image, regardless of the current <a href="#/p5/imageMode">imageMode()</a>.
992+
*
993+
* @method get
994+
* @param {Number} x x-coordinate of the pixel
995+
* @param {Number} y y-coordinate of the pixel
996+
* @param {Number} w width of the section to be returned
997+
* @param {Number} h height of the section to be returned
998+
* @return {p5.Image} the rectangle <a href="#/p5.Image">p5.Image</a>
999+
*/
1000+
/**
1001+
* @method get
1002+
* @return {p5.Image} the whole <a href="#/p5.Image">p5.Image</a>
1003+
*/
1004+
/**
1005+
* @method get
1006+
* @param {Number} x
1007+
* @param {Number} y
1008+
* @return {Number[]} color of pixel at x,y in array format [R, G, B, A]
1009+
*/
1010+
get(x, y, w, h) {
1011+
p5._validateParameters('p5.Framebuffer.get', arguments);
1012+
const colorFormat = this._glColorFormat();
1013+
if (x === undefined && y === undefined) {
1014+
x = 0;
1015+
y = 0;
1016+
w = this.width;
1017+
h = this.height;
1018+
} else if (w === undefined && h === undefined) {
1019+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) {
1020+
console.warn(
1021+
'The x and y values passed to p5.Framebuffer.get are outside of its range and will be clamped.'
1022+
);
1023+
x = this.target.constrain(x, 0, this.width - 1);
1024+
y = this.target.constrain(y, 0, this.height - 1);
1025+
}
1026+
1027+
return readPixelWebGL(
1028+
this.gl,
1029+
this.framebuffer,
1030+
x * this.density,
1031+
y * this.density,
1032+
colorFormat.format,
1033+
colorFormat.type
1034+
);
1035+
}
1036+
1037+
x = this.target.constrain(x, 0, this.width - 1);
1038+
y = this.target.constrain(y, 0, this.height - 1);
1039+
w = this.target.constrain(w, 1, this.width - x);
1040+
h = this.target.constrain(h, 1, this.height - y);
1041+
1042+
const rawData = readPixelsWebGL(
1043+
undefined,
1044+
this.gl,
1045+
this.framebuffer,
1046+
x * this.density,
1047+
y * this.density,
1048+
w * this.density,
1049+
h * this.density,
1050+
colorFormat.format,
1051+
colorFormat.type
1052+
);
1053+
// Framebuffer data might be either a Uint8Array or Float32Array
1054+
// depending on its format, and it may or may not have an alpha channel.
1055+
// To turn it into an image, we have to normalize the data into a
1056+
// Uint8ClampedArray with alpha.
1057+
const fullData = new Uint8ClampedArray(
1058+
w * h * this.density * this.density * 4
1059+
);
1060+
1061+
// Default channels that aren't in the framebuffer (e.g. alpha, if the
1062+
// framebuffer is in RGB mode instead of RGBA) to 255
1063+
fullData.fill(255);
1064+
1065+
const channels = colorFormat.type === this.gl.RGB ? 3 : 4;
1066+
for (let y = 0; y < h * this.density; y++) {
1067+
for (let x = 0; x < w * this.density; x++) {
1068+
for (let channel = 0; channel < 4; channel++) {
1069+
const idx = (y * w * this.density + x) * 4 + channel;
1070+
if (channel < channels) {
1071+
// Find the index of this pixel in `rawData`, which might have a
1072+
// different number of channels
1073+
const rawDataIdx = channels === 4
1074+
? idx
1075+
: (y * w * this.density + x) * channels + channel;
1076+
fullData[idx] = rawData[rawDataIdx];
1077+
}
1078+
}
1079+
}
1080+
}
1081+
1082+
// Create an image from the data
1083+
const region = new p5.Image(w * this.density, h * this.density);
1084+
region.imageData = region.canvas.getContext('2d').createImageData(
1085+
region.width,
1086+
region.height
1087+
);
1088+
region.imageData.data.set(fullData);
1089+
region.pixels = region.imageData.data;
1090+
region.updatePixels();
1091+
if (this.density !== 1) {
1092+
// TODO: support get() at a pixel density > 1
1093+
region.resize(w, h);
1094+
}
1095+
return region;
1096+
}
1097+
1098+
/**
1099+
* Call this after initially calling <a href="#/p5.Framebuffer/loadPixels">
1100+
* loadPixels()</a> and updating <a href="#/p5.Framebuffer/pixels">pixels</a>
1101+
* to replace the content of the framebuffer with the data in the pixels
1102+
* array.
1103+
*
1104+
* This will also clear the depth buffer so that any future drawing done
1105+
* afterwards will go on top.
1106+
*
1107+
* @example
1108+
* <div>
1109+
* <code>
1110+
* let framebuffer;
1111+
* function setup() {
1112+
* createCanvas(100, 100, WEBGL);
1113+
* framebuffer = createFramebuffer();
1114+
* }
1115+
1116+
* function draw() {
1117+
* noStroke();
1118+
* lights();
1119+
*
1120+
* // Draw a sphere to the framebuffer
1121+
* framebuffer.begin();
1122+
* background(0);
1123+
* sphere(25);
1124+
* framebuffer.end();
1125+
*
1126+
* // Load its pixels and draw a gradient over the lower half of the canvas
1127+
* framebuffer.loadPixels();
1128+
* for (let y = height/2; y < height; y++) {
1129+
* for (let x = 0; x < width; x++) {
1130+
* const idx = (y * width + x) * 4;
1131+
* framebuffer.pixels[idx] = (x / width) * 255;
1132+
* framebuffer.pixels[idx + 1] = (y / height) * 255;
1133+
* framebuffer.pixels[idx + 2] = 255;
1134+
* framebuffer.pixels[idx + 3] = 255;
1135+
* }
1136+
* }
1137+
* framebuffer.updatePixels();
1138+
*
1139+
* // Draw a cube on top of the pixels we just wrote
1140+
* framebuffer.begin();
1141+
* push();
1142+
* translate(20, 20);
1143+
* rotateX(0.5);
1144+
* rotateY(0.5);
1145+
* box(20);
1146+
* pop();
1147+
* framebuffer.end();
1148+
*
1149+
* image(framebuffer, -width/2, -height/2);
1150+
* noLoop();
1151+
* }
1152+
* </code>
1153+
* </div>
1154+
*
1155+
* @alt
1156+
* A sphere partly occluded by a gradient from cyan to white to magenta on
1157+
* the lower half of the canvas, with a 3D cube drawn on top of that in the
1158+
* lower right corner.
1159+
*/
1160+
updatePixels() {
1161+
const gl = this.gl;
1162+
this.colorP5Texture.bindTexture();
1163+
const colorFormat = this._glColorFormat();
1164+
1165+
const channels = colorFormat.format === gl.RGBA ? 4 : 3;
1166+
const len =
1167+
this.width * this.height * this.density * this.density * channels;
1168+
const TypedArrayClass = colorFormat.type === gl.UNSIGNED_BYTE
1169+
? Uint8Array
1170+
: Float32Array;
1171+
if (
1172+
!(this.pixels instanceof TypedArrayClass) || this.pixels.length !== len
1173+
) {
1174+
throw new Error(
1175+
'The pixels array has not been set correctly. Please call loadPixels() before updatePixels().'
1176+
);
1177+
}
1178+
1179+
gl.texImage2D(
1180+
gl.TEXTURE_2D,
1181+
0,
1182+
colorFormat.internalFormat,
1183+
this.width * this.density,
1184+
this.height * this.density,
1185+
0,
1186+
colorFormat.format,
1187+
colorFormat.type,
1188+
this.pixels
1189+
);
1190+
this.colorP5Texture.unbindTexture();
1191+
1192+
this.prevFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
1193+
if (this.antialias) {
1194+
// We need to make sure the antialiased framebuffer also has the updated
1195+
// pixels so that if more is drawn to it, it goes on top of the updated
1196+
// pixels instead of replacing them.
1197+
// We can't blit the framebuffer to the multisampled antialias
1198+
// framebuffer to leave both in the same state, so instead we have
1199+
// to use image() to put the framebuffer texture onto the antialiased
1200+
// framebuffer.
1201+
this.begin();
1202+
this.target.push();
1203+
this.target.imageMode(this.target.CENTER);
1204+
this.target.resetMatrix();
1205+
this.target.noStroke();
1206+
this.target.clear();
1207+
this.target.image(this, 0, 0);
1208+
this.target.pop();
1209+
if (this.useDepth) {
1210+
gl.clearDepth(1);
1211+
gl.clear(gl.DEPTH_BUFFER_BIT);
1212+
}
1213+
this.end();
1214+
} else {
1215+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
1216+
if (this.useDepth) {
1217+
gl.clearDepth(1);
1218+
gl.clear(gl.DEPTH_BUFFER_BIT);
1219+
}
1220+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.prevFramebuffer);
1221+
}
1222+
}
9341223
}
9351224

9361225
/**

src/webgl/p5.RendererGL.Retained.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,22 @@ p5.RendererGL.prototype.drawBuffers = function(gId) {
118118
const gl = this.GL;
119119
const geometry = this.retainedMode.geometry[gId];
120120

121+
if (this._doFill) {
122+
this._useVertexColor = (geometry.model.vertexColors.length > 0);
123+
const fillShader = this._getRetainedFillShader();
124+
this._setFillUniforms(fillShader);
125+
for (const buff of this.retainedMode.buffers.fill) {
126+
buff._prepareBuffer(geometry, fillShader);
127+
}
128+
if (geometry.indexBuffer) {
129+
//vertex index buffer
130+
this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER);
131+
}
132+
this._applyColorBlend(this.curFillColor);
133+
this._drawElements(gl.TRIANGLES, gId);
134+
fillShader.unbindShader();
135+
}
136+
121137
if (this._doStroke && geometry.lineVertexCount > 0) {
122138
const faceCullingEnabled = gl.isEnabled(gl.CULL_FACE);
123139
// Prevent strokes from getting removed by culling
@@ -136,21 +152,6 @@ p5.RendererGL.prototype.drawBuffers = function(gId) {
136152
strokeShader.unbindShader();
137153
}
138154

139-
if (this._doFill) {
140-
this._useVertexColor = (geometry.model.vertexColors.length > 0);
141-
const fillShader = this._getRetainedFillShader();
142-
this._setFillUniforms(fillShader);
143-
for (const buff of this.retainedMode.buffers.fill) {
144-
buff._prepareBuffer(geometry, fillShader);
145-
}
146-
if (geometry.indexBuffer) {
147-
//vertex index buffer
148-
this._bindBuffer(geometry.indexBuffer, gl.ELEMENT_ARRAY_BUFFER);
149-
}
150-
this._applyColorBlend(this.curFillColor);
151-
this._drawElements(gl.TRIANGLES, gId);
152-
fillShader.unbindShader();
153-
}
154155
return this;
155156
};
156157

0 commit comments

Comments
 (0)