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笔试题目附答案@20081025_jb51.net
Oct 26 Javascript
JavaScript高级程序设计 客户端存储学习笔记
Sep 10 Javascript
修复IE9&amp;safari 的sort方法
Oct 21 Javascript
node+express制作爬虫教程
Nov 11 Javascript
微信小程序 支付功能实现PHP实例详解
May 12 Javascript
详解vue-router2.0动态路由获取参数
Jun 14 Javascript
使用SVG基本操作API的实例讲解
Sep 14 Javascript
详解Vue路由钩子及应用场景(小结)
Nov 07 Javascript
webpack4与babel配合使es6代码可运行于低版本浏览器的方法
Oct 12 Javascript
Vue使用Proxy监听所有接口状态的方法实现
Jun 07 Javascript
微信小程序转发事件实现解析
Oct 22 Javascript
vue路由切换时取消之前的所有请求操作
Sep 01 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
php iconv() : Detected an illegal character in input string
2010/12/05 PHP
PHP逐行输出(ob_flush与flush的组合)
2012/02/04 PHP
PHP实现的英文名字全拼随机排号脚本
2014/07/04 PHP
PHP 5.3和PHP 5.4出现FastCGI Error解决方法
2015/02/12 PHP
php使用GD库创建图片缩略图的方法
2015/06/10 PHP
PHP实现绘制二叉树图形显示功能详解【包括二叉搜索树、平衡树及红黑树】
2017/11/16 PHP
ext 代码生成器
2009/08/07 Javascript
JavaScript实现QueryString获取GET参数的方法
2013/07/02 Javascript
js的alert弹出框出现乱码解决方案
2013/09/02 Javascript
深入理解javascript中defer的作用
2013/12/11 Javascript
深入了解JavaScript中的Symbol的使用方法
2015/07/28 Javascript
JavaScript数组对象赋值用法实例
2015/08/04 Javascript
使用jquery如何获取时间
2016/10/13 Javascript
使用微信小程序开发前端【快速入门】
2016/12/05 Javascript
微信小程序之滚动视图容器的实现方法
2017/09/26 Javascript
Vue结合Video.js播放m3u8视频流的方法示例
2018/05/04 Javascript
vue组件(全局,局部,动态加载组件)
2018/09/02 Javascript
JavaScript中构造函数与原型链之间的关系详解
2019/02/25 Javascript
taro开发微信小程序的实践
2019/05/21 Javascript
总结Python编程中三条常用的技巧
2015/05/11 Python
Python中列表的一些基本操作知识汇总
2015/05/20 Python
python数据结构之图深度优先和广度优先实例详解
2015/07/08 Python
Python爬取京东的商品分类与链接
2016/08/26 Python
python pygame实现五子棋小游戏
2020/10/26 Python
html5-canvas中使用clip抠出一个区域的示例代码
2018/05/25 HTML / CSS
关于老式浏览器兼容HTML5和CSS3的问题
2016/06/01 HTML / CSS
世界上最大的餐具公司:Oneida
2016/12/17 全球购物
英国高档百货连锁店:John Lewis
2017/11/20 全球购物
医科大学毕业生自荐信
2014/02/03 职场文书
2014年生产部工作总结
2014/12/17 职场文书
企业开业庆典答谢词
2015/01/20 职场文书
有关骆驼祥子的读书笔记
2015/06/26 职场文书
小区物业管理2015年度工作总结
2015/10/22 职场文书
详解JAVA中的OPTIONAL
2021/06/14 Java/Android
NGINX 权限控制文件预览和下载的实现原理
2022/01/18 Servers
spring 项目实现限流方法示例
2022/07/15 Java/Android