WebGL 繪制多個(gè)東西

2021-02-01 15:03 更新

WebGL 繪制多個(gè)東西

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

學(xué)到WebGL的一些基礎(chǔ)以后,面臨的一個(gè)問(wèn)題可能是如何繪制多個(gè)物體。

這里有一些特別的地方你需要提前了解,WebGL就像是一個(gè)方法, 但不同于一般的方法直接傳遞參數(shù),它需要調(diào)用一些方法去設(shè)置狀態(tài), 最后用某個(gè)

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

或者你可以像如下一樣編寫(xiě)代碼

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è)試,開(kāi)啟撲殺,等等)

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

    • 為你想要書(shū)寫(xiě)的程序調(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` 來(lái)為質(zhì)地單元分配質(zhì)地  
    • 調(diào)用 gl.drawArrays 或者 gl.drawElements

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

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

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

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

所以這里是代碼。我們的著色器,與從我們的角度看示例的一個(gè)簡(jiǎn)單著色器相同,除了我們已經(jīng)通過(guò)添加另外一個(gè) u-colorMult 來(lái)增加頂點(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ǎn)化的地方。這里結(jié)合了 3 個(gè)主要的有效的事情。

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

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

  3. 制服需要用給出的著色器來(lái)繪制你想要繪制的東西

所以,一個(gè)簡(jiǎn)單的簡(jiǎn)化可能會(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è)簡(jiǎn)單的循環(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);
});

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

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

所以,一個(gè)很簡(jiǎn)單的優(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);
});

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

// 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ì)象

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

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

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

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

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

你將會(huì)注意到我并沒(méi)有在循環(huán)中放置 computeMatrix。那是因?yàn)槌尸F(xiàn)應(yīng)該與計(jì)算矩陣分開(kāi)。從場(chǎng)景圖和我們?cè)诹硪黄恼轮凶x到的內(nèi)容,計(jì)算矩陣是非常常見(jiàn)的。

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


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)