敲一敲threejs的门

9,945 阅读15分钟

本文开门见山,走马观花式的过一下three,尽量不讲api细节, threeJS就是对webgl的封装和扩展,其特点是面向对象的。

用threeJs渲染页面的基本要素有 相机、场景、 渲染器。 下面就这三个对象略微展开一下。

相机

相机的作用就是用来变换视角。 个人认为, 3D游戏和伪3D或者2.5D的区别就是可以自由转动移动视角。 像《神庙逃亡》《王者荣耀》这种游戏,虽然本质上也是3d但是限制了镜头。

透视相机

透视相机就如同人的眼睛和现实生活中的相机类似,特点就是 近大远小

鸽子.jpg

近大远小是光的直线传播造成的,就是小孔成像,只会接受进入小孔的光线。 人是不能直接看到自己的后脑勺的,因为可视区域有限,大概是两个圆锥体的范围。

而threeJS的透视相机,其可视范围是一个四棱台,是方的。 我们在方形显示器上看见的画面,就是相机看见的画面。

PerspectiveCamera的参数可以清晰的从下图看出。

fov是这个四棱台上下两面的夹角 视角越大可视范围越大,实际画面的物体便越小。

aspect 是近裁剪面(就是一个矩形)的宽高比。

near 相机(视点)到近裁剪面的距离。

far 相机(视点)到远裁剪面的距离。

cosnt  [near, far,fov, aspect]=[
        1,
        8,
        45, 
      viewW/viewH
    ]
    const camera = new PerspectiveCamera(fov, aspect, near, far)

image.png

正交相机

之所以后说正交相机,是因为用的比较少。 一般我们是不会直接用到正交相机的,但是制造平行光影子的时候,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);

image.png

相机控制器

前面就说了,用户如果不能自由掌控相机,虽然是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 组

GroupObject3D几乎没有任何区别, 之所以搞这么一个类,其目的和我们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的立方体几何体绘制的点线面。

image.png three中除了立方体、球、多面体等常规几何体之外,还有车削,管道等有意思的几何体,它们通常是结合2d图形使用的。

image.png

image.png

image.png

image.png

image.png

image.png

当然了,用的最多的还是直接加载模型,一个模型是由多个几何体构成的,当然模型要好看,肯定是要材质的。

Material 材质

材质决定了这个几何体怎么上色。 虽然有什么木头材质,金属材质,但是请注意,这里的材质仅仅是视觉效果上的,木头材质的立方体它也不可燃,金属材质的小球它也不导电。

这里不展开讲材质的属性和应用,大概看一下常用的材质。

MeshBasicMaterial基础材质,简单理解就是纯色材质,单一颜色。

MeshNormal法线材质 会根据顶点的坐标的方向呈现不同的颜色,常用于debug。上面的几张几何体都是这个材质

MeshPhongMaterial 冯氏模型,经典光照模型,因为它是按人的经验去模拟自然界的光照效果,所以是一个经验模型。

MeshStandardMaterial 标准材质,物理光照模型,是基于物理光学去模拟自然的光照效果。 加载模型后默认使用此材质

MeshPhysicalMaterial 物理材质,继承于标准材质,能更好的模拟真实世界,目前还在不断更新中,它将是下一个标准材质。

不知是否有注意到上面几种材质的命名,都是Mesh开头,因为这些材质都是适用于Mesh类的面绘制模式。 线当然也可以用,但是视觉效果上大概看不出来。 点绘图模式就需要用专门的点材质。

image.png

Line

const line1 =  new line(geometry, material)
const line2 =  new Mesh(geometry, new MeshNormalMaterial({wireframe:true}))

Line就是线,作为线来说它是没有宽度的,当然实际上是宽度固定为1px,如果要画有宽度的线,那实际上就是面。 除了直接用Line类,也可以使用Mesh,然后开启材质的wireframe属性,都是将绘图模式转变为线模式。所以通常都是直接用mesh类,线和面都能绘制。

image.png

Points

    const cube = new Points(geometry,  new PointsMaterial({color: 0xffccee,size: 20,sizeAttenuation:false})) ;

Points 就是是点, 虽然看上就是一个很小的矩形面,但是它真的不是面, 因为你不论从任何角度(转动相机)去看它, 它永远展示的都是那一个矩形面。 所以我姑且将其类比于脱标元素。

基于点的这一个特性,如果你想要一个永远朝向相机的标签之类的东西,点就可以做到。

它还有一个重要属性sizeAttenuation, 点的大小是否被相机影响。我们已经知道透视相机的特性,近大远小,three默认是开启这个属性,点的大小同样会表现为近大远小, 关闭此属性,则点的大小不受相机影响。

点也常用于制作粒子特效,别看点是方的,当它足够小,数量足够多的时候,视觉上是分辨不出来的。虽然,也可以把方的变成圆的,但很多时候没有必要增加这个负担。

image.png

image.png

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的官网是自带国际化的。

image.png