WebGL 文本 紋理

2018-10-03 11:50 更新

WebGL 文本 紋理

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

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

然后我們構(gòu)造一些平面幾何并顯示它。這實(shí)際上是一些游戲中構(gòu)造所有的文本的方式。例如 Locoroco 只有大約 270 個(gè)字符串。它本地化成 17 種語言。我們有一個(gè)包含所有語言的 Excel 表和一個(gè)腳本,該腳本將啟動(dòng) Photoshop 并生成紋理,每個(gè)紋理都對(duì)應(yīng)一種語言里的一個(gè)消息。

當(dāng)然你也可以在運(yùn)行時(shí)生成紋理。因?yàn)樵跒g覽器中 WebGL 是依靠畫布 2d api 來幫助生成紋理的。

我們來看上一篇文章的例子,在其中添加一個(gè)函數(shù):用文本填補(bǔ)一個(gè) 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 個(gè)不同東西:“F”和文本,我想切換到使用一些前一篇文章中所描述的輔助函數(shù)。如果你還不清楚 programInfobufferInfo 等,你需要瀏覽那篇文章。

現(xiàn)在,讓我們創(chuàng)建一個(gè)“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));

一個(gè)四元組單元是一個(gè) 1 單元大小的四元組(方形),中心在原點(diǎn)。createPlaneBufferInfo 在 xz 平面創(chuàng)建一個(gè)平面。我們通過一個(gè)矩陣旋轉(zhuǎn)它,就得到一個(gè) xy 平面四元組單元。

接下來創(chuàng)建 2 個(gè)著色器:

// 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)我們計(jì)算 F 的矩陣時(shí),保存 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 的原點(diǎn)位置,我們還需要測(cè)量和單元四元組相匹配的紋理尺寸。最后,我們需要多種投影矩陣。

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

即:

你會(huì)發(fā)現(xiàn)有時(shí)候我們文本的一部分遮蓋了我們 Fs 的一部分。這是因?yàn)槲覀兝L制一個(gè)四元組。畫布的默認(rèn)顏色是透明的黑色(0,0,0,0)和我們?cè)谒脑M中使用這種顏色繪制。我們也可以混合像素。

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

根據(jù)混合函數(shù),將源像素(這個(gè)顏色取自片段著色器)和 目的像素(畫布顏色)結(jié)合在一起。在混合函數(shù)中,我們?yōu)樵聪袼卦O(shè)置:SRC_ALPHA,為目的像素設(shè)置:ONE_MINUS_SRC_ALPHA。

result = dest * (1 - src_alpha) + src * src_alpha

舉個(gè)例子,如果目的像素是綠色的 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

對(duì)于紋理的部分內(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ì)看,有時(shí)能看到這個(gè)問題

發(fā)生什么事情了?我們正在繪制一個(gè) F 然后是它的文本,然后下一個(gè) F 的重復(fù)文本。所以當(dāng)我們繪制文本時(shí),我們?nèi)匀恍枰粋€(gè)深度緩沖,即使混合了一些像素來保持背景顏色,深度緩沖仍然需要更新。當(dāng)我們繪制下一個(gè) F,如果 F 的部分是之前繪制文本的一些像素,他們就不會(huì)再繪制。

我們剛剛遇到的最困難的問題之一,在 GPU 上渲染 3D。透明度也存在問題。

針對(duì)幾乎所有透明呈現(xiàn)問題,最常見的解決方案是先畫出所有不透明的東西,之后,按中心距的排序,繪制所有的透明的東西,中心距的排序是在深度緩沖測(cè)試開啟但深度緩沖更新關(guān)閉的情況下得出的。

讓我們先單獨(dú)繪制透明材料(文本)中不透明材料(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);

繪制文本時(shí),我們將打開混合并關(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)在啟動(dòng):

請(qǐng)注意我們沒有像我上面提到的那樣分類。在這種情況下,因?yàn)槲覀兝L制大部分是不透明文本,所以即使排序也沒有明顯差異,所以就省去了這一步驟,節(jié)省資源用于其他文章。

另一個(gè)問題是文本的“F”總是交叉。實(shí)際上這個(gè)問題沒有一個(gè)具體的解決方案。如果你正在構(gòu)造一個(gè) MMO,希望每個(gè)游戲者的文本總是出現(xiàn)在你試圖使文本出現(xiàn)的頂部。只需要將之轉(zhuǎn)化為一些單元 +Y,足以確保它總是位于游戲者之上。

你也可以使之向 cameara 移動(dòng)。在這里我們這樣做只是為了好玩。因?yàn)?“pos” 是在坐標(biāo)系中,意味著它是相對(duì)于眼(在坐標(biāo)系中即:0,0,0)。所以如果我們使之標(biāo)準(zhǔn)化,我們可以得到一個(gè)單位向量,這個(gè)向量的指向是從原點(diǎn)到某一點(diǎ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);

即:

你還可能會(huì)注意到一個(gè)字母邊緣問題。

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

為了解決這個(gè)問題,使 WebGL 不會(huì) 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)整我們的對(duì)象使其在距離上更小。所以,我們可以以 -Z 倍數(shù)調(diào)整以達(dá)到我們想要的規(guī)模作為補(bǔ)償。

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

如果你想在每個(gè) F 中繪制不同文本,你應(yīng)該為每個(gè) F 構(gòu)造一個(gè)新紋理,為每個(gè) 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)時(shí)選擇一個(gè)紋理

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)文本更有用。然后我們?cè)僭黾游谋镜念伾?,以便得到我們想要的任何顏色?/p>

首先我們改變文本材質(zhì),通過復(fù)合一個(gè)顏色

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)我們繪制文本到畫布上時(shí)使用白色

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,
];

在繪制時(shí)選擇一個(gè)顏色

// set color uniform
textUniforms.u_color = colors[ndx];

結(jié)果如下:

這個(gè)技術(shù)實(shí)際上是大多數(shù)瀏覽器使用 GPU 加速時(shí)的技術(shù)。他們用 HTML 的內(nèi)容和你應(yīng)用的各種風(fēng)格生成紋理,只要這些內(nèi)容沒有改變,他們就可以在滾動(dòng)時(shí)再次渲染紋理。當(dāng)然,如果你一直都在更新那么這技術(shù)可能會(huì)有點(diǎn)慢,因?yàn)橹匦律杉y理并更新它對(duì)于 GPU 來說是一個(gè)相對(duì)緩慢的操作。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)