three.js 作为一个知名 JavaScript 的 3D 模型加载库,当我们想要在 web 中展示模型就可以尝试使用一下,接下来我将梳理一遍我尝试在 vue3 中使用 three.js 来进行模型的加载,本博客中的模型来自于 @zixisun02 提供的免费 Shiba 模型 基于 CC-BY-4.0 许可引用,这里下载的模型文件为 gltf。

首先前期准备工作当然是先通过 npm 安装 three.js 和 @types/three (由于项目使用 typescript 进行开发),随后将下载好的模型解压拷贝至 public 目录下

npm install three
npm install -D @types/three

首先我们需要创建一个 div 容器用于挂载 three.js

/*
 * @Author: Bin
 * @Date: 2024-03-09
 * @FilePath: /three_vue/package/ThreeComponent.tsx
 */
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';

export default defineComponent({
    setup(props, ctx) {
        const containerRef = ref<HTMLElement>();

        onMounted(() => {

        });

        onUnmounted(() => { });

        return () => {
            return <div {...props} ref={containerRef}></div>;
        };
    },
});

渲染模型

接下来定义一个 ThreeController 用于定义后续对模型的控制器,定义一个 loadThreeContainer 函数,将在 loadThreeContainer 中具体通过调用 three 对 gltf 模型文件进行加载。

import * as THREE from 'three';
import WebGL from 'three/examples/jsm/capabilities/WebGL';
import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

type ThreeController = {
};

function loadThreeContainer(el: HTMLElement): ThreeController {
    const containerConf: { width: number; height: number } = {
        width: (el.parentElement?.clientWidth || 300) * 0.8,
        height: (el.parentElement?.clientWidth || 300) * 0.8,
    };


    // 初始化 WebGLRenderer 渲染器
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(containerConf.width * 0.6, containerConf.height * 0.6);
    renderer.setClearColor(0xeeeeee, 0.0);

    // 初始化 Scene 场景
    const scene = new THREE.Scene();
    const pmremGenerator = new THREE.PMREMGenerator(renderer);
    scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.01).texture;

    // 构造一个 PerspectiveCamera 相机
    const camera = new THREE.PerspectiveCamera(6, containerConf.width / containerConf.height, 0.1, 1000);
    camera.position.z = 150;
    camera.position.y = 0;

    // 添加辅助线
    const axesHelper = new THREE.AxesHelper(5);
    scene.add(axesHelper);

    // loader model
    let model: THREE.Group;
    const loader = new GLTFLoader();
    loader.load(
        'shiba/scene.gltf',
        function (gltf) {
            // 将模型加载到场景上
            model = gltf.scene;
            model.scale.set(5, 5, 5);
            scene.add(model);

            // 判断当前环境是否支持 WebGL
            if (WebGL.isWebGLAvailable()) {
                // animate(); // 渲染场景,执行动画
                renderer.render(scene, camera);
                el.appendChild(renderer.domElement);
            } else {
                // 渲染错误信息
                const warning = WebGL.getWebGLErrorMessage();
                el.appendChild(warning);
            }
        },
        undefined,
        function (err) {
            console.error(err);
        },
    );

    return {}
}

在 loadThreeContainer 函数中接收一个 HTMLElement 类型的参数作为模型渲染的容器,首先通过容器获取到容器的宽高,接着初始化一个 WebGLRenderer 渲染器,设置渲染器的像素比为设备的像素比,将渲染器的宽高设置为容器的 60% 并且设置容器背景颜色为透明。

接着通过 THREE.Scene 加载一个场景,这里使用了一个 RoomEnvironment 对场景进行材质的配置。通过 THREE.PerspectiveCamera 构造一个相机,并且配置好相机的位置和画布等信息。使用 GLTFLoader 加载器将 gltf 模型文件进行一个加载,在 onLoad 函数中就能够获取到 GLTF 对象,通过 GLTF.scene 获取到模型对象,通过调用 scene.add 函数将模型添加到环境场景上,最后通过 WebGL.isWebGLAvailable 函数判断当前运行环境是否支持 WebGL 不支持则显示错误信息,否则通过调用 renderer.render 函数将环境场景和相机加载到渲染器中,最后将 renderer.domElement 挂载到容器里,到这里我们就简单的将一个模型加载到网页上了,目前效果应该如下。

three.js 使用之 Vue3 Component 封装-天真的小窝

添加 OrbitControls 控制模型旋转交互

通过添加一个控制器,实现通过滑动屏幕控制模型旋转。

type ThreeController = {
    unload: () => void
};

function loadThreeContainer(el: HTMLElement): ThreeController {
    ……

    // 初始化 OrbitControls 控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 0.5, 0);
    controls.enablePan = false;
    controls.enableDamping = true;
    controls.enableRotate = true;
    controls.enableZoom = false;

    // loader model
    let model: THREE.Group;
    const loader = new GLTFLoader();
    loader.load(
        'shiba/scene.gltf',
        function (gltf) {
            ……
            // 判断当前环境是否支持 WebGL
            if (WebGL.isWebGLAvailable()) {
                animate(); // 渲染场景,执行动画
                renderer.render(scene, camera);
                el.appendChild(renderer.domElement);
            } else {
                // 渲染错误信息
                const warning = WebGL.getWebGLErrorMessage();
                el.appendChild(warning);
            }
            ……
        },
        undefined,
        function (err) {
            console.error(err);
        },
    );

    // 动画任务 id
    let animationTaskID: number = 0;

    // 执行动画函数
    function animate() {
        if (animationTaskID) {
            cancelAnimationFrame(animationTaskID);
        }
        animationTaskID = requestAnimationFrame(animate);

        controls.update();
        renderer.render(scene, camera);
    }

    function unload() {
        if(animationTaskID) {
            cancelAnimationFrame(animationTaskID);
        }
    }

    return {
        unload,
    }
}

通过构造一个 OrbitControls 控制器,并且配置相关控制属性(相关文档),增加一个 animate 函数,利用 requestAnimationFrame api 创建一个帧动画任务,在 animate 函数中对控制器进行更新以及刷新渲染器,当然在 animate 函数中我们还可以进行更多的动画配置,最后声明一个 unload 用于在销毁组件时释放动画任务或者进行其他模型相关的卸载任务。

完成这一步之后模型应该可以通过滑动鼠标或者屏幕进行旋转控制了,但是现在模型是允许 X 和 Y 轴随意旋转的,那么如何禁用 Y 轴旋转呢?

// 调整控制器不允许竖轴旋转  https://threejs.org/docs/#examples/zh/controls/OrbitControls.enableRotate
controls.minPolarAngle = 1.5;
controls.maxPolarAngle = 0;

最后创建一个 Vue Component 将模型加载进去就基本可以啦。

import { defineComponent, ref, onMounted, onUnmounted } from 'vue';

……

export default defineComponent({
    setup(props, ctx) {
        const containerRef = ref<HTMLElement>();
        const threeCtlRef = ref<ThreeController>();

        onMounted(() => {
            threeCtlRef.value = loadThreeContainer(containerRef.value!)
        });

        onUnmounted(() => {
            threeCtlRef.value?.unload && threeCtlRef.value.unload()
        });

        return () => {
            return <div {...props} ref={containerRef}></div>;
        };
    },
});

好了,今天的博客就先写到这里啦我们下期见,要想用于实际项目的话这才只是一个开始,要根据业务需求去做的话还有很多需要去探索的,我在业务上就尝试了自动旋转动画,动态替换模型材质…