从Blender建模到React Hooks + ThreeJS + TS前端交互展示流程全记录

1,380 阅读4分钟

github 的地址 欢迎 star!

1 下载Blender到我们的电脑

为什么选择Blender作为我的初学建模软件呢?因为这是一款功能强大且支持建模工作全流程的免费开源软件,个人使用了一段时间感觉还是非常不错的安利给大家 ~ 下载地址截屏2023-03-30 18.56.38.png 进入到官网首页后点击按钮 Download 下载完成后安装到电脑即可。

2 开始我们的Blender建模

本人也是新人关于Blender的使用也在慢慢学习熟悉中。这里推荐来自Blendergo海龙老师的课程讲的非常的不错哦。接下来我们也会用到里面教我们做的萌三兄弟建模后导出的glb文件。教程地址: 八个案例教程带你从0到1入门blender【已完结】

2.1 建模完毕

这是我跟着做完的,做的不是很好看大家不要介意哈。注意图片右上角我将三兄弟合并为一个物体角色,物体命名记得和下面代码命名保持一致哦。 截屏2023-03-30 19.09.08.png

2.2 导出glb文件

截屏2023-03-30 19.09.44.jpg 我们将其命名为 threeCuteBrothers.glb

3 搭建前端项目

3.1 使用create-react-app脚手架搭建前端项目

在VS Code终端输入如下命令,这里我们选择的是TS的模版(记得先配置好电脑的node环境)。

create-react-app 【your app name】--template typescript 

截屏2023-03-30 20.11.00.png 执行完命令我们等待一下就好了。

3.2 运行项目

我们打开VS Code进入我们刚刚使用脚手架生成的项目。不出意外你可以看到如下这样的代码文件结构。 截屏2023-03-30 20.16.19.png OK 我们执行 npm run start / yarn start 运行一下我们的项目。不出意外你可以在浏览器看到这样一个网页。 截屏2023-03-30 20.19.52.png

3.3 删除无用代码。

在src文件夹下我们只保留如下三个文件代码。

  1. src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
  1. src/App.tsx
import React from 'react';

const App = () => {
  return (
    <div>萌三兄弟</div>
  );
}

export default App;
  1. src/index.css
body {
  margin: 0;
  padding: 0;
}

3.4 安装接下来需要用到的代码库

安装 three.js,three.js类型库 和 lil-gui

yarn add three @types/three lil-gui

4 编码时刻

4.1 资源准备

我们把导出的threeCubeBrothers.glb 文件放在src同级目录下 public/glb/threeCubeBrothers.glb 将draco模型压缩工具文件放在 public/draco里。

4.2 主页面代码

src/App.tsx

import React, { FC, useCallback, useEffect, useRef } from "react";
import * as THREE from 'three';
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { useLilGui } from "./hooks";
import './index.css';

type Params = {
  angleIndex: number,
  axisOfRotation: number,
  lightIndex: number,
}

const params: Params = {
  angleIndex: 0.005,
  axisOfRotation: 1,
  lightIndex: 0.5,
};

// 场景
const scene = new THREE.Scene();
// 渲染器
const renderer = new THREE.WebGLRenderer({ alpha: true })
// 正投影 相机
const camera = new THREE.OrthographicCamera();
// 环境光
let ambient = new THREE.AmbientLight(0xffffff, params.lightIndex)
scene.add(ambient)
// 方向光
const directionalLight = new THREE.DirectionalLight(0xffffff)
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.1;
directionalLight.shadow.camera.far = 80;
directionalLight.shadow.normalBias = 0.05;
directionalLight.position.set(-20, 0, 2);
scene.add(directionalLight);
// 萌三兄弟对象
let threeCuteBrothers: any = null;
// 加载萌三兄弟glb模型文件
const gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
// draco是谷歌出的一款模型压缩工具,可以将glb/gltf格式的模型进行压缩用以提高页面加载速度。
dracoLoader.setDecoderPath('/draco/');
gltfLoader.setDRACOLoader(dracoLoader);
gltfLoader.load('/glb/3CBros.glb', (glb) => {
  glb.scene.children.forEach(item => {
    item.castShadow = true;
    item.receiveShadow = true;
    // 注意模型内部的命名要与这里代码的命名保持一致哦, 我在Blender里把三个模型合成里一个模型。
    if (item.name === 'threeCuteBrothers') threeCuteBrothers = item;
  })
  scene.add(glb.scene)
})
// 坐标轴助手
const axes = new THREE.AxesHelper( 6 );
scene.add(axes);
// 网格助手
const gridHelper = new THREE.GridHelper( 10, 10 );
gridHelper.rotateX( Math.PI );
scene.add( gridHelper );
// 轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);

const App: FC = () => {
  const threeRef = useRef<HTMLDivElement>(null);
  const [isload, guiEntity, destroyEntity] = useLilGui('Modify Model Parameters');
  const timer = useRef<number>(0);

  const initSize = useCallback(() => {
    const _threeRef = threeRef.current;
    let width = _threeRef?.offsetWidth || 0;
    let height = _threeRef?.offsetHeight || 0;
    let aspect = width / height;
    let frustrum = 10;
    let pixelRatio = Math.min(window.devicePixelRatio, 3);
    camera.left = (-aspect * frustrum) / 2;
    camera.right = (aspect * frustrum) / 2;
    camera.top = frustrum / 2;
    camera.bottom = -frustrum / 2;
    camera.position.set(-20, 4, 0);
    // threejs会重新计算相机对象的投影矩阵值。无论正投影相机还是投影投影相机对象的.near和.far属性变化,都需要手动更新相机对象的投影矩阵。
    camera.updateProjectionMatrix();
    renderer.setSize(width, height);
    renderer.setPixelRatio(pixelRatio);
  }, [])

  const updateParams = (e: number, type: keyof Params) => params[type] = e;

  // 添加 Gui 调参可选项
  const addParametersForGui = useCallback(() => {
    const aboutAxis =  guiEntity.addFolder('Axis');
    aboutAxis?.add(params, 'axisOfRotation').min(0).max(2).step(1).onFinishChange((e: number) => updateParams(e, 'axisOfRotation'));
    const aboutIndex =  guiEntity.addFolder('Index');
    aboutIndex?.add(params, 'angleIndex').min(-0.2).max(0.2).step(0.005).onFinishChange((e: number) => updateParams(e, 'angleIndex'));
    aboutIndex?.add(params, 'lightIndex').min(0).max(1).step(0.1).onFinishChange((e: number) => updateParams(e, 'lightIndex'));
  }, [guiEntity])

  useEffect(() => {
    if (!isload) return;
    addParametersForGui();
    return () => {
      // 销毁 Gui 实例
      destroyEntity();
    }
  }, [addParametersForGui, destroyEntity, isload])

  const animate = useCallback(() => {
    timer.current = requestAnimationFrame(() => {
      // 修改模型的旋转轴
      let axis = new THREE.Vector3(1, 0, 0);
      if (params.axisOfRotation === 1) axis = new THREE.Vector3(0, 1, 0);
      if (params.axisOfRotation === 2) axis = new THREE.Vector3(0, 0, 1);
      // Quaternion四元数方法修改模型每帧旋转的角度
      const qInitial = new THREE.Quaternion().setFromAxisAngle( axis, params.angleIndex );
      threeCuteBrothers?.applyQuaternion(qInitial)
      // 修改环境光照的亮度
      scene.remove(ambient)
      ambient = new THREE.AmbientLight(0xffffff, params.lightIndex)
      scene.add(ambient)
      
      controls.update();
      renderer.render(scene, camera);
      animate()
    })
  }, [])

  useEffect(() => {
    initSize();
    threeRef.current?.appendChild(renderer.domElement);
    animate();
    return () => {
      cancelAnimationFrame(timer.current);
    }
  }, [animate, initSize])

  return (
    <div className='container' ref={threeRef} />
  )
}

export default App;

/src/index.css

body {
  margin: 0;
  padding: 0;
  --bg-color: #E0DADA;
}
.container {
  background-color: var(--bg-color);
  position: absolute;
  height: 100%;
  width: 100%;
}

4.3 自定义hooks useLilGui 编写

/src/use-lil-gui.ts

import * as lilGui from 'lil-gui';
import { useEffect, useRef, useState } from 'react';

const useLilGui = (title: string = 'Title') => {
  const GUI = useRef<any>(null);
  const isload = useRef<boolean>(false);
  const [guiEntity, setGuiEntity] = useState<any>(null)
  
  useEffect(() => {
    if (isload.current) return;
    isload.current = true;
    GUI.current = new lilGui.GUI({ title });
    const lilGuiStyle: any = document.getElementsByClassName('lil-gui')[0];
    lilGuiStyle.style.right = '0';
    GUI.current.close();
    setGuiEntity(GUI.current);
  }, [title])

  const destroyEntity: any = () => {
    setGuiEntity(null);
    GUI.current?.destroy();
    GUI.current = null;
    isload.current = false;
  }

  return [isload.current, guiEntity, destroyEntity]
}

export default useLilGui;

/src/hooks/index.ts

import useLilGui from './use-lil-gui';

export {
  useLilGui
}

不出意外的话打开浏览器你将看到这个页面。 3月30日.gif ok 至此我们的编码过程算是结束了。

5 该项目已经放到github上,需要的朋友自取哦【地址】同时也欢迎star哈!觉得我写的还不错的希望可以给这个博文一键三连哈后期继续分享更多编程知识。