react 里面使用three.js

8,155 阅读5分钟

弄项目,总想搞点3D的东西,到项目里面,即能提高项目的B格,又能拿去和领导吹牛皮。

但项目开发,又一般是react、vue那种模块化开发。threejs开发,网上找下例子又全是基础的html、js。那炸糕?

1、文件放入public内,iframe引入、a标签跳转

之前有次面试的时候,面试官一个问题让我印象深刻。他看我项目中使用react、threejs,就问:“你是怎么在react中使用threejs的?为啥要在react使用three?使用啥react特性优化了threejs性能?”

我giao!好像真的是呀!为啥要这样做?!!!

threejs 是使用 requestAnimationFrame 去 renderer.render( scene, camera );(重新渲染canvas)。而react 是使用 state 状态的更新,重新render dom节点!

threejs:"老资,不需要react你的更新!我的canvas自己动。"

这里就有了第一种方式。我称之为:分居2地式!

react项目一般有pubilc放静态资源的地方,通过绝对路径能访问。所以,我们可以把threejs文件,放到public内,然后react组件,通过iframe啥的进行引入,或者放a标签链接。

优缺点

优点:简单粗暴。网上找的例子直接往项目里面怼,甚至都不用改啥的。(也不用考虑react对three的影响)

这种,其实和分成2个项目没啥区别。

缺点:交互比较麻烦,如果three模块内和外面模块有啥交互的,,,很麻烦。(只能通过iframe的postmessage啥的进行交互)

总结:如果three模块比较独立,和其他模块基本没关系,同时又不想起另外的项目。可以使用这种方式,进行快速开发。

2、模块引入threejs等,组件内开发

继续上面的问题:为啥要在react使用three?使用啥react特性优化了threejs性能?

他喵的,被面试官,“性能”2个词一绕,我脑袋只思考性能去了····

我react 内开发,three模块,肯定是想使用模块化来开发 three模块阿!

举个实际的应用场景,我有一堆设备,每个设备详情,都需要展示 这个设备的3D模型。我如果有这个3D模型的three组件,我只需要传不同的参数就可以了?如果涉及到3D模块整体背景色啥的调整。我只需要调整three组件就好了!

import * as THREE from 'three';

使用babel-plugin-import等,引入BSP等模块。

umi 内可以使用:externals 参数进行配置

然后开发组件就好了。(举个例子)

interface MyProps {
  style?: Record<string, any>;
  [key: string]: any;
}
let timeNow = new Date().valueOf();
const values = { tep: '40.0', sec: '20.0' };
const meshcolor = 0xffffff;
let renderer: any,
  camera: any,
  scene: any,
  light: any,
  controls: any,
  isAutoRotate: boolean,
  cube: any;

export default (props: MyProps) => {
  const { style, ...pros } = props;
  const [btnRender, setBtnRender] = useState<boolean>(false);
  const renderRef = useRef<any>();
  // 初始化render
  const threeInit = (curDom: any) => {
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(curDom.clientWidth, curDom.clientHeight);
    renderer.setClearColor(0xeeeeee);
    renderer.shadowMap.enabled = true;
    // curDom.innerHTML = null;
    curDom.appendChild(renderer.domElement);
  };
  // 初始化相机
  const initCamera = (curDom: any) => {
    camera = new THREE.PerspectiveCamera(45, curDom.clientWidth / curDom.clientHeight, 0.1, 300);
    camera.position.set(0, 15, 40);
  };
  // 初始化场景
  const initScene = () => {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x39609b);
    // scene.fog = new THREE.Fog( 0xa0a0a0, 5, 250 );
  };
  // 初始化光线
  const initLight = (myScene: any) => {
    light = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
    myScene.add(light);
  };
  // 初始化控制器
  const initControls = (myCamera: any) => {
    controls = new OrbitControls(myCamera, renderer.domElement);
    //设置控制器的中心点
    controls.target.set(0, 10, 0);
    // 如果使用animate方法时,将此函数删除
    //controls.addEventListener( 'change', render );
    // 使动画循环使用时阻尼或自转 意思是否有惯性
    controls.enableDamping = false;
    //动态阻尼系数 就是鼠标拖拽旋转灵敏度
    // 阻尼系数
    controls.dampingFactor = 0.1;

    controls.minPolarAngle = Math.PI / 12;
    controls.maxPolarAngle = (Math.PI * 19) / 40;
    //是否可以缩放
    controls.enableZoom = true;
    //是否自动旋转
    controls.autoRotate = true;
    controls.autoRotateSpeed = 0.5;
    //设置相机距离原点的最远距离
    controls.minDistance = 10;
    //设置相机距离原点的最远距离
    controls.maxDistance = 200;
    //是否开启右键拖拽
    controls.enablePan = true;
    controls.update();
  };

  // 获取带数据的canvas
  const getTextCanvas = async ({ tep, sec }: any) => {
    const time = moment().format('HH:mm:ss');
    const width = 200,
      height = 300;
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');
    return new Promise((resole) => {
      if (ctx) {
        const img = new Image();
        img.src = collectorImg;
        //图片加载完后,将其显示在canvas中
        img.onload = () => {
          ctx.drawImage(img, 0, 0, width, height);
          ctx.font = 30 + 'px " bold';
          ctx.fillStyle = '#333';
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillText(tep, (width * 11) / 24, (height * 10) / 40);
          ctx.fillText(sec, (width * 11) / 24, (height * 20) / 40);
          ctx.font = 10 + 'px " bold';
          ctx.fillText(time, (width * 16.5) / 24, (height * 26.6) / 40);
          resole(canvas);
        };
      }
    });
  };

  // 改变材质种类
  const changeMaterial = async (myScene: any) => {
    const canvas: any = await getTextCanvas({ tep: 40, sec: 20 });
    if (canvas) {
      const texture = new THREE.CanvasTexture(canvas);
      const materials = [
        new THREE.MeshLambertMaterial({ color: meshcolor, opacity: 1, transparent: true }),
        new THREE.MeshLambertMaterial({ color: meshcolor, opacity: 1, transparent: true }),
        new THREE.MeshLambertMaterial({ color: meshcolor, opacity: 1, transparent: true }),
        new THREE.MeshLambertMaterial({ color: meshcolor, opacity: 1, transparent: true }),
        new THREE.MeshLambertMaterial({
          color: meshcolor,
          opacity: 1,
          transparent: true,
          map: texture,
        }),
        new THREE.MeshLambertMaterial({ color: meshcolor, opacity: 1, transparent: true }),
      ];
      const geometry = new THREE.BoxGeometry(20, 20, 5);
      cube = new THREE.Mesh(geometry, materials);
      cube.position.set(0, 10, 0);
      myScene.add(cube);
    }
  };
  // 初始化物体
  const initModel = (myScene: any) => {
    // 添加collector
    changeMaterial(myScene);
  };
  // 更新value数据
  const changeValues = () => {
    values.tep = (39 + Math.random() * 2).toFixed(1);
    values.sec = (19 + Math.random() * 2).toFixed(1);
  };

  const updateData = async () => {
    if (new Date().valueOf() - timeNow > 5000) {
      timeNow = new Date().valueOf();
      changeValues();
    }
    const canvas: any = await getTextCanvas(values);
    if (canvas && cube) {
      cube.material[4].map = new THREE.CanvasTexture(canvas);
      cube.material.map.needsUpdate = true;
    }
  };

  const render = () => {
    if (renderer && scene && camera) {
      renderer.render(scene, camera);
    }
  };
  const animate = () => {
    //更新控制器
    render();
    if (controls && isAutoRotate) {
      controls.update();
    }
    requestAnimationFrame(animate);
  };

  useEffect(() => {
    if (renderRef && renderRef.current) {
      threeInit(renderRef.current);
      initCamera(renderRef.current);

      initScene();
      animate();
    }
    const timer = setInterval(() => {
      updateData();
    }, 1000);
    return () => {
      cancelAnimationFrame(1);
      clearInterval(timer);
    };
  }, [renderRef]);

  useEffect(() => {
    if (scene) {
      initLight(scene);
      initModel(scene);
    }
  }, [scene]);
  useEffect(() => {
    if (camera) {
      initControls(camera);
    }
  }, [camera]);

  return (
    <div
      ref={renderRef}
      style={{ width: '300px', height: '200px', position: 'relative', ...style }}
      {...pros}
    >
      <div className={styles.btns}>
        <div
          className={styles.btn}
          onClick={() => {
            isAutoRotate = !isAutoRotate;
            setBtnRender(isAutoRotate);
          }}
        >
          {btnRender ? '自动旋转' : '旋转关闭'}
        </div>
      </div>
    </div>
  );
};

优缺点:

优点:模块化开发,交互、传值啥的方便。

缺点

1、引入相关模块的时候,比较恶心(挺难弄的)。

2、基础代码啥的还是得自己慢慢封装啥的。

3、渲染问题:react state值不能控制 three更新;(当然,如果你把渲染直接交给react,当我没说)

3、@react-three/fiber

自己搞太麻烦了,有没有现成的?肯定有阿,网上全是大神!!(又是鉴定自己辣鸡的一天)

github地址:github.com/pmndrs/reac…

demo:codesandbox.io/s/rrppl0y8l…

npm install three @react-three/fiber

import ReactDOM from 'react-dom'
import React, { useRef, useState } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'

function Box(props) {
  // This reference gives us direct access to the THREE.Mesh object
  const ref = useRef()
  // Hold state for hovered and clicked events
  const [hovered, hover] = useState(false)
  const [clicked, click] = useState(false)
  // Subscribe this component to the render-loop, rotate the mesh every frame
  useFrame((state, delta) => (ref.current.rotation.x += 0.01))
  // Return the view, these are regular Threejs elements expressed in JSX
  return (
    <mesh
      {...props}
      ref={ref}
      scale={clicked ? 1.5 : 1}
      onClick={(event) => click(!clicked)}
      onPointerOver={(event) => hover(true)}
      onPointerOut={(event) => hover(false)}>
      <boxGeometry args={[1, 1, 1]} />
      <meshStandardMaterial color={hovered ? 'hotpink' : 'orange'} />
    </mesh>
  )
}

ReactDOM.render(
  <Canvas>
    <ambientLight />
    <pointLight position={[10, 10, 10]} />
    <Box position={[-1.2, 0, 0]} />
    <Box position={[1.2, 0, 0]} />
  </Canvas>,
  document.getElementById('root'),
)

这没啥好说的,光是看到这个简洁的代码,就一个字:“爽!”

我简单使用下,还是挺香的。

但是,恶心的事来了,我的业务使用的架构是umi,它会统一对props.children做处理。它对react-three/fiber,解析处理会失败!!!

在座的各位大佬,有相关的解决思路没有? help

结束语

新年快乐

其他文章:

3D抽奖页面(threejs)

自定义编辑3D房间工具(一) 画线成墙

自定义编辑3D房间工具(二) 添加门等新模块、模块选中及删除

three.js 粒子组成字体