WebGL 場(chǎng)景圖

2018-10-03 11:46 更新

WebGL 場(chǎng)景圖

我很肯定一些 CS 大師或者圖形大師會(huì)給我們講很多東西,但是...一個(gè)場(chǎng)景圖通常是一個(gè)樹(shù)結(jié)構(gòu),在這個(gè)樹(shù)結(jié)構(gòu)中的每個(gè)節(jié)點(diǎn)都生成一個(gè)矩陣...嗯,這并不是一個(gè)非常有用的定義。也許講一些例子會(huì)非常有用。

大多數(shù)的 3D 引擎都使用一個(gè)場(chǎng)景圖。你在場(chǎng)景圖中放置你想要在場(chǎng)景圖中出現(xiàn)的東西。引擎然后按場(chǎng)景圖行進(jìn),同時(shí)計(jì)算出需要繪制的一系列東西。場(chǎng)景圖都是有層次感的,例如,如果你想要去制作一個(gè)宇宙模擬圖,你可能需要一個(gè)圖與下面所示的圖相似

一個(gè)場(chǎng)景圖的意義是什么?一個(gè)場(chǎng)景圖的 #1 特點(diǎn)是它為矩陣提供了一個(gè)父母子女關(guān)系,正如我們?cè)诙S矩陣數(shù)學(xué)中討論的。因此,例如在一個(gè)簡(jiǎn)單的宇宙中( 但是不是實(shí)際的 )模擬星星( 孩子 ),隨著它們的星系移動(dòng)( 父母 )。同樣,一個(gè)月亮( 孩子 )隨著行星移動(dòng),如果你移動(dòng)了地球,月亮?xí)黄鹨苿?dòng)。如果你移動(dòng)一個(gè)星系,在這個(gè)星系中的所有的星星也會(huì)隨著它一起移動(dòng)。在上面的圖中拖動(dòng)名稱(chēng),希望你可以看到它們之間的關(guān)系。

如果你回到二維矩陣數(shù)學(xué),你可能會(huì)想起我們將大量矩陣相乘來(lái)達(dá)到轉(zhuǎn)化,旋轉(zhuǎn)和縮放對(duì)象。一個(gè)場(chǎng)景圖提供了一個(gè)結(jié)構(gòu)來(lái)幫助決定要將哪個(gè)矩陣數(shù)學(xué)應(yīng)用到對(duì)象上。

通常,在一個(gè)場(chǎng)景圖中的每個(gè)節(jié)點(diǎn)都表示一個(gè)局部空間。給出了正確的矩陣數(shù)學(xué),在這個(gè)局部空間的任何東西都可以忽略在他上面的任何東西。用來(lái)說(shuō)明同一件事的另一種方式是月亮只關(guān)心繞地球軌道運(yùn)行。它不關(guān)心繞太陽(yáng)的軌道運(yùn)行。沒(méi)有場(chǎng)景圖結(jié)構(gòu),你需要做更多的復(fù)雜數(shù)學(xué),來(lái)計(jì)算怎樣才可以得到月亮繞太陽(yáng)的軌道,因?yàn)樗@太陽(yáng)的軌道看起來(lái)像這樣

使用場(chǎng)景圖,你可以將月球看做是地球的孩子,然后簡(jiǎn)單的繞地球轉(zhuǎn)動(dòng)。場(chǎng)景圖很注意地球圍繞太陽(yáng)轉(zhuǎn)的事實(shí)。它是通過(guò)節(jié)點(diǎn)和它走的矩陣相乘來(lái)完成的。

worldMatrix = greatGrandParent * grandParent * parent * self(localMatrix)

在具體的條款中,我們的宇宙模型可能是

worldMatrixForMoon = galaxyMatrix * starMatrix * planetMatrix * moonMatrix;

我們可以使用一個(gè)有效的遞歸函數(shù)來(lái)非常簡(jiǎn)單的完成這些

function computeWorldMatrix(currentNode, parentWorldMatrix) {
// compute our world matrix by multplying our local matrix with
// our parent's world matrix.
var worldMatrix = matrixMultiply(currentNode.localMatrix, parentWorldMatrix);

// now do the same for all of our children
currentNode.children.forEach(function(child) {
computeWorldMatrix(child, worldMatrix);
});
}

這將會(huì)給我們引進(jìn)一些在 3D 場(chǎng)景圖中非常常見(jiàn)的術(shù)語(yǔ)。

  • localMatrix:當(dāng)前節(jié)點(diǎn)的本地矩陣。它在原點(diǎn)轉(zhuǎn)換它和在局部空間它的孩子。

  • worldMatrix:對(duì)于給定的節(jié)點(diǎn),它需要獲取那個(gè)節(jié)點(diǎn)的局部空間的東西,同時(shí)將它轉(zhuǎn)換到場(chǎng)景圖的根節(jié)點(diǎn)的空間?;蛘?,換句話(huà)說(shuō),將它置于世界中。如果我們?yōu)樵虑蛴?jì)算世界矩陣,我們將會(huì)得到上面我們看到的軌道。

制作場(chǎng)景圖非常簡(jiǎn)單。讓我們定義一個(gè)簡(jiǎn)單的節(jié)點(diǎn)對(duì)象。還有無(wú)數(shù)個(gè)方式可以組織場(chǎng)景圖,我不確定哪一種方式是最好的。最常見(jiàn)的是有一個(gè)可以選擇繪制東西的字段。

 var node = {
   localMatrix: ...,  // the "local" matrix for this node
   worldMatrix: ...,  // the "world" matrix for this node
   children: [],  // array of children
   thingToDraw: ??,   // thing to draw at this node
};  

讓我們來(lái)做一個(gè)太陽(yáng)系場(chǎng)景圖。我不準(zhǔn)備使用花式紋理或者類(lèi)似的東西,因?yàn)樗鼤?huì)使例子變的混亂。首先讓我們來(lái)制作一些功能來(lái)幫助管理這些節(jié)點(diǎn)。首先我們將做一個(gè)節(jié)點(diǎn)類(lèi)

var Node = function() {
  this.children = [];
  this.localMatrix = makeIdentity();
  this.worldMatrix = makeIdentity();
};

我們給出一種設(shè)置一個(gè)節(jié)點(diǎn)的父母的方式

Node.prototype.setParent = function(parent) {
  // remove us from our parent
  if (this.parent) {
var ndx = this.parent.children.indexOf(this);
if (ndx >= 0) {
  this.parent.children.splice(ndx, 1);
}
  }

  // Add us to our new parent
  if (parent) {
parent.children.append(this);
  }
  this.parent = parent;
};

這里,這里的代碼是從基于它們的父子關(guān)系的本地矩陣計(jì)算世界矩陣。如果我們從父母和遞歸訪(fǎng)問(wèn)它孩子開(kāi)始,我們可以計(jì)算它們的世界矩陣。

Node.prototype.updateWorldMatrix = function(parentWorldMatrix) {
  if (parentWorldMatrix) {
// a matrix was passed in so do the math and
// store the result in `this.worldMatrix`.
matrixMultiply(this.localMatrix, parentWorldMatrix, this.worldMatrix);
  } else {
// no matrix was passed in so just copy.
copyMatrix(this.localMatrix, this.worldMatrix);
  }

  // now process all the children
  var worldMatrix = this.worldMatrix;
  this.children.forEach(function(child) {
child.updateWorldMatrix(worldMatrix);
  });
}; 

讓我們僅僅做太陽(yáng),地球,月亮,來(lái)保持場(chǎng)景圖簡(jiǎn)單。當(dāng)然我們會(huì)使用假的距離,來(lái)使東西適合屏幕。我們將只使用一個(gè)單球體模型,然后太陽(yáng)為淡黃色,地球?yàn)樗{(lán) - 淡綠色,月球?yàn)榈疑?。如果你?duì) drawInfo,bufferInfoprogramInfo 并不熟悉,你可以查看前一篇文章。

// Let's make all the nodes
var sunNode = new Node();
sunNode.localMatrix = makeTranslation(0, 0, 0);  // sun at the center
sunNode.drawInfo = {
  uniforms: {
u_colorOffset: [0.6, 0.6, 0, 1], // yellow
u_colorMult:   [0.4, 0.4, 0, 1],
  },
  programInfo: programInfo,
  bufferInfo: sphereBufferInfo,
};

var earthNode = new Node();
earthNode.localMatrix = makeTranslation(100, 0, 0);  // earth 100 units from the sun
earthNode.drawInfo = {
  uniforms: {
u_colorOffset: [0.2, 0.5, 0.8, 1],  // blue-green
u_colorMult:   [0.8, 0.5, 0.2, 1],
  },
  programInfo: programInfo,
  bufferInfo: sphereBufferInfo,
};

var moonNode = new Node();
moonNode.localMatrix = makeTranslation(20, 0, 0);  // moon 20 units from the earth
moonNode.drawInfo = {
  uniforms: {
u_colorOffset: [0.6, 0.6, 0.6, 1],  // gray
u_colorMult:   [0.1, 0.1, 0.1, 1],
  },
  programInfo: programInfo,
  bufferInfo: sphereBufferInfo,
};

現(xiàn)在我們已經(jīng)得到了節(jié)點(diǎn),讓我們來(lái)連接它們。

// connect the celetial objects
moonNode.setParent(earthNode);
earthNode.setParent(sunNode);

我們會(huì)再一次做一個(gè)對(duì)象的列表和一個(gè)要繪制的對(duì)象的列表。

var objects = [
  sunNode,
  earthNode,
  moonNode,
];

var objectsToDraw = [
  sunNode.drawInfo,
  earthNode.drawInfo,
  moonNode.drawInfo,
];

在渲染時(shí),我們將會(huì)通過(guò)稍微旋轉(zhuǎn)它來(lái)更新每一個(gè)對(duì)象的本地矩陣。

// update the local matrices for each object.
matrixMultiply(sunNode.localMatrix, makeYRotation(0.01), sunNode.localMatrix);
matrixMultiply(earthNode.localMatrix, makeYRotation(0.01), earthNode.localMatrix);
matrixMultiply(moonNode.localMatrix, makeYRotation(0.01), moonNode.localMatrix);

現(xiàn)在,本地矩陣都更新了,我們會(huì)更新所有的世界矩陣。

sunNode.updateWorldMatrix();

最后,我們有了世界矩陣,我們需要將它們相乘來(lái)為每個(gè)對(duì)象獲取一個(gè)世界觀(guān)投射矩陣。

// Compute all the matrices for rendering
objects.forEach(function(object) {
  object.drawInfo.uniforms.u_matrix = matrixMultiply(object.worldMatrix, viewProjectionMatrix);
});

渲染是我們?cè)谏弦黄恼轮锌吹降南嗤难h(huán)。

你將會(huì)注意到所有的行星都是一樣的尺寸。我們?cè)囍尩厍蚋簏c(diǎn)。

earthNode.localMatrix = matrixMultiply(
makeScale(2, 2, 2),   // make the earth twice as large
makeTranslation(100, 0, 0));  // earth 100 units from the sun

哦。月亮也越來(lái)越大。為了解決這個(gè)問(wèn)題,我們可以手動(dòng)的縮小月亮。但是一個(gè)更好的解決方法是在我們的場(chǎng)景圖中增加更多的節(jié)點(diǎn)。而不僅僅是如下圖所示。

  sun
   |
  earth
   |
  moon

我們將改變它為

 solarSystem
   ||
   |   sun
   |
 earthOrbit
   ||
   |  earth
   |
  moonOrbit
  |
 moon

這將會(huì)使地球圍繞太陽(yáng)系旋轉(zhuǎn),但是我們可以單獨(dú)的旋轉(zhuǎn)和縮放太陽(yáng),它不會(huì)影響地球。同樣,地球與月球可以單獨(dú)旋轉(zhuǎn)。讓我們給太陽(yáng)系,地球軌道月球軌道設(shè)置更多的節(jié)點(diǎn)。

var solarSystemNode = new Node();
var earthOrbitNode = new Node();
earthOrbitNode.localMatrix = makeTranslation(100, 0, 0);  // earth orbit 100 units from the sun
var moonOrbitNode = new Node();
moonOrbitNode.localMatrix = makeTranslation(20, 0, 0);  // moon 20 units from the earth

這些軌道距離已經(jīng)從舊的節(jié)點(diǎn)移除

現(xiàn)在連接它們,如下所示

// connect the celetial objects
sunNode.setParent(solarSystemNode);
earthOrbitNode.setParent(solarSystemNode);
earthNode.setParent(earthOrbitNode);
moonOrbitNode.setParent(earthOrbitNode);
moonNode.setParent(moonOrbitNode);

同時(shí),我們只需要更新軌道

現(xiàn)在你可以看到地球是兩倍大小,而月球不會(huì)。

你可能還會(huì)注意到太陽(yáng)和地球不再旋轉(zhuǎn)到位。它們現(xiàn)在是無(wú)關(guān)的。

讓我們調(diào)整更多的東西。

目前我們有一個(gè) localMatrix,我們?cè)诿恳粠夹薷乃?。但是有一個(gè)問(wèn)題,即在每一幀中我們數(shù)學(xué)都將收集一點(diǎn)錯(cuò)誤。有許多可以解決這種被稱(chēng)為鄰位的正?;仃?/em>的數(shù)學(xué)的方式,但是,甚至是它都不總是奏效。例如,讓我們想象我們縮減零。讓我們?yōu)橐粋€(gè)值 x 這樣做。

x = 246;   // frame #0, x = 246

scale = 1;
x = x * scale  // frame #1, x = 246

scale = 0.5;
x = x * scale  // frame #2, x = 123

scale = 0;
x = x * scale  // frame #3, x = 0

scale = 0.5;
x = x * scale  // frame #4, x = 0  OOPS!

scale = 1;
x = x * scale  // frame #5, x = 0  OOPS!

我們失去了我們的值。我們可以通過(guò)添加其他一些從其他值更新矩陣的類(lèi)來(lái)解決它。讓我們通過(guò)擁有一個(gè) source 來(lái)改變 Node 的定義。如果它存在,我們會(huì)要求 source 給出我們一個(gè)本地矩陣。

現(xiàn)在我們來(lái)創(chuàng)建一個(gè)源。一個(gè)常見(jiàn)的源是那些提供轉(zhuǎn)化,旋轉(zhuǎn)和縮放的,如下所示。

var TRS = function() {
  this.translation = [0, 0, 0];
  this.rotation = [0, 0, 0];
  this.scale = [1, 1, 1];
};

TRS.prototype.getMatrix = function(dst) {
  dst = dst || new Float32Array(16);
  var t = this.translation;
  var r = this.rotation;
  var s = this.scale;

  // compute a matrix from translation, rotation, and scale
  makeTranslation(t[0], t[1], t[2], dst);
  matrixMultiply(makeXRotation(r[0]), dst, dst);
  matrixMultiply(makeYRotation(r[1]), dst, dst);
  matrixMultiply(makeZRotation(r[2]), dst, dst);
  matrixMultiply(makeScale(s[0], s[1], s[2]), dst, dst);
  return dst;
};

我們可以像下面一樣使用它

// at init time making a node with a source
var someTRS  = new TRS();
var someNode = new Node(someTRS);

// at render time
someTRS.rotation[2] += elapsedTime;

現(xiàn)在沒(méi)有問(wèn)題了,因?yàn)槲覀兠看味贾匦聞?chuàng)建矩陣。

你可能會(huì)想,我沒(méi)做一個(gè)太陽(yáng)系,所以這樣的意義何在?好吧,如果你想要去動(dòng)畫(huà)一個(gè)人,你可能會(huì)有一個(gè)跟下面所示一樣的場(chǎng)景圖。

為手指和腳趾添加多少關(guān)節(jié)全部取決于你。你有的關(guān)節(jié)越多,它用于計(jì)算動(dòng)畫(huà)的力量越多,同時(shí)它為所有的關(guān)節(jié)提供的動(dòng)畫(huà)數(shù)據(jù)越多。像虛擬戰(zhàn)斗機(jī)的舊游戲大約有 15 個(gè)關(guān)節(jié)。在 2000 年代早期至中期,游戲有 30 到 70 個(gè)關(guān)節(jié)。如果你為每個(gè)手都設(shè)置關(guān)節(jié),在每個(gè)手中至少有 20 個(gè),所以?xún)芍皇质?40 個(gè)關(guān)節(jié)。許多想要?jiǎng)赢?huà)手的游戲都把大拇指處理為一個(gè),其他的四個(gè)作為一個(gè)大的手指處理,以節(jié)省時(shí)間( 所有的 CPU/GPU 和藝術(shù)家的時(shí)間 )和內(nèi)存。

不管怎樣,這是一個(gè)我組件在一起的塊人。它為上面提到的每個(gè)節(jié)點(diǎn)使用 TRS 源。

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)