鍍金池/ 教程/ 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 攝像機
WebGL 文本 使用字符紋理
WebGL 正交 3D
WebGL 基本原理
WebGL - 更少的代碼,更多的樂趣
WebGL 著色器和 GLSL

WebGL 文本 紋理

在上一篇文章中我們學(xué)習(xí)了在 WebGL 場景中如何使用一個 2D 畫布繪制文本。這個技術(shù)可以工作且很容易做到,但它有一個限制,即文本不能被其他的 3D 對象遮蓋。要做到這一點,我們實際上需要在 WebGL 中繪制文本。

最簡單的方法是繪制帶有文本的紋理。例如你可以使用 photoshop 或其他繪畫程序,來繪制帶有文本的一些圖像。

http://wiki.jikexueyuan.com/project/webgl/images/my-awesme-text.png" alt="" />

然后我們構(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 的一部分。這是因為我們繪制一個四元組。畫布的默認(rèn)顏色是透明的黑色(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)更好了。如果你仔細(xì)看,有時能看到這個問題

http://wiki.jikexueyuan.com/project/webgl/images/text-zbuffer-issue.png" alt="" />

發(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ù)量的單位靠近或遠(yuǎn)離眼。

// 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);

即:

你還可能會注意到一個字母邊緣問題。

http://wiki.jikexueyuan.com/project/webgl/images/text-gray-outline.png" alt="" />

這里的問題是 Canvas2D api 只引入了自左乘 alpha 值。當(dāng)我們上傳內(nèi)容到試圖 unpremultiply 的紋理 WebGL,它就不能完全做到,這是因為自左乘 alpha 會失真。

為了解決這個問題,使 WebGL 不會 unpremultiply:

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

這告訴 WebGL 支持自左乘 alpha 值到 gl.texImage2Dgl.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)整以達(dá)到我們想要的規(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 來說是一個相對緩慢的操作。

上一篇:WebGL 2D 圖像伸縮下一篇:WebGL 2D 矩陣