初探 ThreeJS

806 阅读11分钟

一、简介

three.js 是 JavaScript 编写的 WebGL 第三方库,开发者可以不用关注 WebGL 底层就能制作出一些酷炫的效果

WebGL 是在浏览器中实现三维效果的一套规范,通过 can i use 查询可知,现在的主流浏览器基本支持 webGL:

image.png

应用范围:

在小游戏、产品展示、物联网、数字孪生、智慧城市园区、机械、建筑、全景看房、GIS等各个领域基本上都有 three.js 的身影:

小游戏:____________________ (bruno-simon.com)(碰撞检测,相机跟随)

产品展示:WEBGi Jewelry Landing Page Demo - by Neotix (webgi-jewelry.vercel.app) (轨道控制器,渲染贴图、材质)

智慧城市园区:瞰图智慧物流展示平台 (kantu3d.com)

全景看房:WebVR Showroom by Little Workshop (Raycaster 光线投射)

二、在页面中展示第一个模型

  • 安装 Three 的 npm 模块 npm install three --save
  • 需要一对 canvas 作为容器,渲染的模型会在 canvas 中展示

ThreeJS 基本三要素:

  • scene 场景
  • camera 相机
  • render 渲染器

关于场景:

在真实世界中,天空就是场景,大家都知道天空是蓝白色的,那么开发时,我们试着给场景添加蓝白色的背景颜色

const scene = new THREE.Scene()
scene.background = new THREE.Color('#00DCFF')

在threejs中,场景是右手坐标系,把右手放在原点的位置,使大拇指,食指和中指互成直角

把大拇指指向 x 轴的正方向,食指指向 y 轴的正方向时,中指所指的方向就是z轴的正方向

image.png

关于相机

真实世界中,看见物体需要一双眼睛,在ThreeJS里相机相当于你的眼睛

Threejs 提供两种相机,根据实际场景选择不同的相机

正交相机:

无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变。用于渲染2D场景或者UI元素是非常有用的

  1. 图中红色三角锥体是视野的大小
  2. 红色锥体连着的第一个面是摄像机能看到的最近位置
  3. 从该面通过白色辅助线延伸过去的面是摄像机能看到的最远的位置

image.png

透视相机:

它是3D场景的渲染中使用得最普遍的投影模式,被用来模拟人眼所看到的景象。

image.png

关于透视相机的几个参数,new THREE.PerspectiveCamera(fov, width / height, near, far)

  • fov(field of view) — 摄像机视锥体垂直视野角度
  • aspect(width / height) — 摄像机视锥体长宽比
  • near — 摄像机视锥体近端面
  • far — 摄像机视锥体远端面
const camera = new THREE.PerspectiveCamera(
  50, // 透视摄像机,垂直视野角度
  window.innerWidth / window.innerHeight, // 摄像机横纵比
  10, // 近端面
  6000 // 远端面
)
// 手动调整摄像机位置
camera.position.x = 0
camera.position.z = 600
camera.position.y = 480

关于渲染器

WebGLRenderer 该对象的属性定义了渲染器的行为。也可以完全不传参数。当缺少参数时,它将采用合理的默认值

需要先获取 dom 元素,作为参数创建 renderer 对象,同时渲染这一帧的画面

const canvas = document.querySelector('#three') // 将 canvas 绑定到元素上
    const renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: true // 抗锯齿
    })
renderer.render(scene, camera) // 渲染器只渲染当前帧

得注意,如果场景引入了模型或者使用了动画,需要循环渲染场景!!

关于几何体

ThreeJS 提供很多不同类型的几何体,我们可以使用这些基本模型搭建场景

BoxGeometry – three.js docs (threejs.org) (需开代理)

导入外部模型

实际开发中,大多数项目,通常是3D美术设计师或建筑、机械等行业工程师提供的由3dmx、blender、substence、Solidworks等软件创建好的三维模型文件

官方对 GLTF 格式的模型支持较好

我们试着在在线 GLTF Editor 建模平台,设计一个模型(200 × 200 × 150) glTF Editor (gltfviewer.com)

同时引入GLTF Loader: import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

    const gltfLoader = new GLTFLoader()
    gltfLoader.load('../static/models/scene.gltf', (gltf) => {
      const model = gltf.scene
      scene.add(model)
    })

导入模型后,之前的渲染仅仅只是一帧的画面。为了让场景中的物体能动起来,我们需要使用 requestAnimationFrame,所以我们可以写一个 loop 函数

requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,节约性能

    /**
     * 循环渲染每一帧
     */
    function animate() {
      renderer.render(scene, camera) // 刷新渲染器
      requestAnimationFrame(animate)
    }
    animate()

关于 gltf 模型的性能:

在 ThreeJS 里 3D 模型如同矢量图形,支持不失真缩放,所有无限放大和缩小模型是不影响加载和展示性能

gltf (图形语言传输格式) 本质上和 json 格式相似,主要存储点、线、面等数据,性能主要是受三角面数量影响 glTF Viewer 2.0 - 3D模型在线查看 | BimAnt

关于 gltf 存储格式:

glTF 的特点:传输和解析高效

json中包含的描述信息,内容详细,比如mesh,纹理,蒙皮和动画,定义了accessor的访问器规则,同时还给出了相机,节点这些场景管理的信息

image.png

尝试添加光源

现在模型出来了,但是一片黑色(我们的立方体是红色的),还需要添加一些光源照亮它们

image.png

本次使用 HemisphereLight(半球光)和 DirectionaLight(平行光),能更好的创建出更加贴切⾃然的户外光照效果

    // 添加个平行光
    const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
    dirLight.position.set(-10, 8, -5); // 光源位置
    dirLight.castShadow = true; // 可以产生阴影
    dirLight.shadow.mapSize = new THREE.Vector2(1024, 1024);
    scene.add(dirLight);
    
    // 添加个半球光
    const hemLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
    hemLight.position.set(0, 100, 0);
    scene.add(hemLight);

调整设备物理像素分辨率与CSS像素分辨率的比值

目前的模型较模糊,原因是 ThreeJS 默认的宽高 150×100,而页面中宽高被我们设置成 100%,相当于图片被放大模糊,所以场景也需要同步调整大小以保持比例

不断检查 renderer 的尺寸是否与 canvas 相等,若不等则设置 renderer 的尺寸

    function resizeRendererToDisplaySize(renderer) {
      const canvas = renderer.domElement;
      var width = window.innerWidth;
      var height = window.innerHeight;
      var canvasPixelWidth = canvas.width / window.devicePixelRatio;
      var canvasPixelHeight = canvas.height / window.devicePixelRatio;
    
      const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height // 判断 canvas 的宽高和 renderer 渲染的尺寸是否相等
      if (needResize) {
        renderer.setSize(width, height, false)
      }
      return needResize
    }
    
    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement
      this.camera.aspect = canvas.clientWidth / canvas.clientHeight
      this.camera.updateProjectionMatrix()
    }

三、场景拓展(全景看房,天空盒贴图)

目前常用于实现全景看房的技术主要分两种,分别纹理贴图和 3D 建模

  • 3D 建模实现全景看房 WebVR Showroom by Little Workshop (主要用在展示精细的场景,成本较高,花费时间长)
  • 纹理贴图实现全景看房又分为 天空盒贴图 和 全景图片贴图(主要用在展示大概的场景,例如全景地图)

天空盒的原理

我们所处的场景看成是有前后、左右、上下6个面组成的,将我们所看到的这6个面的视觉镜像处理成图片,将其分别以纹理的形式添加到立方体中,这时,我们如果立身于这个立方体中,即可还原当时场景。

image.png

    const path = './static/img/' // 设置路径
    const directions = ['px', 'nx', 'py', 'ny', 'pz', 'nz'] // 贴图名称, 左右、上下、后前
    const format = '.jpg' // 图片格式

    // 1、创建盒子,并设置盒子的大小
    const skyGeometry = new THREE.BoxGeometry( 10000, 10000, 10000 );

    // 2、导入图片,创建材质
    const skyMaterial = [];
    directions.forEach((item) => {
      // 加载纹理
      const texture = new THREE.TextureLoader().load(path + item + format)
      // 传入加载的纹理,创建材质
      skyMaterial.push(new THREE.MeshBasicMaterial({ map: texture }))
    })

    const skyBox = new THREE.Mesh(skyGeometry, skyMaterial) // 传入盒子+材质
    skyBox.geometry.scale(1, 1, -1) // 贴图反转
    scene.add( skyBox );

全景图片贴图

全景图贴图就是使用一张鱼眼全景图片以纹理的形式添加到球体上

    const url = 'https://cdn.huodao.hk/upload_img/20220621/6bd594e62ea5654c03d7b82718443751.png?proportion=1.99'
    const geometry = new THREE.SphereGeometry(5000, 32, 32)
    const texture = new THREE.TextureLoader().load(url)
    const material = new THREE.MeshBasicMaterial({ map: texture })
    const sphere = new THREE.Mesh(geometry, material)
    sphere.geometry.scale(1, 1, -1)
    scene.add(sphere)

缺点就是:对图片的要求很高(清晰度),目前有缺陷(白色线条)

四、相机拓展(轨道控制器,动态跟踪)

Orbit controls(轨道控制器)可以使得相机围绕目标进行轨道运动,实现类似产品 360° 展示的效果

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' // 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
    controls.autoRotate = true // 自动旋转
    controls.autoRotateSpeed = 5.0 // 自动旋转
    controls.maxPolarAngle = 1.65 // 相机旋转最大角度,不会超过地面
    controls.enabled = false // 当设置为false时,控制器将不会响应用户的操作

同时轨道控制器需要 -> 控制视角(相机朝向),与动画一样必须每一帧循环渲染

    function animate() {
      renderer.render(scene, camera) // 刷新渲染器
      controls.update(); // 刷新轨道控制器
      requestAnimationFrame(animate)
    }

动态跟踪 实现思路

1,定义关键固定位置坐标,用固定坐标值生成一组曲线。

2,将曲线分割成多份获取分割后的一组顶点坐标

3,加载物体并设置初始位置,这里用的是个飞机模型

4,通过Threejs的帧动画实时修改物体坐标和物体朝向,达到运动效果

image.png

(20条消息) Threejs实现镜头跟随物体移动效果,镜头拐弯并保持运动方向_左本的博客-CSDN博客_threejs 相机跟随

五、动画

ThreeJS 中实现动画有很多种方式,一种是直接在render循环中写动画逻辑,比如每次渲染的时候修改动画对象的一些属性

这种方法比较简单粗暴,缺点就是动画一多,控制起来比较麻烦,当然也可以使用一些成熟的第三方库,比如 Tween 来丰富动画,丰富过渡效果

    var render = function () {
        requestAnimationFrame(render);
        step += 0.01;
        cube.position.x = 2 + (1 * (Math.cos(step)));
        cube.position.y = 2 + (10 * Math.abs(Math.sin(step)));
        cube.rotation.x += 0.03;
        cube.rotation.y += 0.03;
        cube.rotation.z += 0.03;
        renderer.render(scene, camera);
    };

另一种是直接利用 GLTF 格式的优势:GLTF 格式中内置了动画信息

efa42a2de0a249ceb3e08619584bc966_5705edf89aea4a3e954a57f20f13c88d.webp

使用官方推荐的 blender 可以很简单的为模型添加上丰富的动画

image.png

使用时

    let mixer
    let carAction
    const gltfLoader = new GLTFLoader()
    gltfLoader.load('./static/models/scene3.gltf', (gltf) => {
      const model = gltf.scene
      scene.add(model)
      
      /**
       * 初始化动画
       */
      // 我们需要创建 AnimationMixer,它就像音乐播放器一样,帮助我们播放动画
      mixer = new THREE.AnimationMixer(gltf.scene)

      // 使用 clipAction 方法将动画信息添加到 mixer 中
    //   action = mixer.clipAction(gltf.animations[0])
    //   action.play() // 自动播放

      // 多个动画
    //   gltf.animations.forEach((item) => {
    //     mixer.clipAction(item).play()
    //   })

      // 同样的,如果想单独控制某个动画,给它
    //   carAction = mixer.clipAction(gltf.animations[4])
    })

同样的,作为帧动画,在必须每一帧循环渲染,用到THREE提供的时钟对象

const clock = new THREE.Clock() // 计帧器
if (mixer) mixer.update(clock.getDelta()) // 加到 animate 中循环更新

控制动画播放或者暂停:

carAction.play() // 动画初始化开始播放

carAction.stop() // 动画销毁

carAction.paused = true // 暂停后只有重新设置 false 才能继续

carAction.paused = false

六、射线检测

在场景中,常常需要对三维场景里的物体进行点击、拖拽等交互,这时候需要判断,鼠标是否点中了三维场景中的物体

通常是通过 raycaster(射线检测)进行检测,判断鼠标确实在这个点上

webGL中获取鼠标交互物体的原理:

通过三维空间中相机视点与鼠标在屏幕上的地位的连线,造成一条直线,捕捉与此直线相交的空间中的物体,即为交互对象物体

a9a5e7ec6bc04a6e915f0dc791752063_055cb9586aac4c6386a6c62d0b123b2f.webp

在three中,Raycaster为咱们封装了大量的逻辑代码,包含生成相机到鼠标的射线、射线与空间物体的碰撞检测、射线相交物体深度计算、相交物体列表等等

Raycaster类的.intersectObject()办法:

检测所有在射线与物体之间,包含或不包含后辈的相交局部。返回后果时,相交局部将按间隔进行排序,最近的位于第一个

    function raycasterTest(e) {
        e.preventDefault()
        const { clientX, clientY } = e
        const dom = renderer.domElement
        // 拿到canvas画布到屏幕的距离
        const domRect = dom.getBoundingClientRect()
        // 计算标准设备坐标 - 归一化设备坐标
        const x = ((clientX - domRect.left) / dom.clientWidth) * 2 - 1
        const y = -((clientY - domRect.top) / dom.clientHeight) * 2 + 1
        const vector = new THREE.Vector3(x, y)
        // 转世界坐标
        const worldVector = vector.unproject(camera)
        console.log('世界坐标', worldVector)
        // 向量相减,并获取单位向量
        const ray = worldVector.sub(camera.position).normalize()
        // 射线投射对象, 第一个参数是射线原点 第二个参数是射线方向
        const raycaster = new THREE.Raycaster(camera.position, ray)
        raycaster.camera = camera

        //返回射线选中的对象 //第一个参数是检测的目标对象 第二个参数是目标对象的子元素
        const intersects= raycaster.intersectObjects(scene.children)

        if (intersects.length > 0) {
            console.log("捕获到对象", intersects);
            if(intersects[0] && intersects[0].object && intersects[0].object.name === '立方体1') {
                carAction.play()
            } 
        } else {
            console.log("没捕获到对象"); 
        }
    }
    window.addEventListener('click', raycasterTest)

项目源码

wind-base: 关于 ThreeJS 一些尝试 (gitee.com)

参考文档

【译】基于 Three.js 实现了交互式 3D 人物 - 知乎 (zhihu.com)

threejs实现3d全景看房 - 掘金 (juejin.cn)

ThreeJs之选中模型中的物体及物体沿轨迹移动 - 简书 (jianshu.com)

(20条消息) Threejs实现镜头跟随物体移动效果,镜头拐弯并保持运动方向_左本的博客-CSDN博客_threejs 相机跟随

glTF简介 - 腾讯云开发者社区-腾讯云 (tencent.com)

glTF - 统一应用程序渲染 3D 内容的传输格式 - 掘金 (juejin.cn)

详解 glTF 格式及数据加载细节 - 掘金 (juejin.cn)

后续分享

  • 深入场景、相机、渲染器
  • 深入材质,贴图,丰富模型展示效果
  • 分享碰撞检测、第一人称移动
  • 性能优化,GLTF文件压缩 Draco Compression