Three.js源碼閱讀筆記-3


這是Three.js源碼閱讀筆記第三篇。之前兩篇主要是關於核心對象的,這些核心對象主要圍繞着矢量vector3對象和矩陣matrix4對象展開的,關注的是空間中的單個頂點的位置和變化。這一篇將主要討論Three.js中的物體是如何組織的:即如何將頂點、表面、材質組合成為一個具體的對象。

Object::Mesh

該構造函數構造了一個空間中的物體。之所以叫“網格”是因為,實際上具有體積的物體基本都是建模成為“網格”的。

THREE.Mesh = function ( geometry, material ) {
    THREE.Object3D.call( this );
    this.geometry = geometry;
    this.material = ( material !== undefined ) ? material : new THREE.MeshBasicMaterial( { color: Math.random() * 0xffffff, wireframe: true } );
    /* 一些其他的與本節無關的內容 */
}

實際上,Mesh類只有兩個屬性,表示幾何形體的geometry對象和表示材質的material對象。geometry對象在上一篇博文中已經介紹過,還有部分派生類會在這篇博文中介紹(通過這些派生類的構造過程,能更加清晰地了解到Mesh對象的工作原理);matrial對象及其派生類也將在這篇筆記中介紹。Mesh對象的這兩個屬性相互緊密關聯,geometry對象中的face數組中,每個face對象的materialIndex用來匹配material屬性對象,face對象的vertexUVs數組用以依次匹配每個頂點在數組上的取值。值得注意的是,Mesh只能有一個material對象(不知這樣設計的意圖何在),如果需要用到多個材質,應當將材質按照materialIndex順序初始化在geometry本身的materials屬性中。

Geometry::CubeGeometry

該構造函數創建了一個立方體對象。

THREE.CubeGeometry = function ( width, height, depth, widthSegments, heightSegments, depthSegments ) {
    THREE.Geometry.call( this );
    var scope = this;
    this.width = width;
    this.height = height;
    this.depth = depth;
    var width_half = this.width / 2;
    var height_half = this.height / 2;
    var depth_half = this.depth / 2;
    /* 略去 */
    buildPlane( 'z', 'y', - 1, - 1, this.depth, this.height, width_half, 0 ); // px
    /* 略去 */
    function buildPlane( u, v, udir, vdir, width, height, depth, materialIndex ) {
        /* 略去 */
    }
    this.computeCentroids();
    this.mergeVertices();
};

構造函數做的最重要的事在buildPlane中。該函數最重要的事情就是對scope的操作(上面的代碼塊中,scope就是this),包括:調用scope.vertices.push(vector)將頂點加入geometry對象;調用scope.faces.push(face)將表面加入到geometry對象,調用scope.faceVertexUvs[i].push(uv)方法將對應於頂點的材質坐標加入geometry對象。代碼的大部分都是關於生成立方體的邏輯,這些邏輯很容易理解,也很容易擴展到其他類型的geometry對象。

構造函數的參數包括長、寬、高和三個方向的分段數。所謂分段,就是說如果將widthSeqments等三個參數都設定為2的話,那么每個面將被表現成2×2=4個面,整個立方體由24個表面組成,正如同網格一樣。

function buildPlane( u, v, udir, vdir, width, height, depth, materialIndex ) {
        var w, ix, iy,
        gridX = scope.widthSegments,
        gridY = scope.heightSegments,
        width_half = width / 2,
        height_half = height / 2,
        offset = scope.vertices.length;
        if ( ( u === 'x' && v === 'y' ) || ( u === 'y' && v === 'x' ) ) {w = 'z';} 
        else if ( ( u === 'x' && v === 'z' ) || ( u === 'z' && v === 'x' ) ) {w = 'y';gridY = scope.depthSegments;} 
else if ( ( u === 'z' && v === 'y' ) || ( u === 'y' && v === 'z' ) ) {w = 'x';gridX = scope.depthSegments;} var gridX1 = gridX + 1, gridY1 = gridY + 1, segment_width = width / gridX, segment_height = height / gridY, normal = new THREE.Vector3(); normal[ w ] = depth > 0 ? 1 : - 1; for ( iy = 0; iy < gridY1; iy ++ ) { for ( ix = 0; ix < gridX1; ix ++ ) { var vector = new THREE.Vector3(); vector[ u ] = ( ix * segment_width - width_half ) * udir; vector[ v ] = ( iy * segment_height - height_half ) * vdir; vector[ w ] = depth; scope.vertices.push( vector ); } } for ( iy = 0; iy < gridY; iy++ ) { for ( ix = 0; ix < gridX; ix++ ) { var a = ix + gridX1 * iy; var b = ix + gridX1 * ( iy + 1 ); var c = ( ix + 1 ) + gridX1 * ( iy + 1 ); var d = ( ix + 1 ) + gridX1 * iy; var face = new THREE.Face4( a + offset, b + offset, c + offset, d + offset ); face.normal.copy( normal ); face.vertexNormals.push( normal.clone(), normal.clone(), normal.clone(), normal.clone() ); face.materialIndex = materialIndex; scope.faces.push( face ); scope.faceVertexUvs[ 0 ].push( [ new THREE.UV( ix / gridX, 1 - iy / gridY ), new THREE.UV( ix / gridX, 1 - ( iy + 1 ) / gridY ), new THREE.UV( ( ix + 1 ) / gridX, 1- ( iy + 1 ) / gridY ), new THREE.UV( ( ix + 1 ) / gridX, 1 - iy / gridY ) ] ); } } }

 除了一個大部分對象都具有的clone()方法,CubeGeometry沒有其他的方法,其他的XXXGeometry對象也大抵如此。沒有方法說明該對象負責組織和存儲數據,而如何利用這些數據生成三維場景和動畫,則是在另外的對象中定義的。

Geometry::CylinderGeometry

顧名思義,該構造函數創建一個圓柱體(或圓台)對象。

THREE.CylinderGeometry = function ( radiusTop, radiusBottom, height, radiusSegments, heightSegments, openEnded ) {
    /**/
}

有了CubeGeometry構造函數的基礎,自己也應當能夠實現CylinderGeometry,我們只需要注意一下構造函數各參數的意義。radiusTop和radiusBottom表示頂部和底部的半徑,height表示高度。radiusSegments定義了需要將圓周分成多少份(該數字越大,圓柱更圓),heightSegments定義了需要將整個高度分成多少份,openEnded指定是否生成頂面和底面。

源碼中還有兩點值得注意的:該模型的本地原點是中軸線的中點,而不是重心之類的,也就是說上圓面的高度(y軸值)是height/2,下圓面是-height/2,這一點對圓柱體來說沒有差異,但對於上下半徑不同的圓台體就有差異了;還有就是該模型的頂面和地面采用face3類型表面,而側面采用face4類型表面。

Geometry::SphereGeometry

該構造函數創建一個球體。

THREE.SphereGeometry = function ( radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength ){
    /**/
}

各參數的意義:radius指定半徑,widthSegments表示“經度”分帶數目,heightSegments表示“緯度”分帶數目。后面四個參數是可選的,表示經度的起始值和緯度的起始值。熟悉極坐標的都了解,通常用希臘字母φ(phi)表示緯圈角度(經度),而用θ(theta)表示經圈角度(緯度)。這四個數的默認值分別為0,2π,0,π,通過改變他們的值,可以創造出殘缺的球面(但是邊緣必須整齊)。

源碼中,除了北極和南極的極圈內的區域是用face3類型表面,其他部位都是用的face4型表面。本地原點為球心。

Geometry::PlaneGeometry

該構造函數創建一個平面。

THREE.PlaneGeometry = function ( width, height, widthSegments, heightSegments ){
    /**/
}

各參數意義:依次為寬度、高度、寬度分段數、高度分段數。想必讀者對這種構造“格網”的方式應該很熟悉了吧。

源碼中得到一些其他信息:平面被構造在x-y平面上,原點即矩形中心點。

Geometry::ExtrudeGeometry

該對象現在是構造一般幾何形體的方法,但是通常我們是將建模好的對象存儲在某種格式的文件中,並通過loader加載進來,所以似乎鮮有直接用到該函數的機會。而且這個函數看上去還是半成品,很多設定一股腦地堆在options對象里,我也沒有仔細研究。

Material::Material

Material對象是所有其他種類Material的原型對象。

THREE.Material = function () {
    THREE.MaterialLibrary.push( this );
    this.id = THREE.MaterialIdCount ++;
    this.name = '';
    this.side = THREE.FrontSide;
    this.opacity = 1;
    this.transparent = false;
    this.blending = THREE.NormalBlending;
    this.blendSrc = THREE.SrcAlphaFactor;
    this.blendDst = THREE.OneMinusSrcAlphaFactor;
    this.blendEquation = THREE.AddEquation;
    this.depthTest = true;
    this.depthWrite = true;
    this.polygonOffset = false;
    this.polygonOffsetFactor = 0;
    this.polygonOffsetUnits = 0;
    this.alphaTest = 0;
    this.overdraw = false; // Boolean for fixing antialiasing gaps in CanvasRenderer
    this.visible = true;
    this.needsUpdate = true;
};

先看一些較為重要的屬性:

屬性opacity為一個0-1區間的值,表明透明度。屬性transparent指定是否使用透明,只有在該值為真的時候,才會將其與混合(透明是渲染像素時,待渲染值與已存在值共同作用計算出渲染后像素值,達到混合的效果)。

屬性blending,blendSrc,blendDst,blendEquation指定了混合方式和混合源Src和混合像素已有的像元值Dst的權重指定方式。默認情況下(如構造函數中賦的缺省值),新的像元值等於:新值×alpha+舊值×(1-alpha)。

我曾困惑為何Material類中沒有最重要的對象,表示紋理圖片的屬性。后來我理解了,其實材質和紋理還是有區別的,只能說某種材質有紋理的,但也有材質是沒有紋理的。材質影響的是整個形體的渲染效果,比如:“對一根線渲染為5px寬,兩端點為方塊,不透明的紅色”這段描述就可以認為是材質,而沒有涉及任何紋理。

和眾多Geometry對象一樣,Material對象除了通用的clone(),dellocate()和setValues()方法,沒有其他方法。以下是兩種最基本的材質對象。

Material::LineBasicMaterial

該構造函數創建用於渲染線狀形體的材質。

THREE.LineBasicMaterial = function ( parameters ) {
    THREE.Material.call( this );
    this.color = new THREE.Color( 0xffffff );
    this.linewidth = 1;
    this.linecap = 'round';
    this.linejoin = 'round';
    this.vertexColors = false;
    this.fog = true;
    this.setValues( parameters );
};

屬性color和linewidth顧名思義,指線的顏色和線的寬度(線沒有寬度,這里的寬度是用來渲染的)。

屬性linecap和linejoin指定線條端點和連接點的樣式。

屬性fog指定該種材質是否收到霧的影響。

Material::MeshBasicMaterial

該構造函數創建用於渲染Mesh表面的材質。

THREE.MeshBasicMaterial = function ( parameters ) {
    THREE.Material.call( this );
    this.color = new THREE.Color( 0xffffff ); // emissive
    this.map = null;
    this.lightMap = null;
    this.specularMap = null;
    this.envMap = null;
    this.combine = THREE.MultiplyOperation;
    this.reflectivity = 1;
    this.refractionRatio = 0.98;
    this.fog = true;
    this.shading = THREE.SmoothShading;
    this.wireframe = false;
    this.wireframeLinewidth = 1;
    this.wireframeLinecap = 'round';
    this.wireframeLinejoin = 'round';
    this.vertexColors = THREE.NoColors;
    this.skinning = false;
    this.morphTargets = false;
    this.setValues( parameters );
};

這里出現了最重要的紋理屬性,包括map,lightMap和specularMap,他們都是texture類型的對象。

屬性wireframe指定表面的邊界線是否渲染,如果渲染,后面的若干以wireframe開頭的屬性表示如果渲染邊界線,將如何渲染。

Texture::Texture

該構造函數用來創建紋理對象。

THREE.Texture = function ( image, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {
    THREE.TextureLibrary.push( this );
    this.id = THREE.TextureIdCount ++;
    this.name = '';
    this.image = image;
    this.mapping = mapping !== undefined ? mapping : new THREE.UVMapping();
    this.wrapS = wrapS !== undefined ? wrapS : THREE.ClampToEdgeWrapping;
    this.wrapT = wrapT !== undefined ? wrapT : THREE.ClampToEdgeWrapping;
    this.magFilter = magFilter !== undefined ? magFilter : THREE.LinearFilter;
    this.minFilter = minFilter !== undefined ? minFilter : THREE.LinearMipMapLinearFilter;
    this.anisotropy = anisotropy !== undefined ? anisotropy : 1;
    this.format = format !== undefined ? format : THREE.RGBAFormat;
    this.type = type !== undefined ? type : THREE.UnsignedByteType;
    this.offset = new THREE.Vector2( 0, 0 );
    this.repeat = new THREE.Vector2( 1, 1 );
    this.generateMipmaps = true;
    this.premultiplyAlpha = false;
    this.flipY = true;
    this.needsUpdate = false;
    this.onUpdate = null;
};

最重要的屬性是image,這是一個JavaScript Image類型對象。傳入的第一個參數就是該對象,如何創建該對象在后面說。

后面的對象都是可選的,如果缺省就會填充默認值,而且往往都是填充默認值。

屬性magFileter和minFileter指定紋理在放大和縮小時的過濾方式:最臨近點、雙線性內插等。

從url中生成一個texture,需要調用Three.ImageUtils.loadTexture(paras),該函數返回一個texture類型對象。在函數內部又調用了THREE.ImageLoader.load(paras)函數,這個函數內部又調用了THREE.Texture()構造函數,生成紋理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM