利用Three.js 和Cannon.js 构建一个3d物理场景

906 阅读1分钟

预览图(未完成)

image.png

参考

juejin.cn/post/698503…
www.yanhuangxueyuan.com/Three.js/ three.js 教程
schteppe.github.io/cannon.js/d… Cannon.js 官网

three.js 生成3d世界

1 引入three.js

我搭建的项目是vue2项目,首先是利用npm下载three.jsnpm i three --save在页面中import * as THREE from 'three'引入
前期可以利用OrbitControls相机控制器 进行旋转、缩放、平移操作。 首先打开node_modules目录下的/three/examples/js/controls/OrbitControls.js 文件在顶部加上import * as THREE from 'three' 再在引用的页面import "three/examples/js/controls/OrbitControls"

开始搭建

搭建一个3d场景需要几个要素,场景Scene、相机Camera、几何体Geometry、光源Light。场景搭建完成之后需要利用renderer渲染到页面上。

先上目前全部代码,写的不是很好,后面再优化,下篇文章开始正式搭建。

  <view class="content">
    <div id="content" ref="content" style="height:100%;width: 100%;position: absolute;">
    </div>
  </view>
</template>
<script>
import * as THREE from 'three'
import * as CANNON from "cannon";
import "three/examples/js/controls/OrbitControls"
import {
  AmbientLight,
  Scene,
  Group,
  PerspectiveCamera,
  Mesh,
  WebGLRenderer,
  TextureLoader,
  PlaneGeometry,
  OrbitControls,
  BoxGeometry,
  Sprite,
  SphereGeometry,
  DoubleSide,
  SpriteMaterial,
  MeshPhongMaterial,
} from 'three'
export default {
  data () {
    return {
      content: '',
      scene: '',
      camera: '',
      Controls: '',
      renderer: '',
      ambientLight: '',
      sprite: '',
      spriteMap: new TextureLoader().load('./static/image/sprite.png'),
      group: '',
      sun: '',
      sunMap: new TextureLoader().load('./static/image/sun.png'),
      skillsMap: new TextureLoader().load('./static/image/allSkills.png'),
      world: new CANNON.World(),
      sphereBody: '',
      floorMesh: '',
      earthMap: new TextureLoader().load('./static/image/earth.jpg'),
      earth: ''
    }
  },
  mounted () {
    this.init()
  },
  methods: {
    init () {
      this.initScene()
      this.initCamera()
      this.initLight()
      this.initRenderer()
      //   this.initControls()
      this.initfloor()
      this.initSprite()
      this.initCannon()
      this.initAxesHelper()
      this.initEarth()
      this.initCannonWall()
      this.render()
      this.myKeyup()

    },
    //键盘事件 移动地球模型   待优化  //锁定视角下无问题,开放视角无法判断移动方向
    myKeyup () {
      document.onkeydown = (e) => {
        if (e.repeat) return //每次按键只触发一次方法,长时间按下不会连续触发
        if (e.key == 'w') {
          this.sphereBody.velocity.set(-50, 0, 0)
        }
        else if (e.key == 's') {
          this.sphereBody.velocity.set(50, 0, 0)
        }
        else if (e.key == 'a') {
          this.sphereBody.velocity.set(0, 0, 50)
        }
        else if (e.key == 'd') {
          this.sphereBody.velocity.set(0, 0, -50)
        }
        //跳跃方法,禁止连跳🈲
        else if (e.key == ' ') {
          if (this.sphereBody.position.y <= 5) this.sphereBody.velocity.set(0, 50, 0)
        }
        //重置模型位置             待优化  //有惯性阻止不了
        else if (e.key == 'q') {
          this.sphereBody.position.set(0, 5, 0)
          this.sphereBody.velocity.set(0, 0, 0)
        }
      }
    },
    //初始化相机
    initCamera () {
      this.camera = new PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 5000)
      //   this.camera.position.set(0, 30, 500)
      const helper = new THREE.CameraHelper(this.camera);
      //   this.scene.add(helper);
    },
    //初始化相机控制器
    initControls () {
      this.Controls = new OrbitControls(this.camera, this.renderer.domElement);
      this.Controls.enableRotate = false
      this.Controls.enablePan = false
      this.Controls.enableZoom = false
      //   this.controls.autoRotate = true;
      //   this.controls.enableKeys = true;
    },
    // 初始化地面
    initfloor () {
      let grid = new TextureLoader().load('./static/image/grid.webp')
      grid.wrapS = THREE.RepeatWrapping
      grid.wrapT = THREE.RepeatWrapping
      let floorMesh = new PlaneGeometry(1000, 1000)
      let skills = new PlaneGeometry(100, 100)
      let material = new THREE.MeshLambertMaterial({
        color: 0xf7f7f7,
        map: grid,
        side: DoubleSide,
        transparent: true,
        opacity: 0.1
      });
      let material1 = new THREE.MeshLambertMaterial({
        map: this.skillsMap,
        side: DoubleSide,
      });
      this.floorMesh = new Mesh(floorMesh, material)
      let mesh1 = new Mesh(skills, material1)
      this.scene.add(this.floorMesh)
      this.scene.add(mesh1)
      this.floorMesh.rotateX(-Math.PI / 2);
      mesh1.rotateX(-Math.PI / 2);
      mesh1.position.set(0, 0.01, 0)
    },
    // 初始化灯光
    initLight () {
      this.ambientLight = new AmbientLight(0xffffff, 1)
      this.scene.add(this.ambientLight)
    },
    //初始化场景
    initScene () {
       this.scene = new Scene()
      let box = new SpriteMaterial({
        map: this.sunMap
      })
      this.sun = new Sprite(box)
      //太阳精灵图对象
      this.scene.add(this.sun)
      this.sun.position.set(-30, 50, -80)
      this.sun.scale.set(20, 20, 1)
      //墙壁边界对象
      let rlWall = new BoxGeometry(200, 5, 0.1)
      let baWall = new BoxGeometry(0.1, 5, 200)
      let wallMaterial = new MeshPhongMaterial({
        map: this.sunMap
      })
      let rightWallMesh = new Mesh(rlWall, wallMaterial), leftWallMesh = new Mesh(rlWall, wallMaterial) //左右墙壁
      let beforeWallMesh = new Mesh(baWall, wallMaterial), afterWallMesh = new Mesh(baWall, wallMaterial) //前后墙壁
      this.scene.add(rightWallMesh, leftWallMesh, beforeWallMesh, afterWallMesh)
      rightWallMesh.position.set(0, 0, -100)
      leftWallMesh.position.set(0, 0, 100)
      beforeWallMesh.position.set(100, 0,)
      afterWallMesh.position.set(-100, 0,)
    },
    // 坐标系辅助线
    initAxesHelper () {
      let AxesHelper = new THREE.AxesHelper(500);
      this.scene.add(AxesHelper);
    },
    // 初始化星星精灵对象
    initSprite () {
      //创建组对象
        this.group = new Group()
      //循环制造1000只精灵
      for (let i = 0; i < 1000; i++) {
        let spriteMaterial = new SpriteMaterial({
          //   color: 0x000000,
          map: this.spriteMap
        })
        this.sprite = new Sprite(spriteMaterial)
        this.group.add(this.sprite)
        // 控制精灵缩放
        this.sprite.scale.set(0.2, 0.2, 1)
        //精灵位置随机    
        let x = Math.random() - 0.5;
        let y = Math.random() - 0.5;
        let z = Math.random() - 0.5;
        this.sprite.position.set(-300 * x, -300 * y, -300 * z)

      }
      this.scene.add(this.group)
    },
    // 初始化渲染对象
    initRenderer () {
      this.renderer = new WebGLRenderer({
        antialias: true
      })
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.content = this.$refs.content
      this.renderer.shadowMapEnabled = true
      this.content.appendChild(this.renderer.domElement)
    },
    //渲染方法
    render () {
      this.world.step(1 / 60);
      requestAnimationFrame(this.render); //重复执行渲染方法
      //让星空旋转
      this.group.rotation.x += 0.001
      this.group.rotation.y += 0.001
      this.sun.rotation.x += 0.001
      //渲染场景和相机
      this.renderer.render(this.scene, this.camera);
      this.earth.position.copy(this.sphereBody.position)  //绑定3d和物理世界圆球的位置
      this.earth.quaternion.copy(this.sphereBody.quaternion); //绑定3d和物理世界圆球的旋转
      //   this.Controls.target = this.earth.position //相机聚焦模型
      this.camera.lookAt(this.earth.position) //相机聚焦模型
      this.camera.position.set(this.earth.position.x + 100, this.earth.position.y + 100, this.earth.position.z) //相机位置跟随模型
      //   this.Controls.update(); //更新相机
    },
    //初始化地球模型
    initEarth () {
      let mesh = new SphereGeometry(5, 50, 50)
      let material = new MeshPhongMaterial({
        map: this.earthMap,
      })
      this.earth = new THREE.Mesh(mesh, material)
      this.scene.add(this.earth)
      this.earth.position.set(10, 50, 50)
    },
    // 初始化物理世界
    initCannon () {
      // 生成物理世界
      this.world = new CANNON.World(); //该方法初始化物理世界,里面包含着物理世界的相关数据(如刚体数据,世界中所受外力等等)
      this.world.gravity.set(0, -100, 0); //设置物理世界的重力为沿y轴向上-100米每二次方秒
      this.world.broadphase = new CANNON.NaiveBroadphase(); //NaiveBroadphase是默认的碰撞检测方式,该碰撞检测速度比较高
      this.world.solver.iterations = 5; //解算器的迭代次数,更高的迭代次数意味着更加精确同时性能将会降低

      //生成物理地面
      let groundShape = new CANNON.Plane();
      let groundBody = new CANNON.Body({ mass: 0, shape: groundShape });
      groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 1, 1), -Math.PI / 2);
      this.world.addBody(groundBody);
      //生成物理圆球对象
      let sphereShape = new CANNON.Sphere(5); // 5为对象半径
      this.sphereBody = new CANNON.Body({
        mass: 1, //重量,单位kg
        shape: sphereShape,
        position: new CANNON.Vec3(0, 5, 0), //初始位置
        angularDamping: 0.5,
        fixedRotation: false,
      }); // Step 2
      this.world.add(this.sphereBody); // Step 3
    },
    //初始化物理世界边界墙壁
    initCannonWall () {
      let rightWallShape = new CANNON.Box(new CANNON.Vec3(200, 100, 0.1)),
        leftWallShape = new CANNON.Box(new CANNON.Vec3(200, 100, 0.1)),
        beforeWallShape = new CANNON.Box(new CANNON.Vec3(0.1, 100, 200)),
        afterWallShape = new CANNON.Box(new CANNON.Vec3(0.1, 100, 200)),
        rightWallBody = new CANNON.Body({
          mass: 0,
          shape: rightWallShape,
          position: new CANNON.Vec3(0, 100, -100)
        }),
        leftWallBody = new CANNON.Body({
          mass: 0,
          shape: leftWallShape,
          position: new CANNON.Vec3(0, 100, 100)
        }),
        beforeWallBody = new CANNON.Body({
          mass: 0,
          shape: beforeWallShape,
          position: new CANNON.Vec3(100, 100, 0)
        }),
        afterWallBody = new CANNON.Body({
          mass: 0,
          shape: afterWallShape,
          position: new CANNON.Vec3(-100, 100, 0)
        })
      let bodyArr = [leftWallBody, rightWallBody, beforeWallBody, afterWallBody]
      //这里使用for循环的原因是  this.world.add(leftWallBody, rightWallBody)⬅左边这种写法只生效第一个 官方文档没看懂
      for (let i = 0; i < bodyArr.length; i++) {
        this.world.add(bodyArr[i])
      }
    }
  }
}
</script>

<style>
</style>