Last active
March 18, 2021 13:56
-
-
Save jialiang/0c254ee25f903673f136b6a1a58d3cd3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
Pre-requisite: | |
At least understand the concepts involved in creating a square in WebGL | |
--> | |
<!-- | |
Multisampled Frame Buffer Object with Depth Testing and Multi Render Target | |
GLOSSARY: | |
Multi Render Target (MRT) | |
- Feature allowing multiple output from a single fragment shader pass. | |
Frame Buffers | |
- Framebuffers store the output of GPUs. GPUs draw to framebuffers and not directly to the canvas. | |
- There are 2 types of framebuffer: the Default Framebuffer and Frame Buffer Objects (user-defined framebuffers). | |
- GPU draws to the Default Framebuffer if no FBO is bound. | |
- Canvas shows the contents of the Default Framebuffer. | |
- FBO has multiple slots for attachments: | |
- Up to 8 or more color attachment slots (stores color output from the fragment shader) | |
- depth attachment slot (stores depth information for depth testing) | |
- stencil attachment slot (mask to hide portions of an image) | |
- Note that if your FBO has no color attachment, it can't store any color output from your fragment shader. | |
- The type of attachments FBOs accept are Renderbuffer and Texture. | |
- Renderbuffer can be used as depth, stencil or color attachment, texture can be used for color attachment only. | |
- Renderbuffer can be multisampled, textures cannot. | |
- You can use a combination of texture and renderbuffer for the color attachment slots. | |
- All attachments (color, depth, stencil) must be either multisampled or not multisampled. | |
They must also have the same amount of samples (2x, 4x, 8x, 16x (max)). | |
The number of samples need not be power of 2. e.g. 5, 6, 7 are valid values. | |
- Renderbuffer cannot be used as textures. | |
Multisample Antialiasing (MSAA) | |
- Disabled by default on FBOs, mostly enabled by default on the Default Framebuffer. | |
- Only anti-aliases polygon edges, does not anti-aliases textures! | |
INTRO: | |
In this example: | |
1. Our fragment shader will use MRT to output a red triangle and a green triangle onto the 1st color attachment of our MSAA-enabled FBO, | |
and a yellow triangle and a white triangle onto its 2nd color attachment. | |
2. We will copy the left half of our the 1st color attachment of our MSAA-enabled FBO to our Post-processing FBO | |
and the right half of the 2nd color attachment. | |
3. We will use our Post-processing FBO as a texture for a square that covers the entire canvas. | |
4. We will render that square through our post-processing fragment shader into our Default Framebuffer. | |
This post-processing fragment shader will invert the colors of our square. | |
When inverted: | |
red -> cyan | |
green -> pink | |
yellow -> blue | |
white -> black | |
--> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<title>UBO Example</title> | |
<style> | |
body { | |
margin: 0; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas></canvas> | |
<script> | |
// Initialize our canvas | |
const canvas = document.querySelector("canvas"); | |
const gl = canvas.getContext("webgl2"); | |
// We need to enable depth test so that closer objects cover up further objects | |
gl.enable(gl.DEPTH_TEST); | |
canvas.style.width = "100%"; | |
canvas.style.height = "100%"; | |
canvas.width = innerWidth * devicePixelRatio; | |
canvas.height = innerHeight * devicePixelRatio; | |
gl.viewport( | |
0, | |
0, | |
innerWidth * devicePixelRatio, | |
innerHeight * devicePixelRatio | |
); | |
// Prepare our shader programs | |
const vertexShaderSource = `#version 300 es | |
layout(location = 0) in vec3 a_Position; | |
layout(location = 1) in vec3 a_Color; | |
out vec2 uv; | |
out vec3 color; | |
void main(void) { | |
// uv is used by our square only, I'm too lazy to pass in another attribute | |
uv = vec2((a_Position.x + 1.0) / 2.0, (a_Position.y + 1.0) / 2.0); | |
color = vec3(a_Color); | |
gl_Position = vec4(a_Position, 1.0); | |
} | |
`; | |
/** | |
* To enable MRT on your fragment shader, | |
* declare and assign multiple outs and assign them locations | |
**/ | |
const fragmentShaderSource = `#version 300 es | |
precision mediump float; | |
in vec3 color; | |
layout(location = 0) out vec4 finalColor_0; | |
layout(location = 1) out vec4 finalColor_1; | |
void main(void) { | |
finalColor_0 = vec4(color, 1.0); | |
finalColor_1 = vec4(1.0, 1.0, color.y, 1.0); | |
} | |
`; | |
// Fragment shader for performing post-processing (inverts colors) | |
const fragmentShaderSource_post = `#version 300 es | |
precision mediump float; | |
uniform sampler2D tex; | |
in vec2 uv; | |
out vec4 finalColor; | |
void main(void) { | |
vec4 originalColor = texture(tex, uv); | |
finalColor = vec4( | |
1.0 - originalColor.r, | |
1.0 - originalColor.g, | |
1.0 - originalColor.b, | |
1.0 | |
); | |
} | |
`; | |
const vertexShader = gl.createShader(gl.VERTEX_SHADER); | |
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); | |
const fragmentShader_post = gl.createShader(gl.FRAGMENT_SHADER); | |
gl.shaderSource(vertexShader, vertexShaderSource); | |
gl.shaderSource(fragmentShader, fragmentShaderSource); | |
gl.shaderSource(fragmentShader_post, fragmentShaderSource_post); | |
gl.compileShader(vertexShader); | |
gl.compileShader(fragmentShader); | |
gl.compileShader(fragmentShader_post); | |
const program = gl.createProgram(); | |
const program_post = gl.createProgram(); | |
gl.attachShader(program, vertexShader); | |
gl.attachShader(program, fragmentShader); | |
gl.attachShader(program_post, vertexShader); | |
gl.attachShader(program_post, fragmentShader_post); | |
gl.linkProgram(program); | |
gl.linkProgram(program_post); | |
// Create vao for our red (longish) triangle | |
const redTriangleVao = gl.createVertexArray(); | |
{ | |
gl.bindVertexArray(redTriangleVao); | |
// prettier-ignore | |
const positionArray = new Float32Array([ | |
1, 0.1, 0, | |
1, -0.1, 0, | |
-1, -0.1, 0, | |
]); | |
// prettier-ignore | |
const colorArray = new Float32Array([ | |
1, 0, 0, | |
1, 0, 0, | |
1, 0, 0 | |
]); | |
const positionBuffer = gl.createBuffer(); | |
const colorBuffer = gl.createBuffer(); | |
{ | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, positionArray, gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(0); | |
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); | |
gl.bindBuffer(gl.ARRAY_BUFFER, null); | |
} | |
{ | |
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(1); | |
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0); | |
gl.bindBuffer(gl.ARRAY_BUFFER, null); | |
} | |
gl.bindVertexArray(null); | |
} | |
// Create vao for our green (squarish) triangle | |
const greenTriangleVao = gl.createVertexArray(); | |
{ | |
gl.bindVertexArray(greenTriangleVao); | |
// prettier-ignore | |
const positionArray = new Float32Array([ | |
0.5, 0.5, 0.5, | |
0.5, -0.5, 0.5, | |
-0.5, -0.5, 0.5, | |
]); | |
// prettier-ignore | |
const colorArray = new Float32Array([ | |
0, 1, 0, | |
0, 1, 0, | |
0, 1, 0 | |
]); | |
const positionBuffer = gl.createBuffer(); | |
const colorBuffer = gl.createBuffer(); | |
{ | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, positionArray, gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(0); | |
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); | |
gl.bindBuffer(gl.ARRAY_BUFFER, null); | |
} | |
{ | |
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, colorArray, gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(1); | |
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0); | |
gl.bindBuffer(gl.ARRAY_BUFFER, null); | |
} | |
gl.bindVertexArray(null); | |
} | |
// Create vao for square | |
const squareVao = gl.createVertexArray(); | |
{ | |
gl.bindVertexArray(squareVao); | |
// prettier-ignore | |
const positionArray = new Float32Array([ | |
-1, 1, 0, | |
1, 1, 0, | |
-1, -1, 0, | |
1, -1, 0 | |
]); | |
const positionBuffer = gl.createBuffer(); | |
{ | |
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); | |
gl.bufferData(gl.ARRAY_BUFFER, positionArray, gl.STATIC_DRAW); | |
gl.enableVertexAttribArray(0); | |
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); | |
gl.bindBuffer(gl.ARRAY_BUFFER, null); | |
} | |
gl.bindVertexArray(null); | |
} | |
// | |
// | |
// | |
// | |
// | |
// Create MSAA FBO | |
const frameBuffer_msaa = gl.createFramebuffer(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer_msaa); | |
// Create renderbuffer and attach as first color attachment | |
{ | |
// create our renderbuffer | |
const renderbuffer = gl.createRenderbuffer(); | |
// tell WebGL we now want to work on this renderbuffer | |
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); | |
// initialize our renderbuffer's data store | |
gl.renderbufferStorageMultisample( | |
gl.RENDERBUFFER, | |
gl.getParameter(gl.MAX_SAMPLES), | |
gl.RGBA8, | |
canvas.width, | |
canvas.height | |
); | |
// attach our render buffer to our framebuffer | |
gl.framebufferRenderbuffer( | |
gl.FRAMEBUFFER, | |
gl.COLOR_ATTACHMENT0, | |
gl.RENDERBUFFER, | |
renderbuffer | |
); | |
// tell webgl we are done working on our renderbuffer | |
gl.bindRenderbuffer(gl.RENDERBUFFER, null); | |
} | |
// Repeat for second color attachment | |
{ | |
const renderbuffer = gl.createRenderbuffer(); | |
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); | |
gl.renderbufferStorageMultisample( | |
gl.RENDERBUFFER, | |
gl.getParameter(gl.MAX_SAMPLES), | |
gl.RGBA8, | |
canvas.width, | |
canvas.height | |
); | |
gl.framebufferRenderbuffer( | |
gl.FRAMEBUFFER, | |
gl.COLOR_ATTACHMENT1, | |
gl.RENDERBUFFER, | |
renderbuffer | |
); | |
gl.bindRenderbuffer(gl.RENDERBUFFER, null); | |
} | |
// If we have multiple color attachments on our framebuffer, | |
// we need to tell webgl all the color attachments we want it to draw to | |
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1]); | |
// Setup our depth attachment for our msaa fbo | |
// otherwise our fbo won't do any depth testing | |
// must also be multisampled since our color attachments are multisampled | |
// take note of the 3rd argument (internal format), always use an appropriate one. | |
{ | |
const renderbuffer = gl.createRenderbuffer(); | |
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); | |
gl.renderbufferStorageMultisample( | |
gl.RENDERBUFFER, | |
gl.getParameter(gl.MAX_SAMPLES), | |
gl.DEPTH_COMPONENT16, | |
canvas.width, | |
canvas.height | |
); | |
gl.framebufferRenderbuffer( | |
gl.FRAMEBUFFER, | |
gl.DEPTH_ATTACHMENT, | |
gl.RENDERBUFFER, | |
renderbuffer | |
); | |
gl.bindRenderbuffer(gl.RENDERBUFFER, null); | |
} | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
// Create our post-processing FBO | |
// Its the same as the above but we are using texture instead of renderbuffer | |
const frameBuffer_post = gl.createFramebuffer(); | |
const texture_post = gl.createTexture(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer_post); | |
{ | |
gl.bindTexture(gl.TEXTURE_2D, texture_post); | |
gl.texImage2D( | |
gl.TEXTURE_2D, | |
0, | |
gl.RGBA, | |
canvas.width, | |
canvas.height, | |
0, | |
gl.RGBA, | |
gl.UNSIGNED_BYTE, | |
null | |
); | |
// Setup what method to downscale and upscale our texture | |
// Yes, this is essential, otherwise you'll get a texture incomplete error | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
// we use framebufferTexture2D here and not framebufferRenderbuffer! | |
gl.framebufferTexture2D( | |
gl.FRAMEBUFFER, | |
gl.COLOR_ATTACHMENT0, | |
gl.TEXTURE_2D, | |
texture_post, | |
0 | |
); | |
gl.bindTexture(gl.TEXTURE_2D, null); | |
} | |
// we don't need a depth buffer for our post-processing fbo because | |
// we already have a depth buffer on our msaa fbo which already position things nicely | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
// Render | |
const onRender = () => { | |
// Tell webgl that our msaa is our target for drawing | |
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, frameBuffer_msaa); | |
// draw our triangle | |
gl.useProgram(program); | |
// even through we render our green (squarish) triangle second | |
// it will still appear behind our red (longish) triagle | |
// because our green triangle's z-position is further away and we have depth testing | |
gl.bindVertexArray(redTriangleVao); | |
gl.drawArrays(gl.TRIANGLES, 0, 3); | |
gl.bindVertexArray(null); | |
gl.bindVertexArray(greenTriangleVao); | |
gl.drawArrays(gl.TRIANGLES, 0, 3); | |
gl.bindVertexArray(null); | |
// now tell it we want to read from our msaa fbo | |
// and the post-processing fbo is our target for drawing | |
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, frameBuffer_msaa); | |
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, frameBuffer_post); | |
// Read from the 1st color attachment | |
gl.readBuffer(gl.COLOR_ATTACHMENT0); | |
// blit means copy. So here we copy pixels from 1 framebuffer to another | |
// you can use blit on framebuffer with both texture and renderbuffer attachments | |
// however, you cannot blit to a multisampled target! | |
// e.g. you cannot blit to the Default Framebuffer, unless anti-alising is turned off on it | |
// e.g. using canvas.getContext("webgl2", { antialias: false }) | |
// also, if you blit from a multisampled source, | |
// the source & destination must be the same size and have the same offset. | |
// they must have the same internal format (line 297) e.g. RGBA8, RGBA4 etc | |
gl.blitFramebuffer( | |
0, | |
0, | |
canvas.width / 2, | |
canvas.height, | |
0, | |
0, | |
canvas.width / 2, | |
canvas.height, | |
gl.COLOR_BUFFER_BIT, // we want to copy the color. | |
gl.LINEAR | |
); | |
// repeat for the 2nd color attachment | |
gl.readBuffer(gl.COLOR_ATTACHMENT1); | |
gl.blitFramebuffer( | |
canvas.width / 2, | |
0, | |
canvas.width, | |
canvas.height, | |
canvas.width / 2, | |
0, | |
canvas.width, | |
canvas.height, | |
gl.COLOR_BUFFER_BIT, | |
gl.LINEAR | |
); | |
// now tell it we want to read from our post-processing fbo | |
// and the target for drawing is our Default Framebuffer | |
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, frameBuffer_post); | |
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null); | |
// switch over to the post-processing program | |
gl.useProgram(program_post); | |
// remember how we used a texture as the color attachment for our post-processin fbo? | |
// now set that texture as the texture for our square | |
gl.activeTexture(gl.TEXTURE0); | |
gl.bindTexture(gl.TEXTURE_2D, texture_post); | |
// draw our square | |
// if you don't know what a triangle strip is | |
// it mean we can draw a Z and webgl will understand its suppose to be a square | |
gl.bindVertexArray(squareVao); | |
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); | |
gl.bindVertexArray(null); | |
// clean up stuff | |
gl.bindTexture(gl.TEXTURE_2D, null); | |
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null); | |
setTimeout(() => { | |
requestAnimationFrame(onRender); | |
}, 200); | |
}; | |
requestAnimationFrame(onRender); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment