在上一篇文章中我們復(fù)習(xí)了在 WebGL 場(chǎng)景中如何使用紋理繪制文本。技術(shù)是很常見的,對(duì)一些事物也是極重要的,例如在多人游戲中你想在一個(gè)頭像上放置一個(gè)名字。同時(shí)這個(gè)名字也不能影響它的完美性。
比方說你想呈現(xiàn)大量的文本,這需要經(jīng)常改變 UI 之類的事物。前一篇文章給出的最后一個(gè)例子中,一個(gè)明顯的解決方案是給每個(gè)字母加紋理。我們來嘗試一下改變上一個(gè)例子。
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);
相對(duì)于為每個(gè)名字呈現(xiàn)一個(gè)四元組,我們將為每個(gè)名字的每個(gè)字母呈現(xiàn)一個(gè)四元組。
// 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 個(gè)四元組,還看不出來差別。我們計(jì)算 73 個(gè)矩陣和 292 個(gè)矩陣倍數(shù)。一個(gè)典型的 UI 可能有 1000 個(gè)字母要顯示。這是眾多工作可以得到一個(gè)合理的幀速率的方式。
解決這個(gè)問題通常的方法是構(gòu)造一個(gè)紋理圖譜,其中包含所有的字母。我們討論給立方體的 6 面加紋理時(shí),復(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)在我們?cè)囋嚳矗?/p>
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)建了一個(gè)我們需要使用的字符紋理??纯葱Ч鯓?,我們?yōu)槊總€(gè)字符建四個(gè)頂點(diǎn)。這些頂點(diǎn)將使用紋理坐標(biāo)來選擇特殊的字符。
給定一個(gè)字符串,來建立頂點(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,
};
}
為了使用它,我們手動(dòng)創(chuàng)建一個(gè) 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)成動(dòng)態(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)它。
這里不打算涉及的另一個(gè)大問題是:紋理大小有限,但字體實(shí)際上是無限的。如果你想支持所有的 unicode,你就必須處理漢語、日語和阿拉伯語等其他所有語言,2015 年在 unicode 有超過 110000 個(gè)符號(hào)!你不可能在紋理中適配所有這些,也沒有足夠的空間供你這樣做。
操作系統(tǒng)和瀏覽器 GPU 加速處理這個(gè)問題的方式是:通過使用一個(gè)字符紋理緩存實(shí)現(xiàn)。上面的實(shí)現(xiàn)他們是把紋理處理成紋理集,但他們?yōu)槊總€(gè) glpyh 布置一個(gè)固定大小的區(qū)域,保留紋理集中最近使用的符號(hào)。如果需要繪制一個(gè)字符,而這個(gè)字符不在紋理集中,他們就用他們需要的這個(gè)新的字符取代最近最少使用的一個(gè)。當(dāng)然如果他們即將取代的字符仍被有待繪制的四元組引用,他們需要繪制他們之前所取代的字符。
雖然我不推薦它,但是還有另一件事你可以做,將這項(xiàng)技術(shù)和以前的技術(shù)結(jié)合在一起。你可以直接渲染另一種紋理的符號(hào)。當(dāng)然 GPU 加速畫布已經(jīng)這樣做了,你可能沒有自己動(dòng)手的理由。
另一種在 WebGL 中繪制文本的方法實(shí)際上是使用了 3D 文本。在上面所有的例子中 “F” 是一個(gè) 3D 的字母。你已經(jīng)為每個(gè)字母都構(gòu)成了一個(gè)相應(yīng)的 3D 字符。3D 字母常見于標(biāo)題和電影標(biāo)志,此外的用處就少了。
更多建議: