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 相关文章推荐
获取服务器传来的数据 用JS去空格的正则表达式
Mar 26 Javascript
JS中数组Array的用法示例介绍
Feb 20 Javascript
Node.js与PHP、Python的字符处理性能对比
Jul 06 Javascript
Javascript 构造函数详解
Oct 22 Javascript
JavaScript正则表达式的分组匹配详解
Feb 13 Javascript
Bootstrap响应式侧边栏改进版
Sep 17 Javascript
js实现碰撞检测特效代码分享
Oct 16 Javascript
JS中动态创建元素的三种方法总结(推荐)
Oct 20 Javascript
jQuery实现选项卡功能(两种方法)
Mar 08 Javascript
Quasar Input:type=&quot;number&quot; 去掉上下小箭头 实现加减按钮样式功能
Apr 09 Javascript
小程序实现背景音乐播放和暂停
Jun 19 Javascript
Javascript基于OOP实实现探测器功能代码实例
Aug 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
php adodb连接带密码access数据库实例,测试成功
2008/05/14 PHP
php学习 函数 课件
2008/06/15 PHP
使用PHP批量生成随机用户名
2008/07/10 PHP
PHP $_FILES函数详解
2011/03/09 PHP
php截取字符串并保留完整xml标签的函数代码
2013/02/06 PHP
执行、获取远程代码返回:file_get_contents 超时处理的问题详解
2013/06/25 PHP
php中debug_backtrace、debug_print_backtrace和匿名函数用法实例
2014/12/01 PHP
php生成条形码的图片的实例详解
2017/09/13 PHP
thinkphp5 模型实例化获得数据对象的教程
2019/10/18 PHP
调用jQuery滑出效果时闪烁的解决方法
2014/03/27 Javascript
基于NodeJS的前后端分离的思考与实践(六)Nginx + Node.js + Java 的软件栈部署实践
2014/09/26 NodeJs
Javascript原型链的原理详解
2016/01/05 Javascript
js 动态给元素添加、移除事件的实现方法
2016/07/19 Javascript
js实现密码强度检验
2017/01/15 Javascript
JS中如何实现Laravel的route函数详解
2017/02/12 Javascript
vue2.0实现导航菜单切换效果
2017/05/08 Javascript
将angular-ui的分页组件封装成指令的方法详解
2017/05/10 Javascript
微信小程序实现团购或秒杀批量倒计时
2020/11/01 Javascript
vue如何在用户要关闭当前网页时弹出提示的实现
2020/05/31 Javascript
[00:15]TI9观赛名额抽取
2019/07/10 DOTA
python实现简易云音乐播放器
2018/01/04 Python
python画图把时间作为横坐标的方法
2019/07/07 Python
Pycharm远程调试原理及具体配置详解
2019/08/08 Python
python使用SQLAlchemy操作MySQL
2020/01/02 Python
Node.js 和 Python之间该选择哪个?
2020/08/05 Python
HTML5中图片之间的缝隙完美解决方法
2017/07/07 HTML / CSS
马来西亚在线购物市场:PGMall.my
2019/10/13 全球购物
在阿联酋购买翻新手机和平板电脑:Teckzu
2021/02/12 全球购物
计算机通信工程专业毕业生推荐信
2013/12/24 职场文书
初一英语教学反思
2014/01/11 职场文书
辅导员评语
2014/05/04 职场文书
2014年社区民政工作总结
2014/12/02 职场文书
Go语言中break label与goto label的区别
2021/04/28 Golang
SQL Server表分区删除详情
2021/10/16 SQL Server
使用 CSS 轻松实现一些高频出现的奇形怪状按钮
2021/12/06 HTML / CSS
vmware虚拟机打不开vmx文件怎么办 ?vmware虚拟机vmx文件打开方法
2022/04/08 数码科技