github 的地址 欢迎 star!
1 下载Blender到我们的电脑
为什么选择Blender作为我的初学建模软件呢?因为这是一款功能强大且支持建模工作全流程的免费开源软件,个人使用了一段时间感觉还是非常不错的安利给大家 ~ 下载地址。
进入到官网首页后点击按钮 Download 下载完成后安装到电脑即可。
2 开始我们的Blender建模
本人也是新人关于Blender的使用也在慢慢学习熟悉中。这里推荐来自Blendergo海龙老师的课程讲的非常的不错哦。接下来我们也会用到里面教我们做的萌三兄弟建模后导出的glb文件。教程地址: 八个案例教程带你从0到1入门blender【已完结】
2.1 建模完毕
这是我跟着做完的,做的不是很好看大家不要介意哈。注意图片右上角我将三兄弟合并为一个物体角色,物体命名记得和下面代码命名保持一致哦。
2.2 导出glb文件
我们将其命名为 threeCuteBrothers.glb
3 搭建前端项目
3.1 使用create-react-app脚手架搭建前端项目
在VS Code终端输入如下命令,这里我们选择的是TS的模版(记得先配置好电脑的node环境)。
create-react-app 【your app name】--template typescript
执行完命令我们等待一下就好了。
3.2 运行项目
我们打开VS Code进入我们刚刚使用脚手架生成的项目。不出意外你可以看到如下这样的代码文件结构。
OK 我们执行 npm run start / yarn start 运行一下我们的项目。不出意外你可以在浏览器看到这样一个网页。
3.3 删除无用代码。
在src文件夹下我们只保留如下三个文件代码。
- 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>
);
- src/App.tsx
import React from 'react';
const App = () => {
return (
<div>萌三兄弟</div>
);
}
export default App;
- 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
}
不出意外的话打开浏览器你将看到这个页面。
ok 至此我们的编码过程算是结束了。