Three.js源码阅读笔记(物体是如何组织的)


Posted in Javascript onDecember 27, 2012

这是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()构造函数,生成纹理。

Javascript 相关文章推荐
Highslide.js是一款基于js实现的网页中图片展示插件
Mar 30 Javascript
JS获取后台Cookies值的小例子
Mar 04 Javascript
jQuery使用post方法提交数据实例
Mar 25 Javascript
详解Bootstrap按钮
Jan 04 Javascript
详解JavaScript中数组和字符串的lastIndexOf()方法使用
Mar 13 Javascript
总结javascript中的六种迭代器
Aug 16 Javascript
Web前端框架bootstrap实战【第一次接触使用】
Dec 28 Javascript
[js高手之路]寄生组合式继承的优势详解
Aug 28 Javascript
浅谈在node.js进入文件目录的问题
May 13 Javascript
vue引入微信sdk 实现分享朋友圈获取地理位置功能
Jul 04 Javascript
微信小程序 拍照或从相册选取图片上传代码实例
Aug 28 Javascript
Vue检测屏幕变化来改变不同的charts样式实例
Oct 26 Javascript
Three.js源码阅读笔记(光照部分)
Dec 27 #Javascript
通过jQuery源码学习javascript(三)
Dec 27 #Javascript
JS原型对象通俗&quot;唱法&quot;
Dec 27 #Javascript
通过jQuery源码学习javascript(二)
Dec 27 #Javascript
js 判断一个元素是否在页面中存在
Dec 27 #Javascript
通过jQuery源码学习javascript(一)
Dec 27 #Javascript
Eval and new funciton not the same thing
Dec 27 #Javascript
You might like
首页四格,首页五格For6.0(GBK)(UTF-8)[12种组合][9-18][版主安装测试通过]
2007/09/24 PHP
PHP删除数组中指定值的元素常用方法实例分析【4种方法】
2018/08/21 PHP
实例介绍PHP中zip_open()函数用法
2019/02/15 PHP
laravel excel 上传文件保存到本地服务器功能
2019/11/14 PHP
jquery tools系列 expose 学习
2009/09/06 Javascript
Javascript中的相等与不等运算
2010/04/25 Javascript
javascript中的关于类型转换的性能优化
2010/12/14 Javascript
jQuery中delegate与on的用法与区别示例介绍
2013/12/20 Javascript
再谈Jquery Ajax方法传递到action(补充)
2014/05/12 Javascript
javascript根据时间生成m位随机数最大13位
2014/10/30 Javascript
jquery中添加属性和删除属性
2015/06/03 Javascript
js实现仿qq消息的弹出窗效果
2016/01/06 Javascript
Jquery easyui开启行编辑模式增删改操作
2016/01/14 Javascript
dedecms页面如何获取会员状态的实例代码
2016/03/15 Javascript
Web 开发中Ajax的Session 超时处理方法
2017/01/19 Javascript
Base64(二进制)图片编码解析及在各种浏览器的兼容性处理
2017/02/09 Javascript
关于Node.js中Buffer的一些你可能不知道的用法
2017/03/28 Javascript
JS实现css hover操作的方法示例
2017/04/07 Javascript
JavaScript闭包_动力节点Java学院整理
2017/06/27 Javascript
js设置鼠标悬停改变背景色实现详解
2019/06/26 Javascript
简单了解JavaScript sort方法
2019/11/25 Javascript
JavaScript 面向对象程序设计详解【类的创建、实例对象、构造函数、原型等】
2020/05/12 Javascript
python检测服务器是否正常
2014/02/16 Python
浅谈python中的变量默认是什么类型
2016/09/11 Python
利用numpy实现一、二维数组的拼接简单代码示例
2017/12/15 Python
django表单实现下拉框的示例讲解
2018/05/29 Python
关于Python 解决Python3.9 pandas.read_excel(‘xxx.xlsx‘)报错的问题
2020/11/28 Python
python 批量将中文名转换为拼音
2021/02/07 Python
css3 中实现炫酷的loading效果
2019/04/26 HTML / CSS
写出SQL四条最基本的数据操作语句(DML)
2012/12/12 面试题
大四自我鉴定范文
2013/10/06 职场文书
物业管理工作方案
2014/05/10 职场文书
2014年司法局工作总结
2014/12/11 职场文书
事业单位财务人员岗位职责
2015/04/14 职场文书
python 字典和列表嵌套用法详解
2021/06/29 Python
详解SQL的窗口函数
2022/04/21 Oracle