├── p5Fbo-demo.gif
├── example
├── index.html
└── sketch.js
├── README.md
└── p5Fbo.js
/p5Fbo-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aferriss/p5Fbo/HEAD/p5Fbo-demo.gif
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | p5Fbo example
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/example/sketch.js:
--------------------------------------------------------------------------------
1 | let fbo;
2 | let sh;
3 | let randSh;
4 |
5 | const vertexShader = `
6 | attribute vec3 aPosition;
7 | attribute vec2 aTexCoord;
8 |
9 | uniform mat4 uModelViewProjectionMatrix;
10 |
11 | varying vec2 vTexCoord;
12 |
13 | void main() {
14 | vTexCoord = aTexCoord;
15 |
16 | vec4 positionVec4 = vec4(aPosition, 1.0);
17 |
18 | gl_Position = uModelViewProjectionMatrix * positionVec4;
19 | }`
20 | ;
21 |
22 | const fragmentShader = `
23 | precision mediump float;
24 |
25 | varying vec2 vTexCoord;
26 |
27 | uniform sampler2D uTex0;
28 |
29 | void main() {
30 | vec2 uv = vTexCoord;
31 | uv.y = 1.0 - uv.y;
32 |
33 | vec4 tex = texture2D(uTex0, uv);
34 |
35 | gl_FragColor = tex;//vec4(uv, 0.0, 1.0);
36 | // gl_FragColor.rgb = 1.0 - gl_FragColor.rgb;
37 | }
38 | `;
39 |
40 | const randFrag = `
41 | precision highp float;
42 | varying vec2 vTexCoord;
43 |
44 | float rand(vec2 co){
45 | return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
46 | }
47 |
48 | void main(){
49 | float r = rand(floor(vTexCoord * 5.0) / 5.0);
50 | gl_FragColor = vec4(r);
51 | gl_FragColor.a = 1.0;
52 | }
53 | `;
54 |
55 |
56 | function setup() {
57 |
58 | // Framebuffers will only work in webGL mode.
59 | const canvas = createCanvas(512, 512, WEBGL);
60 | pixelDensity(1);
61 |
62 | sh = createShader(vertexShader, fragmentShader);
63 | randSh = createShader(vertexShader, randFrag);
64 |
65 | // Create our fbo
66 | // It's required to pass in the current renderer, and width and height.
67 | fbo = new p5Fbo({
68 | renderer: canvas,
69 | width: 256,
70 | height: 256
71 | });
72 | }
73 |
74 | function draw() {
75 |
76 | // Here's out fbo drawing code, separated in its own block for readability
77 | {
78 | // Start Drawing into our fbo
79 | fbo.begin();
80 |
81 | // Clear it out before we draw anything
82 | clear(0, 0, 0, 1);
83 |
84 | // Render a rotating box with random b&w colors on it
85 | noStroke();
86 | background(0, 255, 255);
87 | shader(randSh);
88 | push();
89 | rotateX(frameCount * 0.01);
90 | rotateY(frameCount * 0.02);
91 | box(150);
92 | pop();
93 |
94 | // Finished working in the fbo!
95 | fbo.end();
96 | }
97 |
98 |
99 | // Start rendering normally in p5
100 | // We will draw the fbo as a texture onto another box
101 | {
102 | background(255, 0, 255);
103 |
104 | // p5Fbo textures can be used with custom shaders
105 | // shader(sh);
106 | // sh.setUniform('uTex0', fbo.getTexture());
107 |
108 | texture(fbo.getTexture());
109 | push();
110 | translate(-100, -100);
111 | rotateX(frameCount * -0.003);
112 | rotateY(frameCount * -0.002);
113 | box(150);
114 | pop();
115 |
116 | // Or they can be drawn as images
117 | image(fbo.getTexture(), 0, 0);
118 | }
119 |
120 | }
121 |
122 | function keyPressed() { }
123 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # p5Fbo
2 |
3 | 
4 |
5 | p5Fbo is an extension to p5 that allows you to use framebuffer objects. Framebuffers allow us to do our rendering into a webGL texture instead of directly to the screen in a canvas.
6 |
7 | This project is very much a work in progress.
8 |
9 | ## Why not just use createGraphics()
10 |
11 | The `createGraphics()` function is great, but it has some limitations. Every time you call createGraphics, p5 creates an entirely new canvas object, containing a new webGL context. Browsers limit the number of webGL contexts that you can have on a single page. If you try and create too many, the browser will remove the oldest ones. It likely varies by browser, but in my recent testing I hit the limit around 14.
12 |
13 | Framebuffers allow us to get around this limitation. Now we can have as many offscreen buffers as we like, without the overhead of creating a new webGL context for each one.
14 |
15 | Framebuffers are potentially faster as well. I haven't done any bench marking yet, but I have a hunch that using frame buffers will be much more performant than using `createGraphics();`.
16 |
17 | Syntactically cleaner (imo). It's nice to not have to prefix all of your graphics calls with `graphics.`.
18 |
19 | ## Installation
20 |
21 | Just add the p5.js library, as well as the p5Fbo.js library somewher in your html file.
22 |
23 | ```html
24 |
25 | p5Fbo example
26 |
27 |
28 |
29 |
30 | ```
31 |
32 | ## Usage
33 |
34 | The best way to get started is probably to duplicate the example in this repo.
35 |
36 | To use p5Fbo, just instantiate a new p5Fbo object in your setup function.
37 |
38 | ```javascript
39 | let fbo;
40 | let shader;
41 | void setup(){
42 | const canvas = createCanvas(500, 500, WEBGL);
43 | fbo = new p5Fbo({renderer: canvas, width: 500, height: 500});
44 |
45 | // Lets load a shader as well.
46 | shader = createShader(vertSrc, fragSrc)
47 | }
48 | ```
49 |
50 | To do some drawing into the fbo you first need to call fbo.begin(). When you're done drawing, just call fbo.end(). If you've ever used fbo's in openFrameworks, this will be very familiar.
51 |
52 | ```javascript
53 | void draw(){
54 | // activate our fbo
55 | fbo.begin();
56 |
57 | // Call clear at the beginning of each frame
58 | clear();
59 |
60 | // Do our drawing
61 | background(0, 255, 0);
62 | push();
63 | rotateX(frameCount * 0.01);
64 | fill(255, 0, 0);
65 | box(100);
66 | pop();
67 |
68 | // We're done drawing into the fbo so call .end()
69 | fbo.end();
70 |
71 | // Now we need to draw the fbo to the screen.
72 | fbo.draw();
73 | }
74 | ```
75 |
76 | ## Api
77 |
78 | ### Constructor
79 |
80 | ``` javascript
81 | const fbo = new p5Fbo({renderer, width, height, interpolationMode, wrapMode});
82 | ```
83 |
84 | ### Constructor settings
85 |
86 | - renderer: The p5 renderer.
87 | - For the base canvas, this will be what is returned from createCanvas. `const canvas = createGraphics(100, 100, WEBGL)`
88 | - For p5.Graphics objects, this will be the `._renderer` property returned from createGraphics.
89 | - `const graphics = createGraphics(100, 100, WEBGL);`
90 | - `const renderer = graphics._renderer;`
91 | - width: The width of the fbo render texture
92 | - height: the height of the fbo render texture
93 | - interpolationMode: (optional) either LINEAR or NEAREST. defaults to LINEAR
94 | - wrapMode: (optional) either CLAMP, REPEAT, or MIRROR. defaults to CLAMP
95 | - floatTexture: (optional) either true or false. defaults to false
96 |
97 | ### `p5Fbo.begin()`
98 |
99 | Tells p5 to start rendering into the framebuffer. Call this before you before you want to use the fbo.
100 |
101 | ### `p5Fbo.end()`
102 |
103 | Tells p5 to stop rendering into the framebuffer. It's very important to remember to call this function, otherwise p5 will keep rendering into the framebuffer, and you'll get weird results on screen.
104 |
105 | ### `p5fbo.getTexture()`
106 |
107 | returns the p5.Texture that the framebuffer is rendering into.
108 |
109 | ### `p5Fbo.copyTo(dstFbo)`
110 |
111 | Copies contents of one fbo to another.
112 | Example: `fboA.copyTo(fboB);`.
113 |
114 | ### `p5Fbo.draw(x, y, w, h)`
115 |
116 | A convenience method to draw the FBO to the screen at a given x, y, width, and height. This also flips the FBO vertically on the Y axis, to correct the inversion that FBO's do by default.
117 |
118 | ### `p5Fbo.draw()`
119 |
120 | Draws the fbo to the screen at the same size as your canvas.
121 |
122 | ## Limitations
123 |
124 | 1. I haven't tested if this works with all of the custom camera functions. I think if you put the camera function calls **after** `fbo.begin()` they may work.
125 |
126 | 2. WebGL1 (which p5 uses) does not support multisampling, so your graphics may be a little more jagged than with default rendering. One solution is to render at 2x resolution, and then scale down when you draw. Or you can try to implement a post processing anti-aliasign shader like FXAA.
127 |
128 | 3. Fbo's render with the y axis flipped by default. When you go to draw the texture, you'll need to unflip it `scale(1, -1)`, unless you use the `.draw()` method in p5Fbo.
129 |
130 | ## Todo list
131 |
132 | - [ ] Test all perspective and ortho camera functions
133 | - [ ] Add a read to pixels function
134 |
135 | ## Credits
136 |
137 | I learned a lot about framebuffers from [Gregg Tavares WebGL fundamentals series](https://webglfundamentals.org/webgl/lessons/webgl-render-to-texture.html). It's worth checking out if you'd like to learn more.
138 |
--------------------------------------------------------------------------------
/p5Fbo.js:
--------------------------------------------------------------------------------
1 |
2 | class p5Fbo {
3 | constructor({
4 | renderer,
5 | width,
6 | height,
7 | interpolationMode = LINEAR,
8 | wrapMode = CLAMP,
9 | floatTexture = false
10 | } = {}) {
11 | this.width = width;
12 | this.height = height;
13 | this.gl = renderer.GL;
14 | const gl = this.gl;
15 | this.renderer = renderer;
16 |
17 | // Create and bind texture
18 | let im = new p5.Image(this.width, this.height);
19 | this.texture = new p5.Texture(this.renderer, im, { dataType: floatTexture ? gl.FLOAT : gl.UNSIGNED_BYTE });
20 |
21 | this.texture.setInterpolation(interpolationMode, interpolationMode);
22 | this.texture.setWrapMode(wrapMode);
23 |
24 | this.originalProjectionMatrix = this.renderer.uPMatrix.copy();
25 | this.originalModelViewMatrix = this.renderer.uMVMatrix.copy();
26 |
27 | // define size and format of level 0
28 | const level = 0;
29 |
30 | // Create and bind the framebuffer
31 | this.frameBuffer = gl.createFramebuffer();
32 | gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);
33 |
34 | // attach the texture as the first color attachment
35 | const attachmentPoint = gl.COLOR_ATTACHMENT0;
36 | gl.framebufferTexture2D(
37 | gl.FRAMEBUFFER,
38 | attachmentPoint,
39 | gl.TEXTURE_2D,
40 | this.texture.glTex,
41 | level
42 | );
43 |
44 | // create a depth renderbuffer
45 | const depthBuffer = gl.createRenderbuffer();
46 | gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
47 |
48 | // make a depth buffer and the same size as the targetTexture
49 | gl.renderbufferStorage(
50 | gl.RENDERBUFFER,
51 | gl.DEPTH_COMPONENT16,
52 | this.width,
53 | this.height
54 | );
55 |
56 | gl.framebufferRenderbuffer(
57 | gl.FRAMEBUFFER,
58 | gl.DEPTH_ATTACHMENT,
59 | gl.RENDERBUFFER,
60 | depthBuffer
61 | );
62 |
63 | // Bind back to null
64 | gl.bindFramebuffer(gl.FRAMEBUFFER, null);
65 |
66 |
67 | this.defaultCamera = this.renderer._curCamera;
68 |
69 | // console.log(this.renderer.pop);
70 | // this.fboCamera = createCamera()
71 | // this.fboCamera.perspective(this.defaultCamera.defaultCameraFOV, this.width / this.height, 0.1, 500);
72 | // console.log(this.defaultCamera);
73 | // setCamera(this.defaultCamera);
74 | }
75 |
76 | // Call this function whenever you want to start rendering into your fbo
77 | begin() {
78 | const gl = this.gl;
79 | // This is necessary to prevent p5 from using the wrong shader
80 | this.renderer._tex = null;
81 |
82 | // render to our targetTexture by binding the framebuffer
83 | gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);
84 |
85 | // render cube with our 3x2 texture
86 | gl.bindTexture(gl.TEXTURE_2D, this.texture.glTex);
87 |
88 | // Tell WebGL how to convert from clip space to pixels
89 | gl.viewport(0, 0, this.width, this.height);
90 |
91 | this.renderer._pInst.push();
92 |
93 | // set projection matrix to size of fbo texture
94 | this.computeCameraSettings();
95 | }
96 |
97 | // Updates camera to the correct aspect and size
98 | computeCameraSettings() {
99 | this.renderer._curCamera.defaultCameraFOV = 60 / 180 * Math.PI;
100 | this.renderer._curCamera.defaultAspectRatio = this.width / this.height;
101 | this.renderer._curCamera.defaultEyeX = 0;
102 | this.renderer._curCamera.defaultEyeY = 0;
103 | this.renderer._curCamera.defaultEyeZ = this.height / 2.0 / Math.tan(this.renderer._curCamera.defaultCameraFOV / 2.0);
104 | this.renderer._curCamera.defaultCenterX = 0;
105 | this.renderer._curCamera.defaultCenterY = 0;
106 | this.renderer._curCamera.defaultCenterZ = 0;
107 | this.renderer._curCamera.defaultCameraNear = this.renderer._curCamera.defaultEyeZ * 0.1;
108 | this.renderer._curCamera.defaultCameraFar = this.renderer._curCamera.defaultEyeZ * 10;
109 |
110 | this.cameraFOV = this.renderer._curCamera.defaultCameraFOV;
111 | this.aspectRatio = this.renderer._curCamera.defaultAspectRatio;
112 | this.eyeX = this.renderer._curCamera.defaultEyeX;
113 | this.eyeY = this.renderer._curCamera.defaultEyeY;
114 | this.eyeZ = this.renderer._curCamera.defaultEyeZ;
115 | this.centerX = this.renderer._curCamera.defaultCenterX;
116 | this.centerY = this.renderer._curCamera.defaultCenterY;
117 | this.centerZ = this.renderer._curCamera.defaultCenterZ;
118 | this.upX = 0;
119 | this.upY = 1;
120 | this.upZ = 0;
121 | this.cameraNear = this.renderer._curCamera.defaultCameraNear;
122 | this.cameraFar = this.renderer._curCamera.defaultCameraFar;
123 |
124 | this.renderer._curCamera.perspective();
125 | this.renderer._curCamera.camera();
126 |
127 | this.cameraType = 'default';
128 |
129 | }
130 |
131 | // Call end once you've done all your render. Super important to do this!
132 | end() {
133 | const gl = this.gl;
134 | gl.bindFramebuffer(gl.FRAMEBUFFER, null);
135 | gl.bindTexture(gl.TEXTURE_2D, null);
136 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
137 | resetShader();
138 |
139 | // Restore original projection matrix
140 | this.renderer._curCamera._computeCameraDefaultSettings();
141 | this.renderer._curCamera._setDefaultCamera();
142 |
143 | this.renderer._pInst.pop();
144 |
145 | }
146 |
147 | getTexture() {
148 | return this.texture;
149 | }
150 |
151 | // Copies this framebuffer to another
152 | copyTo(dst) {
153 | const gl = this.gl;
154 | gl.bindFramebuffer(gl.FRAMEBUFFER, this.frameBuffer);
155 | gl.bindTexture(gl.TEXTURE_2D, dst.texture.glTex);
156 | gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, dst.width, dst.height, 0);
157 | gl.bindFramebuffer(gl.FRAMEBUFFER, null);
158 | }
159 |
160 | // Draw at a given x, y, width, and height.
161 | // You can call without any parameters to draw the fbo at full screen size
162 | draw(x, y, w, h) {
163 | x = x || 0;
164 | y = y || 0;
165 | w = w || width;
166 | h = h || height;
167 |
168 | push();
169 | translate(x, y);
170 | scale(1, -1);
171 | texture(this.texture);
172 | plane(w, h);
173 | pop();
174 | }
175 |
176 |
177 | }
--------------------------------------------------------------------------------