threejs快速入门

2,029 阅读7分钟

个人觉得目前threejs的相关资料还是比较零散的,需要不断整理自己知识体系,这里分享一下自己的一些总结,很多内容都是别人的博客中学习而来。

在项目(vue-cli)中使用

首先当然是引入我们的模块包

npm install three --save
//在需要使用的地方引入
import * as THREE from "three";

这样安装的threejs与CDN引入或者外部的方式会有些不一样,好处在于我们不在需要引入各种各样的外部文件,例如OrbitControls,各种loader等等这些文件大部分可以在three/examples/jsm/目录中找到.

  1. three/examples/jsm/loaders 加载器
  2. three/examples/jsm/controls 控制器
  3. three/examples/jsm/libs
//参考链接
//https://blog.csdn.net/lin5165352/article/details/96120362
使用方式也很简单:
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
这样就可以愉快地在项目中使用。

整理一些常用的效果,包括点击,后期组合通道,贴图

创建场景天空盒(skybox)

所谓的天空盒其实就是将一个立方体展开,然后在六个面上贴上相应的贴图,如上图所示。天空盒是用于增强场景表现力的一个常用技术,它一般通过在相机周围包裹一个纹理来实现。 OpenGL中天空盒的思想就是绘制一个大的立方体,然后将观察者放在立方体的中心,当相机移动时,这个立方体也跟着相机一起移动,这样相机就永远不会运动到场景的边缘。

在Three.js中,除了这种方法外还可以设置场景的背景,将六个面的贴图通过CubeTextureLoader()载入,顺序为[right,left,up,down,front,back]。

//方法1 六张图片 注意贴面图片的顺序
let urls = [
        "image/skybox/远山_RT.jpg", // right
        "image/skybox/远山_LF.jpg", // left
        "image/skybox/远山_UP.jpg", // top
        "image/skybox/远山_DN.jpg", // bottom
        "image/skybox/远山_BK.jpg", // back
        "image/skybox/远山_FR.jpg" // front
      ];
this.scene.background = new THREE.CubeTextureLoader().load(urls);
//方法二 一张图片
var skyMap = new THREE.TextureLoader().load("/ground/image/星空球2.jpg");
this.scene.background = skyMap;

模型

新手在加载模型的过程中经常遇到各种奇怪的问题(说的不就是我吗),然后经过一番琢磨之后整理了一些可能的原因。当然第一步还是先打开浏览器控制台,出现报错信息先解决报错信息

  1. 模型加载后,不显示也不报错? 检查场景是否正常渲染了,摄像机在哪里,摄像机是否对着模型,灯光是否配置,模型是否太大或者太小了,尝试将模型放大或缩小到原来的1000倍。模型的中心点不在模型内等等
//设置中心点在几何中心
geometry.computeBoundingBox();
geometry.center()
  1. 模型可以正常加载,但是贴图不显示? 首先检查network是否报404错误,如果报错,一般都是mtl贴图文件没有正常加载,或者路径配置的不是相对路径,如果贴图没错误,模型是黑色的,在mtl文件中可以更改ka或kd的三个值(对应rgb),或者打印出模型属性,在material.color中更改点色值或别的属性

  2. 模型过大加载速度慢 obj2gltf先将模型转成浏览器最喜欢的gltf格式,这个过程将文件体积大大减小,同时写的代码也少了,起飞芜湖。别急,还能进一步压缩 gltf-pipeline-github

npm install obj2gltf  -g //全局安装后
             obj文件所在目录                             输出目录 
obj2gltf  -i ./examples/models/obj/hanchuan/city.obj -o ./gltf/city.gltf --unlit --separate

gltf-pipeline -i  源文件  -o  输出文件 -d --separate
//-d是--draco.compressMeshes的缩写,使用draco算法压缩模型
//--separate就是将贴图文件提取出来,不提可以不加

管道流动效果

此效果一般用在路线的循环流动,思路是创建一个管道之后给路径增加贴图,然后在animate中不断改变texture的偏移量,达到循环流动的效果。

 //http://wjceo.com/three.js/docs/#api/zh/geometries/TubeGeometry
 var curve = new THREE.CatmullRomCurve3([
     new THREE.Vector3(-80, 0, -40),
     new THREE.Vector3(-70, 0, 40),
     new THREE.Vector3(70, 0, 40),
     new THREE.Vector3(80, 0, -40),
 ], false/*是否闭合*/);
 var tubeGeometry = new THREE.TubeGeometry(curve, 100, 2, 8, false);
 var textureLoader = new THREE.TextureLoader();
 var texture = textureLoader.load('./model/roll.png');
 // 设置阵列模式为 RepeatWrapping
 texture.wrapS = THREE.RepeatWrapping
 texture.wrapT = THREE.RepeatWrapping
 // 设置x方向的重复数(沿着管道路径方向)
 // 设置y方向的重复数(环绕管道方向)
 texture.repeat.set(2, 4);
 texture.needsUpdate = true;
 texture.transparent = true;
 texture.side = THREE.DoubleSide;
     
 var tubeMaterial = new THREE.MeshPhongMaterial({
     map: texture,
     transparent: true,
 });
 var tube = new THREE.Mesh(tubeGeometry, tubeMaterial);
 scene.add(tube)
 ....
 // 渲染函数
 function render() {
     renderer.render(scene, camera); //执行渲染操作
     requestAnimationFrame(render);
     // 使用加减法可以设置不同的运动方向
     // 设置纹理偏移
     texture.offset.x += 0.001
  }

再进一步可以作出物体沿着路径移动的效果,只需要动态改变物体的位置就行了,下面是奔跑的那撸多。注意要让人物朝向奔跑的方向,我们只要lookAt(下一个点的position)就可以了。 人物的动画方面我们可以用骨骼来实现,这里为了偷懒直接拿了别人的那撸多过来用,是个fbx格式的模型,这种格式本身就存储了动画内容。核心代码如下:

let loader = new THREE.FBXLoader()
let mesh, mixer;
let actions = []; //所有的动画数组
loader.load('./model/Naruto.fbx', function (obj) {
    mesh = obj
    //添加骨骼辅助
    meshHelper = new THREE.SkeletonHelper(mesh);
    scene.add(meshHelper);
    //设置模型的每个部位都可以投影
    mesh.traverse(function (child) {
        if (child.isMesh) {
            child.castShadow = true;
            child.receiveShadow = true;
        }
    });
    //AnimationMixer是场景中特定对象的动画播放器。当场景中的多个对象独立动画时,可以为每个对象使用一AnimationMixer
    mixer = mesh.mixer = new THREE.AnimationMixer(mesh);
    //mixer.clipAction 返回一个可以控制动画的AnimationAction对象  参数需要一个AnimationClip 对象
    //AnimationAction.setDuration 设置一个循环所需要的时间,当前设置了一秒
    //告诉AnimationAction启动该动作
    //action = mixer.clipAction(mesh.animations[0]);
    //action.play();
    mesh.scale.set(0.1, 0.1, 0.1);
    mesh.position.set(80, 0, -40);
    scene.add(mesh);
})
// 渲染函数
function render() {
    var time = clock.getDelta();
    if (mixer) {
        mixer.update(time);
        actions[3].play()
        move()
    }	
    renderer.render(scene, camera); //执行渲染操作
    requestAnimationFrame(render);
    // 使用加减法可以设置不同的运动方向
    // 设置纹理偏移
    texture.offset.x += 0.001
}
function move() {
    if (progress < 0.002) {
        actions[3].stop(); //到了终点就别跑了,action[3]是人物的奔跑动作
        return;    //停留在管道末端,否则会一直跑到起点 循环再跑
    }
    progress -= 0.001;
    // console.log(progress);
    if (curve) {
        let point = curve.getPoint(progress); //当前点的位置
        let next_point = curve.getPoint(progress - 0.001); //这是下一个点的位置
        if (point && next_point) {
            mesh.position.set(point.x, point.y, point.z);
            mesh.lookAt(next_point.x, next_point.y, next_point.z)
        }
    }
}

甚至可以在人物的 运动轨迹上留下一条线。核心代码如下


//在场景中的线条
  var _material = new THREE.LineBasicMaterial({
      color: 0xff0000
  });
  var _geometry = new THREE.BufferGeometry();
  var _pointsBuf = [
  ]
  var _vertices = new Float32Array(_pointsBuf);
  _geometry.addAttribute('position', new THREE.BufferAttribute(_vertices, 3));
  var _lineA = new THREE.Line(_geometry, _material);
  scene.add(_lineA);
  let _i = 0;
  // 渲染函数
  function render() {
      var time = clock.getDelta();
      if (mixer) {
          mixer.update(time);
          actions[3].play()
          move()
      }
      renderer.render(scene, camera); //执行渲染操作
      requestAnimationFrame(render);
  }
  function move() {
      console.log(_pointsBuf.length);
      if (_pointsBuf.length < 3003) { //多加一个单位的长度为了让首位连起来
          _pointsBuf.push(points[_i].x, points[_i].y, points[_i].z)
      }
      _vertices = new Float32Array(_pointsBuf)
      _geometry.addAttribute('position', new THREE.BufferAttribute(_vertices, 3));
      mesh.position.set(points[_i].x, points[_i].y, points[_i].z);
      mesh.lookAt(points[_i + 1].x, points[_i + 1].y, points[_i + 1].z)
      _i++;
      if (_i > 1000 - 1) _i = 0  //回到了运动的起点 可以循环运动
  }

Sprite精灵的应用

精灵是一个总是面朝着摄像机的平面,通常含有使用一个半透明的纹理。

关于用途,你可以在三维场景中把精灵模型作为一个模型的标签,标签上可以显示一个写模型的信息,你可以通过足够多的精灵模型对象,构建一个粒子系统,来模拟一个下雨、森林、或下雪的场景效果。图片或canvas都可以作为纹理

var texture = new THREE.TextureLoader().load("sprite.png");
// 创建精灵材质对象SpriteMaterial
var spriteMaterial = new THREE.SpriteMaterial({
  color:0xff00ff,//设置精灵矩形区域颜色
  rotation:Math.PI/4,//旋转精灵对象45度,弧度值
  map: texture,//设置精灵纹理贴图
});
// 创建精灵模型对象,不需要几何体geometry参数
var sprite = new THREE.Sprite(spriteMaterial);
scene.add(sprite);
// 控制精灵大小,比如可视化中精灵大小表征数据大小
sprite.scale.set(10, 10, 1); //// 只需要设置x、y两个分量就可以

使用ThreeBSP库进行Three.js网格组合

之前一直使用Three.js默认提供的几何体,使用ThreeBSP库可以将现有的模型组合出更多个性的模型来使用。我们可以使用ThreeBSP库里面的三个函数进行现有模型的组合,分别是:差集(相减)、并集(组合、相加)、交集(两几何体重合的部分)。下图的墙壁就是使用threebsp将简单几何体拼接而成

入门 ThreeBSP进行几何体组合

    //1.使用几何体分别创建墙面mesh与窗户(objects_cube数组中保存窗几何体)
    //2.墙几何体减去窗户几何体,生成BSP对象
    //3.转成场景里的mesh对象并更新向量与uv
    
    createResultBsp(bsp, objects_cube) {
      var material = new THREE.MeshPhongMaterial({
        color: 0x9cb2d1,
        specular: 0x9cb2d1,
        shininess: 30,
        transparent: true,
        opacity: 1
      });
      var BSP = new ThreeBSP(bsp);
      for (var i = 0; i < objects_cube.length; i++) {
        var less_bsp = new ThreeBSP(objects_cube[i]);
        BSP = BSP.subtract(less_bsp);
      }
      var result = BSP.toMesh(material);
      result.material.flatshading = THREE.FlatShading; //使用平面着色
      result.geometry.computeFaceNormals(); //重新计算几何体侧面法向量
      result.geometry.computeVertexNormals();
      result.material.needsUpdate = true; //更新纹理
      result.geometry.buffersNeedUpdate = true;
      result.geometry.uvsNeedUpdate = true;
      this.scene.add(result);
    },

OpenGL GLSL

着色器是个好东西,但是入门门槛还是蛮高的~

推荐一下暮志未晚大佬的博客:www.wjceo.com/