├── 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 | ![p5Fbo demo](p5Fbo-demo.gif) 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 | } --------------------------------------------------------------------------------