Skip to content

Instantly share code, notes, and snippets.

@jialiang
Last active March 18, 2021 13:56
Show Gist options
  • Save jialiang/0c254ee25f903673f136b6a1a58d3cd3 to your computer and use it in GitHub Desktop.
Save jialiang/0c254ee25f903673f136b6a1a58d3cd3 to your computer and use it in GitHub Desktop.
<!--
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