Split-Screen

Multiple Viewports for Split-Screen Rendering

Split screens, minimaps and multi-view editors draw one scene into several regions per frame: set the viewport and an identical scissor box (clear() ignores the viewport), clear, change what differs, and draw. BookNest's overview and close-up panes differ only in two uniforms, applied as (aPosition - uCenter) * uZoom:

Drawing the shelf's VAO into an overview pane and a close-up paneJavaScript
const { vertices, indices } = GLH.coverWall();
gl.bindVertexArray(GLH.vao(gl, attributes, vertices,
  [['aPosition', 2], ['aUV', 2], ['aBook', 1]], indices));
gl.enable(gl.SCISSOR_TEST);
for (const [x, y, w, h, gray, center, zoom] of [
  [0, 0, 840, 300, 0.9, [0, 0], [1, 0.8]],                // overview of all six books
  [850, 0, 350, 300, 0.2, [-0.17, 0], [3, 1.1]]]) {       // close-up of book 3
  gl.viewport(x, y, w, h);
  gl.scissor(x, y, w, h);
  gl.clearColor(gray, gray, gray, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  uniforms.uCenter(center);
  uniforms.uZoom(zoom);
  gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
}

One VAO bind serves both panes; MVP Matrices gives each pane a real camera through view and projection matrices.

The shelf's VAO drawn into an overview pane and a roaming close-up pane each frameHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="wall" width="1200" height="300"></canvas>
<script>
const covers = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
const rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16) / 255);
const gl = document.getElementById('wall').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPosition; layout(location = 1) in vec2 aUV; layout(location = 2) in float aBook;
uniform vec3 uColors[6]; uniform vec2 uCenter; uniform vec2 uZoom;
out vec2 vUV; out vec3 vColor;
void main() { vUV = aUV; vColor = uColors[int(aBook)];
  gl_Position = vec4((aPosition - uCenter) * uZoom, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec2 vUV; in vec3 vColor; out vec4 outColor;
void main() {
  float title = step(abs(vUV.y - 0.25), 0.04) * step(abs(vUV.x - 0.55), 0.3);   // a title band
  vec3 c = vColor * mix(0.55, 1.0, smoothstep(0.07, 0.10, vUV.x)) * mix(1.2, 0.7, vUV.y);
  outColor = vec4(mix(c, vec3(0.97), title * 0.8), 1.0);
}`]]) {
  const s = gl.createShader(type);
  gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
gl.uniform3fv(gl.getUniformLocation(program, 'uColors'), covers.flatMap(rgb));
const uCenter = gl.getUniformLocation(program, 'uCenter');
const uZoom = gl.getUniformLocation(program, 'uZoom');

// The cover wall as indexed quads, recorded once in a VAO.
const vertices = [], indices = [];
covers.forEach((hex, i) => {
  for (const [u, v] of [[0, 0], [1, 0], [1, 1], [0, 1]]) vertices.push(-0.94 + i * 0.32 + u * 0.26, -0.82 + v * 1.64, u, 1 - v, i);
  indices.push(i * 4, i * 4 + 1, i * 4 + 2, i * 4, i * 4 + 2, i * 4 + 3);
});
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
[[0, 2, 0], [1, 2, 8], [2, 1, 16]].forEach(([loc, size, offset]) => {
  gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 20, offset);
  gl.enableVertexAttribArray(loc);
});
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

gl.enable(gl.SCISSOR_TEST);
function frame(ms) {
  const focus = Math.sin(ms * 0.0005) * 0.8;          // the close-up roams along the shelf
  for (const [x, y, w, h, gray, center, zoom] of [
    [0, 0, 840, 300, 0.9, [0, 0], [1, 0.8]],           // overview of all six books
    [850, 0, 350, 300, 0.2, [focus, 0], [3, 1.1]]]) {  // close-up
    gl.viewport(x, y, w, h);
    gl.scissor(x, y, w, h);                            // clear() ignores the viewport
    gl.clearColor(gray, gray, gray, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.uniform2fv(uCenter, center);
    gl.uniform2fv(uZoom, zoom);
    gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>