【问题标题】:WebGL/OpenGL text labeling animated instanced shapesWebGL/OpenGL 文本标记动画实例形状
【发布时间】:2021-02-13 12:04:46
【问题描述】:

我正在使用实例化在平面中渲染可变数量的圆圈,这些圆圈具有可变的大小、颜色和位置。我希望达到 10k-100k 圈/标签的数量级。

    in float instanceSize;
    in vec3 instanceColor;
    in vec2 instanceCenter;

支持instanceCenter 属性的缓冲区每帧都会发生变化,为圆圈设置动画,但其余大部分是静态的。

我每个圆有一个四边形,我正在片段着色器中创建圆。

现在我正在研究用字体大小与圆圈大小成比例的标签来标记形状,以圆圈为中心,随着圆圈移动。从我读到的最有效的方法是使用位图纹理图集或有符号距离场纹理图集为每个字母使用带有四边形的字形纹理。我见过的示例似乎在 Javascript 方面做了很多工作,然后对每个字符串使用了一个绘图调用,例如:https://webgl2fundamentals.org/webgl/lessons/webgl-text-glyphs.html

有没有办法通过一次绘制调用(使用实例化或其他方式?)呈现文本,同时重用 Float32Array 支持 instanceCenter 每一帧?似乎需要在着色器中完成更多工作,但我不确定如何。因为每个标签都有可变数量的字形,所以我不确定如何将单个 instanceCenter 与单个标签相关联。

除此之外,基本上我想知道如何将文本集中在一个点上?

任何帮助表示赞赏

【问题讨论】:

    标签: webgl opengl-es-2.0 twgl.js


    【解决方案1】:

    在我的脑海中,您可以将消息存储在纹理中,并为每个实例添加消息 texcoord 和长度。然后,您可以计算在顶点着色器中绘制消息所需的矩形大小,并使用它来居中。

    attribute float msgLength;
    attribute vec2 msgTexCoord;
    ...
    
    widthOfQuad = max(minSizeForCircle, msgLength * glphyWidth)
    
    

    在片段着色器中,从纹理中读取消息并使用它查找字形(基于图像或基于 SDF)。

    varying vec2 v_msgTexCoord;  // passed in from vertex shader
    varying float v_msgLength;   // passed in from vertex shader
    varying vec2 uv;             // uv that goes 0 to 1 across quad
    
    float glyphIndex = texture2D(
         messageTexture,
         v_msgTexCoord + vec2(uv.x * v_msgLength / widthOfMessageTexture)).r;
    
    // now convert glyphIndex to tex coords to look up glyph in glyph texture
    
    glyphUV = (up to you)
    
    textColor = texture2D(glyphTexture, 
       glyphUV + glyphSize * vec2(fract(uv.x * v_msgLength), uv.v) / glyphTextureSize);
    

    或者类似的东西。我不知道它会有多慢

    async function main() {
      const gl = document.querySelector('canvas').getContext('webgl');
      twgl.addExtensionsToContext(gl);
    
      function convertToGlyphIndex(c) {
        c = c.toUpperCase();
        if (c >= 'A' && c <= 'Z') {
          return c.charCodeAt(0) - 0x41;
        } else if (c >= '0' && c <= '9') {
          return c.charCodeAt(0) - 0x30 + 26;
        } else {
          return 255;
        }
      }
    
      const messages = [
        'pinapple',
        'grape',
        'banana',
        'strawberry',
      ];
      
      const glyphImg = await loadImage("https://webglfundamentals.org/webgl/resources/8x8-font.png");
    
      const glyphTex = twgl.createTexture(gl, {
        src: glyphImg,
        minMag: gl.NEAREST,
      });
      // being lazy about size, making them all the same.
      const glyphsAcross = 8;
    
      // too lazy to pack these in a texture in a more compact way
      // so just put one message per row
      const longestMsg = Math.max(...messages.map(m => m.length));
      const messageData = new Uint8Array(longestMsg * messages.length * 4);
      messages.forEach((message, row) => {
        for (let i = 0; i < message.length; ++i) {
          const c = convertToGlyphIndex(message[i]);
          const offset = (row * longestMsg + i) * 4; 
          const u = c % glyphsAcross;
          const v = c / glyphsAcross | 0;
          messageData[offset + 0] = u;
          messageData[offset + 1] = v;
        }
      });
    
      const messageTex = twgl.createTexture(gl, {
        src: messageData,
        width: longestMsg,
        height: messages.length,
        minMag: gl.NEAREST,
      });
    
      const vs = `
      attribute vec4 position;  // a centered quad (-1 + 1)
      attribute vec2 texcoord;
      attribute float messageLength;  // instanced
      attribute vec4 center;          // instanced
      attribute vec2 messageUV;       // instanced
    
      uniform vec2 glyphDrawSize;
    
      varying vec2 v_texcoord;
      varying vec2 v_messageUV;
      varying float v_messageLength;
    
      void main() {
        vec2 size = vec2(messageLength * glyphDrawSize.x, glyphDrawSize.y);
        gl_Position = position * vec4(size, 1, 0) + center;
        v_texcoord = texcoord;
        v_messageUV = messageUV;
        v_messageLength = messageLength;
      }
      `;
    
      const fs = `
      precision highp float;
    
      varying vec2 v_texcoord;
      varying vec2 v_messageUV;
      varying float v_messageLength;
    
      uniform sampler2D messageTex;
      uniform vec2 messageTexSize;
    
      uniform sampler2D glyphTex;
      uniform vec2 glyphTexSize;
    
      uniform vec2 glyphSize;
    
      void main() {
        vec2 msgUV = v_messageUV + vec2(v_texcoord.x * v_messageLength / messageTexSize.x, 0);
        vec2 glyphOffset = texture2D(messageTex, msgUV).xy * 255.0;
        vec2 glyphsAcrossDown = glyphTexSize / glyphSize;
        vec2 glyphUVOffset = glyphOffset / glyphsAcrossDown;
        vec2 glyphUV = fract(v_texcoord * vec2(v_messageLength, 1)) * glyphSize / glyphTexSize;
    
        vec4 glyphColor = texture2D(glyphTex, glyphUVOffset + glyphUV);
    
        // do some math here for a circle
        // TBD
    
        if (glyphColor.a < 0.1) discard;
    
        gl_FragColor = glyphColor;
      }
      `;
    
      const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
    
      const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
        position: {
          numComponents: 2,
          data: [
            -1, -1,
             1, -1,
            -1,  1,
            -1,  1,
             1, -1,
             1,  1,
          ],
        },
        texcoord: [
           0, 1,
           1, 1,
           0, 0,
           0, 0,
           1, 1,
           1, 0,
        ],
        center: {
          numComponents: 2,
          divisor: 1,
          data: [
            -0.4, 0.1,
            -0.3, -0.5,
             0.6, 0,
             0.1, 0.5,
          ],
        },
        messageLength: {
          numComponents: 1,
          divisor: 1,
          data: messages.map(m => m.length),
        },
        messageUV: {
          numComponents: 2, 
          divisor: 1,
          data: messages.map((m, i) => [0, i / messages.length]).flat(),
        },
      });
      
      gl.clearColor(0, 0, 1, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
    
      gl.useProgram(prgInfo.program);
    
      twgl.setBuffersAndAttributes(gl, prgInfo, bufferInfo);
      twgl.setUniformsAndBindTextures(prgInfo, {
        glyphDrawSize: [16 / gl.canvas.width, 16 / gl.canvas.height],
        messageTex,
        messageTexSize: [longestMsg, messages.length],
        glyphTex,
        glyphTexSize: [glyphImg.width, glyphImg.height],
        glyphSize: [8, 8],
      });
      // ext.drawArraysInstancedANGLE(gl.TRIANGLES, 0, 6, messages.length);
      gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, messages.length);
    }
    
    function loadImage(url) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.crossOrigin = "anonymous";
        img.onerror = reject;
        img.onload = () => resolve(img);
        img.src = url;
      });
    }
    main();
    <canvas></canvas>
    <script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>

    请注意,如果字形大小不同,它似乎会变得非常慢,至少在我的脑海中,在绘制四边形时找到每个字形的唯一方法是循环遍历所有字形每个像素的消息。

    另一方面,您可以构建一个类似于the article 的字形网格,对于每条消息,对于该消息中的每个字形,添加一个每个顶点的消息 id 或消息 uv,用于查找偏移量或矩阵从纹理。通过这种方式,您可以独立移动每条消息,但可以在一次绘图调用中完成所有操作。这个会 允许非等宽字形。作为在纹理中存储位置或矩阵的示例,请参阅this article on skinning。它将骨骼矩阵存储在纹理中。

    async function main() {
      const gl = document.querySelector('canvas').getContext('webgl');
      const ext = gl.getExtension('OES_texture_float');
      if (!ext) {
        alert('need OES_texture_float');
        return;
      }
      twgl.addExtensionsToContext(gl);
    
      function convertToGlyphIndex(c) {
        c = c.toUpperCase();
        if (c >= 'A' && c <= 'Z') {
          return c.charCodeAt(0) - 0x41;
        } else if (c >= '0' && c <= '9') {
          return c.charCodeAt(0) - 0x30 + 26;
        } else {
          return 255;
        }
      }
    
      const messages = [
        'pinapple',
        'grape',
        'banana',
        'strawberry',
      ];
    
      const glyphImg = await loadImage("https://webglfundamentals.org/webgl/resources/8x8-font.png");
    
      const glyphTex = twgl.createTexture(gl, {
        src: glyphImg,
        minMag: gl.NEAREST,
      });
      // being lazy about size, making them all the same.
      const glyphsAcross = 8;
      const glyphsDown = 5;
      const glyphWidth = glyphImg.width / glyphsAcross;
      const glyphHeight = glyphImg.height / glyphsDown;
      const glyphUWidth = glyphWidth / glyphImg.width;
      const glyphVHeight = glyphHeight / glyphImg.height;
    
      // too lazy to pack these in a texture in a more compact way
      // so just put one message per row
      const positions = [];
      const texcoords = [];
      const messageIds = [];
      const matrixData = new Float32Array(messages.length * 16);
      const msgMatrices = [];
      const quadPositions = [
         -1, -1,
          1, -1,
         -1,  1,
         -1,  1,
          1, -1,
          1,  1,
      ];
      const quadTexcoords = [
          0,  1,
          1,  1,
          0,  0,
          0,  0,
          1,  1,
          1,  0,
      ];
      messages.forEach((message, id) => {
        msgMatrices.push(matrixData.subarray(id * 16, (id + 1) * 16));
        
        for (let i = 0; i < message.length; ++i) {
          const c = convertToGlyphIndex(message[i]);
          const u = (c % glyphsAcross) * glyphUWidth;
          const v = (c / glyphsAcross | 0) * glyphVHeight;
          for (let j = 0; j < 6; ++j) {
            const offset = j * 2;
            positions.push(
              quadPositions[offset    ] * 0.5 + i - message.length / 2,
              quadPositions[offset + 1] * 0.5,
            );
            texcoords.push(
              u + quadTexcoords[offset    ] * glyphUWidth,
              v + quadTexcoords[offset + 1] * glyphVHeight,
            );
            messageIds.push(id);
          }
        }
      });
    
      const matrixTex = twgl.createTexture(gl, {
        src: matrixData,
        type: gl.FLOAT,
        width: 4,
        height: messages.length,
        minMag: gl.NEAREST,
        wrap: gl.CLAMP_TO_EDGE,
      });
    
      const vs = `
    attribute vec4 position;
    attribute vec2 texcoord;
    attribute float messageId;
    
    uniform sampler2D matrixTex;
    uniform vec2 matrixTexSize;
    uniform mat4 viewProjection;
    
    varying vec2 v_texcoord;
    
    void main() {
      vec2 uv = (vec2(0, messageId) + 0.5) / matrixTexSize;
      mat4 model = mat4(
        texture2D(matrixTex, uv),
        texture2D(matrixTex, uv + vec2(1.0 / matrixTexSize.x, 0)),
        texture2D(matrixTex, uv + vec2(2.0 / matrixTexSize.x, 0)),
        texture2D(matrixTex, uv + vec2(3.0 / matrixTexSize.x, 0)));
      gl_Position = viewProjection * model * position;
      v_texcoord = texcoord;
    }
    `;
    
      const fs = `
    precision highp float;
    
    varying vec2 v_texcoord;
    uniform sampler2D glyphTex;
    
    void main() {
      vec4 glyphColor = texture2D(glyphTex, v_texcoord);
    
      // do some math here for a circle
      // TBD
    
      if (glyphColor.a < 0.1) discard;
    
      gl_FragColor = glyphColor;
    }
    `;
    
      const prgInfo = twgl.createProgramInfo(gl, [vs, fs]);
    
      const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
        position: {
          numComponents: 2,
          data: positions,
        },
        texcoord: texcoords,
        messageId: {
          numComponents: 1,
          data: messageIds
        },
      });
    
      gl.clearColor(0, 0, 1, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
    
      gl.useProgram(prgInfo.program);
      
      const m4 = twgl.m4;
      const viewProjection = m4.ortho(0, gl.canvas.width, 0, gl.canvas.height, -1, 1);
      msgMatrices.forEach((mat, i) => {
        m4.translation([80 + i * 30, 30 + i * 25, 0], mat);
        m4.scale(mat, [16, 16, 1], mat)
      });
      
      // update the matrices
      gl.bindTexture(gl.TEXTURE_2D, matrixTex);
      gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, 4, messages.length, gl.RGBA, gl.FLOAT, matrixData);
      
      twgl.setBuffersAndAttributes(gl, prgInfo, bufferInfo);
      twgl.setUniformsAndBindTextures(prgInfo, {
        viewProjection,
        matrixTex,
        matrixTexSize: [4, messages.length],
        glyphTex,
      });
      gl.drawArrays(gl.TRIANGLES, 0, positions.length / 2);
    }
    
    function loadImage(url) {
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.crossOrigin = "anonymous";
        img.onerror = reject;
        img.onload = () => resolve(img);
        img.src = url;
      });
    }
    main();
    <canvas></canvas>
    <script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

    另见https://stackoverflow.com/a/54720138/128511

    【讨论】:

    • 太棒了.. 需要研究一段时间才能完全掌握它。我确实想到了其他一些解决方案,我想知道您对此有何想法。我想到的最简单的事情就是将支持instanceCenter的缓冲区元素复制适当的次数。基本上,解决实例除数不能变化的事实(afaik)。我可能只是将性能与您的解决方案进行比较,但从直觉上看,您的解决方案似乎在片段着色器中做了很多工作?
    猜你喜欢
    • 1970-01-01
    • 2016-07-29
    • 1970-01-01
    • 2018-05-13
    • 2013-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多