Atlaslog
Atlaslog / Examples / luma.gl / Texture Mapping
luma.gl · Runnable Example

Texture Mapping

Bind a texture, sample it in a fragment shader, and handle UV coordinates.

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

Source

demos/lumagl-texture-mapping.html
<!--
  说明:用 JS 生成的棋盘格像素写入纹理,矩形表面按 UV 采样,采样器参数可切换
  核心逻辑:Texture.writeData 上传像素,Model 的 bindings 绑定 sampler2D,着色器里 texture() 取值
  依赖:@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>64 × 64</b> · 格式 <b>rgba8unorm</b></span>
  <span>采样 <b id="mode">repeat</b></span>
  <button id="pattern">换图案</button>
  <button id="wrap">切换为 clamp-to-edge</button>
  <span class="sp"></span>
  <span>UV 取 0–2,边缘模式决定接缝处的表现</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 SZ = 64;
  var seed = 0;
  var pixels = new Uint8Array(SZ * SZ * 4);

  function paint() {
    for (var y = 0; y < SZ; y++) {
      for (var x = 0; x < SZ; x++) {
        var i = (y * SZ + x) * 4;
        var cell = ((x >> 3) + (y >> 3)) & 1;
        /* 每八像素一道暗线,放大后能看清纹素边界 */
        var edge = (x % 8 === 0 || y % 8 === 0) ? 0.62 : 1;
        var glow = 0.5 + 0.5 * Math.sin((x + y + seed * 7) * 0.09);
        pixels[i]     = Math.round((cell ? 232 : 52) * edge);
        pixels[i + 1] = Math.round((cell ? 186 : 118) * edge * (0.7 + 0.3 * glow));
        pixels[i + 2] = Math.round((cell ? 84 : 214) * edge);
        pixels[i + 3] = 255;
      }
    }
  }

  paint();

  var positions = new Float32Array([
    -0.72, -0.72, 0,
     0.72, -0.72, 0,
     0.72,  0.72, 0,
    -0.72,  0.72, 0
  ]);

  /* UV 超出 0–1,repeat 会平铺两次,clamp 会把边缘拉成色带 */
  var uvs = new Float32Array([
    0, 0,
    2, 0,
    2, 2,
    0, 2
  ]);

  var indices = new Uint16Array([0, 1, 2, 0, 2, 3]);

  luma.luma.createDevice({
    type: 'webgl',
    adapters: [luma.webgl2Adapter],
    createCanvasContext: {
      canvas: cv,
      useDevicePixels: Math.min(window.devicePixelRatio, 2),
      autoResize: true
    }
  }).then(function (device) {
    var posBuffer = device.createBuffer({
      data: positions,
      usage: luma.Buffer.VERTEX
    });

    var uvBuffer = device.createBuffer({
      data: uvs,
      usage: luma.Buffer.VERTEX
    });

    var idxBuffer = device.createBuffer({
      data: indices,
      usage: luma.Buffer.INDEX,
      indexType: 'uint16'
    });

    var tex = device.createTexture({
      id: 'checker',
      width: SZ,
      height: SZ,
      format: 'rgba8unorm',
      usage: luma.Texture.SAMPLE | luma.Texture.COPY_DST,
      sampler: {
        minFilter: 'linear',
        magFilter: 'linear',
        mipmapFilter: 'none',
        addressModeU: 'repeat',
        addressModeV: 'repeat'
      }
    });

    /* 首次上传整张贴图,之后换图案只需再写一次 */
    tex.writeData(pixels);

    var model = new luma.Model(device, {
      id: 'textured-quad',
      topology: 'triangle-list',
      vertexCount: 4,
      indexCount: 6,
      indexBuffer: idxBuffer,
      attributes: {
        positions: posBuffer,
        uvs: uvBuffer
      },
      bufferLayout: [
        { name: 'positions', stepMode: 'vertex', format: 'float32x3' },
        { name: 'uvs', stepMode: 'vertex', format: 'float32x2' }
      ],
      bindings: {
        uTexture: tex
      },
      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 uTexture;',
        'out vec4 fragColor;',
        'void main() {',
        '  fragColor = texture(uTexture, vUV);',
        '}'
      ].join('\n')
    });

    var loop = new luma.AnimationLoop({
      device: device,
      onRender: function (props) {
        var pass = props.device.beginRenderPass({ clearColor: [0.04, 0.06, 0.09, 1] });
        model.draw(pass);
        pass.end();
      },
      onError: function (e) { fail(e.message); }
    });

    loop.start();

    function samplerProps(wrapMode) {
      return {
        minFilter: 'linear',
        magFilter: 'linear',
        mipmapFilter: 'none',
        addressModeU: wrapMode,
        addressModeV: wrapMode
      };
    }

    document.getElementById('pattern').addEventListener('click', function () {
      seed++;
      paint();
      tex.writeData(pixels);
      loop.setNeedsRedraw('pattern');
    });

    var wrapMode = 'repeat';
    var modeEl = document.getElementById('mode');
    var wrapBtn = document.getElementById('wrap');

    wrapBtn.addEventListener('click', function () {
      wrapMode = wrapMode === 'repeat' ? 'clamp-to-edge' : 'repeat';
      /* 采样器挂在纹理上,替换后立即生效,不必重建模型 */
      tex.setSampler(samplerProps(wrapMode));
      modeEl.textContent = wrapMode;
      wrapBtn.textContent = wrapMode === 'repeat' ? '切换为 clamp-to-edge' : '切换为 repeat';
      loop.setNeedsRedraw('sampler');
    });
  }).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 · EXAMPLEBuffer Update luma.gl · EXAMPLEClear Triangle luma.gl · EXAMPLEColor Triangle