在 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.createTexture
和 gl.texImage2D
,讓你可以上傳緩沖區(qū)( 頂點(diǎn) )和質(zhì)地 ( 顏色,等等 )數(shù)據(jù)到 WebGL。gl.createProgram
, gl.createShader
, gl.compileProgram
和 gl.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ù)
在渲染時(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ì)地
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è)主要的有效的事情。
一個(gè)著色器程序(同時(shí)還有它的制服和屬性 信息/設(shè)置)
你想要繪制的東西的緩沖區(qū)和屬性
所以,一個(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)繪制一些文本。
更多建議: