Three.js 的初步学习和实践

Three.js 的初步学习和实践

写在前面:作为一个前端小白菜,在校期间学习过一点点的Three.js,做过一些小demo,想在这里和大家做下分享,顺便记录一下学习的过程。
Three官方文档

WebGL相信大家已经耳熟能详,是OpenGL的一个衍生版本。能够使得我们不需要安装任何插件就可以构建Web 3D场景。但是WebGL本身的语法比较偏向底层,直接使用WebGL的GLSl语言构建3D场景开发难度比较大。于是乎,Three.js对WebGL做了一层封装,并向我们前端开发人员提供了非常友好的API,使得我们前端开发人员也能快速上手,构建3D场景。

本文将要讲述的内容:

  1. Three.js中最重要的三个内容: 相机,场景,渲染器
  2. 使用Three的loader加载3D模型
  3. 在场景中加入雪花,模拟下雪效果
  4. 在Three的场景中使用天空盒模型
  5. 在Three构建的3D场景中实现模型拾取(RayCaster算法)

1 Three中最重要的三个概念

1.1 相机

相机就像人的眼睛一样,人站在不同位置,抬头或者低头都能够看到不同的景色。相机决定了场景中那个角度的景色会显示出来。

类型名称构造函数参数说明备注
ArrayCamera相机阵列ArrayCamera(array:Array)很多相机组成的数组
Camera相机Camera()这是camera的基类
CubeCamera立方体相机CubeCamera( near : Number, far : Number, cubeResolution : Number, options : Object )最近距离,最远距离,设置立方体边缘的长度,保存传递给自动生成的WebGLRenderTargetCube的纹理参数的对象最近距离,最远距离,立方体分辨率
OrthographicCamera正交相机OrthographicCamera( left : Number, right : Number, top : Number, bottom : Number, near : Number, far : Number )相机视锥体左,右,顶,下平面,相机视锥体近,相机视锥体远
PerspectiveCamera透视相机PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )相机视锥体垂直视野,相机平截头宽高比,相机视锥体近,相机视锥体远视角,视图宽高比,近平面,远平面
StereoCamera立体照相机StereoCamera( )用于‘3D立体影像’或‘视差屏障’等效果

正交相机和透视相机的区别

1.2 场景

// scene 的可配置参数
scene = new THREE.Scene();
// 可以给场景增加fog雾化效果 第一个参数为雾化颜色,第二为'开始施雾的最小距离',第三为'雾停止计算和应用的最大距离'。注意:这里的'100'不能小于相机中的near,'400'不能大于相机中的far
scene.fog = new THREE.Fog( 0x444466, 100, 400 );
// 设置场景的颜色
scene.background = new THREE.Color( 0x444466 );
// 设置场景中的所有材质
scene.overrideMaterial = this.materialDepth;
// autoUpdate 默认为true。如果设置,则渲染器会检查场景及其对象是否需要矩阵更新的每一帧。如果不是,那么你必须自己维护场景中的所有矩阵。
scene.autoUpdate 

1.3 渲染器

渲染器决定了渲染的结果应该画在页面的什么元素上面,并且以怎样的方式来绘制。

var scene = new THREE.Scene(); //场景
var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );  // 相机

var renderer = new THREE.WebGLRenderer(); // 渲染器
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

2. 在Three中加载3D模型

虽然Three提供了很多API供我们画出各种各样的几何图形,但是代码来建模效率往往不太高,而且难度比较大。市面上有很多比较成熟的3D建模软件,比如3dmax,Maya,Blender等。

Three这个库同时也提供了很多的loader去加载各种的模型,比如GLTFLoader,OBJLoader,PLYLoader,FBXLoader等等(不要觉得很神奇,其实3D模型本质上就是一些有规则的二进制文件,我们的loader就是按照模型的规则去读取二进制文件,然后加载到我们的场景中)

下面我就以一个.gltf格式的模型为例

function initCamera_Light_Renderer_Stats_Controls() {
      //这里等于是在用脚本添加dom节点
      container = document.createElement('div');
      document.body.appendChild(container);

      //camera 相机
      camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 2000);
      camera.position.set(-9.91836908539192, 26.217305658821992, 101.97602220273721);

      //controls 控制器
      //轨道控制器OrbitControls.js是一个相当神奇的控件,用它可以实现场景用鼠标交互,让场景动起来,控制场景的旋转、平移,缩放
      controls = new THREE.OrbitControls(camera);

      //场景
      scene = new THREE.Scene();
      scene.background = new THREE.Color(0xf0f0f0);

      //light 光源
      var light = new THREE.HemisphereLight(0xffffff, 0x444444);
      light.position.set(0, 20, 0);
      scene.add(light);
      hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
      hemiLight.color.setHSL(0.6, 1, 0.6);
      hemiLight.groundColor.setHSL(0.095, 1, 0.75);
      hemiLight.position.set(0, 50, 0);
      scene.add(hemiLight);
      // 渲染器
      renderer = new THREE.WebGLRenderer({
        antialias: true
      });
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.body.appendChild(renderer.domElement);

      initSky();

      effectController.azimuth = 0.44;
      // stats性能监视器
      // 其中FPS表示:图形处理器每秒钟能够刷新几次;MS表示渲染一帧需要的毫秒数;
      stats = new Stats();
      container.appendChild(stats.dom);

      // model 模型							
      var onProgress = function(xhr) {
        if (xhr.lengthComputable) {
          var percentComplete = xhr.loaded / xhr.total * 100;
          var percent = document.getElementById("percent");
          //percent.innerText = Math.round(percentComplete, 2) + '% 已经加载';
        }
      };
      const gltfLoader = new THREE.GLTFLoader();
        gltfLoader.setCrossOrigin('*');
        gltfLoader.load(
          'nit/model.gltf',
          (object) => {
          object.scene.scale.set(4, 4, 4);
          object.scene.rotation.y = (Math.PI / 2) * 1.3;
          // 在加载模型后,在canBeSelectedMeshes变量中加入模型的信息
          // 射线摄取法 https://blog.csdn.net/ahilll/article/details/84185576
          // canBeSelectedMeshes = object.scene.children;
          scene.add(object.scene);
          },
      );
      var onError = function(xhr) {};

      window.addEventListener('resize', onWindowResize, false);

}

查看效果:

源代码地址

(多提一嘴,由于我们要加载本地的模型数据,那么如果克隆代码之后直接打开html的话会出现跨域的问题,建议在本地启一个http-server服务,或者使用HBUilder编译器打开,Hbuilder会自动帮我们启一个服务)

3. 在场景中加入下雪的效果

大概的实现思路是这样的,首先我们假设整个场景外围有一个立方体,在这里立方体里面有很多随机飘动的雪花,每个雪花可以看作一个对象,然后调用Web的RequestAnimationFrame()来使得我们的场景每一秒渲染60次,也就是60帧,从视觉上达到一个很好的效果,然后每次渲染的时候改变我们每一片雪花的位置。核心代码如下

// 初始化雪花
function initContent() {

      /* 雪花图片 */
      let texture = new THREE.TextureLoader().load('textures/Snow.png');

      let geometry = new THREE.Geometry();

      let pointsMaterial = new THREE.PointsMaterial({

        size: 2,
        transparent: true,
        opacity: 0.8,
        map: texture,
        blending: THREE.AdditiveBlending,
        sizeAttenuation: true,
        depthTest: false
      });

      let range = 800;

      for (let i = 0; i < 15000; i++) {

        let vertice = new THREE.Vector3(
          Math.random() * range - range / 2,
          Math.random() * range * 1.5,
          Math.random() * range - range / 2);
        /* 纵向移动速度 */
        vertice.velocityY = 0.1 + Math.random() / 3;
        /* 横向移动速度 */
        vertice.velocityX = (Math.random() - 0.5) / 3;

        /* 将顶点加入几何 */
        geometry.vertices.push(vertice);
      }

      geometry.center();

      points = new THREE.Points(geometry, pointsMaterial);
      points.position.y = -30;

      scene.add(points);
    }
  
  // ...
  
  // 递归调用,requestAnimationFrame每秒会执行60次
  function animate() {

    requestAnimationFrame(animate);

    renderer.render(scene, camera);

    update();

  }
  
  // ...
  
  // 每次重新渲染的时候遍历下雪花 改变位置
  function update() {

    stats.update();
    let vertices = points.geometry.vertices;
    vertices.forEach(function(v) {
      v.y = v.y - (v.velocityY);
      v.x = v.x - (v.velocityX);
      if (v.y <= 0) v.y = 60;
      if (v.x <= -20 || v.x >= 20)
        v.velocityX = v.velocityX * -1;

    });
    /* 顶点变动之后需要更新,否则无法实现雨滴特效 */
    points.geometry.verticesNeedUpdate = true;

  }
  

查看效果:

4. 使用天空盒作为场景的背景

在Three官方提供的demo中,我看到这样一个案例,觉得很神奇。不管如何转,就像在真实的世界一样.

在线地址

于是乎,去研究了下他的代码,是这样的

scene = new THREE.Scene();
scene.background = new THREE.CubeTextureLoader()
  .setPath( 'textures/cube/Park3Med/' )
  .load( [ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg' ] );

后来,我才知道,实际这叫做天空盒模型,只需要六张图,分别作为3D场景的六个面,就可以做出这种效果,类似于这样,然后把图片分成6张,上、下、左、右、前、后.

然后,应用到我们的场景中来

 scene = new THREE.Scene();
scene.background = new THREE.CubeTextureLoader()
  .setPath('https://zaoren.oss-cn-beijing.aliyuncs.com/')
  .load(['skyrender0001.png', 'skyrender0004.png',
    'skyrender0003.png', 'skyrender0006.png', 'skyrender0005.png', 'skyrender0002.png']);

效果是这样的:

5. 使用RayCaster算法实现模型拾取(如何在Three的场景中区分两个建筑物)

由于目前我们所有的事情都是使用HTML中的Canvas标签来实现的。在通常情况下,在web中我们区分标签主要是通过DOM节点,像在svg我们也可以给对应的标签加上事件,那么Canvas中,我们是如何去区分3D场景中的对象的呢?

由于我们的Canvas就像是一张画布,显然我们需要区分Canvas中的不同内容只能通过坐标来区分。

由于浏览器是一个2d视口,而在里面显示Three.js的内容是3d场景,所以,现在有一个问题就是如何将2d视口的x和y坐标转换成three.js场景中的3d坐标。好在three.js已经有了解决相关问题的方案,那就是THREE.Raycaster射线,用于鼠标拾取(计算出鼠标移过的三维空间中的对象)等等。我们看一张图片:

大概的意思是,我们相机是一个点,我们鼠标点击的位置是一个点,这两个点可以形成一条射线,穿过我们的3D场景,那么在这条射线上的对象,就是被我们点击到的对象(可能会有多个,我们一般只取第一个)

那么我们如何知道这些模型对象的位置信息呢?问的好!

我们使用loader去加载模型完成以后,会有一个回调函数,带出我们模型中所有建筑的点位信息,我们现在这个回调函数里面用变量将其存储起来,在点击之后就可以通过RayCaster算法去计算到底哪个模型被点击了!下面看代码:

// 第一步: 在回调函数中用 canBeSelectedMeshes 变量接受模型的点位信息
function loadGLTF() {
  const gltfLoader = new THREE.GLTFLoader();
  gltfLoader.setCrossOrigin('*');
  gltfLoader.load(
    'https://zaoren.oss-cn-beijing.aliyuncs.com/NIT.gltf',
    (object) => {
      object.scene.scale.set(4, 4, 4);
      object.scene.rotation.y = (Math.PI / 2) * 1.3;
      // 在加载模型后,在canBeSelectedMeshes变量中加入模型的信息
      // 射线摄取法 https://blog.csdn.net/ahilll/article/details/84185576
      canBeSelectedMeshes = object.scene.children.filter((item) => {
        if (item.name[0] === 'S') {
          if (item.type === 'Mesh') {
            item.name = getChineseName(item.name);
            return item;
          } if (item.type === 'Group') {
            item.name = getChineseName(item.name);
            item.children.forEach((child) => {
              child.name = getChineseName(child.name);
            });
            return item;
          }
        }
        return false;
      });
      scene.add(object.scene);
      // 用来控制动画显隐的
      self.setState({
        contentVisibility: true,
      });
    },
  );
}

// 二步: 监听点击事件
renderer.domElement.addEventListener('click', handleClick, false);

// 第三步: 使用RayCaster算法进行碰撞检测
function handleClick(e) {
  // 获取点击的位置
  const coords = tranformMouseCoord(
    e.clientX,
    e.clientY,
    renderer.domElement,
  );
  // 检测被射线碰撞到的模型
  const intersects = getSelectedMeshes(
    coords,
    camera,
    canBeSelectedMeshes,
  );
  if (intersects.length > 0) {
    getDetail(intersects[0].object.name);
  }
}

function getSelectedMeshes(coords, camera, Meshes) {
  const raycaster = new THREE.Raycaster();
  // 可以看到,我们碰撞检测用到的两个参数 1.点击的相对位置 2.相机的位置
  raycaster.setFromCamera(coords, camera);
  const intersects = raycaster.intersectObjects(Meshes, true);
  return intersects;
}

查看效果:

OK,对Three的分享就到这里拉,因为之后的作内容可能和这块没有很大的交集,暂时不会花太多的时间在这一块上,作为刚毕业的前端小白菜,毕业后的头两年还是先打好基础,哈哈。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值