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

WebGL 繪制多個(gè)東西

在 WebGL 中第一次得到東西后最常見的問題之一是,我怎樣繪制多個(gè)東西。

除了少數(shù)例外情況,首先要意識(shí)到的東西是,WebGL 就像某人寫的包含某個(gè)函數(shù),而不是向函數(shù)中傳遞大量參數(shù),相反,你有一個(gè)獨(dú)自的函數(shù)來繪制東西,同時(shí)有 70 + 函數(shù)來為一個(gè)函數(shù)設(shè)置狀態(tài)。因此,例如假設(shè)你有一個(gè)可以繪制一個(gè)圓的函數(shù)。你可以像如下一樣編寫程序

function drawCircle(centerX, centerY, radius, color) { ... }

或者你可以像如下一樣編寫代碼

var centerX;
var centerY;
var radius;
var color;

function setCenter(x, y) {
   centerX = x;
   centerY = y;
}

function setRadius(r) {
   radius = r;
}

function setColor(c) {
   color = c;
}

function drawCircle() {
   ...
}

WebGL 以第二種方式工作。函數(shù),諸如 gl.createBuffer, gl.bufferData, gl.createTexturegl.texImage2D,讓你可以上傳緩沖區(qū)( 頂點(diǎn) )和質(zhì)地 ( 顏色,等等 )數(shù)據(jù)到 WebGL。gl.createProgram, gl.createShader, gl.compileProgramgl.linkProgram 讓你可以創(chuàng)建你的 GLSL 著色器。當(dāng) gl.drawArrays或者 gl.drawElements 函數(shù)被調(diào)用時(shí),幾乎所有的 WebGL 的其余函數(shù)都正在設(shè)置要被使用的全局變量或者狀態(tài)。

我們知道,這個(gè)典型的 WebGL 程序基本上遵循這個(gè)結(jié)構(gòu)。

在初始化時(shí)

  • 創(chuàng)建所有的著色器和程序

  • 創(chuàng)建緩沖區(qū)和上傳頂點(diǎn)數(shù)據(jù)

  • 創(chuàng)建質(zhì)地和上傳質(zhì)地?cái)?shù)據(jù)

在渲染時(shí)

  • 清除并且設(shè)置視區(qū)和其他全局狀態(tài)(啟用深度測(cè)試,開啟撲殺,等等)

  • 對(duì)于你想要繪制的每一件事

    • 為你想要書寫的程序調(diào)用 gl.useProgram

    • 為你想要繪制的東西設(shè)置屬性

      - 對(duì)于每個(gè)屬性調(diào)用 `gl.bindBuffer`, `gl.vertexAttribPointer`, `gl.enableVertexAttribArray` 函數(shù)       
    • 為你想要繪制的東西設(shè)置制服

      - 為每一個(gè)制服調(diào)用 `gl.uniformXXX`  
      
      - 調(diào)用 `gl.activeTexture` 和 `gl.bindTexture` 來為質(zhì)地單元分配質(zhì)地  
    • 調(diào)用 gl.drawArrays 或者 gl.drawElements

這就是最基本的。怎樣組織你的代碼來完成這一任務(wù)取決于你。

一些事情諸如上傳質(zhì)地?cái)?shù)據(jù)( 甚至頂點(diǎn)數(shù)據(jù) )可能會(huì)異步的發(fā)生,因?yàn)槟阈枰却麄冊(cè)诰W(wǎng)上下載完。

讓我們來做一個(gè)簡單的應(yīng)用程序來繪制 3 種東西。一個(gè)立方體,一個(gè)球體和一個(gè)圓錐體。

我不會(huì)去詳談如何計(jì)算立方體,球體和圓錐體的數(shù)據(jù)。假設(shè)我們有函數(shù)來創(chuàng)建它們,然后我們返回在之前篇章中介紹的 bufferInfo 對(duì)象。

所以這里是代碼。我們的著色器,與從我們的角度看示例的一個(gè)簡單著色器相同,除了我們已經(jīng)通過添加另外一個(gè) u-colorMult 來增加頂點(diǎn)顏色。

// Passed in from the vertex shader.
varying vec4 v_color;

uniform vec4 u_colorMult;

void main() {
   gl_FragColor = v_color * u_colorMult;
}

在初始化時(shí)

// Our uniforms for each thing we want to draw
var sphereUniforms = {
  u_colorMult: [0.5, 1, 0.5, 1],
  u_matrix: makeIdentity(),
};
var cubeUniforms = {
  u_colorMult: [1, 0.5, 0.5, 1],
  u_matrix: makeIdentity(),
};
var coneUniforms = {
  u_colorMult: [0.5, 0.5, 1, 1],
  u_matrix: makeIdentity(),
};

// The translation for each object.
var sphereTranslation = [  0, 0, 0];
var cubeTranslation   = [-40, 0, 0];
var coneTranslation   = [ 40, 0, 0];

在繪制時(shí)

var sphereXRotation =  time;
var sphereYRotation =  time;
var cubeXRotation   = -time;
var cubeYRotation   =  time;
var coneXRotation   =  time;
var coneYRotation   = -time;

// ------ Draw the sphere --------

gl.useProgram(programInfo.program);

// Setup all the needed attributes.
setBuffersAndAttributes(gl, programInfo.attribSetters, sphereBufferInfo);

sphereUniforms.u_matrix = computeMatrix(
viewMatrix,
projectionMatrix,
sphereTranslation,
sphereXRotation,
sphereYRotation);

// Set the uniforms we just computed
setUniforms(programInfo.uniformSetters, sphereUniforms);

gl.drawArrays(gl.TRIANGLES, 0, sphereBufferInfo.numElements);

// ------ Draw the cube --------

// Setup all the needed attributes.
setBuffersAndAttributes(gl, programInfo.attribSetters, cubeBufferInfo);

cubeUniforms.u_matrix = computeMatrix(
viewMatrix,
projectionMatrix,
cubeTranslation,
cubeXRotation,
cubeYRotation);

// Set the uniforms we just computed
setUniforms(programInfo.uniformSetters, cubeUniforms);

gl.drawArrays(gl.TRIANGLES, 0, cubeBufferInfo.numElements);

// ------ Draw the cone --------

// Setup all the needed attributes.
setBuffersAndAttributes(gl, programInfo.attribSetters, coneBufferInfo);

coneUniforms.u_matrix = computeMatrix(
viewMatrix,
projectionMatrix,
coneTranslation,
coneXRotation,
coneYRotation);

// Set the uniforms we just computed
setUniforms(programInfo.uniformSetters, coneUniforms);

gl.drawArrays(gl.TRIANGLES, 0, coneBufferInfo.numElements);

如下所示

需要注意的一件事情是,因?yàn)槲覀冎挥幸粋€(gè)著色器程序,我們僅調(diào)用了 gl.useProgram 一次。如果我們有不同的著色器程序,你需要在使用每個(gè)程序之前調(diào)用 gl.useProgram。

這是另外一個(gè)值得去簡化的地方。這里結(jié)合了 3 個(gè)主要的有效的事情。

  1. 一個(gè)著色器程序(同時(shí)還有它的制服和屬性 信息/設(shè)置)

  2. 你想要繪制的東西的緩沖區(qū)和屬性

  3. 制服需要用給出的著色器來繪制你想要繪制的東西

所以,一個(gè)簡單的簡化可能會(huì)繪制出一個(gè)數(shù)組的東西,同時(shí)在這個(gè)數(shù)組中將 3 個(gè)東西放在一起。

var objectsToDraw = [
  {
programInfo: programInfo,
bufferInfo: sphereBufferInfo,
uniforms: sphereUniforms,
  },
  {
programInfo: programInfo,
bufferInfo: cubeBufferInfo,
uniforms: cubeUniforms,
  },
  {
programInfo: programInfo,
bufferInfo: coneBufferInfo,
uniforms: coneUniforms,
  },
];

在繪制時(shí),我們?nèi)匀恍枰戮仃?

var sphereXRotation =  time;
var sphereYRotation =  time;
var cubeXRotation   = -time;
var cubeYRotation   =  time;
var coneXRotation   =  time;
var coneYRotation   = -time;

// Compute the matrices for each object.
sphereUniforms.u_matrix = computeMatrix(
viewMatrix,
projectionMatrix,
sphereTranslation,
sphereXRotation,
sphereYRotation);

cubeUniforms.u_matrix = computeMatrix(
viewMatrix,
projectionMatrix,
cubeTranslation,
cubeXRotation,
cubeYRotation);

coneUniforms.u_matrix = computeMatrix(
viewMatrix,
projectionMatrix,
coneTranslation,
coneXRotation,
coneYRotation);

但是這個(gè)繪制代碼現(xiàn)在只是一個(gè)簡單的循環(huán)

// ------ Draw the objects --------

objectsToDraw.forEach(function(object) {
  var programInfo = object.programInfo;
  var bufferInfo = object.bufferInfo;

  gl.useProgram(programInfo.program);

  // Setup all the needed attributes.
  setBuffersAndAttributes(gl, programInfo.attribSetters, bufferInfo);

  // Set the uniforms.
  setUniforms(programInfo.uniformSetters, object.uniforms);

  // Draw
  gl.drawArrays(gl.TRIANGLES, 0, bufferInfo.numElements);
});

這可以說是大多數(shù) 3 D 引擎的主渲染循環(huán)都存在的。一些代碼所在的地方或者是代碼決定將什么放入 objectsToDraw 的列表中,基本上是這樣。

這里有幾個(gè)基本的優(yōu)化。如果這個(gè)我們想要繪制東西的程序與我們已經(jīng)繪制東西的之前的程序一樣,就不需要重新調(diào)用 gl.useProgram 了。同樣,如果我們現(xiàn)在正在繪制的與我們之前已經(jīng)繪制的東西有相同的形狀 / 幾何 / 頂點(diǎn),就不需要再次設(shè)置上面的東西了。

所以,一個(gè)很簡單的優(yōu)化會(huì)與以下代碼類似

var lastUsedProgramInfo = null;
var lastUsedBufferInfo = null;

objectsToDraw.forEach(function(object) {
  var programInfo = object.programInfo;
  var bufferInfo = object.bufferInfo;
  var bindBuffers = false;

  if (programInfo !== lastUsedProgramInfo) {
lastUsedProgramInfo = programInfo;
gl.useProgram(programInfo.program);

// We have to rebind buffers when changing programs because we
// only bind buffers the program uses. So if 2 programs use the same
// bufferInfo but the 1st one uses only positions the when the
// we switch to the 2nd one some of the attributes will not be on.
bindBuffers = true;
  }

  // Setup all the needed attributes.
  if (bindBuffers || bufferInfo != lastUsedBufferInfo) {
lastUsedBufferInfo = bufferInfo;
setBuffersAndAttributes(gl, programInfo.attribSetters, bufferInfo);
  }

  // Set the uniforms.
  setUniforms(programInfo.uniformSetters, object.uniforms);

  // Draw
  gl.drawArrays(gl.TRIANGLES, 0, bufferInfo.numElements);
});

這次讓我們來繪制更多的對(duì)象。與之前的僅僅 3 個(gè)東西不同,讓我們做一系列的東西來繪制更大的東西。

// put the shapes in an array so it's easy to pick them at random
var shapes = [
  sphereBufferInfo,
  cubeBufferInfo,
  coneBufferInfo,
];

// make 2 lists of objects, one of stuff to draw, one to manipulate.
var objectsToDraw = [];
var objects = [];

// Uniforms for each object.
var numObjects = 200;
for (var ii = 0; ii < numObjects; ++ii) {
  // pick a shape
  var bufferInfo = shapes[rand(0, shapes.length) | 0];

  // make an object.
  var object = {
uniforms: {
  u_colorMult: [rand(0, 1), rand(0, 1), rand(0, 1), 1],
  u_matrix: makeIdentity(),
},
translation: [rand(-100, 100), rand(-100, 100), rand(-150, -50)],
xRotationSpeed: rand(0.8, 1.2),
yRotationSpeed: rand(0.8, 1.2),
  };
  objects.push(object);

  // Add it to the list of things to draw.
  objectsToDraw.push({
programInfo: programInfo,
bufferInfo: bufferInfo,
uniforms: object.uniforms,
  });
}

在渲染時(shí)

// Compute the matrices for each object.
objects.forEach(function(object) {
  object.uniforms.u_matrix = computeMatrix(
  viewMatrix,
  projectionMatrix,
  object.translation,
  object.xRotationSpeed * time,
  object.yRotationSpeed * time);
});

然后使用上面的循環(huán)繪制對(duì)象

你也可以通過 programInfo 和 / 或者 bufferInfo 來對(duì)列表進(jìn)行排序,以便優(yōu)化開始的更加頻繁。大多數(shù)游戲引擎都是這樣做。不幸的是它不是那么簡單。如果你現(xiàn)在正在繪制的任何東西都不透明,然后你可以只排序。但是,一旦你需要繪制半透明的東西,你就需要以特定的順序來繪制它們。大多數(shù) 3 D 引擎都通過有 2 個(gè)或者更多的要繪制的對(duì)象的列表來處理這個(gè)問題。不透明的東西有一個(gè)列表。透明的東西有另外一個(gè)列表。不透明的列表按程序和幾何來排序。透明的列表按深度排序。對(duì)于其他東西,諸如覆蓋或后期處理效果,還會(huì)有其他單獨(dú)的列表。

這里有一個(gè)已經(jīng)排好序的例子。在我的機(jī)器上,我得到了未排序的 ~31 fps 和排好序的 ~37.發(fā)現(xiàn)幾乎增長了 20 %。但是,它是在最糟糕的案例和最好的案例相比較下,大多數(shù)的程序?qū)?huì)做的更多,因此,它可能對(duì)于所有情況來說不值得考慮,但是最特別的案例值得考慮。

注意到你不可能僅僅使用任何著色器來僅僅繪制任何幾何是非常重要的。例如,一個(gè)需要法線的著色器在沒有法線的幾何情況下將不會(huì)起作用。同樣,一個(gè)組要質(zhì)地的著色器在沒有質(zhì)地時(shí)將不會(huì)工作。

選擇一個(gè)像 Three.js 的 3D 庫是很重要的,這是眾多原因之一,因?yàn)樗鼤?huì)為你處理所有這些東西。你創(chuàng)建了一些幾何,你告訴 three.js 你想讓它怎樣呈現(xiàn),它會(huì)在運(yùn)行時(shí)產(chǎn)生著色器來處理你需要的東西。幾乎所有的 3D 引擎都將它們從 Unity3D 到虛幻的 Crytek 源。一些離線就可以生成它們,但是最重要的事是意識(shí)到是它們生成了著色器。

當(dāng)然,你正在讀這些文章的原因,是你想要知道接下來將會(huì)發(fā)生什么。你自己寫任何東西都是非常好且有趣的。意識(shí)到 WebGL 是超級(jí)低水平的是非常重要的,因此如果你想要自己做,這里有許多你可以做的工作,這經(jīng)常包括寫一個(gè)著色器生成器,因?yàn)椴煌墓δ芡枰煌闹鳌?

你將會(huì)注意到我并沒有在循環(huán)中放置 computeMatrix。那是因?yàn)槌尸F(xiàn)應(yīng)該與計(jì)算矩陣分開。從場(chǎng)景圖和我們將另一篇文章中讀到的內(nèi)容,計(jì)算矩陣是非常常見的。

現(xiàn)在,我們已經(jīng)有了一個(gè)繪制多對(duì)象的框架,讓我們來繪制一些文本。