Reflection Effect

A Picture-in-Picture Reflection Effect

Imagine a mirror on the shop wall behind BookNest's shelf. Pass 1 renders the shelf from behind into a 512 x 256 texture; pass 2 renders the main view; pass 3 draws the texture into a small viewport with GLH.paint(), flipping u so that it reads as a reflection and darkening a frame around it:

The shelf with a picture-in-picture mirror rendered through a framebufferHTML
<canvas id="c" width="1240" height="280" style="width:620px;height:140px"></canvas>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/gl-matrix-min.js"></script>
<script src="webgl-helper.js"></script>
<script>
const gl = GLH.context('c'), { attributes, uniforms } = GLH.tint(gl);
const shelf = GLH.shelf(gl, attributes), mirror = GLH.framebuffer(gl, 512, 256);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE);
const drawShelf = (target, width, height, eye) => {
  gl.bindFramebuffer(gl.FRAMEBUFFER, target);                // null: the canvas
  gl.viewport(0, 0, width, height);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  uniforms.uViewProjection(shelf.camera(width / height, eye));
  shelf.draw(uniforms);
};
drawShelf(mirror.fbo, 512, 256, [-1.2, 1.4, -2.6]);          // 1: seen from behind
drawShelf(null, 1240, 280, [0.9, 0.9, 2.9]);                 // 2: the main view
gl.disable(gl.DEPTH_TEST);
gl.viewport(30, 150, 330, 120);                              // 3: the mirror, top left
gl.bindTexture(gl.TEXTURE_2D, mirror.color);
GLH.paint(gl.canvas, `#version 300 es
  precision mediump float; uniform sampler2D uMirror; in vec2 vUV; out vec4 c;
  void main() {
    float frame = step(0.03, min(min(vUV.x, 1.0 - vUV.x), min(vUV.y, 1.0 - vUV.y)) * 3.0);
    vec3 view = texture(uMirror, vec2(1.0 - vUV.x, vUV.y)).rgb * 0.9 + 0.08;   // flipped
    c = vec4(mix(vec3(0.25, 0.2, 0.15), view, frame), 1.0);
  }`);
window.__done = true;
</script>
Browser output of Listing 3.102
Browser output of 102

The mirror shows the books' darker backs, with book 1 on the left, as a mirror behind the shelf would. A true floor reflection renders a mirrored camera the same way, then samples at each fragment's screen position.

The shelf with a picture-in-picture mirror: the view from behind rendered into a texture and shown flippedHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="c" width="1240" height="280"></canvas>
<script>
const m4 = {
  perspective(fovy, aspect, near, far) {
    const f = 1 / Math.tan(fovy * Math.PI / 360), d = near - far;
    return [f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) / d, -1, 0, 0, 2 * far * near / d, 0];
  },
  lookAt(eye, target, up = [0, 1, 0]) {
    const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
    const unit = (v) => v.map((c) => c / Math.hypot(...v));
    const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
    const z = unit(eye.map((c, i) => c - target[i])), x = unit(cross(up, z)), y = cross(z, x);
    return [x[0], y[0], z[0], 0, x[1], y[1], z[1], 0, x[2], y[2], z[2], 0, -dot(x, eye), -dot(y, eye), -dot(z, eye), 1];
  },
  multiply(a, b) {
    const out = new Array(16).fill(0);
    for (let c = 0; c < 4; c++) for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) out[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
    return out;
  },
  model(x, y, z, sx, sy, sz, turn = 0) {
    const c = Math.cos(turn), s = Math.sin(turn);
    return [c * sx, 0, -s * sx, 0, 0, sy, 0, 0, s * sz, 0, c * sz, 0, x, y, z, 1];
  },
};
function cube() {
  const faces = [[[0,0,1],[1,0,0],[0,1,0]], [[0,0,-1],[-1,0,0],[0,1,0]], [[1,0,0],[0,0,-1],[0,1,0]],
                 [[-1,0,0],[0,0,1],[0,1,0]], [[0,1,0],[1,0,0],[0,0,-1]], [[0,-1,0],[1,0,0],[0,0,1]]];
  const out = [];
  for (const [n, u, v] of faces) for (const [a, b] of [[-1,-1],[1,-1],[1,1],[-1,-1],[1,1],[-1,1]])
    out.push(...[0, 1, 2].map(i => (n[i] + a * u[i] + b * v[i]) / 2), ...n);
  return new Float32Array(out);
}
function program(vs, fs) {
  const p = gl.createProgram();
  for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) {
    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(p, s);
  }
  gl.linkProgram(p);
  return p;
}
const gl = document.getElementById('c').getContext('webgl2');
const tint = program(`#version 300 es
layout(location = 0) in vec3 aPosition; layout(location = 1) in vec3 aNormal;
uniform mat4 uViewProjection, uModel; out float vLight;
void main() { vLight = 0.45 + 0.55 * max(dot(normalize(mat3(uModel) * aNormal), normalize(vec3(0.4, 0.8, 0.6))), 0.0);
  gl_Position = uViewProjection * uModel * vec4(aPosition, 1.0); }`, `#version 300 es
precision mediump float; uniform vec3 uColor; in float vLight; out vec4 c; void main() { c = vec4(uColor * vLight, 1.0); }`);
const mirrorPaint = program(`#version 300 es
out vec2 vUV;
void main() { vUV = vec2(gl_VertexID & 1, gl_VertexID >> 1) * 2.0; gl_Position = vec4(vUV * 2.0 - 1.0, 0.0, 1.0); }`,
`#version 300 es
precision mediump float; uniform sampler2D uMirror; in vec2 vUV; out vec4 c;
void main() {
  float frame = step(0.03, min(min(vUV.x, 1.0 - vUV.x), min(vUV.y, 1.0 - vUV.y)) * 3.0);
  vec3 view = texture(uMirror, vec2(1.0 - vUV.x, vUV.y)).rgb * 0.9 + 0.08;   // flipped like a mirror
  c = vec4(mix(vec3(0.25, 0.2, 0.15), view, frame), 1.0);
}`);
const cubeVAO = gl.createVertexArray();
gl.bindVertexArray(cubeVAO);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, cube(), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12); gl.enableVertexAttribArray(1);

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 items = covers.map((hex, i) => ({ color: rgb(hex), model: m4.model(-1.3 + i * 0.52, 0.5, 0, 0.42, 0.98, 0.12, (30 - i * 12) * Math.PI / 180) }));
items.push({ color: rgb('#8a6240'), model: m4.model(0, -0.03, 0, 3.4, 0.06, 0.7) });

// The mirror's render target: 512 x 256 colour texture + depth renderbuffer.
const mirror = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, mirror);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 512, 256);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
const depth = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, depth);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, 512, 256);
const mirrorFBO = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, mirrorFBO);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, mirror, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depth);

gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE);
gl.clearColor(0.93, 0.91, 0.87, 1);
function drawShelf(target, width, height, eye) {
  gl.bindFramebuffer(gl.FRAMEBUFFER, target);           // null: the canvas
  gl.viewport(0, 0, width, height);                     // the viewport is not stored per framebuffer
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.useProgram(tint);
  gl.uniformMatrix4fv(gl.getUniformLocation(tint, 'uViewProjection'), false,
    m4.multiply(m4.perspective(40, width / height, 0.1, 20), m4.lookAt(eye, [0.1, 0.4, 0])));
  gl.bindVertexArray(cubeVAO);
  for (const { color, model } of items) {
    gl.uniformMatrix4fv(gl.getUniformLocation(tint, 'uModel'), false, model);
    gl.uniform3fv(gl.getUniformLocation(tint, 'uColor'), color);
    gl.drawArrays(gl.TRIANGLES, 0, 36);
  }
}
function frame(ms) {
  drawShelf(mirrorFBO, 512, 256, [-1.2 + Math.sin(ms * 0.0005) * 0.6, 1.4, -2.9]);   // 1: seen from behind
  drawShelf(null, 1240, 280, [0.9, 1.1, 3.4]);                                       // 2: the main view
  gl.disable(gl.DEPTH_TEST);
  gl.viewport(30, 150, 330, 120);                                                    // 3: the mirror, top left
  gl.useProgram(mirrorPaint);
  gl.bindTexture(gl.TEXTURE_2D, mirror);
  gl.drawArrays(gl.TRIANGLES, 0, 3);
  gl.enable(gl.DEPTH_TEST);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>