在上一篇文章中我們學(xué)習(xí)了在 WebGL 場景中如何使用一個 2D 畫布繪制文本。這個技術(shù)可以工作且很容易做到,但它有一個限制,即文本不能被其他的 3D 對象遮蓋。要做到這一點,我們實際上需要在 WebGL 中繪制文本。
最簡單的方法是繪制帶有文本的紋理。例如你可以使用 photoshop 或其他繪畫程序,來繪制帶有文本的一些圖像。
然后我們構(gòu)造一些平面幾何并顯示它。這實際上是一些游戲中構(gòu)造所有的文本的方式。例如 Locoroco 只有大約 270 個字符串。它本地化成 17 種語言。我們有一個包含所有語言的 Excel 表和一個腳本,該腳本將啟動 Photoshop 并生成紋理,每個紋理都對應(yīng)一種語言里的一個消息。
當(dāng)然你也可以在運行時生成紋理。因為在瀏覽器中 WebGL 是依靠畫布 2d api 來幫助生成紋理的。
我們來看上一篇文章的例子,在其中添加一個函數(shù):用文本填補一個 2D 畫布。
var textCtx = document.createElement("canvas").getContext("2d");
// Puts text in center of canvas.
function makeTextCanvas(text, width, height) {
textCtx.canvas.width = width;
textCtx.canvas.height = height;
textCtx.font = "20px monospace";
textCtx.textAlign = "center";
textCtx.textBaseline = "middle";
textCtx.fillStyle = "black";
textCtx.clearRect(0, 0, textCtx.canvas.width, textCtx.canvas.height);
textCtx.fillText(text, width / 2, height / 2);
return textCtx.canvas;
}
現(xiàn)在我們需要在 WebGL 中繪制 2 個不同東西:“F”和文本,我想切換到使用一些前一篇文章中所描述的輔助函數(shù)。如果你還不清楚 programInfo,bufferInfo 等,你需要瀏覽那篇文章。
現(xiàn)在,讓我們創(chuàng)建一個“F”和四元組單元。
// Create data for 'F'
var fBufferInfo = primitives.create3DFBufferInfo(gl);
// Create a unit quad for the 'text'
var textBufferInfo = primitives.createPlaneBufferInfo(gl, 1, 1, 1, 1, makeXRotation(Math.PI / 2));
一個四元組單元是一個 1 單元大小的四元組(方形),中心在原點。createPlaneBufferInfo 在 xz 平面創(chuàng)建一個平面。我們通過一個矩陣旋轉(zhuǎn)它,就得到一個 xy 平面四元組單元。
接下來創(chuàng)建 2 個著色器:
// setup GLSL programs
var fProgramInfo = createProgramInfo(gl, ["3d-vertex-shader", "3d-fragment-shader"]);
var textProgramInfo = createProgramInfo(gl, ["text-vertex-shader", "text-fragment-shader"]);
創(chuàng)建我們的文本紋理:
// create text texture.
var textCanvas = makeTextCanvas("Hello!", 100, 26);
var textWidth = textCanvas.width;
var textHeight = textCanvas.height;
var textTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas);
// make sure we can render it even if it's not a power of 2
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
為“F”和文本設(shè)置 uniforms:
var fUniforms = {
u_matrix: makeIdentity(),
};
var textUniforms = {
u_matrix: makeIdentity(),
u_texture: textTex,
};
當(dāng)我們計算 F 的矩陣時,保存 F 的矩陣視圖:
var matrix = makeIdentity();
matrix = matrixMultiply(matrix, preTranslationMatrix);
matrix = matrixMultiply(matrix, scaleMatrix);
matrix = matrixMultiply(matrix, rotationZMatrix);
matrix = matrixMultiply(matrix, rotationYMatrix);
matrix = matrixMultiply(matrix, rotationXMatrix);
matrix = matrixMultiply(matrix, translationMatrix);
matrix = matrixMultiply(matrix, viewMatrix);
var fViewMatrix = copyMatrix(matrix); // remember the view matrix for the text
matrix = matrixMultiply(matrix, projectionMatrix);
像這樣繪制 F:
gl.useProgram(fProgramInfo.program);
setBuffersAndAttributes(gl, fProgramInfo.attribSetters, fBufferInfo);
copyMatrix(matrix, fUniforms.u_matrix);
setUniforms(fProgramInfo.uniformSetters, fUniforms);
// Draw the geometry.
gl.drawElements(gl.TRIANGLES, fBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
文本中我們只需要知道 F 的原點位置,我們還需要測量和單元四元組相匹配的紋理尺寸。最后,我們需要多種投影矩陣。
// scale the F to the size we need it.
// use just the view position of the 'F' for the text
var textMatrix = makeIdentity();
textMatrix = matrixMultiply(textMatrix, makeScale(textWidth, textHeight, 1));
textMatrix = matrixMultiply(
textMatrix,
makeTranslation(fViewMatrix[12], fViewMatrix[13], fViewMatrix[14]));
textMatrix = matrixMultiply(textMatrix, projectionMatrix);
然后渲染文本
// setup to draw the text.
gl.useProgram(textProgramInfo.program);
setBuffersAndAttributes(gl, textProgramInfo.attribSetters, textBufferInfo);
copyMatrix(textMatrix, textUniforms.u_matrix);
setUniforms(textProgramInfo.uniformSetters, textUniforms);
// Draw the text.
gl.drawElements(gl.TRIANGLES, textBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
即:
你會發(fā)現(xiàn)有時候我們文本的一部分遮蓋了我們 Fs 的一部分。這是因為我們繪制一個四元組。畫布的默認顏色是透明的黑色(0,0,0,0)和我們在四元組中使用這種顏色繪制。我們也可以混合像素。
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
根據(jù)混合函數(shù),將源像素(這個顏色取自片段著色器)和 目的像素(畫布顏色)結(jié)合在一起。在混合函數(shù)中,我們?yōu)樵聪袼卦O(shè)置:SRC_ALPHA,為目的像素設(shè)置:ONE_MINUS_SRC_ALPHA。
result = dest * (1 - src_alpha) + src * src_alpha
舉個例子,如果目的像素是綠色的 0,1,0,1 和源像素是紅色的 1,0,0,1,如下:
src = [1, 0, 0, 1]
dst = [0, 1, 0, 1]
src_alpha = src[3] // this is 1
result = dst * (1 - src_alpha) + src * src_alpha
// which is the same as
result = dst * 0 + src * 1
// which is the same as
result = src
對于紋理的部分內(nèi)容,使用透明的黑色 0,0,0,0
src = [0, 0, 0, 0]
dst = [0, 1, 0, 1]
src_alpha = src[3] // this is 0
result = dst * (1 - src_alpha) + src * src_alpha
// which is the same as
result = dst * 1 + src * 0
// which is the same as
result = dst
這是啟用了混合的結(jié)果。
你可以看到盡管它還不完美,但它已經(jīng)更好了。如果你仔細看,有時能看到這個問題
發(fā)生什么事情了?我們正在繪制一個 F 然后是它的文本,然后下一個 F 的重復(fù)文本。所以當(dāng)我們繪制文本時,我們?nèi)匀恍枰粋€深度緩沖,即使混合了一些像素來保持背景顏色,深度緩沖仍然需要更新。當(dāng)我們繪制下一個 F,如果 F 的部分是之前繪制文本的一些像素,他們就不會再繪制。
我們剛剛遇到的最困難的問題之一,在 GPU 上渲染 3D。透明度也存在問題。
針對幾乎所有透明呈現(xiàn)問題,最常見的解決方案是先畫出所有不透明的東西,之后,按中心距的排序,繪制所有的透明的東西,中心距的排序是在深度緩沖測試開啟但深度緩沖更新關(guān)閉的情況下得出的。
讓我們先單獨繪制透明材料(文本)中不透明材料(Fs)的部分。首先,我們要聲明一些來記錄文本的位置。
var textPositions = [];
在循環(huán)中渲染記錄位置的 Fs
matrix = matrixMultiply(matrix, viewMatrix);
var fViewMatrix = copyMatrix(matrix); // remember the view matrix for the text
textPositions.push([matrix[12], matrix[13], matrix[14]]); // remember the position for the text
在我們繪制 “F”s之前,我們禁用混合并打開寫深度緩沖
gl.disable(gl.BLEND);
gl.depthMask(true);
繪制文本時,我們將打開混合并關(guān)掉寫作深度緩沖
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.depthMask(false);
然后在我們保存的所有位置繪制文本
textPositions.forEach(function(pos) {
// draw the text
// scale the F to the size we need it.
// use just the position of the 'F' for the text
var textMatrix = makeIdentity();
textMatrix = matrixMultiply(textMatrix, makeScale(textWidth, textHeight, 1));
textMatrix = matrixMultiply(textMatrix, makeTranslation(pos[0], pos[1], pos[2]));
textMatrix = matrixMultiply(textMatrix, projectionMatrix);
// setup to draw the text.
gl.useProgram(textProgramInfo.program);
setBuffersAndAttributes(gl, textProgramInfo.attribSetters, textBufferInfo);
copyMatrix(textMatrix, textUniforms.u_matrix);
setUniforms(textProgramInfo.uniformSetters, textUniforms);
// Draw the text.
gl.drawElements(gl.TRIANGLES, textBufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
});
現(xiàn)在啟動:
請注意我們沒有像我上面提到的那樣分類。在這種情況下,因為我們繪制大部分是不透明文本,所以即使排序也沒有明顯差異,所以就省去了這一步驟,節(jié)省資源用于其他文章。
另一個問題是文本的“F”總是交叉。實際上這個問題沒有一個具體的解決方案。如果你正在構(gòu)造一個 MMO,希望每個游戲者的文本總是出現(xiàn)在你試圖使文本出現(xiàn)的頂部。只需要將之轉(zhuǎn)化為一些單元 +Y,足以確保它總是位于游戲者之上。
你也可以使之向 cameara 移動。在這里我們這樣做只是為了好玩。因為 “pos” 是在坐標(biāo)系中,意味著它是相對于眼(在坐標(biāo)系中即:0,0,0)。所以如果我們使之標(biāo)準(zhǔn)化,我們可以得到一個單位向量,這個向量的指向是從原點到某一點,我們可以乘一定數(shù)值將文本特定數(shù)量的單位靠近或遠離眼。
// 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 textMatrix = makeIdentity();
textMatrix = matrixMultiply(textMatrix, makeScale(textWidth, textHeight, 1));
textMatrix = matrixMultiply(textMatrix, makeTranslation(viewX, viewY, viewZ));
textMatrix = matrixMultiply(textMatrix, projectionMatrix);
即:
你還可能會注意到一個字母邊緣問題。
這里的問題是 Canvas2D api 只引入了自左乘 alpha 值。當(dāng)我們上傳內(nèi)容到試圖 unpremultiply 的紋理 WebGL,它就不能完全做到,這是因為自左乘 alpha 會失真。
為了解決這個問題,使 WebGL 不會 unpremultiply:
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
這告訴 WebGL 支持自左乘 alpha 值到 gl.texImage2D 和 gl.texSubImage2D。如果數(shù)據(jù)傳遞給 gl.texImage2D 已經(jīng)自左乘,就像 canvas2d 數(shù)據(jù),那么 WebGL 就可以通過。
我們還需要改變混合函數(shù)
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
老方法是源色乘以 alpha。這是 SRC_ALPHA 意味著什么。但是現(xiàn)在我們的紋理數(shù)據(jù)已經(jīng)被乘以其 alpha。這是 premultipled 意味著什么。所以我們不需要 GPU 做乘法。將其設(shè)置為 ONE 意味著乘以 1。
邊緣現(xiàn)在沒有了。
如果你想保持文本在一種固定大小,但仍然正確?那么,如果你還記得透視文章中透視矩陣以 -Z 調(diào)整我們的對象使其在距離上更小。所以,我們可以以 -Z 倍數(shù)調(diào)整以達到我們想要的規(guī)模作為補償。
...
// 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, makeScale(textWidth * scale, textHeight * scale, 1));
textMatrix = matrixMultiply(textMatrix, makeTranslation(viewX, viewY, viewZ));
textMatrix = matrixMultiply(textMatrix, projectionMatrix);
...
如果你想在每個 F 中繪制不同文本,你應(yīng)該為每個 F 構(gòu)造一個新紋理,為每個 F 更新文本模式。
// create text textures, one for each F
var textTextures = [
"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,
].map(function(name) {
var textCanvas = makeTextCanvas(name, 100, 26);
var textWidth = textCanvas.width;
var textHeight = textCanvas.height;
var textTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textCanvas);
// make sure we can render it even if it's not a power of 2
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
return {
texture: textTex,
width: textWidth,
height: textHeight,
};
});
然后在呈現(xiàn)時選擇一個紋理
textPositions.forEach(function(pos, ndx) {
+// select a texture
+var tex = textTextures[ndx];
// scale the F to the size we need it.
// use just the position of the 'F' for the text
var textMatrix = makeIdentity();
*textMatrix = matrixMultiply(textMatrix, makeScale(tex.width, tex.height, 1));
并在繪制前為紋理設(shè)置統(tǒng)一結(jié)構(gòu)
textUniforms.u_texture = tex.texture;
我們一直用黑色繪制到畫布上的文本。這比用白色呈現(xiàn)文本更有用。然后我們再增加文本的顏色,以便得到我們想要的任何顏色。
首先我們改變文本材質(zhì),通過復(fù)合一個顏色
varying vec2 v_texcoord;
uniform sampler2D u_texture;
uniform vec4 u_color;
void main() {
gl_FragColor = texture2D(u_texture, v_texcoord) * u_color;
}
當(dāng)我們繪制文本到畫布上時使用白色
textCtx.fillStyle = "white";
然后我們添加一些其他顏色
// colors, 1 for each F
var colors = [
[0.0, 0.0, 0.0, 1], // 0
[1.0, 0.0, 0.0, 1], // 1
[0.0, 1.0, 0.0, 1], // 2
[1.0, 1.0, 0.0, 1], // 3
[0.0, 0.0, 1.0, 1], // 4
[1.0, 0.0, 1.0, 1], // 5
[0.0, 1.0, 1.0, 1], // 6
[0.5, 0.5, 0.5, 1], // 7
[0.5, 0.0, 0.0, 1], // 8
[0.0, 0.0, 0.0, 1], // 9
[0.5, 5.0, 0.0, 1], // 10
[0.0, 5.0, 0.0, 1], // 11
[0.5, 0.0, 5.0, 1], // 12,
[0.0, 0.0, 5.0, 1], // 13,
[0.5, 5.0, 5.0, 1], // 14,
[0.0, 5.0, 5.0, 1], // 15,
];
在繪制時選擇一個顏色
// set color uniform
textUniforms.u_color = colors[ndx];
結(jié)果如下:
這個技術(shù)實際上是大多數(shù)瀏覽器使用 GPU 加速時的技術(shù)。他們用 HTML 的內(nèi)容和你應(yīng)用的各種風(fēng)格生成紋理,只要這些內(nèi)容沒有改變,他們就可以在滾動時再次渲染紋理。當(dāng)然,如果你一直都在更新那么這技術(shù)可能會有點慢,因為重新生成紋理并更新它對于 GPU 來說是一個相對緩慢的操作。
更多建議: