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 相关文章推荐
Javascript里使用Dom操作Xml
Sep 20 Javascript
Prototype最新版(1.5 rc2)使用指南(1)
Jan 10 Javascript
原生JS可拖动弹窗效果实例代码
Nov 09 Javascript
js星星评分效果
Jul 24 Javascript
js console.log打印对像与数组用法详解
Jan 21 Javascript
JS 数字转换为大写金额的简单实例
Aug 04 Javascript
vuejs动态组件给子组件传递数据的方法详解
Sep 09 Javascript
Javascript 跨域知识详细介绍
Oct 30 Javascript
利用canvas实现的加载动画效果实例代码
Jul 05 Javascript
在vue中使用express-mock搭建mock服务的方法
Nov 07 Javascript
Angular6 用户自定义标签开发的实现方法
Jan 08 Javascript
layui写后台表格思路和赋值用法详解
Nov 14 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
一个简洁的多级别论坛
2006/10/09 PHP
将OICQ数据转成MYSQL数据
2006/10/09 PHP
CodeIgniter上传图片成功的全部过程分享
2013/08/12 PHP
PHP中的use关键字概述
2014/07/23 PHP
CI框架入门示例之数据库取数据完整实现方法
2014/11/05 PHP
php源码 fsockopen获取网页内容实例详解
2016/09/24 PHP
Laravel框架中VerifyCsrfToken报错问题的解决
2017/08/30 PHP
Jquery公告滚动+AJAX后台得到数据
2011/04/14 Javascript
javascript权威指南 学习笔记之变量作用域分享
2011/09/28 Javascript
document.getElementBy(&quot;id&quot;)与$(&quot;#id&quot;)有什么区别
2013/09/22 Javascript
node.js中的require使用详解
2014/12/15 Javascript
JS实现一个按钮的方法
2015/02/05 Javascript
JavaScript实现模仿桌面窗口的方法
2015/07/18 Javascript
jquery+css实现的红色线条横向二级菜单效果
2015/08/22 Javascript
基于angular中的重要指令详解($eval,$parse和$compile)
2016/10/21 Javascript
Node.js的Mongodb使用实例
2016/12/30 Javascript
jQuery zTree树插件动态加载实例代码
2017/05/11 jQuery
ionic2自定义cordova插件开发以及使用(Android)
2017/06/19 Javascript
深入理解 webpack 文件打包机制(小结)
2018/01/08 Javascript
Vue.js 2.0和Cordova开发webApp环境搭建方法
2018/02/26 Javascript
Vue.js实现开发购物车功能的方法详解
2019/02/22 Javascript
小程序如何定位所在城市及发起周边搜索
2020/02/11 Javascript
Python with语句上下文管理器两种实现方法分析
2018/02/09 Python
Python学习笔记之open()函数打开文件路径报错问题
2018/04/28 Python
TensorFlow车牌识别完整版代码(含车牌数据集)
2019/08/05 Python
基于python使用tibco ems代码实例
2019/12/20 Python
如何解决cmd运行python提示不是内部命令
2020/07/01 Python
运动会入场解说词
2014/02/07 职场文书
学习标兵获奖感言
2014/02/20 职场文书
大学生就业意向书范文
2014/04/01 职场文书
2014年会计主管工作总结
2014/12/20 职场文书
解析:创业计划书和商业计划书二者之间到底有什么区别
2019/08/14 职场文书
Python爬虫中urllib3与urllib的区别是什么
2021/07/21 Python
Python中的变量与常量
2021/11/11 Python
MySQL选择合适的备份策略和备份工具
2022/06/01 MySQL
Redis配置外网可访问(redis远程连接不上)的方法
2022/12/24 Redis