ThreeJs 基础入门

本文来自网易云社区

作者:唐钊


Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它在 web 中创建各种三维场景,包括了摄影机、光影、材质等各种对象。使用它可以让我们更加直观的了解 webgl 的世界。

3D 场景前置知识

1.场景(Scene):是物体、光源等元素的容器,可以配合 chrome 插件使用,抛出 window.scene即可实时调整 obj 的信息和材质信息。2.相机(Camera):场景中的相机,代替人眼去观察,场景中只能添加一个,一般常用的是透视相机(PerspectiveCamera)3.物体对象(Mesh):包括二维物体(点、线、面)、三维物体,模型等等4.光源(Light):场景中的光照,如果不添加光照场景将会是一片漆黑,包括全局光、平行光、点光源等5.渲染器(Renderer):场景的渲染方式,如webGL\canvas2D\Css3D。6.控制器(Control): 可通过键盘、鼠标控制相机的移动

下面我们依次详细学习以上的细分知识点。

相机

Three.js中我们常用的有两种类型的相机:正交(orthographic)相机、透视(perspective)相机。一般情况下为了模拟人眼我们都是使用透视相机; 正交镜头的特点是,物品的渲染尺寸与它距离镜头的远近无关。也就是说在场景中移动一个物体,其大小不会变化。正交镜头适合2D游戏。 透视镜头则是模拟人眼的视觉特点,距离远的物体显得更小。透视镜头通常更适合3D渲染。

THREE.PerspectiveCamera(fov,aspect,near,far)

参数 描述
fov 视野角度,从镜头可以看到的场景的部分。通常3D游戏的FOV取值在60-90度之间较好的默认值为60
aspect 渲染区域的纵横比。较好的默认值为window.innerWidth/window.innerHeight
near 最近离镜头的距离
far 远离镜头的距离

透视相机示意图:


创建摄像机以后还要对其进行移动、然后对准物体积聚的场景中心位置,分别是设置其 position和调用 lookAt 方法,参数均是一个 xyz向量(new THREE.Vector3(x,y,z))

camera.position:控制相机在整个3D环境中的位置(取值为3维坐标对象-THREE.Vector3(x,y,z))
camera.lookAt:控制相机的焦点位置,决定相机的朝向(取值为3维坐标对象-THREE.Vector3(x,y,z))

灯光

在Three.js中光源是必须的,如果一个场景你不设置灯光那么世界将会是一片漆黑。Three.js内置了多种光源以满足特定场景的需要。大家可以根据自己的项目需要来选择何种灯光

光源分类

光源 说明
AmbientLight 环境光,其颜色均匀的应用到场景及其所有对象上,这种光源为场景添加全局的环境光。
这种光没有特定的方向,不会产生阴影。通常不会把AmbientLight作为唯一的光源,
而是和SpotLight、DirectionalLight等光源结合使用,从而达到柔化阴影、添加全局色调的效果。
指定颜色时要相对保守,例如#0c0c0c。设置太亮的颜色会导致整个画面过度饱和,什么都看不清:
PointLight 3D空间中的一个点光源,向所有方向发出光线
SpotLight 产生圆锥形光柱的聚光灯,台灯、天花板射灯通常都属于这类光源,这种光源的使用场景最多
,特别是在你需要阴影效果的时候。
DirectionalLight 也就无限光,光线是平行的。典型的例子是日光,用于模拟遥远的,类似太阳那样的光源。
该光源与SpotLight的主要区别是,它不会随着距离而变暗,所有被照耀的地方获得相同的光照强度。
HemisphereLight 特殊光源,用于创建户外自然的光线效果,
此光源模拟物体表面反光效果、微弱发光的天空,模拟穹顶(半球)的微弱发光效果,
让户外场景更加逼真。使用DirectionalLight + AmbientLight可以在某种程度上来模拟户外光线,
但是不够真实,因为无法体现大气层的散射效果、地面或物体的反射效果
AreaLight 面光源,指定一个发光的区域
LensFlare 不是光源,用于给光源添加镜头光晕效果

关于光源的详细 API 大家可以参考 threejs 官网,很详细,demo 也很完整 传送门

Mesh

在计算机的世界里,一条弧线是由有限个点构成的有限条线段连接得到的。当线段数量越多,长度就越短,当达到你无法察觉这是线段时,一条平滑的弧线就出现了。 计算机的三维模型也是类似的。只不过线段变成了平面,普遍用三角形组成的网格来描述。我们把这种模型称之为 Mesh 模型。 在 threeJs 的世界中,材质(Material)+几何体(Geometry)就是一个 mesh。设置其name属性可以通过scene.getObjectByName(name)获取该物体对象;Geometry就好像是骨架,材质则类似于皮肤,对于材质和几何体的分类见下表格

材质分类

材质 说明
MeshBasicMaterial 基本的材质,显示为简单的颜色或者显示为线框。不考虑光线的影响
MeshDepthMaterial 使用简单的颜色,但是颜色深度和距离相机的远近有关
MeshNormalMaterial 基于面Geometry的法线(normals)数组来给面着色
MeshFacematerial 容器,允许为Geometry的每一个面指定一个材质
MeshLambertMaterial 考虑光线的影响,哑光材质
MeshPhongMaterial 考虑光线的影响,光泽材质
ShaderMaterial 允许使用自己的着色器来控制顶点如何被放置、像素如何被着色
LineBasicMaterial 用于THREE.Line对象,创建彩色线条
LineDashMaterial 用于THREE.Line对象,创建虚线条
RawShaderMaterial 仅和THREE.BufferedGeometry联用,优化静态Geometry(顶点、面不变)的渲染
SpriteCanvasMaterial 在针对单独的点进行渲染时用到
SpriteMaterial 在针对单独的点进行渲染时用到
PointCloudMaterial 在针对单独的点进行渲染时用到


几何图形

2D

图形 说明
矩形 THREE.PlaneGeometry 外观上是一个矩形
new THREE.PlaneGeometry(width, height, widthSegments, heightSegments);
THREE.CircleGeometry
传送门
外观上是一个圆形或者扇形
// 半径为3的圆
new THREE.CircleGeometry(3, 12);
// 半径为3的半圆
new THREE.CircleGeometry(3, 12, 0, Math.PI);
第三个参数和第四个分别是起始角度和结束角度,默认0-2*PI
THREE.RingGeometry
传送门
外观上是一个圆环或者扇环
new THREE.RingGeometry(innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength)
THREE.ShapeGeometry
传送门
该形状允许你创建自定义的二维图形,其操作方式类似于SVG/Canvas中的画布

3D

图形 说明
THREE.BoxGeometry
传送门
这是一个具有长宽高的盒子
BoxGeometry(width, height, depth, widthSegments, heightSegments, depthSegments)
THREE.SphereGeometry
传送门
这是一个三维球体/不完整球体
SphereGeometry(radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength)
THREE. CylinderGeometry
传送门
可以绘制圆柱、圆筒、圆锥或者截锥
new THREE.CylinderGeometry(radiusTop,radiusBottom,height,radialSegments,heightSegments,openEnded)


加载外部模型

一般来讲我们的场景中不可能都是一些奇奇怪怪的形状,或多或少项目中都会用到一些外部的模型资源,不如动物啊,装饰物啊什么的,再加上一些动画,这样整个场景更加显得生动,那么 threejs 中我们可以通过哪些方式来加载外部的模型资源呢?

加载外部模型,是通过Three.js加载器(Loader)实现的。加载器把文本/二进制的模型文件转化为Three.js对象结构。 每个加载器理解某种特定的文件格式。

需要注意的是,由于贴图的尺寸必须是(2的幂数)X (2的幂数),如:1024X512,所以为了防止贴图变形,平面的宽度比例需要与贴图的比例一致。

支持的格式

格式 说明
JSON Three.js自定义的、基于JSON的格式。可以声明式的定义一个Geometry或者Scene.利用该格式,你可以方便的重用复杂的Geometry或Scene
OBJ / MTL OBJ是Wavefront开发的一种简单3D格式,此格式被广泛的支持,用于定义Geometry,MTL用于配合OBJ,它指定OBJ使用的材质
Collada(dae) 基于XML的格式,被大量3D应用程序、渲染引擎支持
STL STereoLithography的简写,在快速原型领域被广泛使用。3D打印模型通常使用该格式定义Three.js提供了STLExporter.js,使用它可以把Three.js模型导出为STL格式
CTM openCTM定义的格式,以紧凑的格式存储基于三角形的Mesh
VTK Visualization Toolkit定义的格式,用于声明顶点和面。此格式有二进制/ASCII两种变体,Three.js仅支持ASCII变体
AWD 3D场景的二进制格式,主要被away3d引擎使用,Three.js不支持AWD压缩格式
Assimp 开放资产导入库(Open asset import library)是导入多种3D模型的标准方式。使用该Loader你可以导入多种多样的3D模型格式
VRML 虚拟现实建模语言(Virtual Reality Modeling Language)是一种基于文本的格式,现已经被X3D格式取代尽管Three.js不直接支持X3D,但是后者很容易被转换为其它格式
Babylon 游戏引擎Babylon的私有格式
PLY 常用于存储来自3D扫描仪的信息

在项目一开始尝试是使用 dae 文件,后面发现 json 文件更加方便一点,所以最终使用的是 jsonloader 导入 json 文件。json文件可以通过 blender 或者3DsMax 导出,他们都有各自的 export json的插件。在软件中处理好模型贴图和动画以后,导出 json 文件和相应的贴图文件给到前端即可。

var jsonLoader = new THREE.JSONLoader();
 jsonLoader.load('model.json', function (geometry, materials) {
    materials.forEach(function (mat) {         //这里面可以设置材质的各种信息
        mat.skinning = true;
        mat.color = new THREE.Color("rgb(233,203,113)"); //模型颜色
        mat.emissive = new THREE.Color("rgb(110,110,110)");//自发光颜色
    });    var model = new THREE.SkinnedMesh(geometry, new THREE.MeshFaceMaterial(materials));
    model.name = “model name”;
    scene.add(model);    //下面是播放模型中的动画内容
    var sceneAnimationClip = model.geometry.animations[0]    var mixer = new THREE.AnimationMixer(model);
    mixers.push(mixer);    var sceneAnimation = mixer.clipAction(sceneAnimationClip);
    sceneAnimation.play();

});

同理其他类型的文件也可以使用相应的 loader 导入文件,控制其材质信息和动画播放,具体的可以查看官网的 demo。

粒子

THREE.Sprite

在WebGlRenderer渲染器中使用THREE.Sprite创建的粒子可以直接添加到scene中。创建出来的精灵总是面向镜头的。即不会有倾斜变形之类透视变化,只有近大远小的变化。

比如一个纹理为花瓣的粒子示例:

//花瓣的贴图var textureList = [
        __uri("../../img/flower-1.png"),
        __uri("../../img/flower-2.png"),
        __uri("../../img/flower-3.png"),
        __uri("../../img/flower-4.png"),
        __uri("../../img/flower-5.png"),
        __uri("../../img/flower-6.png"),
        __uri("../../img/flower-7.png"),
        __uri("../../img/flower-8.png"),
        __uri("../../img/flower-9.png"),
        __uri("../../img/flower-10.png")]var particles = []; //存储生成的粒子//粒子从Z轴产生区间在-20到20for (var zpos = -20; zpos < 20; zpos += 0.5) {    var texturerain = textLoader.load(textureList[Math.floor(Math.random() * 10)])    var material = new THREE.SpriteMaterial({
            transparent: true,
            opacity: util.getRandomInt(0.7, 1),
            map: texturerain
        }
    );    //生成粒子
    particle = new THREE.Sprite(material);
    particle.name = "particle"
    //随即产生x轴,y轴
    particle.position.x = Math.random() * 100 
    particle.position.y = Math.random() * 100;    //设置z轴
    particle.position.z = zpos;    //将产生的粒子添加到场景
    scene.add(particle);    //将粒子位置的值保存到数组
    particles.push(particle);
}//移动粒子的函数function updateParticles() {    //遍历每个粒子
    for (var i = 0; i < particles.length; i++) {
        particle = particles[i];        //设置粒子向前移动的速度依赖于鼠标在平面Y轴上的距离
        particle.position.y -= i / particles.length / 50;
        particle.position.x -= i / particles.length / 80;        if (particle.position.y < -7) { //溢出视野以后设置回原位置
            particle.position.x = Math.random() * 100 - 50;
            particle.position.y = 7;
        }
    }
   
    }

场景交互

Three.js中并没有直接提供“点击”功能,一开始使用的时候我也觉得一脸懵逼,后来才发现我们可以基于THREE.Raycaster来判断鼠标当前对应到哪个物体,用来进行碰撞检测.

//核心代码var clickObjects = []; //存储哪些 obj 需要交互var _raycaster = new THREE.Raycaster();//射线拾取器var raycAsix = new THREE.Vector2();//屏幕点击点二维坐标var container = null;function onMouseMove(event) {
    event.preventDefault();
    container = document.getElementById("Canvas1");
    raycAsix.x = ( (event.pageX - $(container).offset().left) / container.offsetWidth ) * 2 - 1;
    raycAsix.y = -( (event.pageY - $(container).offset().top) / container.offsetHeight ) * 2 + 1;
    _raycaster.setFromCamera(raycAsix, Camera);    var intersects = _raycaster.intersectObjects(clickObjects);//获取射线上与存储的可被点击物体的集合的交集,集合的第一个物体为距离相机最近的物体,最后一个则为离相机最远的。
    if (intersects.length > 0) {        document.body.style.cursor = 'pointer';        console.log(intersects[0].object.name) //打印导入模型时设置的model name
    } else {        document.body.style.cursor = 'default';
    }
}

其他的交互比如点击事件都是基于此。

动画

场景中如果我们添加了各种 mesh 和模型并给他加入了一些 tweend动画会发现他并不会运动,因为你的场景并没有实时渲染,所以要让场景真的动起来,我们需要用到requestAnimationFrame;关于它的详细使用请大家自行 google,核心代码如下

var requestAnimationFrame = window.requestAnimationFrame
        || window.mozRequestAnimationFrame
        || window.webkitRequestAnimationFrame
        || window.msRequestAnimationFrame; function animate() {    var delta = clock.getDelta();    if (mixers.length > 0) {        for (var i = 0; i < mixers.length; i++) {
            mixers[i].update(delta);
        }
    }    //Renderer即我们实例化的 webglRender 对象;
    updateParticles()
    Renderer.clear();
    Renderer.render(scene, Camera);
    requestAnimationFrame(animate);    //如果有使用 Tween做一些补间动画,也需要在此调用 TWEEN.update();
    TWEEN.update();
}

另外我们如果想自己 K 动画也是可以的,不过我觉得应该没有人这么无聊

jsonLoader.load('../resource/hudie.json', function ( geometry, materials ) {
    materials.forEach(function (mat){
        mat.skinning = true;
        mat.color =  new THREE.Color("rgb(0,255, 0)");
        mat.emissive =new THREE.Color("rgb(255, 0, 255)");
    });    var tracks = [];    //NumberKeyframeTrack(name,times,values),依次去K模型的位置,缩放和旋转
    tracks.push( new THREE.NumberKeyframeTrack( '.position', [ 0, 1, 2 ], [ -15,-5,-7,  0, 0, 0, 5,0.3,-9 ] ) );
    tracks.push( new THREE.NumberKeyframeTrack( '.scale', [ 0, 1, 2 ], [  0.04, 0.04, 0.04,  0.04, 0.04, 0.04,     0.04, 0.04, 0.04] ) );
    tracks.push( new THREE.NumberKeyframeTrack( '.rotation', [ 0, 1, 2 ], [  0, 0.2, 0,  0, 0, 0, 0, 0.2, 0.2 ] ) );    var  model = new THREE.SkinnedMesh(geometry, new THREE.MeshFaceMaterial(materials));
    model.name="蝴蝶";     var clip = new THREE.AnimationClip( 'Action', -1, tracks );     var mixer = new THREE.AnimationMixer( model );
     mixers.push(mixer);
     mixer.clipAction( clip ).play();
     scene.add( model );
} );



网易云免费体验馆,0成本体验20+款云产品! 

更多网易研发、产品、运营经验分享请访问网易云社区


相关文章:
【推荐】 搜索实时个性化模型——基于FTRL和个性化推荐的搜索排序优化
【推荐】 消息推送平台高可用实践(下)

推荐阅读更多精彩内容