初始项目的搭建

0 阅读3分钟

课程链接:www.bilibili.com/cheese/play…

代码链接:github.com/buglas/robo…

课程目标

  • 搭建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')

效果如下:

image-20260525084009247.png

总结

这节课我们完成了vue3+three.js 项目的初始搭建,下节课我们会说一下urdf 的解析方法。