本文开门见山,走马观花式的过一下three,尽量不讲api细节, threeJS就是对webgl的封装和扩展,其特点是面向对象的。
用threeJs渲染页面的基本要素有 相机、场景、 渲染器。 下面就这三个对象略微展开一下。
相机
相机的作用就是用来变换视角。 个人认为, 3D游戏和伪3D或者2.5D的区别就是可以自由转动移动视角。 像《神庙逃亡》《王者荣耀》这种游戏,虽然本质上也是3d但是限制了镜头。
透视相机
透视相机就如同人的眼睛和现实生活中的相机类似,特点就是 近大远小 。
近大远小是光的直线传播造成的,就是小孔成像,只会接受进入小孔的光线。 人是不能直接看到自己的后脑勺的,因为可视区域有限,大概是两个圆锥体的范围。
而threeJS的透视相机,其可视范围是一个四棱台,是方的。 我们在方形显示器上看见的画面,就是相机看见的画面。
PerspectiveCamera的参数可以清晰的从下图看出。
fov是这个四棱台上下两面的夹角 视角越大可视范围越大,实际画面的物体便越小。
aspect 是近裁剪面(就是一个矩形)的宽高比。
near 相机(视点)到近裁剪面的距离。
far 相机(视点)到远裁剪面的距离。
cosnt [near, far,fov, aspect]=[
1,
8,
45,
viewW/viewH
]
const camera = new PerspectiveCamera(fov, aspect, near, far)
正交相机
之所以后说正交相机,是因为用的比较少。 一般我们是不会直接用到正交相机的,但是制造平行光影子的时候,threeJs就会在背地里用到。 如果要讲投影的话,是应该从正交投影看起的。
正交相机就是只会接收垂直于承光面的光线,而我们这个承光面(近裁剪面)又是个矩形,所以可视区域就是一个立方体。
上下左右远近 这六个参数就决定了这个立方体的位置和大小,需要注意的是,近裁剪面是垂直于z轴的,垂足就在矩阵中心,所以上下左右这个四个参数是两正两负的,而远近都是正数,就姑且认为是距离吧。
同样的,近裁剪面越大,可视区域就越大,虽然还有一个深度,但是视觉上不一定看得出来, 可视区域大了,物体就小了。
const [near, far, left, right, top, bottom , ] = [
1,
8,
-2 /ratio,
2/ ratio,
2 ,
-2 ,
];
const camera = new OrthographicCamera(left, right, top, bottom, near, far);
相机控制器
前面就说了,用户如果不能自由掌控相机,虽然是3D但是给人的感觉就少了点什么。
相机控制器就是用来操控相机的移动旋转,参考一下人的视觉。
一个人沿直线行走,正视前方,此时相机在平移,没有转动。 当他走到路口时,站定左顾右盼有无危险,此时相机在转动,当他看见红灯还有一段时间,仰头望天,这也是转动。
转动了相机就是转动了整个世界,人眼也是一样,虽然世界没有转,但是我眼中的世界转了。
轨道控制器
最常用的相机控制器是轨道控制器OrbitControl,之所以叫轨道控制器,是因为,在不平移相机的情况下,转动视角,实际上是在一个球面内的自由移动,就像环球卫星轨道一样。
初始化只要传入相机和画布元素就可以了。 然后什么也不用做,你就可以自由操控相机了。
const orbitControls = new OrbitControls(camera, canvas)
轨道控制器是可以很方便地限制用户可操控相机的范围。
轨迹球控制器
const trackControls = new TrackBallControls(camera, renderer.domElement)
轨道控制器在转动方面实际上是分水平和竖直两个方向的,它用球坐标实现,决定了在俯仰这个方向上必定会有所限制。
轨迹球控制器,就像一个躺在凹槽里的小球,你扒拉一下,它就能转一下,你不停地扒拉它就可以不停地转。
它可以任意方向720°旋转。 但是它太自由了,更多时候用户需要一些束缚,所以还是轨道控制器用的多。
玩一玩就知道了。
场景
scene就是我们希望呈现在画布上的东西。 可以类比我们的dom树, Scene 是一个最外层的节点,类似body 。当然你可以scene里面嵌套scene,这和div套div是一样的。
Object3D
const obj3d = new Object3D()
3d物体是一个抽象的基类,就好比我们的html的Node类。 所以Scene也是继承了它的。
就是说现在有一个三维物体,但是我不知道它的形状和颜色,只知道它是三维的。
既然它是三维的那它就可以在三维空间中平移、 旋转、 缩放。 它可以有子节点和父节点。 它可以被添加到某个节点下面,也可以增删子节点。 就是一套增删改,也可以查。
const cube2 = new THREE.Mesh(geometry, material2) ;
const cube3 = new THREE.Mesh(geometry, material3) ;
scene.add(cube2);
scene.attach(cube3)
scene.remove(cube2)
cube3.removeFromParent() ;
增
Object3D.add(Object3D , Object3D ,...);
Object3D.attach(Object3D , Object3D,...);
有方法add和attach , 区别就是attach方法会保留子物体的世界坐标位,而add不会。
把一个三维物体添加到新的父及下面,会使得这个物体脱离它原本的父级,这很合理,一个节点不能有两个父级。
关于世界坐标位和本地坐标位,这个仍然可以参照我们的div嵌套。
所谓本地坐标就是相对于其父级的位置坐标 , 就像我们常用的子绝父相那样。
世界坐标就是相对于最外层Scene的位置坐标,就像一个元素的clientXY,相对窗口的位置一样。
不同的是,三维物体的相对,还包括旋转,严格来说是世界矩阵和本地矩阵。
只要记住,一个物体的世界位就等于其父级的世界位乘以其本地位即可
删
Object3D.remove(Object3D , Object3D,...);
Object3D.removeFromParent();
当我们确定不再需要一个物体时,就可将它抛弃了,父级调用remove方法,自我删除调用removeFromParent。
改
改的花样比较多。一般来说无非是两种,一种是改变其外观,另一种是改变其携带的信息。
变换属性
常用的变换属性就是下面三个,可以简单认为都是三维向量。
Vector3就是用包含xyz三个属性的一个对象来表示向量, 对象还提供了完整计算api。
new Vector2(x,y)
new Vector2(x,y,z)
new Vector4(x,y,z,w)
const obj = new Object3D()
obj.position= new Vector3(1,1,1) ;
obj.rotation.set(0,0,0,'XYZ')
obj.scale.y = 2 ;
position 相对父级的坐标位置,之所以不说是相对父级位置,是因为可能还有缩放系数。
rotation 同样是相对父级的旋转, 它其是一个欧拉对象,但是也有xyz属性,通常也是直接修改xyz。
scale 相对父级的缩放。
变换transform
平移、旋转、缩放都是变换。 可以通过对应的api,或者直接修改其对应属性,来达到变换的效果。
简单列举一下常用的,矩阵是通用的就不列举了。 方法和属性都有本地和世界之分, 没有World字眼那就是本地。
需要额外注意的是旋转,直接修改rotation 和调用rotate方法的结果可能不同,因为四元数和欧拉的不同。 角度都是弧度值。四元数暂且按下不表。
缩放没有提供方法。
// 旋转
Object3D.rotateX( angle )
Object3D.rotateY( angle )
Object3D.rotateZ( angle )
Object3D.rotateOnAxis( axis, angle )
Object3D.rotateOnWorldAxis( axis, angle )
Object3D.rotation.set(x,y,z,order)
Object3D.rotation.x = Math.PI
// 位移
Object3D.translateOnAxis( axis, distance )
Object3D.translateX( distance )
Object3D.translateY( distance )
Object3D.translateZ( distance )
Object3D.position.set(x,y,z) ;
Object3D.position.x = 0 ;
//缩放
Object3D.scale.set(x,y,z)
Object3D.scale.x= 0 ;
自定义属性
Object3D提供了userData这个属性,让用户自由处置,就像我们HTML元素上的dataset。
```
Object3D.userData.author = '莫石'
```
查
Object3D提供了遍历方法traverse,是深度优先遍历,会递归遍历全部后代节点。
也提供了查询方法getObjectByProperty,根据某个属性查询后代节点。
提醒,add 和attach方法会使得节点从原本的父级移除,所以在遍历的时候最好不要增删节点。
Object3D.traverse(callback)
scene.traverse((child) => {
if (child.name == "outer" || child.name == "mask") {
child.material.envMap = texture;
child.material.envMap.mapping = EquirectangularReflectionMapping;
child.material.envMapIntensity = 2;
} else if (child.name == "body") {
var map = child.material.map;
child.material = new MeshToonMaterial({ map: map });
}
})
byId 和 byName就是调用getObjectByProperty方法。
Object3D.getObjectById( id )
Object3D.getObjectByName( name )
Object3D.getObjectByProperty(keyname, value)
除此之外,还有获取世界位的方法族,大部分Object3D的世界位并不在属性上。
getWorldPosition( vector3 )
getWorldQuaternion( Quaternion )
getWorldScale( vector3 )
具体的三维物体可以分为点线面, 意思是说这个三维物体的构成,全部由点线面的一种构成。
类比我们的元素可以强行类比为块级元素,行内元素,以及脱标元素。
Group 组
Group和Object3D几乎没有任何区别, 之所以搞这么一个类,其目的和我们HTML的标签语义化差不多,其目的是使得组中对象在语法上的结构更加清晰。
const geo = new BoxGeometry( 1, 1, 1 );
const mat = new MeshBasicMaterial( {color: 0x00ff00} );
const a = new Mesh (geo, mat),b = new Mesh(geo,mat)
const group = new Group() ;
group.add(a,b);
group.type === 'Group' // true
group.isGroup // true
Mesh 网格
直译是网格,我猜这是因为光栅(栅格)化的缘故,所以大家都用这么个单词。 一般来说,mesh对应点线面里面的面。
const mesh = new Mesh(geometry, material)
mesh是我们最常用的一种三维物体。可以说,没有mesh展示不了的东西,就像万能的div一样。
Mesh 可以类比 element,是一个更为具体的三维物体,同样继承自Object3D。
作为一个mesh ,肯定就有具体的形状和颜色。简单来说 Geometry 确定形状,Material确定颜色 。
Geometry 几何体
const box = new BoxGeometry(2,2,2,2,2,3) // width分段就是平行yoz平面切几刀, height分段就是平行与 xoy切几刀,depth就是xoz
const cube2 = new TetrahedronGeometry(2) //四面体,半径 detail 额外加点 ,大于零 不再是四面体,1的时候在每个面上又生长出一个四面体
const cube3 = new OctahedronGeometry(2) //八面体,半径 detail 额外加点 ,大于零 不再是八面体,
const cube4 = new DodecahedronGeometry(2)// 五边形 十二面体
const ball =new SphereGeometry(1.5) ;// 方位角起始 0是x负半轴,弧度增量
const verticesOfCube = [
-1,-1,-1, 1,-1,-1, 1, 1,-1, -1, 1,-1,
-1,-1, 1, 1,-1, 1, 1, 1, 1, -1, 1, 1,
];
const indicesOfFaces = [
2,1,0,
0,6,1,
0,1,5,
1,2,6,
2,3,7,
4,5,6, ];
const polyhedron = new PolyhedronGeometry(verticesOfCube, indicesOfFaces, 3) // 多面体 顶点 每个面的索引 半径,detail ,就是多个面的集合,完全把顶点交给用户
const cone = new ConeGeometry(2,2,12,3, true, )// 圆锥 底部半径 高,锥面弧度分组,纵向分组(就是对应圆底 和高) 是否露底,起始角度,结束弧度,0是z轴正半轴
const cylinder = new CylinderGeometry(1,2,2,12,3,false) // 圆柱,上圆半径 底圆半径
const ring = new TorusGeometry(1.2,0.8,5,9) ; // 立体圆环 环的半径, 管道半径 , 管道圆柱纵向分段(2就近乎平面了), 圆环分段
const tubeknot = new TorusKnotGeometry(1.5,) //扭结 外接球半径 管道半径 整个管道分段,管道切圆分段,
点线面是三种不同的绘图模式,但是可以用同一个几何体。 这就要说一下几何体是如何决定形状的。
那就是顶点,三维空间的一个点,其坐标是三维向量。 一个几何体就是一组顶点,这一组顶点是有序的。
比如说,现在这个几何体包含了36个顶点.
每三个点组成一个三角形,那么最终就是12个三角形(面)。
如果每两个点组成一条线段,最终就是18条线段。
如果每个顶点,都画一个点,最后就是36个点。
这里说的是绘制,但是顶点坐标可能有重复的,最终视觉看到的未必是这样。 下面就是用three的立方体几何体绘制的点线面。
three中除了立方体、球、多面体等常规几何体之外,还有车削,管道等有意思的几何体,它们通常是结合2d图形使用的。
当然了,用的最多的还是直接加载模型,一个模型是由多个几何体构成的,当然模型要好看,肯定是要材质的。
Material 材质
材质决定了这个几何体怎么上色。 虽然有什么木头材质,金属材质,但是请注意,这里的材质仅仅是视觉效果上的,木头材质的立方体它也不可燃,金属材质的小球它也不导电。
这里不展开讲材质的属性和应用,大概看一下常用的材质。
MeshBasicMaterial基础材质,简单理解就是纯色材质,单一颜色。
MeshNormal法线材质 会根据顶点的坐标的方向呈现不同的颜色,常用于debug。上面的几张几何体都是这个材质
MeshPhongMaterial 冯氏模型,经典光照模型,因为它是按人的经验去模拟自然界的光照效果,所以是一个经验模型。
MeshStandardMaterial 标准材质,物理光照模型,是基于物理光学去模拟自然的光照效果。 加载模型后默认使用此材质
MeshPhysicalMaterial 物理材质,继承于标准材质,能更好的模拟真实世界,目前还在不断更新中,它将是下一个标准材质。
不知是否有注意到上面几种材质的命名,都是Mesh开头,因为这些材质都是适用于Mesh类的面绘制模式。 线当然也可以用,但是视觉效果上大概看不出来。 点绘图模式就需要用专门的点材质。
Line
const line1 = new line(geometry, material)
const line2 = new Mesh(geometry, new MeshNormalMaterial({wireframe:true}))
Line就是线,作为线来说它是没有宽度的,当然实际上是宽度固定为1px,如果要画有宽度的线,那实际上就是面。 除了直接用Line类,也可以使用Mesh,然后开启材质的wireframe属性,都是将绘图模式转变为线模式。所以通常都是直接用mesh类,线和面都能绘制。
Points
const cube = new Points(geometry, new PointsMaterial({color: 0xffccee,size: 20,sizeAttenuation:false})) ;
Points 就是是点, 虽然看上就是一个很小的矩形面,但是它真的不是面, 因为你不论从任何角度(转动相机)去看它, 它永远展示的都是那一个矩形面。 所以我姑且将其类比于脱标元素。
基于点的这一个特性,如果你想要一个永远朝向相机的标签之类的东西,点就可以做到。
它还有一个重要属性sizeAttenuation, 点的大小是否被相机影响。我们已经知道透视相机的特性,近大远小,three默认是开启这个属性,点的大小同样会表现为近大远小, 关闭此属性,则点的大小不受相机影响。
点也常用于制作粒子特效,别看点是方的,当它足够小,数量足够多的时候,视觉上是分辨不出来的。虽然,也可以把方的变成圆的,但很多时候没有必要增加这个负担。
Renderer
cosnt renderer = new WebGLRenderer({canvas, antialias:true,}) // 抗锯齿
renderer.render(scene, camera);
设置好场景,摆放好相机之后,调用渲染器的渲染方法,画面就呈现出来了。
渲染器,它决定了如何将物体渲染到画布上。 对于使用者来说,就是可以配置一些参数,来改变渲染结果。
调用其渲染方法画面开始绘制, 当你发现画布元素全黑,又没有报错,可能就是没有调用render方法。
属性配置
上面在初始化的时候给了一些参数。
canvas 就是画布元素,如果不传,它会自己创建一个。初始化完成后,domElement对应画布元素
antialias 抗锯齿。
还有一些参数,一般不会用到, 我们来看看一些常用操作。
下面的操作都是把属性改成和默认值相反的。
关闭自动清理背景,关闭了之后如果不手动清理画布,下一次绘制的时候底色将不会被绘制,不绘制就是白色, 默认开启
开启物理准确光照, 开启之后,使用的光照将更加接近真实光照,所以也更加耗费性能,默认关闭。
开启投影贴图的绘制, 开启之后才能使用阴影,默认关闭。
renderer.autoClear = false;
renderer.physicallyCorrectLights = false;
renderer.shadowmap.enabled = true
常用方法
设置背景色, color可以是three的Color 类 也可以是数字(通常用16进制表示),其他表示颜色的字符串。
主动清理画布,当需要手动清理时,可调用此方法,可以看到默认参数都是true,可以直接不传参。
渲染方法, 就两个参数。 所以,如果你需要切换场景,或者切镜头,只要改变参数即可。
renderer.setClearColor(color);
renderer.clear(color =true , depth = true , stencil = true)
renderer.render(scene,camera);
结束
本文到这里就结束了。 看完本文,并不能让你立即掌握threeJS,但是可以让你有一个大致的印象,其余的遇到问题的时候查阅文档和示例即可。
如果想熟练使用threeJS ,除了上文所说的,还需要熟悉一下它的数学库的用法以及材质属性配置。
本文没有提及的还有光照阴影,物体拾取,加载模型,自定义材质,自定义几何体等内容,本文仅仅是敲门文,要登堂入室还需要一定的练习。
threejs有大量的示例,这些示例就是非常好的参照,如果你没有合适练习素材,这些示例也是极好的。最后,three.js的官网是自带国际化的。