ThreeJS学习笔记(1)— 下雪场景

121 阅读4分钟

一、效果图

image.png

二、安装

1)Vite + Three + Cannon

安装
npm init @vitejs/app 项目名
npm install three cannon-es

三、在vue项目中使用three

1)概念

  • 1)光:环境光、平行光、聚光灯
  • 2)物体:平面、物体(长方体,球体,圆形平面、方形平面)
  • 场景:
  // 创建Three.js场景
  var scene = new THREE.Scene();
  • ThreeJS渲染器
  // 创建Three.js渲染器
  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);
  • ThreeJS相机
  // 创建Three.js相机
  var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(200, 200, 200);
  camera.lookAt(0, 0, 0);
  • CannonJS物理世界
  // 创建Cannon.js物理世界
  const world = new CANNON.World();
  world.gravity.set(0, -9.8, 0); 
  world.broadphase = new CANNON.NaiveBroadphase();
  • 动画循环
  // 创建动画循环,并将物理世界里的物体位置赋值给ThreeJS里的物体。
  function render() {
    // 更新物理引擎里世界的物体
    world.step(1 / 60);

    sphere.quaternion.copy( sphereBody.quaternion );
    sphere.position.copy(sphereBody.position); // 将物理世界里的物体位置赋值给three.js里的物体
    
    if (ballNumer > 0) {
      for (const  i in ballMother) {
        ballMother[i].b.quaternion.copy(ballMother[i].bBody.quaternion);
        ballMother[i].b.position.copy(ballMother[i].bBody.position);
      }
    }

    renderer.render(scene, camera);
    //   渲染下一帧的时候就会调用render函数
    requestAnimationFrame(render);
    controls.update()
  }

  render();

2)源码

<script setup>

  // 1.引入Three.js和Cannon.js库。
  import * as THREE from 'three';  
  import * as CANNON from 'cannon-es';  
  // 引入轨道控制器扩展库OrbitControls.js
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';


  // 2.创建Three.js场景。
  var scene = new THREE.Scene();


  // 3.创建Three.js渲染器。
  var renderer = new THREE.WebGLRenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  document.body.appendChild(renderer.domElement);


  // 4.创建Three.js相机。
  var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(200, 200, 200);
  camera.lookAt(0, 0, 0);


  // 5.创建Cannon.js物理世界。
  const world = new CANNON.World(); //该方法初始化物理世界,里面包含着物理世界的相关数据(如刚体数据,世界中所受外力等等)
  world.gravity.set(0, -9.8, 0); //设置物理世界的重力为沿y轴向上-9.8米每二次方秒
  world.broadphase = new CANNON.NaiveBroadphase();//NaiveBroadphase是默认的碰撞检测方式,该碰撞检测速度比较高
  // 创建物理小球形状
  const sphereShape = new CANNON.Sphere(1);

  //设置物体材质
  const sphereWorldMaterial = new CANNON.Material({
    friction: 0.05,
    restitution: 1
  }) //材质数据,里面规定了摩擦系数和弹性系数;


  // 创建物理世界的物体
  const sphereBody = new CANNON.Body({
    shape: sphereShape, // 小球形状
    position: new CANNON.Vec3(0, 10, 0), // 小球位置
    mass: 1, //   小球质量
    material: sphereWorldMaterial, //   物体材质
  });

  // 将物体添加至物理世界
  world.addBody(sphereBody);


  // 6.物理世界创建地面和球体
  const floorShape = new CANNON.Plane(); // 创建地面形状
  const floorBody = new CANNON.Body(); // 创建地面物体
  const floorMaterial = new CANNON.Material("floor");
  floorBody.material = floorMaterial;
  floorBody.mass = 0; // 当质量为0的时候,可以使得物体保持不动
  floorBody.addShape(floorShape); // 添加形状
  floorBody.position.set(0, 0, 0); // 地面位置
  floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); // 旋转地面的位置
  world.addBody(floorBody);


  // 7.创建Three.js球和平面。
  const sphereGeometry = new THREE.SphereGeometry(1);
  const sphereMaterial = new THREE.MeshStandardMaterial({ color: "#ffffff" });
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  sphere.castShadow = true;
  scene.add(sphere);


  // 自定义下雪场景,以及地面颜色渐变
  const MathRound = (num = 0) => {
    let mathNum = Math.random() > 0.5 ? 1 : -1
    return Math.round(mathNum * Math.random() * 100 + num)
  }
  let ballMother = [],
    ballNumer = 0;
  const addBall = () => {
    let ballDefined = {
      bWorldMaterial: '',
      bShape: '',
      bBody: '',
      bGeometry: '',
      bMaterial: '',
      b: ''
    };
    let { bWorldMaterial, bShape, bBody, bGeometry, bMaterial, b } = ballDefined
    bWorldMaterial = new CANNON.Material({
      friction: 0.001,
      restitution: 0.01
    });
    bShape = new CANNON.Sphere(1);
    bBody = new CANNON.Body({
      shape: bShape, // 小球形状
      position: new CANNON.Vec3(MathRound(), MathRound(100), MathRound()), // 小球位置
      mass: 1, //   小球质量
      material: bWorldMaterial, //   物体材质
    });
    bGeometry = new THREE.SphereGeometry(1);
    bMaterial = new THREE.MeshStandardMaterial({
      color: "#ffffff",
      opacity: 0.5
    });
    b = new THREE.Mesh(bGeometry, bMaterial);
    b.castShadow = true;
    b.position.set(MathRound(), MathRound(100), MathRound());
    world.addBody(bBody);
    scene.add(b);

    ballMother.push({ bWorldMaterial, bShape, bBody, bGeometry, bMaterial, b })
    ++ballNumer;
  }
  const changeFloor1 = () => {
    (floor1.material.opacity > 0) && (floor1.material.opacity -= 0.01)
  }
  setInterval(() => {  
    addBall()
    changeFloor1()
  }, 83.3);



  const floor = new THREE.Mesh(
    new THREE.CircleGeometry(400),
    new THREE.MeshStandardMaterial({
      color: "#999999",
      opacity: 1,
      side: THREE.DoubleSide, //两面可见
    })
  );
  floor.position.set(0, 0, 1);
  floor.rotation.x = -Math.PI / 2;
  floor.receiveShadow = true;
  scene.add(floor);


  
  const floorShape1 = new CANNON.Plane(); // 创建地面形状
  const floorBody1 = new CANNON.Body(); // 创建地面物体
  const floorMaterial1 = new CANNON.Material("floor1");
  floorBody1.material = floorMaterial1;
  floorBody1.mass = 0; // 当质量为0的时候,可以使得物体保持不动
  floorBody1.addShape(floorShape1); // 添加形状
  floorBody1.position.set(0, 2, 0); // 地面位置
  floorBody1.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); // 旋转地面的位置
  world.addBody(floorBody1);
  const floor1 = new THREE.Mesh(
    new THREE.CircleGeometry(400),
    new THREE.MeshStandardMaterial({
      color: "#008000",
      opacity: 1,
      transparent: true,//允许透明
    })
  );
  floor1.position.set(0, 2, 0);
  floor1.rotation.x = -Math.PI / 2;
  floor1.receiveShadow = true;
  scene.add(floor1);


  // 创建环境光
  const ambientLight = new THREE.AmbientLight('#fff', 1)
  scene.add(ambientLight)
  scene.background = new THREE.Color(0x999999)
  // 平行光
  const directionalLight = new THREE.DirectionalLight(0xffffff, 10);
  directionalLight.position.set(0, 200, 200);
  directionalLight.castShadow = true
  scene.add(directionalLight);
  const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 1, 0xff0000);
  scene.add(dirLightHelper);
  
  
  // 设置相机控件轨道控制器OrbitControls
  const controls = new OrbitControls(camera, renderer.domElement);
  // 设置带阻尼的惯性
  controls.enableDamping = true
  // 设置阻尼的系数
  controls.dempingFactor = 0.01
  controls.autoRotate = true
  
  
  // AxesHelper:辅助观察的坐标系
  // const axesHelper = new THREE.AxesHelper(1000);
  // scene.add(axesHelper)
  
  
  // onresize 事件会在窗口被调整大小时发生
  window.onresize = function () {
      // 重置渲染器输出画布canvas尺寸
      renderer.setSize(window.innerWidth, window.innerHeight);
      // 全屏情况下:设置观察范围长宽比aspect为窗口宽高比
      camera.aspect = window.innerWidth / window.innerHeight;
      // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
      // 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
      // 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
      camera.updateProjectionMatrix();
  };
  
  

  // 8.创建动画循环,并将物理世界里的物体位置赋值给three.js里的物体。
  function render() {
    // 更新物理引擎里世界的物体
    world.step(1 / 60);

    sphere.quaternion.copy( sphereBody.quaternion );
    sphere.position.copy(sphereBody.position); // 将物理世界里的物体位置赋值给three.js里的物体
    
    if (ballNumer > 0) {
      for (const  i in ballMother) {
        ballMother[i].b.quaternion.copy(ballMother[i].bBody.quaternion);
        ballMother[i].b.position.copy(ballMother[i].bBody.position);
      }
    }

    renderer.render(scene, camera);
    //   渲染下一帧的时候就会调用render函数
    requestAnimationFrame(render);
    // controls.update()
  }

  render();

</script>

<template>
  <div>
    <h1>{{ msg }}</h1>
    <div class="comedythreeBox"></div>  
  </div>
</template>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>