课程链接:www.bilibili.com/cheese/play…
课程目标
- 搭建vue3项目
- 搭建three.js 渲染环境
1-创建一个 Vue 项目
选择vue3,只是因为最近用vue3 比较多。若大家喜欢react,也可以用react 。
安装最新版本的 Node.js,创建一个 Vue 项目。
npm create vue@latest
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。
你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示,记得在“Add TypeScript?” 后面选yes。其它的功能无所谓。
大家可以从我的github 上下载项目。
2-搭建three.js 环境
1.安装three.js 及其ts 依赖
npm i three @types/three
2.在项目的src 中建立一个robot 文件夹,用于放置机器人相关的类。
3.在robot 文件夹中建立ResourceTracker.ts 文件,用于清理webgl 缓存。
其中的具体原理可看此文章:WebGL的内存泄露与清理
- src/robot/ResourceTracker.ts
import { BufferGeometry, Material, Mesh, Object3D, Texture } from "three";
type SourceType = BufferGeometry | Material | Texture | Object3D;
class ResourceTracker {
resources: Set<SourceType>;
constructor() {
this.resources = new Set();
}
track(resource: SourceType | Material[] | Object3D[]) {
if (Array.isArray(resource)) {
for (let child of resource) {
this.track(child);
}
} else {
this.resources.add(resource);
if (resource instanceof Material) {
for (const value of Object.entries(resource)) {
if (value instanceof Texture) {
this.track(value);
}
}
if ("uniforms" in resource) {
for (const uniform of Object.values(resource.uniforms as object)) {
if (!uniform) {
continue;
}
const uniformValue = uniform.value;
if (
uniformValue instanceof Texture ||
Array.isArray(uniformValue)
) {
this.track(uniformValue);
}
}
}
}
if (resource instanceof Object3D) {
if (resource instanceof Mesh) {
this.track(resource.geometry);
this.track(resource.material);
}
this.track(resource.children);
}
}
return resource;
}
untrack(resource: SourceType) {
this.resources.delete(resource);
}
dispose() {
for (const resource of this.resources) {
if (resource instanceof Object3D) {
resource.parent?.remove(resource);
} else {
resource.dispose();
}
}
this.resources.clear();
}
}
export { ResourceTracker };
4.在robot 文件夹中建立RobotVisual.ts 文件,用于搭建three.js 渲染环境。
import {
Color,
DirectionalLight,
EquirectangularReflectionMapping,
EventDispatcher,
Fog,
GridHelper,
Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer,
OrthographicCamera,
MeshBasicMaterial,
ShadowMaterial,
} from 'three'
import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { ResourceTracker } from './ResourceTracker'
/* 机器可视化类 */
class RobotVisual extends EventDispatcher<any> {
renderer = new WebGLRenderer({
antialias: true,
logarithmicDepthBuffer: true,
})
scene = new Scene()
camera: OrthographicCamera | PerspectiveCamera = new PerspectiveCamera(
45,
1,
0.01,
200,
)
orbitControls = new OrbitControls(this.camera, this.renderer.domElement)
continuousFrame = 0
resourceTracker = new ResourceTracker()
constructor(hdrURL?: string) {
super()
const { renderer, scene, resourceTracker, orbitControls, camera } = this
// 适应屏幕分辨率
renderer.setPixelRatio(window.devicePixelRatio)
// 投影
renderer.shadowMap.enabled = true
// 场景背景色
scene.background = new Color(0xf6f6f8)
// 场景雾效
scene.fog = new Fog(0xf6f6f8, 20, 50)
// 环境光
hdrURL &&
new HDRLoader().loadAsync(hdrURL).then((texture) => {
texture.mapping = EquirectangularReflectionMapping
scene.environment = texture
resourceTracker.track(texture)
})
// 灯光
const light = new DirectionalLight(0xffffff, 1)
light.position.set(0, 10, 5)
light.castShadow = true
scene.add(light)
resourceTracker.track(light)
// 地面网格
const floorGrid = new GridHelper(100, 100, 0x9c9aa5, 0xbcbac7)
scene.add(floorGrid)
resourceTracker.track(floorGrid)
// 地面Geometry
const floorGeometry = new PlaneGeometry(100, 100)
// 地面阴影
const floorShadowMaterial = new ShadowMaterial({
transparent: true,
opacity: 0.1,
})
const floorShadowMesh = new Mesh(floorGeometry, floorShadowMaterial)
floorShadowMesh.rotateX(-Math.PI / 2)
floorShadowMesh.receiveShadow = true
scene.add(floorShadowMesh)
resourceTracker.track(floorShadowMesh)
// 地面
const floorMaterial = new MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.5,
})
const floorMesh = new Mesh(floorGeometry, floorMaterial)
floorMesh.rotateX(-Math.PI / 2)
floorMesh.position.y = -0.005
scene.add(floorMesh)
resourceTracker.track(floorMesh)
// 设置镜头
camera.position.set(0, 1.2, 3.6)
orbitControls.target.set(0, 0.8, 0)
orbitControls.update()
}
// 加载URDF模型
loadURDF(url: string) {}
// 响应式布局
resize(width: number, height: number) {
const { renderer, camera } = this
if (camera instanceof OrthographicCamera) {
const halfWidth = (camera.top * width) / height
camera.left = -halfWidth
camera.right = halfWidth
} else {
camera.aspect = width / height
}
camera.updateProjectionMatrix()
renderer.setSize(width, height, true)
}
// 渲染
render() {
const { renderer, scene, camera } = this
renderer.render(scene, camera)
}
// 连续渲染
continuousRender() {
this.render()
this.continuousFrame = requestAnimationFrame(
this.continuousRender.bind(this),
)
}
// 清理数据
dispose() {
this.resourceTracker.dispose()
this.renderer.dispose()
this.orbitControls.dispose()
this.renderer.domElement.remove();
cancelAnimationFrame(this.continuousFrame)
}
}
export { RobotVisual }
5.在App.vue 中测试three.js 场景。
其中用到了一张hdr 贴图venice_sunset_1k.hdr,大家可以从我的github中下载课件。
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { RobotVisual } from "./robot/RobotVisual";
/* canvas 画布的Ref对象 */
const canvasWrapperRef = ref<HTMLDivElement>();
/* 机器人可视化 */
const hdrURL = "/texture/venice_sunset_1k.hdr";
let robotVisual = new RobotVisual(hdrURL);
robotVisual.continuousRender();
/* 自适应窗口尺寸 */
window.addEventListener("resize", onResize);
function onResize() {
const canvasWrapper = canvasWrapperRef.value;
canvasWrapper&&robotVisual.resize(canvasWrapper.clientWidth, canvasWrapper.clientHeight);
}
onMounted(() => {
onResize();
const canvasWrapper = canvasWrapperRef.value;
canvasWrapper && canvasWrapper.append(robotVisual.renderer.domElement);
});
onUnmounted(() => {
window.removeEventListener("resize", onResize);
robotVisual.dispose();
});
</script>
<template>
<div id="robotVisual">
<div id="cont">
<div id="canvasWrapper" ref="canvasWrapperRef"></div>
</div>
</div>
</template>
<style scoped>
#robotVisual {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
#cont {
display: flex;
flex: 1;
font-size: 14px;
color: #303133;
overflow: hidden;
}
#canvasWrapper {
flex: 1;
position: relative;
height: 100%;
overflow: hidden;
}
</style>
6.使canvas 画布充满窗口。
- src/style.css
html {
height: 100%;
}
body {
margin: 0;
height: 100%;
}
#app {
height: 100%;
}
- src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'
createApp(App).mount('#app')
效果如下:
总结
这节课我们完成了vue3+three.js 项目的初始搭建,下节课我们会说一下urdf 的解析方法。