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

推荐订阅源

AI
AI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
N
News and Events Feed by Topic
O
OpenAI News
W
WeLiveSecurity
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Heimdal Security Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
Cloudbric
Cloudbric
T
Threatpost
T
Troy Hunt's Blog
T
Tenable Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Hacker News
The Hacker News
Know Your Adversary
Know Your Adversary
Forbes - Security
Forbes - Security
Cyberwarzone
Cyberwarzone
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
L
Lohrmann on Cybersecurity
博客园 - 司徒正美
腾讯CDC
博客园 - 三生石上(FineUI控件)
爱范儿
爱范儿
J
Java Code Geeks
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
GbyAI
GbyAI
D
Docker
V
Visual Studio Blog
云风的 BLOG
云风的 BLOG
I
InfoQ
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
A
Arctic Wolf
L
LINUX DO - 热门话题
The Cloudflare Blog
雷峰网
雷峰网
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件

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