|
6 | 6 | import p5 from '../core/main';
|
7 | 7 | import * as constants from '../core/constants';
|
8 | 8 | import { checkWebGLCapabilities } from './p5.Texture';
|
| 9 | +import { readPixelsWebGL, readPixelWebGL } from './p5.RendererGL'; |
9 | 10 |
|
10 | 11 | class FramebufferCamera extends p5.Camera {
|
11 | 12 | /**
|
@@ -109,6 +110,24 @@ class Framebuffer {
|
109 | 110 | this.target = target;
|
110 | 111 | this.target._renderer.framebuffers.add(this);
|
111 | 112 |
|
| 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 | + |
112 | 131 | this.format = settings.format || constants.UNSIGNED_BYTE;
|
113 | 132 | this.channels = settings.channels || (
|
114 | 133 | target._renderer._pInst._glAttributes.alpha
|
@@ -931,6 +950,276 @@ class Framebuffer {
|
931 | 950 | callback();
|
932 | 951 | this.end();
|
933 | 952 | }
|
| 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 | + } |
934 | 1223 | }
|
935 | 1224 |
|
936 | 1225 | /**
|
|
0 commit comments