我很肯定一些 CS 大師或者圖形大師會給我們講很多東西,但是...一個場景圖通常是一個樹結(jié)構(gòu),在這個樹結(jié)構(gòu)中的每個節(jié)點都生成一個矩陣...嗯,這并不是一個非常有用的定義。也許講一些例子會非常有用。
大多數(shù)的 3D 引擎都使用一個場景圖。你在場景圖中放置你想要在場景圖中出現(xiàn)的東西。引擎然后按場景圖行進(jìn),同時計算出需要繪制的一系列東西。場景圖都是有層次感的,例如,如果你想要去制作一個宇宙模擬圖,你可能需要一個圖與下面所示的圖相似
一個場景圖的意義是什么?一個場景圖的 #1 特點是它為矩陣提供了一個父母子女關(guān)系,正如我們在二維矩陣數(shù)學(xué)中討論的。因此,例如在一個簡單的宇宙中( 但是不是實際的 )模擬星星( 孩子 ),隨著它們的星系移動( 父母 )。同樣,一個月亮( 孩子 )隨著行星移動,如果你移動了地球,月亮?xí)黄鹨苿?。如果你移動一個星系,在這個星系中的所有的星星也會隨著它一起移動。在上面的圖中拖動名稱,希望你可以看到它們之間的關(guān)系。
如果你回到二維矩陣數(shù)學(xué),你可能會想起我們將大量矩陣相乘來達(dá)到轉(zhuǎn)化,旋轉(zhuǎn)和縮放對象。一個場景圖提供了一個結(jié)構(gòu)來幫助決定要將哪個矩陣數(shù)學(xué)應(yīng)用到對象上。
通常,在一個場景圖中的每個節(jié)點
都表示一個局部空間。給出了正確的矩陣數(shù)學(xué),在這個局部空間的任何東西都可以忽略在他上面的任何東西。用來說明同一件事的另一種方式是月亮只關(guān)心繞地球軌道運(yùn)行。它不關(guān)心繞太陽的軌道運(yùn)行。沒有場景圖結(jié)構(gòu),你需要做更多的復(fù)雜數(shù)學(xué),來計算怎樣才可以得到月亮繞太陽的軌道,因為它繞太陽的軌道看起來像這樣
使用場景圖,你可以將月球看做是地球的孩子,然后簡單的繞地球轉(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);
});
}
這將會給我們引進(jìn)一些在 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
};
讓我們來做一個太陽系場景圖。我不準(zhǔn)備使用花式紋理或者類似的東西,因為它會使例子變的混亂。首先讓我們來制作一些功能來幫助管理這些節(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)然我們會使用假的距離,來使東西適合屏幕。我們將只使用一個單球體模型,然后太陽為淡黃色,地球為藍(lán) - 淡綠色,月球為淡灰色。如果你對 drawInfo
,bufferInfo
和 programInfo
并不熟悉,你可以查看前一篇文章。
// 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),但是我們可以單獨(dú)的旋轉(zhuǎn)和縮放太陽,它不會影響地球。同樣,地球與月球可以單獨(dú)旋轉(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é)點移除
現(xiàn)在連接它們,如下所示
// connect the celetial objects
sunNode.setParent(solarSystemNode);
earthOrbitNode.setParent(solarSystemNode);
earthNode.setParent(earthOrbitNode);
moonOrbitNode.setParent(earthOrbitNode);
moonNode.setParent(moonOrbitNode);
同時,我們只需要更新軌道
現(xiàn)在你可以看到地球是兩倍大小,而月球不會。
你可能還會注意到太陽和地球不再旋轉(zhuǎn)到位。它們現(xiàn)在是無關(guān)的。
讓我們調(diào)整更多的東西。
目前我們有一個 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
給出我們一個本地矩陣。
現(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)建矩陣。
你可能會想,我沒做一個太陽系,所以這樣的意義何在?好吧,如果你想要去動畫一個人,你可能會有一個跟下面所示一樣的場景圖。
為手指和腳趾添加多少關(guān)節(jié)全部取決于你。你有的關(guān)節(jié)越多,它用于計算動畫的力量越多,同時它為所有的關(guān)節(jié)提供的動畫數(shù)據(jù)越多。像虛擬戰(zhàn)斗機(jī)的舊游戲大約有 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
源。
更多建議: