在 WebGL 中第一次得到東西后最常見的問題之一是,我怎樣繪制多個東西。
學到WebGL的一些基礎(chǔ)以后,面臨的一個問題可能是如何繪制多個物體。
這里有一些特別的地方你需要提前了解,WebGL就像是一個方法, 但不同于一般的方法直接傳遞參數(shù),它需要調(diào)用一些方法去設(shè)置狀態(tài), 最后用某個
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.createTexture
和 gl.texImage2D
,讓你可以上傳緩沖區(qū)( 頂點 )和質(zhì)地 ( 顏色,等等 )數(shù)據(jù)到 WebGL。gl.createProgram
, gl.createShader
, gl.compileProgram
和 gl.linkProgram
讓你可以創(chuàng)建你的 GLSL 著色器。當 gl.drawArrays
或者 gl.drawElements
函數(shù)被調(diào)用時,幾乎所有的 WebGL 的其余函數(shù)都正在設(shè)置要被使用的全局變量或者狀態(tài)。
我們知道,這個典型的 WebGL 程序基本上遵循這個結(jié)構(gòu)。
在初始化時
創(chuàng)建所有的著色器和程序
創(chuàng)建緩沖區(qū)和上傳頂點數(shù)據(jù)
在渲染時
清除并且設(shè)置視區(qū)和其他全局狀態(tài)(啟用深度測試,開啟撲殺,等等)
對于你想要繪制的每一件事
為你想要書寫的程序調(diào)用 gl.useProgram
為你想要繪制的東西設(shè)置屬性
- 對于每個屬性調(diào)用 `gl.bindBuffer`, `gl.vertexAttribPointer`, `gl.enableVertexAttribArray` 函數(shù)
為你想要繪制的東西設(shè)置制服
- 為每一個制服調(diào)用 `gl.uniformXXX`
- 調(diào)用 `gl.activeTexture` 和 `gl.bindTexture` 來為質(zhì)地單元分配質(zhì)地
gl.drawArrays
或者 gl.drawElements
這就是最基本的。怎樣組織你的代碼來完成這一任務取決于你。
一些事情諸如上傳質(zhì)地數(shù)據(jù)( 甚至頂點數(shù)據(jù) )可能會異步的發(fā)生,因為你需要等待他們在網(wǎng)上下載完。
讓我們來做一個簡單的應用程序來繪制 3 種東西。一個立方體,一個球體和一個圓錐體。
我不會去詳談如何計算立方體,球體和圓錐體的數(shù)據(jù)。假設(shè)我們有函數(shù)來創(chuàng)建它們,然后我們返回在之前篇章中介紹的 bufferInfo 對象。
所以這里是代碼。我們的著色器,與從我們的角度看示例的一個簡單著色器相同,除了我們已經(jīng)通過添加另外一個 u-colorMult
來增加頂點顏色。
// Passed in from the vertex shader.
varying vec4 v_color;
uniform vec4 u_colorMult;
void main() {
gl_FragColor = v_color * u_colorMult;
}
在初始化時
// 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];
在繪制時
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);
如下所示
需要注意的一件事情是,因為我們只有一個著色器程序,我們僅調(diào)用了 gl.useProgram
一次。如果我們有不同的著色器程序,你需要在使用每個程序之前調(diào)用 gl.useProgram
。
這是另外一個值得去簡化的地方。這里結(jié)合了 3 個主要的有效的事情。
一個著色器程序(同時還有它的制服和屬性 信息/設(shè)置)
你想要繪制的東西的緩沖區(qū)和屬性
所以,一個簡單的簡化可能會繪制出一個數(shù)組的東西,同時在這個數(shù)組中將 3 個東西放在一起。
var objectsToDraw = [
{
programInfo: programInfo,
bufferInfo: sphereBufferInfo,
uniforms: sphereUniforms,
},
{
programInfo: programInfo,
bufferInfo: cubeBufferInfo,
uniforms: cubeUniforms,
},
{
programInfo: programInfo,
bufferInfo: coneBufferInfo,
uniforms: coneUniforms,
},
];
在繪制時,我們?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);
但是這個繪制代碼現(xià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);
});
這可以說是大多數(shù) 3 D 引擎的主渲染循環(huán)都存在的。一些代碼所在的地方或者是代碼決定將什么放入 objectsToDraw
的列表中,基本上是這樣。
這里有幾個基本的優(yōu)化。如果這個我們想要繪制東西的程序與我們已經(jīng)繪制東西的之前的程序一樣,就不需要重新調(diào)用 gl.useProgram
了。同樣,如果我們現(xiàn)在正在繪制的與我們之前已經(jīng)繪制的東西有相同的形狀 / 幾何 / 頂點,就不需要再次設(shè)置上面的東西了。
所以,一個很簡單的優(yōu)化會與以下代碼類似
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);
});
這次讓我們來繪制更多的對象。與之前的僅僅 3 個東西不同,讓我們做一系列的東西來繪制更大的東西。
// 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,
});
}
在渲染時
// 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)繪制對象
你也可以通過 programInfo
和 / 或者 bufferInfo
來對列表進行排序,以便優(yōu)化開始的更加頻繁。大多數(shù)游戲引擎都是這樣做。不幸的是它不是那么簡單。如果你現(xiàn)在正在繪制的任何東西都不透明,然后你可以只排序。但是,一旦你需要繪制半透明的東西,你就需要以特定的順序來繪制它們。大多數(shù) 3 D 引擎都通過有 2 個或者更多的要繪制的對象的列表來處理這個問題。不透明的東西有一個列表。透明的東西有另外一個列表。不透明的列表按程序和幾何來排序。透明的列表按深度排序。對于其他東西,諸如覆蓋或后期處理效果,還會有其他單獨的列表。
在我的機器上,我得到了未排序的 ~31 fps 和排好序的 ~37.發(fā)現(xiàn)幾乎增長了 20 %。但是,它是在最糟糕的案例和最好的案例相比較下,大多數(shù)的程序?qū)龅母?,因此,它可能對于所有情況來說不值得考慮,但是最特別的案例值得考慮。
注意到你不可能僅僅使用任何著色器來僅僅繪制任何幾何是非常重要的。例如,一個需要法線的著色器在沒有法線的幾何情況下將不會起作用。同樣,一個組要質(zhì)地的著色器在沒有質(zhì)地時將不會工作。
選擇一個像 Three.js 的 3D 庫是很重要的,這是眾多原因之一,因為它會為你處理所有這些東西。你創(chuàng)建了一些幾何,你告訴 three.js 你想讓它怎樣呈現(xiàn),它會在運行時產(chǎn)生著色器來處理你需要的東西。幾乎所有的 3D 引擎都將它們從 Unity3D 到虛幻的 Crytek 源。一些離線就可以生成它們,但是最重要的事是意識到是它們生成了著色器。
當然,你正在讀這些文章的原因,是你想要知道接下來將會發(fā)生什么。你自己寫任何東西都是非常好且有趣的。意識到 WebGL 是超級低水平的是非常重要的,因此如果你想要自己做,這里有許多你可以做的工作,這經(jīng)常包括寫一個著色器生成器,因為不同的功能往往需要不同的著色器。
你將會注意到我并沒有在循環(huán)中放置 computeMatrix
。那是因為呈現(xiàn)應該與計算矩陣分開。從場景圖和我們在另一篇文章中讀到的內(nèi)容,計算矩陣是非常常見的。
現(xiàn)在,我們已經(jīng)有了一個繪制多對象的框架,讓我們來繪制一些文本。
更多建議: