WebGL/OpenGL 文本标签动画实例形状

WebGL/OpenGL text labeling animated instanced shapes

我正在使用实例化在平面上渲染可变大小、颜色和位置的可变数量的圆圈。我希望达到 10k-100k 的数量级 circles/labels。

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

支持 instanceCenter 属性的缓冲区每帧都会更改,使圆圈动画化,但其余大部分是静态的。

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

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

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

除此之外,更重要的是,我想知道如何将文本居中于某一点?

感谢任何帮助

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

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从纹理向上偏移或矩阵。通过这种方式,您可以独立移动每条消息,但在一次绘制调用中全部发生。这个会 允许 non-monospaced 字形。作为在纹理中存储位置或矩阵的示例,请参见 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>

另见