Atlaslog
Atlaslog / Examples / luma.gl / Offscreen
luma.gl · Runnable Example

Offscreen

Runnable luma.gl example: Offscreen. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.

Open standalone → Chinese notes All luma.gl examples
/demos/lumagl-offscreen.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/lumagl-offscreen.html
<!--
  说明:先把三个三角形画进 256×256 的离屏帧缓冲,再把该帧缓冲的颜色纹理采样到屏幕上
  核心逻辑:Framebuffer 作为 RenderPass 的 framebuffer,其颜色附件即下一趟的纹理
  依赖:@luma.gl/core 9.4.2、webgl、shadertools、engine · jsDelivr CDN(UMD)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>离屏渲染</title>
<style>
  html,body{height:100%;margin:0;background:#0d1117}
  body{display:flex;flex-direction:column;overflow:hidden;
       font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#e6e8ec}
  .bar{flex:0 0 auto;padding:8px 12px;background:#161b22;border-bottom:1px solid #252c36;
       display:flex;gap:12px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
  .bar b{color:#58a6ff;font-weight:600}
  .bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:5px;cursor:pointer;
       border:1px solid #303845;background:#1d2531;color:#c8d2e0}
  .bar button:hover{background:#253040}
  .bar .sp{flex:1 1 auto}
  .wrap{position:relative;flex:1 1 auto;min-height:0;display:flex}
  #cv{flex:1 1 auto;min-height:0;width:100%;display:block}
  #err{position:absolute;left:0;right:0;top:0;padding:16px;color:#ff9d85;
       font:12px/1.7 ui-monospace,Consolas,monospace;white-space:pre-wrap;display:none}
</style>
</head>
<body>
<div class="bar">
  <span>离屏 <b>256 × 256</b> · 附件 <b>rgba8unorm + 深度</b></span>
  <button id="spin">暂停旋转</button>
  <span class="sp"></span>
  <span>两趟渲染:帧缓冲里画场景,屏幕上采样该纹理</span>
</div>
<div class="wrap">
  <canvas id="cv"></canvas>
  <div id="err"></div>
</div>

<script src="https://cdn.jsdelivr.net/npm/@luma.gl/core@9.4.2/dist/dist.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@luma.gl/webgl@9.4.2/dist/dist.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@luma.gl/shadertools@9.4.2/dist/dist.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@luma.gl/engine@9.4.2/dist/dist.min.js"></script>
<script>
(function () {
  'use strict';

  var cv = document.getElementById('cv');
  var errBox = document.getElementById('err');

  function fail(msg) {
    errBox.style.display = 'block';
    errBox.textContent = '运行失败:' + msg;
  }

  if (!window.luma || !luma.Model || !luma.AnimationLoop) {
    fail('luma.gl 未加载成功,请检查能否访问 cdn.jsdelivr.net');
    return;
  }

  var SIZE = 256;

  /* 三个三角形,故意留出暗色缝隙,便于辨认离屏画面 */
  var scenePositions = new Float32Array([
    -1.0, -1.0, 0,   -0.1, -1.0, 0,   -1.0,  0.15, 0,
     0.05, -1.0, 0,   1.0, -0.25, 0,    0.15,  0.55, 0,
    -0.85, 0.35, 0,   0.5,  1.0, 0,    1.0,  0.15, 0
  ]);

  var sceneColors = new Float32Array([
    0.20, 0.78, 0.72,   0.20, 0.78, 0.72,   0.13, 0.42, 0.62,
    0.96, 0.66, 0.26,   0.96, 0.66, 0.26,   0.92, 0.42, 0.18,
    0.66, 0.46, 0.96,   0.66, 0.46, 0.96,   0.38, 0.30, 0.84
  ]);

  var quadUvs = new Float32Array([
    0, 0,
    1, 0,
    1, 1,
    0, 1
  ]);

  var quadIndices = new Uint16Array([0, 1, 2, 0, 2, 3]);
  var quadPositions = new Float32Array(12);
  var CORNERS = [[-0.78, -0.78], [0.78, -0.78], [0.78, 0.78], [-0.78, 0.78]];

  /* 屏幕上的四边形按 CPU 旋转,用来证明取到的是每帧新渲染的离屏结果 */
  function rotateQuad(angle) {
    var c = Math.cos(angle);
    var s = Math.sin(angle);
    for (var i = 0; i < 4; i++) {
      var x = CORNERS[i][0];
      var y = CORNERS[i][1];
      quadPositions[i * 3]     = x * c - y * s;
      quadPositions[i * 3 + 1] = x * s + y * c;
      quadPositions[i * 3 + 2] = 0;
    }
  }

  rotateQuad(0);

  luma.luma.createDevice({
    type: 'webgl',
    adapters: [luma.webgl2Adapter],
    createCanvasContext: {
      canvas: cv,
      useDevicePixels: Math.min(window.devicePixelRatio, 2),
      autoResize: true
    }
  }).then(function (device) {
    var depthFormat = device.preferredDepthFormat;

    /* 颜色附件要同时具备渲染与采样用途,下一趟才能把它当纹理读 */
    var colorTex = device.createTexture({
      id: 'offscreen-color',
      width: SIZE,
      height: SIZE,
      format: 'rgba8unorm',
      usage: luma.Texture.SAMPLE | luma.Texture.RENDER | luma.Texture.COPY_DST,
      sampler: { minFilter: 'linear', magFilter: 'linear', mipmapFilter: 'none' }
    });

    var depthTex = device.createTexture({
      id: 'offscreen-depth',
      width: SIZE,
      height: SIZE,
      format: depthFormat,
      usage: luma.Texture.RENDER
    });

    var fbo = device.createFramebuffer({
      id: 'offscreen-fbo',
      width: SIZE,
      height: SIZE,
      colorAttachments: [colorTex],
      depthStencilAttachment: depthTex
    });

    var sceneModel = new luma.Model(device, {
      id: 'offscreen-scene',
      topology: 'triangle-list',
      vertexCount: 9,
      colorAttachmentFormats: ['rgba8unorm'],
      depthStencilAttachmentFormat: depthFormat,
      parameters: { depthCompare: 'less-equal', depthWriteEnabled: true },
      attributes: {
        positions: device.createBuffer({ data: scenePositions, usage: luma.Buffer.VERTEX }),
        colors: device.createBuffer({ data: sceneColors, usage: luma.Buffer.VERTEX })
      },
      bufferLayout: [
        { name: 'positions', stepMode: 'vertex', format: 'float32x3' },
        { name: 'colors', stepMode: 'vertex', format: 'float32x3' }
      ],
      vs: [
        '#version 300 es',
        'in vec3 positions;',
        'in vec3 colors;',
        'out vec3 vColor;',
        'void main() {',
        '  vColor = colors;',
        '  gl_Position = vec4(positions, 1.0);',
        '}'
      ].join('\n'),
      fs: [
        '#version 300 es',
        'precision highp float;',
        'in vec3 vColor;',
        'out vec4 fragColor;',
        'void main() {',
        '  fragColor = vec4(vColor, 1.0);',
        '}'
      ].join('\n')
    });

    var quadBuffer = device.createBuffer({
      data: quadPositions,
      usage: luma.Buffer.VERTEX | luma.Buffer.COPY_DST
    });

    var composite = new luma.Model(device, {
      id: 'composite',
      topology: 'triangle-list',
      vertexCount: 4,
      indexCount: 6,
      indexBuffer: device.createBuffer({
        data: quadIndices,
        usage: luma.Buffer.INDEX,
        indexType: 'uint16'
      }),
      attributes: {
        positions: quadBuffer,
        uvs: device.createBuffer({ data: quadUvs, usage: luma.Buffer.VERTEX })
      },
      bufferLayout: [
        { name: 'positions', stepMode: 'vertex', format: 'float32x3' },
        { name: 'uvs', stepMode: 'vertex', format: 'float32x2' }
      ],
      bindings: {
        uScene: colorTex
      },
      vs: [
        '#version 300 es',
        'in vec3 positions;',
        'in vec2 uvs;',
        'out vec2 vUV;',
        'void main() {',
        '  vUV = uvs;',
        '  gl_Position = vec4(positions, 1.0);',
        '}'
      ].join('\n'),
      fs: [
        '#version 300 es',
        'precision highp float;',
        'in vec2 vUV;',
        'uniform sampler2D uScene;',
        'out vec4 fragColor;',
        'void main() {',
        '  vec4 c = texture(uScene, vUV);',
        '  float edge = min(min(vUV.x, 1.0 - vUV.x), min(vUV.y, 1.0 - vUV.y));',
        '  fragColor = vec4(c.rgb * (0.78 + 0.22 * smoothstep(0.0, 0.22, edge)), 1.0);',
        '}'
      ].join('\n')
    });

    var spinning = true;
    var start = performance.now();

    var loop = new luma.AnimationLoop({
      device: device,
      onRender: function (props) {
        /* 第一趟:渲染到帧缓冲,尺寸由帧缓冲决定视口 */
        var offscreen = props.device.beginRenderPass({
          framebuffer: fbo,
          clearColor: [0.02, 0.04, 0.07, 1],
          clearDepth: 1
        });
        sceneModel.draw(offscreen);
        offscreen.end();

        if (spinning) {
          rotateQuad((performance.now() - start) / 1000 * 0.5);
          quadBuffer.write(quadPositions);
        }

        /* 第二趟:画到画布,颜色附件作为纹理传入 */
        var screen = props.device.beginRenderPass({ clearColor: [0.04, 0.06, 0.09, 1] });
        composite.draw(screen);
        screen.end();
      },
      onError: function (e) { fail(e.message); }
    });

    loop.start();

    var spinBtn = document.getElementById('spin');
    spinBtn.addEventListener('click', function () {
      spinning = !spinning;
      spinBtn.textContent = spinning ? '暂停旋转' : '继续旋转';
    });
  }).catch(function (e) {
    fail(e && e.message ? e.message : String(e));
  });
})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.

More luma.gl examples

luma.gl · EXAMPLETexture Mapping luma.gl · EXAMPLEBuffer Update luma.gl · EXAMPLEClear Triangle