three+tween+react实现视角切换观看模型角度

394 阅读3分钟

效果

combined-ezgif.com-video-to-gif-converter.gif

第一步:添加基础three场景

    let scene, renderer, controls;
    // 初始化场景
    scene = new THREE.Scene();

    // 初始化相机
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    camera.position.set(2.5, 1, 2.5);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    cameraRef.current = camera;
    
     // 添加环境光
    const ambient = new THREE.AmbientLight(0xffffff, 1);
    scene.add(ambient);
    
    
        // 初始化渲染器
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 监听屏幕大小变化
    window.addEventListener("resize", () => {
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    });
    
        // 将渲染器添加到页面
    document
      .querySelector(".canvasContainer")
      ?.appendChild(renderer.domElement);
      
      
          // 添加控制器
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    // 渲染函数
    const render = () => {
      circle.rotation.z -= 0.01;
      circle2.rotation.z -= 0.01;

      controls.update();
      renderer.render(scene, camera);
      requestAnimationFrame(render);
      TWEEN.update();
    };

    // 执行函数
    render();

我使用React写的所以写在了useEffect里面 并在最后可以写上 return () => { window.removeEventListener("resize", () => {}); };做简单优化

第二步:做点击事件以及tween补间动画

html加上按钮

 <div className="canvasContainer">
      <div className="btn">
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("you");
          }}
        >
          右转
        </Button>
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("zuo");
          }}
        >
          左转
        </Button>
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("xia");
          }}
        >
          下面
        </Button>
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("return");
          }}
        >
          初始
        </Button>
      </div>
    </div>

样式的话自己定义就好了,我这里用的antd默认样式

添加函数事件

  const cameraRef = useRef(null);
  const modelRef = useRef(null);
 const handleButtonClick = (status) => {
    const { current: camera } = cameraRef;
    const { current: model } = modelRef;
    if (!camera || !model) return;
    animateCameraToModel(camera, model, status);
  };

  const animateCameraToModel = (camera, model, status) => {
    const start = {
      x: camera.position.x,
      y: camera.position.y,
      z: camera.position.z,
    };
    var end;
    const positions = {
      you: { x: 4.5, y: 1, z: 2.5 },
      zuo: { x: 2.5, y: 1, z: 4.5 },
      xia: { x: 2.5, y: -2, z: 2.5 },
      default: { x: 2.5, y: 1, z: 2.5 },
    };
    var end = positions[status] || positions.default;
    // const end = { x: 4.5, y: 1, z: 2.5 };
    const duration = 1000;

    new TWEEN.Tween(start)
      .to(end, duration)
      .onUpdate(() => {
        camera.position.set(start.x, start.y, start.z);
        camera.lookAt(model.position);
      })
      .start();
      

这里有个注意第一步我是在useEffect里面写的,现在这些都是在useEffect外面写的,这样的话按钮的点击函数才可以直接访问到,而且我的tween是另外用npm下载的,其实three自带有tween,直接 import { TWEEN } from "three/examples/jsm/libs/tween.module.min"引入即可,最后文件名有可能有所不同,查看node_modules包找到具体文件引入即可。

结语

以上图片模型资料均等在老陈-陈鹏获取,侵权联系删除,下面展示全部源码:

import React, { useEffect, useRef } from "react";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { Button } from "antd";
import "./bear.css";
import img050 from "../../assets/imgs/050.jpg";
import img1 from "../../assets/imgs/1.png";
import gltf from "../../assets/model/bear.gltf";
import TWEEN from "@tweenjs/tween.js";

function Bear() {
  const cameraRef = useRef(null);
  const modelRef = useRef(null);

  useEffect(() => {
    let scene, renderer, controls;

    // 初始化场景
    scene = new THREE.Scene();

    // 初始化相机
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    camera.position.set(2.5, 1, 2.5);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    cameraRef.current = camera;

    // 加载背景纹理
    const loader = new THREE.TextureLoader();
    const bgTexture = loader.load(img050);
    bgTexture.mapping = THREE.EquirectangularRefractionMapping;

    scene.background = bgTexture;
    scene.environment = bgTexture;

    // 添加环境光
    const ambient = new THREE.AmbientLight(0xffffff, 1);
    scene.add(ambient);

    // 加载小熊模型
    const gltfLoader = new GLTFLoader();
    gltfLoader.load(gltf, (gltf) => {
      const model = gltf.scene.children[0];
      model.material = new THREE.MeshPhongMaterial({
        color: 0xffffff,
        envMap: bgTexture,
        refractionRatio: 0.7,
        reflectivity: 0.99,
      });
      modelRef.current = model;
      scene.add(model);
    });

    //创建平面
    const waterGeometry = new THREE.CircleGeometry(1, 64);
    const waterGeometry2 = new THREE.CircleGeometry(2, 64);
    const exture = new THREE.TextureLoader().load(img1);
    const material = new THREE.MeshBasicMaterial({
      map: exture,
      transparent: true,
    });
    const circle = new THREE.Mesh(waterGeometry, material);
    const circle2 = new THREE.Mesh(waterGeometry2, material);
    circle.rotation.x = -Math.PI / 2;
    circle2.rotation.x = -Math.PI / 2;
    scene.add(circle);
    scene.add(circle2);

    // 初始化渲染器
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 监听屏幕大小变化
    window.addEventListener("resize", () => {
      renderer.setSize(window.innerWidth, window.innerHeight);
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
    });

    // 将渲染器添加到页面
    document
      .querySelector(".canvasContainer")
      ?.appendChild(renderer.domElement);

    // 添加控制器
    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    // 渲染函数
    const render = () => {
      circle.rotation.z -= 0.01;
      circle2.rotation.z -= 0.01;

      controls.update();
      renderer.render(scene, camera);
      requestAnimationFrame(render);
      TWEEN.update();
    };

    render();

    return () => {
      window.removeEventListener("resize", () => {});
    };
  }, []);

  const handleButtonClick = (status) => {
    const { current: camera } = cameraRef;
    const { current: model } = modelRef;
    if (!camera || !model) return;
    animateCameraToModel(camera, model, status);
  };

  const animateCameraToModel = (camera, model, status) => {
    const start = {
      x: camera.position.x,
      y: camera.position.y,
      z: camera.position.z,
    };
    var end;
    const positions = {
      you: { x: 4.5, y: 1, z: 2.5 },
      zuo: { x: 2.5, y: 1, z: 4.5 },
      xia: { x: 2.5, y: -2, z: 2.5 },
      default: { x: 2.5, y: 1, z: 2.5 },
    };
    var end = positions[status] || positions.default;
    // const end = { x: 4.5, y: 1, z: 2.5 };
    const duration = 1000;

    new TWEEN.Tween(start)
      .to(end, duration)
      .onUpdate(() => {
        camera.position.set(start.x, start.y, start.z);
        camera.lookAt(model.position);
      })
      .start();
  };

  return (
    <div className="canvasContainer">
      <div className="btn">
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("you");
          }}
        >
          右转
        </Button>
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("zuo");
          }}
        >
          左转
        </Button>
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("xia");
          }}
        >
          下面
        </Button>
        <Button
          type="primary"
          onClick={() => {
            handleButtonClick("return");
          }}
        >
          初始
        </Button>
      </div>
    </div>
  );
}

export default Bear;