Three.js - JS三维模型库在Vue2中的基础教程

 threejs 官网 :https://threejs.org/

 threejs 案例: https://threejs.org/examples/#webgl_animation_keyframes

threejs API:https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene  

开始使用

npm install --save three;

全局引用

import * as THREE from 'three';

按需引用 ,demo均使用全局

import {class} from 'three';

package-lock.json

"three": "^0.151.3",

"three-orbitcontrols": "^2.110.3",

"vue": "^2.6.14",

一、基础使用

三大核心

new THREE.WebGLRenderer()// 创建渲染器

new THREE.Scene()  // 实例化场景

new THREE.PerspectiveCamera()// 实例化相机

创建四个文件方便场景管理

渲染控制器ThreeController.js 作为 3d渲染的主要入口文件

ThreeController.js

import * as THREE from 'three';

export const renderer = new THREE.WebGLRenderer()  // 创建渲染器

export const scene = new THREE.Scene()  // 实例化场景

export const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000) 相机

export class ThreeController {

   Model = null;

   scene = null;

   constructor(Model) {  //构造器函数

    Model.appendChild(renderer.domElement) //容器

    renderer.setSize(Model.offsetWidth, Model.offsetHeight, true)

    this.Model = Model

    this.scene = scene

    camera.position.set(100, 100, 100) // 设置相机位置

    camera.lookAt(new THREE.Vector3(0, 0, 0))  // 设置相机看先中心点

    camera.up = new THREE.Vector3(0, 1, 0)  // 设置相机自身的方向

    renderer.shadowMap.enabled = true;

    renderer.render(scene, camera);

}

// 外部访问将模型添加到场景中

    addObject(...object) {

      object.forEach(elem => {

        this.scene.add(elem)

      })

    }

}

HomeView.vue

 

<template>

  <div>

    <div class="three-canvas" ref="threeTarget"></div>

  </div>

</template>



import {ThreeController,} from "@/components/ThreeController";

import { ModelListConfig} from"@/components/ModelListConfig";

import { LightList } from "@/components/LightListConfig";

import { allHelper } from "@/components/AxesHelperConfig";

return { ThreeController: null,



}

mounted() {

this.init(); 

},

methods: {


  init() {

      this.ThreeController = newThreeController(this.$refs.threeTarget);

      this.ThreeController.addObject(...ModelListConfig);

      this.ThreeController.addObject(...LightList);

      this.ThreeController.addObject(...allHelper);

    },

此时场景中一片漆黑

接下来添加模型  ModelListConfig.js

import * as THREE from 'three';

具体模型类型参考Api

export const ModelListConfig = []  存储模型数组,也可某个模型单独导出

const sky = new THREE.TextureLoader().load(Require('sky.jpg'))

创建材质贴图

export const MeshModel = new THREE.Mesh( 创建几何体

  new THREE.BoxGeometry(20, 20, 20), 正方体 size 20

  new THREE.MeshStandardMaterial({  材质纹理

    color: 'rgb(36, 172, 242)',

    // roughness: 0 , 光滑度 0最光滑

    // metalness: 0, 金属度 1最像金属

    map: sky

  })

)

ModelListConfig.push(MeshModel) 将模型添加到数组中



// 多人开发用来存储数据

MeshModel.userData = {

  name: 'MeshModel',

  user: '我是正方体模型'

}

 可以看到场景依旧是漆黑,所以要“开灯”即添加光线

 接下来添加光线 LightListConfig.js

import * as THREE from 'three';
export const LightList = []
// // 添加环境光(自然光),设置自然光的颜色,设置自然光的强度(0 最暗, 1 最强)
const hemiLight = new THREE.HemisphereLight("#A09E9E", 0.5);
hemiLight.position.set(0, 40, 15);
LightList.push(hemiLight)
const hemiLighthelper = new THREE.HemisphereLightHelper(hemiLight, 5);
LightList.push(hemiLighthelper)
export const pointLight = new THREE.PointLight(
  'rgb(255,255,255)',
  0.7,
  600,
  0.2
)
pointLight.position.set(0, 1, 50)  // 设置点光源位置 (x,y,z)
LightList.push(pointLight)

这时模型就可以正常显示了

创建辅助线 AxesHelperConfig.js

import { AxesHelper ,GridHelper } from 'three'
 /**
     * 场景中添加辅助线
     * @param  {allHelper.push(new AxesHelper())}  
     * 添加栅格
     * @param  {allHelper.push(new GridHelper())}  
     */
 export const allHelper = []
 // 坐标辅助
 export const axesHelper = new AxesHelper(200)  // 创建坐标辅助 (500 为辅助线的长度)
 export const gridHelper = new GridHelper(500, 20, 'green', 'rgb(255, 255, 255)')
 allHelper.push(gridHelper,axesHelper)  // 添加到辅助列表

 /*
gridHelper Config
size -- 坐标格尺寸. 默认为 10. 这就是网格组成的大正方形最大是多少
divisions -- 坐标格细分次数. 默认为 10. 组成的最大的正方向平均分多少份
colorCenterLine -- 中线颜色. 值可以为 Color 类型, 16进制 和 CSS 颜色名. 默认为 0x444444。这个是指网格和坐标的 x轴 z 轴重合线的颜色。
colorGrid -- 坐标格网格线颜色. 值可以为 Color 类型, 16进制 和 CSS 颜色名. 默认为 0x888888

  */

 效果

二、射线控制器 

相机视角拖拽 ThreeController.js 需要射线控制器 OrbitControls 因为渲染成是三维之后,我们点击的是相机呈现的二维浏览器画面,距离模型到页面上是有一定距离的,简单来说就像我们站在一个物体面前,用手机拍照时,点击的照片中的物体,实际上我们点击的并不是物体,而是相机渲染给我们的一个二维照片。OrbitControls,会穿透整个三维场并返回一个list,第[0]项就是我们想要点击模型。

ThreeController.js

import { OrbitControls }from'three/examples/jsm/controls/OrbitControls'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
const mouse = new THREE.Vector2() // 初始化鼠标位置
const raycaster = new THREE.Raycaster()//初始化射线发射器
//  屏幕鼠标x,屏幕鼠标y  视图宽度,视图高度
let x = 0; let y = 0; let width = 0; let height = 0
constructor(Model) {
     …
      EventInjection(camera) //要在渲染器之前
     …
      renderer.render(scene, camera);

}
export const EventInjection=()=>{
   // 鼠标移动事件
const transformControls = new TransformControls(camera, renderer.domElement)
renderer.domElement.addEventListener("mousemove", event => {
x = event.offsetX
y = event.offsetY
width = renderer.domElement.offsetWidth
height = renderer.domElement.offsetHeight
mouse.x = x / width * 2 - 1
mouse.y = -y * 2 / height + 1
})
let transing = false
transformControls.addEventListener("mouseDown", event => {
transing = true
return event
})
// 鼠标点击事件
renderer.domElement.addEventListener("click", event => {
if (transing) {
transing = false
return
}
scene.remove(transformControls) // 移除变换控制器
transformControls.enabled = false // 停用变换控制器
raycaster.setFromCamera(mouse, camera)  // 配置射线发射器,传递鼠标和相机对象
const intersection = raycaster.intersectObjects(scene.children) // 获取射线发射器捕获的模型列表,传进去场景中所以模型,穿透的会返回我们
if (intersection.length) {
const object = intersection[0].object  // 获取第一个模型
console.log(object) 
scene.add(transformControls) // 添加变换控制器
transformControls.enabled = true // 启用变换控制器
transformControls.attach(object)
}
return event
})
// 监听变换控制器模式更改
document.addEventListener("keyup", event => {
if (transformControls.enabled) {  // 变换控制器为启用状态执行
if (event.key === 'e') { // 鼠标按下e键,模式改为缩放
transformControls.mode = 'scale'
return false
}
if (event.key === 'r') { // 鼠标按下r键,模式改为旋转
transformControls.mode = 'rotate'
return false
}
if (event.key === 't') { // 鼠标按下t键,模式改为平移
transformControls.mode = 'translate'
return false
}
}
return event
})
// three.js自带的方法
const orbitControls = new OrbitControls(camera, renderer.domElement)
// console.log(MOUSE)//查看MOUSE中的配置项
orbitControls.mouseButtons = {  // 设置鼠标功能键(轨道控制器)
LEFT: null,  // 左键无事件
MIDDLE: THREE.MOUSE.DOLLY,  // 中键缩放
RIGHT: THREE.MOUSE.ROTATE// 右键旋转
  }
  scene.add(transformControls)
}

这时我们在log里可以看到之前添加的 userdata

 同时也可以做拽查看

 三、进阶使用

1、外部导入模型  obj、 gltf、 json、 glb等。

开源模型地址 https://github.com/mrdoob/three.js/blob/master

开源的模型 

 可以自己改成其他格式,使用别的引用方法尝试

 ThreeController.js 示例演示,导入glb文件

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
export const LoadingGLTFMethod=(GltfModel)=> {
  loader.load(`${process.env.BASE_URL}model/${GltfModel}`, (gltf) => {
    gltf.scene.scale.set(15, 15, 15)
    gltf.scene.position.set(0, 0, 0)
    gltf.scene.userData={
      name:"LoadingGLTFModel",
      data:"123",
      id:"1212121212",
      title:"人物模型"
    }
    let axis = new THREE.Vector3(0, 1, 0);//向量axis
    gltf.scene.rotateOnAxis(axis, Math.PI);
    gltf.scene.traverse(function (object) {
      if (object.isMesh) {
        object.castShadow = true; //阴影
        object.receiveShadow = true; //接受别人投的阴影
      }
    })
    scene.Model = gltf.scene;
    scene.add(gltf.scene) //公用访问时使用常量向场景中添加引入的模型
    return  gltf.scene
  })
}
LoadingGLTFMethod("Soldier.glb");//看一看模型有没有出现

可以看到已经成功导入

创建地板  ModelListConfig.js

export const Ground = new THREE.Mesh(new THREE.PlaneGeometry(300, 300), new THREE.MeshPhongMaterial({
  color: 0x888888, depthWrite: true,
}));
ModelListConfig.push(Ground) 

 

 当然你也可以自己做一个材质,来作为地板的纹理

const Require = (src) => {
  return require(`../assets/${src}`)
}
const GroundTexture = new THREE.TextureLoader().load(Require('RC.jpg'))
export const Ground = new THREE.Mesh(new THREE.PlaneGeometry(300, 300), new THREE.MeshPhongMaterial({
  color: 0x888888, depthWrite: true,
  map: GroundTexture
}));
Ground.rotation.x = - Math.PI / 2;
Ground.receiveShadow = true;
ModelListConfig.push(Ground)  // 添加到模型数组

贴图效果:

上述我们在导入文件时,设定了导入的物体是接受投影的,也添加过自然光,这时发现并没有影子。在方向光的作用下,物体会形成阴影投影效果,Three.js物体投影模拟计算主要设置三部分,一个是设置产生投影的模型对象,一个是设置接收投影效果的模型,最后一个是光源对象本身的设置,光源如何产生投影。

LightListConfig.js 添加平行光  

const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
directionalLight.position.set(0, 35, 20);// 设置光源位置
directionalLight.castShadow = true; // 设置用于计算阴影的光源对象
// 设置计算阴影的区域,最好刚好紧密包围在对象周围
// 计算阴影的区域过大:模糊  过小:看不到或显示不完整
directionalLight.shadow.camera.near =0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -10;
// 设置mapSize属性可以使阴影更清晰,不那么模糊
// directionalLight.shadow.mapSize.set(1024,1024)
LightList.push(directionalLight)

 

 这时候我们加上外部控制  HomeView.vue

<template>
  <div>
    <ul>
      <li><button @click="LoadingMethod()" >LoadingMethod</button></li>
      <li><button @click="logs()">logs</button></li>
      <li><button @click="LoadingTrack()">LoadingTrack</button></li>
      <li><button @click="Rorate()">Rorate</button></li>
      <li><button@click="LoadingSceneMaterials()">LoadingSceneMaterials
</button></li>
      <li><button @click="run()">Run</button></li>
     </ul>
     <div class="log-content ">
        <h6>name:{{userData.name}}</h6>
        <h6>data:{{userData.data}}</h6>
        <h6>id:{{userData.id}}</h6> 
        <h6>title:{{userData.title}}</h6> 
     </div>
    <div class="three-canvas" ref="threeTarget"></div>
  </div>
</template>
import {
  ThreeController,
  LoadingGLTFMethod,
scene,
} 
from "@/components/ThreeController"; //中央渲染控制
return {
      …
      userData: {},
      test: {},
};
methods: {
…
LoadingMethod() {
      LoadingGLTFMethod("Soldier.glb");
},
    logs() {
        this.test = this.ThreeController.scene.children.find((item) => {
        if(item.userData.name){
              return item.userData.name == "LoadingGLTFModel";
        }
      });
     this.userData=this.test.userData
    },
}

<style lang="scss" scoped>
.three-canvas {
  width: 100vw;
  height: 100vh;
}
ul{
  position: absolute;
  right: 10px;
  top: 10px;
}
.log-content {
  width: 200px;
  height: 60px;
  position: absolute;
  margin: 10px;
  background-color: skyblue;
}

调用 LoadingMethod() 模型加载后,我们再调用logs() 查看我们加上的usedata

 2. requestAnimationFrame 请求动画帧

 它是一个浏览器的宏任务, requestAnimationFrame的用法与settimeout很相似,只是不需要设置时间间隔而已。requestAnimationFrame使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。它返回一个整数,表示定时器的编号,这个值可以传递给cancelAnimationFrame用于取消这个函数的执行,它会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,如果系统绘制率是 60Hz,那么回调函数就会16.7ms再 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是,requestAnimationFrame的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。

ThreeController.js  使用requestAnimationFrame() 让模型“run”起来

export const clock = new THREE.Clock();
export let mixer = null
export class ThreeController {
constructor(Model) {
     …  animate() //执行做好的动画帧
}
}
export const animate = () => { requestAnimationFrame(animate);
  if (mixer) { mixer.update(clock.getDelta());}
  renderer.render(scene, camera);
}

export const startAnimation=(skinnedMesh, animations, animationName)=>{
  const m_mixer = new THREE.AnimationMixer(skinnedMesh);
  const clip = THREE.AnimationClip.findByName(animations,animationName);
  if (clip) {
    const action = m_mixer.clipAction(clip);
    action.play();
  }
  return m_mixer;
}
export const LoadingGLTFMethod=(GltfModel,type)=> {//这里新加了一个type
   …
   mixer = startAnimation(
      gltf.scene,
      gltf.animations,
      gltf.animations[type].name // animationName,1 是"Run" 
   );
}
LoadingGLTFMethod("Soldier.glb",1);

效果  (不会做动图:)) 

此时模型仅仅是原地run ,这时我们加上手动控制

模型中有position属性,代表物体在空间中x、y、z轴的坐标

HomeView.vue

import {
  …
  renderer,
  camera,
} from "@/components/ThreeController";
methods: {
…
Rorate(){
      this.test = this.ThreeController.scene.children.find((item) => {
        return item.userData.name == "LoadingGLTFModel";
      });
      this.animates();
 },
animates() {
      requestAnimationFrame(this.animates);
      //this.test.position.x += 0.01; 
      //this.test.position.y += 0.01;
      this.test.position.z += 0.1;
      renderer.render(scene, camera);
},
}加载完模型后,执行Rorate(),这时它就是真正的往前run了

3、轨道以及轨迹思想

Line类是一种线形状几何体,物体运动的轨迹我们可以看成一条线,让模型围轨道运动

ModelListConfig.js

const curveArr = [0, 0, 0, 350, 0, 0,  0, 0, 350];
const curve = [];
for (let i = 0; i < curveArr.length; i += 3) { //每三个点生成一个坐标
  curve.push(new THREE.Vector3(curveArr[i], curveArr[i + 1], curveArr[i + 2]));
}
const RoutePoints = new THREE.CatmullRomCurve3(curve, true)
const sphereCurve = RoutePoints.clone()
export const pathPoints = sphereCurve.getPoints(200)//取200个点
const line = new THREE.Line(
  new THREE.BufferGeometry().setFromPoints(pathPoints),
  new THREE.LineBasicMaterial({
    color: "red",
    linewidth: 1,
  })
)
ModelListConfig.push(line)

为了更直观一些,我们还可以把取到的点也渲染上 

const addMesh = () => {
  let list = []
  for (let point of pathPoints) {
    const sphere = new THREE.BoxGeometry(3, 1, 1)
    const sphereMaterial = new THREE.MeshBasicMaterial({ map: RouteTexture })
    const sphereMesh = new THREE.Mesh(sphere, sphereMaterial)
    sphereMesh.position.set(point.x, point.y, point.z)
    ModelListConfig.push(sphereMesh)
    list.push(sphereMesh)
  }
  list.push(line)
  return list
}
// 加载轨道
export const LoadingTrack = () => {
  return addMesh()
}

让物体围绕着生成的轨道运动  HomeView.vue

Return{ …  num:0} 
LoadingTrack(){ //加载出轨道
        this.ThreeController.addObject(...LoadingTrack())
 },
 run(){
      this.test = this.ThreeController.scene.children.find((item) => {
        return item.userData.name == "LoadingGLTFModel";
      });
      this.runanimates();
 },
 runanimates(){
            if(this.num<=pathPoints.length-2){
               this.num+=1
            }else{
               this.num=0
            }
            requestAnimationFrame(this.runanimates);
            this.test.position.x = pathPoints[ this.num].x ;
            this.test.position.y = pathPoints[ this.num].y;
            this.test.position.z = pathPoints[ this.num].z
            renderer.render(scene, camera);
    },

 也可以用来实现相机漫游,实时光影 只不过就是运动的物体从模型,变成相机或者光线

加载场景材质,ThreeController.js  天空盒效果

export const SceneMapMaterial=(list)=>{
   const map=new THREE.CubeTextureLoader()
  .setPath( `${process.env.BASE_URL}model/` )
  .load(list );
   return map
}

HomeView.vue

SceneList:[ 'px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg','nz.jpg' ] 
LoadingSceneMaterials(){ 
 scene.background =  SceneMapMaterial(this.SceneList)//max px  1024*1024
},
名字有对应的作用,且不能超过最大限制

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值