threeJs構建3D世界

喬木滴滴 發表於 2023-01-22

threejs官網

https://threejs.org/docs/index.html#manual/zh/introduction/Installation (官網非常的詳細)

匯入安裝

npm install three (下載安裝threejs)

然後就可以在專案中匯入threejs 

import * as THREE from 'three'

建立場景和相機

就是需要一個場景來呈現 3D效果 相機是 對於在哪個位置來觀察這個場景中的3D效果(這裡用到的Vue2)

<script>
import * as THREE from 'three'
export default {
     camera: null,  //相機物件
      scene: null,  //場景物件
      renderer: null,  //渲染器物件
  mounted () {
    this.init()
  },
  methods: {
    init () {
      // 先建立一個場景 
      this.scene = new THREE.Scene();
      // 建立一個相機
      this.camera = new THREE.PerspectiveCamera(
        // 第一個引數是角度 75°
        75,
        // 第二個引數傳入寬高比 
        // window.innerWidth / window.innerHeight,
        600 / 600,
        // 近端
        0.1,
        // 遠端
        1000
      )
      // 建立相機定位 set 設定相機位置 x y z
      this.camera.position.set(0, 0, 10)
      // 把相機新增到場景
      this.scene.add(this.camera)
      // 建立一個物體   引數是寬高比 一樣的大小
      const cubeGeometry = new THREE.BoxGeometry(1, 1, 1)
      // 建立物體材質
      const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0Xffff00 })
      // 根據幾何體和材質建立物體 引數一是物體體 引數二是材質
      const cube = new THREE.Mesh(cubeGeometry, cubeMaterial)
      // 將幾何體新增到場景 
      this.scene.add(cube)

      //初始化渲染器
      this.renderer = new THREE.WebGLRenderer()
      // 設定渲染的尺寸大小 可以填螢幕大小 引數是寬高
      this.renderer.setSize(600, 600)
      // 其實現在的renderer就是畫布 把畫板的dom渲染到畫布上
      document.querySelector('#container').appendChild(this.renderer.domElement)
      // 使用渲染器 透過相機將場景渲染出來
      this.renderer.render(this.scene, this.camera)
    }
  }
}
</script>

 

上面就可以呈現基本的物體了(關於材質或者渲染器什麼的可以去 官網看看非常的詳細)

接下來引入軌道  就是物體可以跟隨滑鼠的移動而移動

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 
// 建立軌道控制器 第一個引數是相機 第二個引數是 要渲染的元素
 const container = new OrbitControls(this.camera, this.renderer.domElement)
// 先執行一下那個渲染函式
   this.render()
    render () {
      // 每一幀都會渲染軌道控制器 引數是場景和攝像頭
      this.renderer.render(this.scene, this.camera)
      // 瀏覽器自帶渲染 下一幀的時候就去在執行這個函式
      requestAnimationFrame(this.render)
    }

 還有一些小功能 (不要在意那些變數都是不同程式碼找到的所以有些變數不一樣嘿嘿)

  //   新增座標 引數是大小的意思
      const axesHelper = new THREE.AxesHelper(5)
      // 新增到場景 中
      this.scene.add(axesHelper)
 // 設定物體的位置
 // this.cube.position.set(5,0,0)
// 也可以單獨設定
      this.cube.position.x += 0.1
  // 物體的縮放 set 引數是xyz
      this.cube.scale.set(2, 3, 1)
   // 也可以設定旋轉 set (x,y,z)
   // 下面例子 Math.PI代表180° 'XYZ' 表示先旋轉什麼
   // eg:this.cube.rotation.set(Math.PI /4 0,0,'XYZ')
    // 下面是代表不斷的旋轉 
    this.cube.rotation.x += 0.1  
   // 這個是threejs 帶的算時間的
      this.clock = new THREE.Clock()
 // 建立控制器
      this.container = new OrbitControls(this.camera, this.renderer.domElement)
      // 設定控制阻尼器,讓控制器更有真實效果 必須在動畫循壞的時候呼叫update().
      container.enableDamping = true

 

當然配合使用一些好用的庫 比如gsap(後期有時間會專門寫一些這個動畫庫特別的厲害各種線性動畫或者3D動畫都是牛的)

https://greensock.com/docs/v3/GSAP 官網

還有一個好用的庫  dat.gui 用來控制一些變數來幫助開發(非常的不錯用起來也非常簡單可以百度一下)

下載安裝 npm install --save dat.gui
// 匯入dat.gui 控制變數
import * as dat from 'dat.gui'

知道了這些就可以做一些簡單的好玩的效果了 但是需要找一些好看的3D模型 這裡推薦一個

https://sketchfab.com/3d-models?features=downloadable&sort_by=-likeCount 這個也是threejs官網給的裡面有好多的模型而且特別多免費的 

下面程式碼是gltf 模型和組合gsap 動畫 包括模型上面帶的動畫組成的一個會走的宇航員 (非常有意思可以試試)

<template>
  <div>
    <div id="container" @dblclick="shopOrPlay"></div>
  </div>
</template>

<script>
import * as THREE from 'three'
// 匯入控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// 匯入模型
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
// 匯入動畫
import gsap from 'gsap'
export default {
  data () {
    return {
      camera: null,  //相機物件
      scene: null,  //場景物件
      renderer: null,  //渲染器物件
      mode: null, // 航天員
      container: null,
      flag: true,
      clock: new THREE.Clock(),
      mixer: '',
      mixers: [],
      animate1: null,
      baseZ: 3,
      baseR: 0.7,
      basePY: -6,
      basePX: -8,
      baseMixer: null,
      stars: null, //星星
      mon: null,//月球
    };
  },
  mounted () {
    this.init()
  },
  methods: {

    init () {
      // 初始化場景
      this.scene = new THREE.Scene();
      //  設定背景
      // 初始化相機 75度  寬高比 最小0.1 最大2000
      this.camera = new THREE.PerspectiveCamera(75,
        (document.documentElement.offsetWidth || window.innerWidth) / (document.documentElement.offsetHeight || window.innerHeight),
        0.1,
        100000)
      // 設定攝像頭位置
      this.camera.position.set(0, 0, 10)
      // // 初始化渲染器
      this.renderer = new THREE.WebGLRenderer({
        // 抗鋸齒
        // antialias: true,
        alpha: true // 這個是背景透明色
      })
      // 設定渲染器寬高
      this.renderer.setSize((document.documentElement.offsetWidth || window.innerWidth), (document.documentElement.offsetHeight || window.innerHeight))
      // 例項化控制器
      this.container = new OrbitControls(this.camera, this.renderer.domElement)
      this.container.enableDamping = true



      // 更新攝像頭寬高比
      this.camera.aspect = (document.documentElement.offsetWidth || window.innerWidth) / (document.documentElement.offsetHeight || window.innerHeight);
      // // 更新攝像頭投影矩陣
      this.camera.updateProjectionMatrix()
      // // 新增相機到場景
      this.scene.add(this.camera);

      // 初始化模型
      const loader = new GLTFLoader();
      // 新增月球
      loader.load('/mon/scene.gltf', (gltf) => {
        this.mon = gltf.scene
        this.mon.scale.set(1500, 1500, 1500)
        this.mon.position.set(0, -12, 0)

        this.scene.add(this.mon);

      }, undefined, function (error) {


      });
      //  建立一個超大球體 半徑一千 後面的是經緯度 切分為各60
      const skyGeomtry = new THREE.SphereGeometry(5000, 50, 50)
      // 建立一個紋理
      const skMaterial = new THREE.MeshBasicMaterial({
        side: THREE.DoubleSide, //兩面可見
        // 新增紋理 為星河紋理
        map: new THREE.TextureLoader().load('./images/bj.jpg')
      })
      // 把球體翻到裡面能看見不然是黑色的 倆面可見就不翻轉了
      skyGeomtry.scale(1, 1, 1)
      // 新增材質
      const sky = new THREE.Mesh(skyGeomtry, skMaterial)

      // 新增到場景
      this.scene.add(sky)
      // 建立宇航員
      loader.load('./yuhangyuan/scene.gltf', (gltf) => {
        this.mode = gltf.scene
        this.mode.scale.set(3, 3, 3)
        this.mode.position.set(this.basePX, this.basePY, -90)
        this.mode.rotation.set(0, 0, 0)
        this.mixer = new THREE.AnimationMixer(gltf.scene.children[0]);
        this.baseMixer = this.mixer.clipAction(gltf.animations[0]).setDuration(1)
        this.baseMixer.play();
        this.animate1 = gsap.to(this.mode.position, {
          z: this.baseZ, duration: 8, onComplete: () => {
            gsap.to(this.mode.rotation, {
              y: this.baseR * Math.PI, duration: 1,
            })
          },
        })
        this.mixers.push(this.mixer);
        this.scene.add(this.mode);
      }, undefined, (error) => {

      });


      // 新增光
      let light2 = new THREE.DirectionalLight(0Xfffff, 0.3)
      light2.position.set(0, 10, 10)

      let light1 = new THREE.HemisphereLight();
      this.scene.add(light1, light2
      )
      // // 設定渲染器編碼
      this.renderer.outputEncoding = THREE.sRGBEncoding;
      // 監聽螢幕大小變化修改渲染器的寬高相機比例
      window.addEventListener('resize', this.size)
      // 監聽螢幕按鍵
      window.addEventListener('keyup', this.spacemanMove)
      document.querySelector('#container').appendChild(this.renderer.domElement)
      this.render()
    },
    render () {

      // 在這裡設定阻尼感
      this.container.update()
      var delta = this.clock.getDelta();
      for (var i = 0; i < this.mixers.length; i++) { // 重複播放動畫
        this.mixers[i].update(delta - 0.011);
      }
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.render)
    },
    size () {
      this.camera.aspect = (document.documentElement.offsetWidth || window.innerWidth) / (document.documentElement.offsetHeight || window.innerHeight);
      this.camera.updateProjectionMatrix()
      this.renderer.setSize((document.documentElement.offsetWidth || window.innerWidth), (document.documentElement.offsetHeight || window.innerHeight))
    },
    // 宇航員移動
    spacemanMove (e) {
      if (!this.animate1) return
      if (!this.animate1.isActive()) {
        if (e.keyCode === 38) {
          this.animate1 = gsap.to(this.mode.position, {
            z: (this.baseZ -= 3)
          })
        }
        if (e.keyCode === 40) {
          this.animate1 = gsap.to(this.mode.position, {
            z: (this.baseZ += 3)
          })
        }
        if (e.keyCode === 37) {
          this.animate1 = gsap.to(this.mode.rotation, {
            y: (this.baseR -= 0.3) * Math.PI
          })
        }
        if (e.keyCode === 39) {
          this.animate1 = gsap.to(this.mode.rotation, {
            y: (this.baseR += 0.3) * Math.PI
          })
        }
        if (e.keyCode === 87) {
          this.animate1 = gsap.to(this.mode.position, {
            y: (this.basePY += 2)
          })
        }
        if (e.keyCode === 83) {
          this.animate1 = gsap.to(this.mode.position, {
            y: (this.basePY -= 2)
          })
        }
        if (e.keyCode === 65) {
          this.animate1 = gsap.to(this.mode.position, {
            x: (this.basePX -= 3)
          })
        }
        if (e.keyCode === 68) {
          this.animate1 = gsap.to(this.mode.position, {
            x: (this.basePX += 3)
          })
        }
      }
    },
    shopOrPlay () {
      if (!this.flag) {
        this.flag = true
        this.baseMixer.play()
      } else {
        this.flag = false
        this.animate1 = gsap.to(this.mode.rotation, {
          y: (this.baseR += 2) * Math.PI,
          yoyo: true,
          duration: 10,
        })
        this.baseMixer.stop()
      }
    }
  },
  beforeDestroy () {
    window.removeEventListener('resize', this.size)
    window.removeEventListener('keyup', this.spacemanMove)
  },
}
</script>
<style scoped lang='scss'>
#container {
  background: url("@/assets/bj1.jpg");
  background-size: cover;
}
</style>

請多多指教