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 远离镜头的距离

透视相机示意图:

image

创建摄像机以后还要对其进行移动、然后对准物体积聚的场景中心位置,分别是设置其 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内置了多种光源以满足特定场景的需要。大家可以根据自己的项目需要来选择何种灯光

光源分类

image.png

关于光源的详细 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

image.png

3D

image.png

加载外部模型

一般来讲我们的场景中不可能都是一些奇奇怪怪的形状,或多或少项目中都会用到一些外部的模型资源,不如动物啊,装饰物啊什么的,再加上一些动画,这样整个场景更加显得生动,那么 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到20
for (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 );
} );

总结

因为时间关系以及项目原因,学完以上的知识点差不多可以完成项目的需要了,但是关于 threejs 中其他的知识点,比如骨骼动画啊,第一人称控制等等由于时间关系还没有深入的学习,后续打算进一步的学习这一部分内容并且整理成文档。另外由于 webgl 的自身原因,如果只是一张全景图还好,但如果是加载了大量的模型以及粒子动画等等,那么对电脑的性能也有一定的要求,比如在 mac 下每次一打开相应的网站就能明显的听到风扇呼呼的转。这些瓶颈的问题是 webgl 开发无法避免的问题,毕竟网页的承载和游戏的引擎不是一个级别。最后希望更多的人喜欢 webgl,喜欢 threejs 这个强大的库,感谢Threejs作者带给我们更好的3D开发体验。

参考:
https://sq.163yun.com/blog/article/203590884053413888

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容