惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
S
Schneier on Security
T
The Exploit Database - CXSecurity.com
Webroot Blog
Webroot Blog
AI
AI
P
Palo Alto Networks Blog
Attack and Defense Labs
Attack and Defense Labs
WordPress大学
WordPress大学
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
Spread Privacy
Spread Privacy
T
Tor Project blog
罗磊的独立博客
小众软件
小众软件
S
Security Affairs
酷 壳 – CoolShell
酷 壳 – CoolShell
量子位
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
NISL@THU
NISL@THU
博客园_首页
PCI Perspectives
PCI Perspectives
大猫的无限游戏
大猫的无限游戏
IT之家
IT之家
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
博客园 - 叶小钗
D
Darknet – Hacking Tools, Hacker News & Cyber Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
L
LINUX DO - 热门话题
T
Threat Research - Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
腾讯CDC
Security Latest
Security Latest
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Cloudflare Blog
A
About on SuperTechFans
爱范儿
爱范儿
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
TaoSecurity Blog
TaoSecurity Blog
宝玉的分享
宝玉的分享
G
GRAHAM CLULEY
雷峰网
雷峰网
F
Full Disclosure
I
Intezer
Cloudbric
Cloudbric
博客园 - 三生石上(FineUI控件)
U
Unit 42

WebGL Fundamentals

WebGL Using 2 or More Textures WebGL Implementing DrawImage WebGL 2D Matrices WebGL Implementing A Matrix Stack WebGL 2D Rotation WebGL 2D Scale WebGL 2D Translation WebGL - Rasterization vs 3D libraries WebGL 3D - Cameras WebGL 3D Geometry - Lathe WebGL 3D - Directional Lighting WebGL 3D - Point Lighting WebGL 3D - Normal Mapping WebGL 3D - Spot Lighting WebGL - Orthographic 3D WebGL 3D Perspective Correct Texture Mapping WebGL 3D Perspective WebGL Textures WebGL and Alpha WebGL - Animation WebGL Anti-Patterns WebGL Attributes WebGL Boilerplate WebGL - Cross Origin Images WebGL Cross Platform Issues WebGL Cubemaps WebGL 3D - Data Textures WebGL - Drawing Multiple Things WebGL Drawing Without Data WebGL Environment Maps (reflections) WebGL Fog WebGL Framebuffers WebGL Fundamentals WebGL GPGPU WebGL How It Works WebGL Image Processing Continued WebGL Image Processing WebGL Indexed Vertices WebGL Optimization - Instanced Drawing WebGL - Less Code, More Fun WebGL Load Obj with Mtl WebGL Load Obj WebGL Matrices vs Math Matrices WebGL Picking WebGL Planar and Perspective Projection Mapping WebGL Points, Lines, and Triangles WebGL Post Processing WebGL Precision Issues WebGL Pulling Vertices Accessing textures by pixel coordinate in WebGL2 A simple way to show the load on the GPU's vertex and fragment processing? Apply a displacement map and specular map Can anyone explain what this GLSL fragment shader is doing? Can I mute the warning about vertex attrib 0 being disabled? Create image warping effect in WebGL Creating a smudge/liquify effect How to draw Depth Sprites Determine min/max values for the entire image Don't blend a polygon that crosses itself Drawing 2D image with depth map to achieve pseudo-3D effect Drawing a heightmap Drawing layers with different points Drawing Many different models in a single draw call Drawing textured sprites with instanced drawing Efficient particle system in javascript? (WebGL) Emulating palette based graphics in WebGL FPS-like camera movement with basic matrix transformations Get the size of a point for collision checking GLSL shader to support coloring and texturing How can I compute for 500 points which of 1000 line segments is nearest to each point? How can I create a 16bit historgram of 16bit data How can I get all the uniforms and uniformBlocks How can I move the perspective vanishing point from the center of the canvas? How to Achieve Moving Line with Trail Effects How to bind an array of textures to a WebGL shader uniform? How to blend colors across 2 triangles How to combine more text drawing into fewer draw calls How to control the color between vertices How to create a torus How to detect clipped triangles in the framgment shader How to determine the average brightness in a scene? How to draw correctly textured trapezoid polygons How to fade the drawing buffer How to figure out how much GPU work to do without crashing WebGL How to get audio data into a shader How to get code completion for WebGL in Visual Studio Code How to get the 3d coordinates of a mouse click How to get pixelize effect in webgl? How to implement zoom from mouse in 2D WebGL How to import a heightmap in WebGL How to load images in the background with no jank How to make a smudge brush tool How to make WebGL canvas transparent How to optimize rendering a UI How to prevent texture bleeding with a texture atlas How to process particle positions How to read a single component with readPixels How to render large scale images like 32000x32000 How to simulate a 3D texture in WebGL How to support both WebGL and WebGL2
WebGL Multiple Views, Multiple Canvases
WebGLFundame · 2025-02-26 · via WebGL Fundamentals

This article assumes you've read the article on less code more fun as it uses the library mentioned there so as to unclutter the example. If you don't understand when a function named webglUtils.setBuffersAndAttributes what it means to set the buffers and attributes, or when a function named webglUtils.setUniforms what it means to set uniforms, etc... then you should probably to go further back and read the fundamentals.

Let's say you wanted to draw multiple views of the same scene, how could we do this? One way would be to render to textures and then draw those textures to the canvas. That is certainly a valid way to do it and there are times it might be the right thing to do. But, it requires that we allocate textures, render stuff to them, then render those textures on the canvas. That means we're effectively double rendering. That might be appropriate, for example in a racing game when we want to render the view in a rear view mirror we'd render what's behind the car to a texture then use that texture to draw the rear view mirror.

Another way is to set the viewport and turn on the scissor test. This is great for situation where our views don't overlap. Even better there is no double rendering like the solution above.

In the very first article it's mentioned that we set how WebGL converts from clip space to pixel space by calling

gl.viewport(left, bottom, width, height);

The most common thing is to set to those to 0, 0, gl.canvas.width, and gl.canvas.height respectively to cover the entire canvas.

Instead we can set them to a portion of the canvas and they'll make it so we only draw to that portion of the canvas. WebGL clips vertices in clip space. As we mentioned before we set gl_Position in our vertex shader to values that go from -1 to +1 in x, y, z. WebGL clips the triangles and lines we pass to that range. After the clipping happens then the gl.viewport settings are applied so for example if we used

gl.viewport(
   10,   // left
   20,   // bottom
   30,   // width
   40,   // height
);

Then a clip space value of x = -1 corresponds to pixel x = 10 and a clip space value of +1 corresponds to a pixel x = 40 (a left of 10 plus a width of 30) (Actually that's a slight over simplification, see below)

So, after clipping if we draw a triangle it would appear to fit inside the viewport.

Let's draw our 'F' from previous articles.

The vertex and fragment shaders are the same as the ones used in the articles on orthographic and perspective projection.

// vertex shader
attribute vec4 a_position;
attribute vec4 a_color;

uniform mat4 u_matrix;

varying vec4 v_color;

void main() {
  // Multiply the position by the matrix.
  gl_Position = u_matrix * a_position;

  // Pass the vertex color to the fragment shader.
  v_color = a_color;
}
// fragment shader
precision mediump float;

// Passed in from the vertex shader.
varying vec4 v_color;

void main() {
  gl_FragColor = v_color;
}

Then at init time we need to create the program and the buffers for the 'F'

// setup GLSL programs
// compiles shaders, links program, looks up locations
const programInfo = webglUtils.createProgramInfo(gl, ["vertex-shader-3d", "fragment-shader-3d"]);

// create buffers and fill with data for a 3D 'F'
const bufferInfo = primitives.create3DFBufferInfo(gl);

And to draw let's make a function we can pass a projection matrix, a camera matrix, and a world matrix

function drawScene(projectionMatrix, cameraMatrix, worldMatrix) {
  // Make a view matrix from the camera matrix.
  const viewMatrix = m4.inverse(cameraMatrix);

  // multiply them together to make a worldViewProjection matrix.
  let mat = m4.multiply(projectionMatrix, viewMatrix);
  mat = m4.multiply(mat, worldMatrix);

  gl.useProgram(programInfo.program);

  // ------ Draw the F --------

  // Setup all the needed attributes.
  webglUtils.setBuffersAndAttributes(gl, programInfo, bufferInfo);

  // Set the uniforms
  webglUtils.setUniforms(programInfo, {
    u_matrix: mat,
  });

  // calls gl.drawArrays or gl.drawElements
  webglUtils.drawBufferInfo(gl, bufferInfo);
}

and then let's call that function to draw the F.

function degToRad(d) {
  return d * Math.PI / 180;
}

const settings = {
  rotation: 150,  // in degrees
};
const fieldOfViewRadians = degToRad(120);

function render() {
  webglUtils.resizeCanvasToDisplaySize(gl.canvas);

  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  gl.enable(gl.CULL_FACE);
  gl.enable(gl.DEPTH_TEST);

  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const near = 1;
  const far = 2000;

  // Compute a perspective projection matrix
  const perspectiveProjectionMatrix =
      m4.perspective(fieldOfViewRadians, aspect, near, far);

  // Compute the camera's matrix using look at.
  const cameraPosition = [0, 0, -75];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const cameraMatrix = m4.lookAt(cameraPosition, target, up);

  // rotate the F in world space
  let worldMatrix = m4.yRotation(degToRad(settings.rotation));
  worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation));
  // center the 'F' around its origin
  worldMatrix = m4.translate(worldMatrix, -35, -75, -5);

  drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix);
}
render();

This is basically the same as the final example from the article on perspective except we're using our library to keep the code simpler.

Now let's make it draw 2 views of the 'F' side by side by using gl.viewport

function render() {
  webglUtils.resizeCanvasToDisplaySize(gl.canvas);

-  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

  gl.enable(gl.CULL_FACE);
  gl.enable(gl.DEPTH_TEST);

  // we're going to split the view in 2
-  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
+  const effectiveWidth = gl.canvas.clientWidth / 2;
+  const aspect = effectiveWidth / gl.canvas.clientHeight;
  const near = 1;
  const far = 2000;

  // Compute a perspective projection matrix
  const perspectiveProjectionMatrix =
      m4.perspective(fieldOfViewRadians, aspect, near, far);

+  // Compute an orthographic projection matrix
+  const halfHeightUnits = 120;
+  const orthographicProjectionMatrix = m4.orthographic(
+      -halfHeightUnits * aspect,  // left
+       halfHeightUnits * aspect,  // right
+      -halfHeightUnits,           // bottom
+       halfHeightUnits,           // top
+       -75,                       // near
+       2000);                     // far

  // Compute the camera's matrix using look at.
  const cameraPosition = [0, 0, -75];
  const target = [0, 0, 0];
  const up = [0, 1, 0];
  const cameraMatrix = m4.lookAt(cameraPosition, target, up);

  let worldMatrix = m4.yRotation(degToRad(settings.rotation));
  worldMatrix = m4.xRotate(worldMatrix, degToRad(settings.rotation));
  // center the 'F' around its origin
  worldMatrix = m4.translate(worldMatrix, -35, -75, -5);

+  const {width, height} = gl.canvas;
+  const leftWidth = width / 2 | 0;
+
+  // draw on the left with orthographic camera
+  gl.viewport(0, 0, leftWidth, height);
+
+  drawScene(orthographicProjectionMatrix, cameraMatrix, worldMatrix);

+  // draw on the right with perspective camera
+  const rightWidth = width - leftWidth;
+  gl.viewport(leftWidth, 0, rightWidth, height);

  drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix);
}

You can see above first we set the viewport to cover the left half of the canvas, draw, then we set it to cover the right half and draw. Otherwise we're drawing the same thing on both sides except we change the projection matrix.

Let's clear both sides to different colors

First, in drawScene lets call gl.clear

  function drawScene(projectionMatrix, cameraMatrix, worldMatrix) {
+    // Clear the canvas AND the depth buffer.
+    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    ...

Then let's set clear colors before calling drawScene

  const {width, height} = gl.canvas;
  const leftWidth = width / 2 | 0;

  // draw on left with orthographic camera
  gl.viewport(0, 0, leftWidth, height);
+  gl.clearColor(1, 0, 0, 1);  // red

  drawScene(orthographicProjectionMatrix, cameraMatrix, worldMatrix);

  // draw on left with orthographic camera
  const rightWidth = width - leftWidth;
  gl.viewport(leftWidth, 0, rightWidth, height);
  gl.clearColor(0, 0, 1, 1);  // blue

+  drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix);

Oops, what happened? Why is there nothing on the left?

It turns out gl.clear does not look at the viewport settings. To fix this we can use the scissor test. The scissor test lets use define a rectangle. Anything outside that rectangle will not be affected if the scissor test is enabled.

The scissor test is off by default. We can enable it by calling

gl.enable(gl.SCISSOR_TEST);

Like the viewport it defaults to the initial size of the canvas but we can set with same parameters as the viewport by calling gl.scissor as in

gl.scissor(
   10,   // left
   20,   // bottom
   30,   // width
   40,   // height
);

So let's add those in

function render() {
  webglUtils.resizeCanvasToDisplaySize(gl.canvas);

  gl.enable(gl.CULL_FACE);
  gl.enable(gl.DEPTH_TEST);
+  gl.enable(gl.SCISSOR_TEST);

  ...

  const {width, height} = gl.canvas;
  const leftWidth = width / 2 | 0;

  // draw on left with orthographic camera
  gl.viewport(0, 0, leftWidth, height);
+  gl.scissor(0, 0, leftWidth, height);
  gl.clearColor(1, 0, 0, 1);  // red

  drawScene(orthographicProjectionMatrix, cameraMatrix, worldMatrix);

  // draw on left with orthographic camera
  const rightWidth = width - leftWidth;
  gl.viewport(leftWidth, 0, rightWidth, height);
+  gl.scissor(leftWidth, 0, rightWidth, height);
  gl.clearColor(0, 0, 1, 1);  // blue

  drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix);
}

and now it should work.

Of course you're not limited to drawing the same scene You can draw whatever you want in each view.

Drawing Multiple Canvases

This is a good solution for simulating multiple canvases. Let's say you wanted to make character selection screen for a game an you want to show 3D models of each head in a list so the user can select one. Or let's say you wanted to make an e-commerce site and show 3d models of each product down the page at the same time.

The most obvious way to do this would be to put a <canvas> each place you want to show an item. Unfortunately you'd run into a bunch of issues.

First, Each canvas would require a different WebGL context WebGL contexts can not share resources so you'd have to compile shaders for each canvas, load textures for each canvas, upload geometry for each canvas.

Another issue is most browsers have a limit on how many simultaneous canvases they support. For many it's as low as 8. That means as soon as you create a webgl context on the 9th canvas the first canvas will lose its context.

We can work around these issues by making just 1 large canvas that covers the entire window. We'll then put a placeholder <div> each place we want to draw an item. We can use element.getBoundingClientRect to find out where to set the viewport and scissor to draw in that area.

This will solve both problems mentioned above. We'll only have one webgl context so we can share resources and we won't run into the context limit.

Let's make an example.

First let's make a canvas that goes in the background with some content that goes in front. First the HTML

<body>
  <canvas id="canvas"></canvas>
  <div id="content"></div>
</body>

Then the CSS

body {
  margin: 0;
}
#content {
  margin: 10px;
}
#canvas {
  position: absolute;
  top: 0;
  width: 100%;
  height: 100vh;
  z-index: -1;
  display: block;
}

Now let's make a few things to draw. Remember that a BufferInfo is just a list of buffers by name and the settings need to set their attributes

// create buffers and fill with data for various things.
const bufferInfos = [
  primitives.createCubeBufferInfo(
      gl,
      1,  // width
      1,  // height
      1,  // depth
  ),
  primitives.createSphereBufferInfo(
      gl,
      0.5,  // radius
      8,    // subdivisions around
      6,    // subdivisions down
  ),
  primitives.createTruncatedConeBufferInfo(
      gl,
      0.5,  // bottom radius
      0,    // top radius
      1,    // height
      6,    // subdivisions around
      1,    // subdivisions down
  ),
];

Now let's make 100 html items. For each one we'll create a container div and inside will be a view and a label. The view is just an empty div where we want to draw the item.

function createElem(type, parent, className) {
  const elem = document.createElement(type);
  parent.appendChild(elem);
  if (className) {
    elem.className = className;
  }
  return elem;
}

function randArrayElement(array) {
  return array[Math.random() * array.length | 0];
}

function rand(min, max) {
  if (max === undefined) {
    max = min;
    min = 0;
  }
  return Math.random() * (max - min) + min;
}

const contentElem = document.querySelector('#content');
const items = [];
const numItems = 100;
for (let i = 0; i < numItems; ++i) {
  const outerElem = createElem('div', contentElem, 'item');
  const viewElem = createElem('div', outerElem, 'view');
  const labelElem = createElem('div', outerElem, 'label');
  labelElem.textContent = `Item ${i + 1}`;
  const bufferInfo = randArrayElement(bufferInfos);
  const color = [rand(1), rand(1), rand(1), 1];
  items.push({
    bufferInfo,
    color,
    element: viewElem,
  });
}

Let's style these items as follows

.item {
  display: inline-block;
  margin: 1em;
  padding: 1em;
}
.label {
  margin-top: 0.5em;
}
.view {
  width: 250px;
  height: 250px;
  border: 1px solid black;
}

The items array has a bufferInfo, a color and an element for each item. We loop over all the items one at a time and call element.getBoundingClientRect and use the rectangle returned to see if that element intersects with the canvas. If it does we set the viewport and scissor to match and then draw that object.

function render(time) {
  time *= 0.001;  // convert to seconds

  webglUtils.resizeCanvasToDisplaySize(gl.canvas);

  gl.enable(gl.CULL_FACE);
  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.SCISSOR_TEST);

  // move the canvas to top of the current scroll position
  gl.canvas.style.transform = `translateY(${window.scrollY}px)`;

  for (const {bufferInfo, element, color} of items) {
    const rect = element.getBoundingClientRect();
    if (rect.bottom < 0 || rect.top  > gl.canvas.clientHeight ||
        rect.right  < 0 || rect.left > gl.canvas.clientWidth) {
      continue;  // it's off screen
    }

    const width  = rect.right - rect.left;
    const height = rect.bottom - rect.top;
    const left   = rect.left;
    const bottom = gl.canvas.clientHeight - rect.bottom;

    gl.viewport(left, bottom, width, height);
    gl.scissor(left, bottom, width, height);
    gl.clearColor(...color);

    const aspect = width / height;
    const near = 1;
    const far = 2000;

    // Compute a perspective projection matrix
    const perspectiveProjectionMatrix =
        m4.perspective(fieldOfViewRadians, aspect, near, far);

    // Compute the camera's matrix using look at.
    const cameraPosition = [0, 0, -2];
    const target = [0, 0, 0];
    const up = [0, 1, 0];
    const cameraMatrix = m4.lookAt(cameraPosition, target, up);

    // rotate the item
    const rTime = time * 0.2;
    const worldMatrix = m4.xRotate(m4.yRotation(rTime), rTime);

    drawScene(perspectiveProjectionMatrix, cameraMatrix, worldMatrix, bufferInfo);
  }
  requestAnimationFrame(render);
}
requestAnimationFrame(render);

I made the code above use a requestAnimationFrame loop so I could animate the objects. I also passed which bufferInfo to draw to drawScene. The shader is just using the normals as colors to keep the shaders simple. If I added lighting the code would get much more complicated.

Of course you could draw whole 3D scenes or whatever for each item. As long as you set the viewport and scissor correctly and then set up your projection matrix to match the aspect of the area it should work.

One other notable thing about the code is we're moving the canvas with this line

gl.canvas.style.transform = `translateY(${window.scrollY}px)`;

Why? We could instead set the canvas to position: fixed; in which case it would not scroll with the page. The difference would be subtle. The browser tries to scroll the page as smooth as possible. That might be faster than we can draw our objects. Because of this we have 2 options.

  1. Use a fixed position canvas

    In this case if we can't update fast enough the HTML in front of the canvas will scroll but the canvas itself won't so for a few moments they will be out of sync

  2. Move the canvas under the content

    In this case if we can't update fast enough the canvas will scroll in sync with the HTML but new areas where we want stuff drawn will be blank until we get a chance to draw.

    This is the solution used above

If you want to handle horizontal scrolling just change this line

gl.canvas.style.transform = `translateY(${window.scrollY}px)`;

to this

gl.canvas.style.transform = `translateX(${window.scrollX}px) translateY(${window.scrollY}px)`;

Hopefully this article gave you some ideas how to draw multiple views. We'll use these techniques in a few future articles where being able to see multiple views is useful for understanding.

Pixel Coordinates

Pixel coordinates in WebGL are referenced by their edges. So for example if we had a canvas that was 3x2 pixels big and we set the viewport as


gl.viewport(
  0, // left
  0, // bottom
  3, // width
  2, // height
);

Then we're really defining this rectangle that surrounds 3x2 pixels

That means a clip space value of X = -1.0 corresponds to the left edge of this rectangle and a clip space value of X = 1.0 corresponds to the right. Above I said X = -1.0 corresponds to the left most pixel but really to corresponds to the left edge