鍍金池/ 教程/ HTML/ WebGL 文本 使用字符紋理
WebGL 文本 HTML
WebGL 文本 Canvas 2D
WebGL 2D 圖像旋轉(zhuǎn)
WebGL 圖像處理(續(xù))
WebGL 2D 矩陣
WebGL 繪制多個東西
WebGL 圖像處理
WebGL 2D 圖像轉(zhuǎn)換
WebGL 3D 透視
WebGL 是如何工作的
WebGL 文本 紋理
WebGL 2D 圖像伸縮
WebGL 場景圖
WebGL 3D 攝像機(jī)
WebGL 文本 使用字符紋理
WebGL 正交 3D
WebGL 基本原理
WebGL - 更少的代碼,更多的樂趣
WebGL 著色器和 GLSL

WebGL 文本 使用字符紋理

在上一篇文章中我們復(fù)習(xí)了在 WebGL 場景中如何使用紋理繪制文本。技術(shù)是很常見的,對一些事物也是極重要的,例如在多人游戲中你想在一個頭像上放置一個名字。同時這個名字也不能影響它的完美性。

比方說你想呈現(xiàn)大量的文本,這需要經(jīng)常改變 UI 之類的事物。前一篇文章給出的最后一個例子中,一個明顯的解決方案是給每個字母加紋理。我們來嘗試一下改變上一個例子。

var names = [
  "anna",   // 0
  "colin",  // 1
  "james",  // 2
  "danny",  // 3
  "kalin",  // 4
  "hiro",   // 5
  "eddie",  // 6
  "shu",// 7
  "brian",  // 8
  "tami",   // 9
  "rick",   // 10
  "gene",   // 11
  "natalie",// 12,
  "evan",   // 13,
  "sakura", // 14,
  "kai",// 15,
];

// create text textures, one for each letter
var textTextures = [
  "a",// 0
  "b",// 1
  "c",// 2
  "d",// 3
  "e",// 4
  "f",// 5
  "g",// 6
  "h",// 7
  "i",// 8
  "j",// 9
  "k",// 10
  "l",// 11
  "m",// 12,
  "n",// 13,
  "o",// 14,
  "p",// 14,
  "q",// 14,
  "r",// 14,
  "s",// 14,
  "t",// 14,
  "u",// 14,
  "v",// 14,
  "w",// 14,
  "x",// 14,
  "y",// 14,
  "z",// 14,
].map(function(name) {
  var textCanvas = makeTextCanvas(name, 10, 26);

相對于為每個名字呈現(xiàn)一個四元組,我們將為每個名字的每個字母呈現(xiàn)一個四元組。

// setup to draw the text.
// Because every letter uses the same attributes and the same progarm
// we only need to do this once.
gl.useProgram(textProgramInfo.program);
setBuffersAndAttributes(gl, textProgramInfo.attribSetters, textBufferInfo);

textPositions.forEach(function(pos, ndx) {
  var name = names[ndx];
  // for each leter
  for (var ii = 0; ii < name.length; ++ii) {
var letter = name.charCodeAt(ii);
var letterNdx = letter - "a".charCodeAt(0);
// select a letter texture
var tex = textTextures[letterNdx];

// use just the position of the 'F' for the text

// because pos is in view space that means it's a vector from the eye to
// some position. So translate along that vector back toward the eye some distance
var fromEye = normalize(pos);
var amountToMoveTowardEye = 150;  // because the F is 150 units long
var viewX = pos[0] - fromEye[0] * amountToMoveTowardEye;
var viewY = pos[1] - fromEye[1] * amountToMoveTowardEye;
var viewZ = pos[2] - fromEye[2] * amountToMoveTowardEye;
var desiredTextScale = -1 / gl.canvas.height;  // 1x1 pixels
var scale = viewZ * desiredTextScale;

var textMatrix = makeIdentity();
textMatrix = matrixMultiply(textMatrix, makeTranslation(ii, 0, 0));
textMatrix = matrixMultiply(textMatrix, makeScale(tex.width * scale, tex.height * scale, 1));
textMatrix = matrixMultiply(textMatrix, makeTranslation(viewX, viewY, viewZ));
textMatrix = matrixMultiply(textMatrix, projectionMatrix);

// set texture uniform
textUniforms.u_texture = tex.texture;
copyMatrix(textMatrix, textUniforms.u_matrix);
setUniforms(textProgramInfo.uniformSetters, textUniforms);

// Draw the text.
gl.drawElements(gl.TRIANGLES, textBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
  }
});

你可以看到它是如何工作的:

不幸的是它很慢。下面的例子:單獨(dú)繪制 73 個四元組,還看不出來差別。我們計算 73 個矩陣和 292 個矩陣倍數(shù)。一個典型的 UI 可能有 1000 個字母要顯示。這是眾多工作可以得到一個合理的幀速率的方式。

解決這個問題通常的方法是構(gòu)造一個紋理圖譜,其中包含所有的字母。我們討論給立方體的 6 面加紋理時,復(fù)習(xí)了紋理圖譜。

下面的代碼構(gòu)造了字符的紋理圖譜。

function makeGlyphCanvas(ctx, maxWidthOfTexture, heightOfLetters, baseLine, padding, letters) {
  var rows = 1;  // number of rows of glyphs
  var x = 0; // x position in texture to draw next glyph
  var y = 0; // y position in texture to draw next glyph
  var glyphInfos = { // info for each glyph
  };

  // Go through each letter, measure it, remember its width and position
  for (var ii = 0; ii < letters.length; ++ii) {
var letter = letters[ii];
var t = ctx.measureText(letter);
// Will this letter fit on this row?
if (x + t.width + padding > maxWidthOfTexture) {
   // so move to the start of the next row
   x = 0;
   y += heightOfLetters;
   ++rows;
}
// Remember the data for this letter
glyphInfos[letter] = {
  x: x,
  y: y,
  width: t.width,
};
// advance to space for next letter.
x += t.width + padding;
  }

  // Now that we know the size we need set the size of the canvas
  // We have to save the canvas settings because changing the size
  // of a canvas resets all the settings
  var settings = saveProperties(ctx);
  ctx.canvas.width = (rows == 1) ? x : maxWidthOfTexture;
  ctx.canvas.height = rows * heightOfLetters;
  restoreProperties(settings, ctx);

  // Draw the letters into the canvas
  for (var ii = 0; ii < letters.length; ++ii) {
var letter = letters[ii];
var glyphInfo = glyphInfos[letter];
var t = ctx.fillText(letter, glyphInfo.x, glyphInfo.y + baseLine);
  }

  return glyphInfos;
}

現(xiàn)在我們試試看:

var ctx = document.createElement("canvas").getContext("2d");
ctx.font = "20px sans-serif";
ctx.fillStyle = "white";
var maxTextureWidth = 256;
var letterHeight = 22;
var baseline = 16;
var padding = 1;
var letters = "0123456789.abcdefghijklmnopqrstuvwxyz";
var glyphInfos = makeGlyphCanvas(
ctx,
maxTextureWidth,
letterHeight,
baseline,
padding,
letters);

結(jié)果如下

現(xiàn)在,我們已經(jīng)創(chuàng)建了一個我們需要使用的字符紋理??纯葱Ч鯓?,我們?yōu)槊總€字符建四個頂點(diǎn)。這些頂點(diǎn)將使用紋理坐標(biāo)來選擇特殊的字符。

給定一個字符串,來建立頂點(diǎn):

function makeVerticesForString(fontInfo, s) {
  var len = s.length;
  var numVertices = len * 6;
  var positions = new Float32Array(numVertices * 2);
  var texcoords = new Float32Array(numVertices * 2);
  var offset = 0;
  var x = 0;
  for (var ii = 0; ii < len; ++ii) {
var letter = s[ii];
var glyphInfo = fontInfo.glyphInfos[letter];
if (glyphInfo) {
  var x2 = x + glyphInfo.width;
  var u1 = glyphInfo.x / fontInfo.textureWidth;
  var v1 = (glyphInfo.y + fontInfo.letterHeight) / fontInfo.textureHeight;
  var u2 = (glyphInfo.x + glyphInfo.width) / fontInfo.textureWidth;
  var v2 = glyphInfo.y / fontInfo.textureHeight;

  // 6 vertices per letter
  positions[offset + 0] = x;
  positions[offset + 1] = 0;
  texcoords[offset + 0] = u1;
  texcoords[offset + 1] = v1;

  positions[offset + 2] = x2;
  positions[offset + 3] = 0;
  texcoords[offset + 2] = u2;
  texcoords[offset + 3] = v1;

  positions[offset + 4] = x;
  positions[offset + 5] = fontInfo.letterHeight;
  texcoords[offset + 4] = u1;
  texcoords[offset + 5] = v2;

  positions[offset + 6] = x;
  positions[offset + 7] = fontInfo.letterHeight;
  texcoords[offset + 6] = u1;
  texcoords[offset + 7] = v2;

  positions[offset + 8] = x2;
  positions[offset + 9] = 0;
  texcoords[offset + 8] = u2;
  texcoords[offset + 9] = v1;

  positions[offset + 10] = x2;
  positions[offset + 11] = fontInfo.letterHeight;
  texcoords[offset + 10] = u2;
  texcoords[offset + 11] = v2;

  x += glyphInfo.width;
  offset += 12;
} else {
  // we don't have this character so just advance
  x += fontInfo.spaceWidth;
}
  }

  // return ArrayBufferViews for the portion of the TypedArrays
  // that were actually used.
  return {
arrays: {
  position: new Float32Array(positions.buffer, 0, offset),
  texcoord: new Float32Array(texcoords.buffer, 0, offset),
},
numVertices: offset / 2,
  };
}

為了使用它,我們手動創(chuàng)建一個 bufferInfo。(如果你已經(jīng)不記得了,可以查看前面的文章:bufferInfo 是什么)。

// Maunally create a bufferInfo
var textBufferInfo = {
  attribs: {
a_position: { buffer: gl.createBuffer(), numComponents: 2, },
a_texcoord: { buffer: gl.createBuffer(), numComponents: 2, },
  },
  numElements: 0,
};

使用 bufferInfo 中的字符創(chuàng)建畫布的 fontInfo 和紋理:

var ctx = document.createElement("canvas").getContext("2d");
ctx.font = "20px sans-serif";
ctx.fillStyle = "white";
var maxTextureWidth = 256;
var letterHeight = 22;
var baseline = 16;
var padding = 1;
var letters = "0123456789.,abcdefghijklmnopqrstuvwxyz";
var glyphInfos = makeGlyphCanvas(
ctx,
maxTextureWidth,
letterHeight,
baseline,
padding,
letters);
var fontInfo = {
  glyphInfos: glyphInfos,
  letterHeight: letterHeight,
  baseline: baseline,
  spaceWidth: 5,
  textureWidth: ctx.canvas.width,
  textureHeight: ctx.canvas.height,
};

然后渲染我們將更新緩沖的文本。我們也可以構(gòu)成動態(tài)的文本:

textPositions.forEach(function(pos, ndx) {

  var name = names[ndx];
  var s = name + ":" + pos[0].toFixed(0) + "," + pos[1].toFixed(0) + "," + pos[2].toFixed(0);
  var vertices = makeVerticesForString(fontInfo, s);

  // update the buffers
  textBufferInfo.attribs.a_position.numComponents = 2;
  gl.bindBuffer(gl.ARRAY_BUFFER, textBufferInfo.attribs.a_position.buffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices.arrays.position, gl.DYNAMIC_DRAW);
  gl.bindBuffer(gl.ARRAY_BUFFER, textBufferInfo.attribs.a_texcoord.buffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices.arrays.texcoord, gl.DYNAMIC_DRAW);

  setBuffersAndAttributes(gl, textProgramInfo.attribSetters, textBufferInfo);

  // use just the position of the 'F' for the text
  var textMatrix = makeIdentity();
  // because pos is in view space that means it's a vector from the eye to
  // some position. So translate along that vector back toward the eye some distance
  var fromEye = normalize(pos);
  var amountToMoveTowardEye = 150;  // because the F is 150 units long
  textMatrix = matrixMultiply(textMatrix, makeTranslation(
  pos[0] - fromEye[0] * amountToMoveTowardEye,
  pos[1] - fromEye[1] * amountToMoveTowardEye,
  pos[2] - fromEye[2] * amountToMoveTowardEye));
  textMatrix = matrixMultiply(textMatrix, projectionMatrix);

  // set texture uniform
  copyMatrix(textMatrix, textUniforms.u_matrix);
  setUniforms(textProgramInfo.uniformSetters, textUniforms);

  // Draw the text.
  gl.drawArrays(gl.TRIANGLES, 0, vertices.numVertices);
});

即:

這是使用字符紋理集的基本技術(shù)??梢蕴砑右恍┟黠@的東西或方式來改進(jìn)它。

  • 重用相同的數(shù)組。
    目前,每次被調(diào)用時,makeVerticesForString 就會分配新的 32 位浮點(diǎn)型數(shù)組。這最終可能會導(dǎo)致垃圾收集出現(xiàn)問題。重用相同的數(shù)組可能會更好。如果不是足夠大,你也放大數(shù)組,但是保留原來的大小。
  • 添加支持回車
    當(dāng)生成頂點(diǎn)時,檢查 \n 是否存在從而實(shí)現(xiàn)換行。這將使文本分隔段落更容易。
  • 添加對各種格式的支持。
    如果你想文本居中,或調(diào)整你添加的一切文本的格式。
  • 添加對頂點(diǎn)顏色的支持。
    你可以為文本的每個字母添加不同的顏色。當(dāng)然你必須決定如何指定何時改變顏色。

這里不打算涉及的另一個大問題是:紋理大小有限,但字體實(shí)際上是無限的。如果你想支持所有的 unicode,你就必須處理漢語、日語和阿拉伯語等其他所有語言,2015 年在 unicode 有超過 110000 個符號!你不可能在紋理中適配所有這些,也沒有足夠的空間供你這樣做。

操作系統(tǒng)和瀏覽器 GPU 加速處理這個問題的方式是:通過使用一個字符紋理緩存實(shí)現(xiàn)。上面的實(shí)現(xiàn)他們是把紋理處理成紋理集,但他們?yōu)槊總€ glpyh 布置一個固定大小的區(qū)域,保留紋理集中最近使用的符號。如果需要繪制一個字符,而這個字符不在紋理集中,他們就用他們需要的這個新的字符取代最近最少使用的一個。當(dāng)然如果他們即將取代的字符仍被有待繪制的四元組引用,他們需要繪制他們之前所取代的字符。

雖然我不推薦它,但是還有另一件事你可以做,將這項技術(shù)和以前的技術(shù)結(jié)合在一起。你可以直接渲染另一種紋理的符號。當(dāng)然 GPU 加速畫布已經(jīng)這樣做了,你可能沒有自己動手的理由。

另一種在 WebGL 中繪制文本的方法實(shí)際上是使用了 3D 文本。在上面所有的例子中 “F” 是一個 3D 的字母。你已經(jīng)為每個字母都構(gòu)成了一個相應(yīng)的 3D 字符。3D 字母常見于標(biāo)題和電影標(biāo)志,此外的用處就少了。

我希望在 WebGL 這可以覆蓋文本。