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

WebGL 場景圖

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

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

http://wiki.jikexueyuan.com/project/webgl/images/webgl-scene-graph1.png" alt="" />

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

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

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

http://wiki.jikexueyuan.com/project/webgl/images/webgl-scene-graph2.png" alt="" />

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

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

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

worldMatrixForMoon = galaxyMatrix * starMatrix * planetMatrix * moonMatrix;

我們可以使用一個有效的遞歸函數(shù)來非常簡單的完成這些

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);
});
}

這將會給我們引進一些在 3D 場景圖中非常常見的術(shù)語。

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

  • worldMatrix:對于給定的節(jié)點,它需要獲取那個節(jié)點的局部空間的東西,同時將它轉(zhuǎn)換到場景圖的根節(jié)點的空間?;蛘?,換句話說,將它置于世界中。如果我們?yōu)樵虑蛴嬎闶澜缇仃?,我們將會得到上面我們看到的軌道?

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

 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
};  

讓我們來做一個太陽系場景圖。我不準備使用花式紋理或者類似的東西,因為它會使例子變的混亂。首先讓我們來制作一些功能來幫助管理這些節(jié)點。首先我們將做一個節(jié)點類

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

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

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)系的本地矩陣計算世界矩陣。如果我們從父母和遞歸訪問它孩子開始,我們可以計算它們的世界矩陣。

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);
  });
}; 

讓我們僅僅做太陽,地球,月亮,來保持場景圖簡單。當(dāng)然我們會使用假的距離,來使東西適合屏幕。我們將只使用一個單球體模型,然后太陽為淡黃色,地球為藍 - 淡綠色,月球為淡灰色。如果你對 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é)點,讓我們來連接它們。

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

我們會再一次做一個對象的列表和一個要繪制的對象的列表。

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

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

在渲染時,我們將會通過稍微旋轉(zhuǎn)它來更新每一個對象的本地矩陣。

// 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)在,本地矩陣都更新了,我們會更新所有的世界矩陣。

sunNode.updateWorldMatrix();

最后,我們有了世界矩陣,我們需要將它們相乘來為每個對象獲取一個世界觀投射矩陣。

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

渲染是我們在上一篇文章中看到的相同的循環(huán)。

你將會注意到所有的行星都是一樣的尺寸。我們試著讓地球更大點。

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

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

  sun
   |
  earth
   |
  moon

我們將改變它為

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

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

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é)點移除

http://wiki.jikexueyuan.com/project/webgl/images/webgl-scene-graph5.png" alt="" />

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

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

同時,我們只需要更新軌道

http://wiki.jikexueyuan.com/project/webgl/images/webgl-scene-graph6.png" alt="" />

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

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

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

http://wiki.jikexueyuan.com/project/webgl/images/webgl-scene-graph8.png" alt="" />

目前我們有一個 localMatrix,我們在每一幀都修改它。但是有一個問題,即在每一幀中我們數(shù)學(xué)都將收集一點錯誤。有許多可以解決這種被稱為鄰位的正?;仃?/em>的數(shù)學(xué)的方式,但是,甚至是它都不總是奏效。例如,讓我們想象我們縮減零。讓我們?yōu)橐粋€值 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!

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

http://wiki.jikexueyuan.com/project/webgl/images/webgl-scene-graph10.png" alt="" />

現(xiàn)在我們來創(chuàng)建一個源。一個常見的源是那些提供轉(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)在沒有問題了,因為我們每次都重新創(chuàng)建矩陣。

你可能會想,我沒做一個太陽系,所以這樣的意義何在?好吧,如果你想要去動畫一個人,你可能會有一個跟下面所示一樣的場景圖。

http://wiki.jikexueyuan.com/project/webgl/images/webgl-scene-graph11.png" alt="" />

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

不管怎樣,這是一個我組件在一起的塊人。它為上面提到的每個節(jié)點使用 TRS 源。藝術(shù)程序員和動畫程序員萬歲。

如果你查看一下,幾乎所有的 3D 圖書館,你都會發(fā)現(xiàn)一個與下圖類似的場景圖。