首页 前端知识 【Threejs进阶教程-优化篇】4.Vue/React与threejs如何解决冲突和卡顿(续)

【Threejs进阶教程-优化篇】4.Vue/React与threejs如何解决冲突和卡顿(续)

2024-08-04 22:08:56 前端知识 前端哥 228 58 我要收藏

Vue/React与threejs如何解决冲突和卡顿-续

  • 使用说明
  • 核心思路
  • 环境搭建(vue+three)
  • vue运行机制分析
  • 业务分离
    • 使用threejs做背景
  • 3D模块封装
    • 使用ES6的Class来让逻辑性更强
    • Threejs尽量按需引入
    • 创建一个类
    • 扩展写法
    • 本次代码执行顺序
  • 扩展内容
    • 添加orbitControls和辅助线
    • 解决事件覆盖
  • 与Vue交互
    • 添加操作代码
    • 绑定ThreeCore到vue原型或window上
    • 在vue中调用函数
  • 协同开发
  • 全框架可行性说明
  • 源码文件已上传,还有有不懂的问题可以在下方留言

上一篇地址
不少人反映,上一篇中讲解的不够详细,或者难以看懂,这一篇我们从0开始来搭建一个Vue+Three的项目

使用说明

  1. 本方案适用于任何条件下任何框架,不受vue,react,angular等版本限制
  2. 本文着重讲解思路,部分代码可能不适用于TS
  3. 本篇教程中使用到了ES6的Class知识,对es6不熟的请优先补一下ES6的相关知识
  4. 本人并不熟悉vue3,所以部分写法比较倾向于vue2,这部分代码请各位自行调整

核心思路

  1. 彻底跳出vue的视界,在js层面来解决冲突
  2. vue只用来处理dom,threejs只用来处理画布内容
  3. 单例化并接口化three部分,参考前后端分离的 dom-canvas分离方案

环境搭建(vue+three)

这一部分基本上都是前端基本功,再不行百度一下也行

  1. 安装vue,这个过程本人就不解释了,本篇教程使用vue create vue-three来创建项目,本篇并不是在讲vue,所以vue的细节部分就不多说了,按照自己喜欢的配置即可
  2. 安装任意版本的Threejs, npm i three,本人使用当前最新版166
  3. node版本: 20.15.0
  4. npm版本: 10.8.1
  5. yarn版本: 1.22.22,本人主要使用yarn来安装依赖
  6. vue/cli版本 5.0.8
  7. 其他库版本

下面是本人的初始文件结构和package.json
在这里插入图片描述

vue运行机制分析

在这里插入图片描述
首先,我们现在关注这两个文件,一个是index.html,一个是main.js

任何的js程序,都应该有一个入口程序,而在vue中,入口程序不是 app.vue,而是main.js,我们来分析main.js的代码

createApp() 从字面意思上是创建vueApplication
然后参数中的mount() 用于获取页面中指定id的dom,也就是index.html中的 < div id=‘app’> 的这个div

也就是说,在mainjs中,vue的脚手架只干了一件事,从页面中读取到id为app的div,然后将vue文件编译后,生成dom,并填充这个div

在这里插入图片描述
我们把项目跑起来,用dom检查来检查这个div,内容基本上是由helloworld.vue文件提供

也就是说,vue其实并没有脱离html + css + js这个系统,而是用自己的系统单独在处理一个div

业务分离

既然是这样,那我们就可以完全视为原生的方式来开发,避免vue系统与threejs出现冲突

在这里插入图片描述
我们在main.js中,加入threejs的Helloworld的代码

//main.js
import { createApp } from 'vue'
import App from './App.vue'
import * as THREE from "three";

createApp(App).mount('#app')


let scene = new THREE.Scene()

let camera = new THREE.PerspectiveCamera(50,window.innerWidth/window.innerHeight,0.1,2000);
camera.position.z += 5;
let renderer = new THREE.WebGLRenderer({
    alpha:true
});
renderer.setSize(window.innerWidth,window.innerHeight);
document.body.appendChild(renderer.domElement);

let geometry = new THREE.BoxGeometry(1,1,1);
let material = new THREE.MeshBasicMaterial({
    color:0xff0000
});
let mesh = new THREE.Mesh(geometry,material);
scene.add(mesh);


render();
function render() {
    renderer.render(scene,camera);
    requestAnimationFrame(render);
    mesh.rotation.x += 0.01;
    mesh.rotation.y += 0.01;
}

此时,我们发现,页面上出现了画布,也出现了跳动的方块,但是,位置不对
在这里插入图片描述

使用threejs做背景

这个问题非常简单,让app和threejs的内容均设定为absolute定位,然后threejs的canvas层级比id为app的div低即可
在这里插入图片描述
在app.vue中添加

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  position: absolute;/*修改app的position*/
  z-index: 10;/*建议大于10*/
}

在main.js中添加,追加位置如图所示

//追加代码
renderer.domElement.style.position = "absolute";
renderer.domElement.style.top = "0";
renderer.domElement.style.left = "0";


//追加自适应代码
//自适应代码
window.addEventListener('resize',()=>{
    renderer.setSize(window.innerWidth,window.innerHeight);
    camera.aspect = window.innerWidth/window.innerHeight;
    camera.updateProjectionMatrix();
});

在这里插入图片描述
这时候再来看效果,基本上就已经做到了基本的融合了

3D模块封装

我们总不可能把所有的代码都挤到main.js中,一般来讲需要一个单独的文件来编写Threejs的部分代码,这里我们命名为Threecore

在src下新建一个文件夹 Threecore,新建一个js文件 Threecore.js
此时的文件结构
在这里插入图片描述

使用ES6的Class来让逻辑性更强

这里我们先上改完后的代码

//ThreeCore.js
import {
    Scene, WebGLRenderer,Mesh,
    PerspectiveCamera, BoxGeometry, MeshBasicMaterial
} from "three";


export default class ThreeCore {

    scene = new Scene();

    renderer = new WebGLRenderer({
        alpha:true
    });

    camera = new PerspectiveCamera(
        50,window.innerWidth/window.innerHeight,0.1,2000
    );

    /**
     * 构造函数,在new的时候会执行
     */
    constructor() {
        this.init();
        this.addMesh();
    }

    /**
     * 初始化代码
     */
    init(){
        this.camera.position.z = 5;
        this.renderer.setSize(window.innerWidth,window.innerHeight);
        this.renderer.domElement.style.position = "absolute";
        this.renderer.domElement.style.top = "0";
        this.renderer.domElement.style.left = "0";
        document.body.appendChild(this.renderer.domElement);
    }

    /**
     * 添加物体代码
     */
    addMesh(){
        let geometry = new BoxGeometry(1,1,1);
        let material = new MeshBasicMaterial({
            color:0xff0000
        });
        this.mesh = new Mesh(geometry,material);
        this.scene.add(this.mesh);
    }

    /**
     * 重置画布大小
     */
    resize = ()=>{
        this.renderer.setSize(window.innerWidth,window.innerHeight);
        this.camera.aspect = window.innerWidth/window.innerHeight;
        this.camera.updateProjectionMatrix();
    }

    /**
     * 渲染函数,这里本人为了干净和逻辑整洁,把requestAnimationFrame写到了main.js中
     */
    render = ()=>{
        this.renderer.render(this.scene,this.camera);
        //编码习惯,使用前判定是否为null,这里的执行频率很高,可能会导致大量报错刷屏
        if(this.mesh){
            this.mesh.rotation.x += 0.01;
            this.mesh.rotation.y += 0.01;
        }
    }
}
//main.js

import { createApp } from 'vue'
import App from './App.vue'
import ThreeCore from "@/ThreeCore/ThreeCore";


createApp(App).mount('#app')

let threeCore = new ThreeCore();

render();

function render() {
    threeCore.render();
    requestAnimationFrame(render);
}

Threejs尽量按需引入

在这里插入图片描述
Threejs本身文件很大,按需引入可以一定程度上降低打包出来的js文件的大小,所以我们在新的写法中做了按需引入

创建一个类

export default class ThreeCore{} 有了这一行之后,在main.js中,就可以

import ThreeCore from “@/ThreeCore/ThreeCore”;

并new出来

let threeCore = new ThreeCore();

new的时候,class系统会自动执行 constructor()函数,这个是类的功能

在类中,只能编写key和value这样的键值对,而不能直接编写代码,所以我们的操作代码都被归结在constructor函数中,并且用了**init()**来对逻辑进行区分,表示这一部分属于初始化阶段执行的代码,后续无需再次执行

扩展写法

当然,我们也可以把addMesh()写到main.js中,写法为:

threeCore.addMesh();

用new出来的实例,去调用它下面的函数addMesh()

本次代码执行顺序

//main.js
let threeCore = new ThreeCore()

//threecore.js
ThreeCore.constructor();
ThreeCore.init();
ThreeCore.addMesh();

//main.js
render();

扩展内容

添加orbitControls和辅助线

//ThreeCore
import {
    Scene, WebGLRenderer, Mesh,
    PerspectiveCamera, BoxGeometry, MeshBasicMaterial, GridHelper
} from "three";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";


export default class ThreeCore {

    scene = new Scene();

    renderer = new WebGLRenderer({
        alpha:true
    });

    camera = new PerspectiveCamera(
        50,window.innerWidth/window.innerHeight,0.1,2000
    );

    //可以对controls进行声明,不用第一时间赋值
    controls;

    /**
     * 构造函数,在new的时候会执行
     */
    constructor() {
        this.init();
        this.addHelpers();
        this.addEvent();
        this.addMesh();
    }

    /**
     * 初始化代码
     */
    init = ()=>{
        //调整相机位置
        this.camera.position.set(10,10,10);
        this.renderer.setSize(window.innerWidth,window.innerHeight);
        this.renderer.domElement.style.position = "absolute";
        this.renderer.domElement.style.top = "0";
        this.renderer.domElement.style.left = "0";
        document.body.appendChild(this.renderer.domElement);

        //个人习惯,喜欢把orbitControls写到init中
        this.controls = new OrbitControls(this.camera,this.renderer.domElement);
        this.controls.enableDamping = true;//开启阻尼效果
    }

    addHelpers(){
        let gridHelper = new GridHelper(10,10);
        this.scene.add(gridHelper);
        
    }

    addEvent = ()=>{
        window.addEventListener('resize',this.resize);
    }

    /**
     * 添加物体代码
     */
    addMesh = ()=>{
        let geometry = new BoxGeometry(1,1,1);
        let material = new MeshBasicMaterial({
            color:0xff0000
        });
        this.mesh = new Mesh(geometry,material);
        this.scene.add(this.mesh);
    }

    /**
     * 重置画布大小
     */
    resize = ()=>{
        this.renderer.setSize(window.innerWidth,window.innerHeight);
        this.camera.aspect = window.innerWidth/window.innerHeight;
        this.camera.updateProjectionMatrix();
    }

    /**
     * 渲染函数,这里本人为了干净和逻辑整洁,把requestAnimationFrame写到了main.js中
     */
    render = ()=>{
        this.renderer.render(this.scene,this.camera);
        //编码习惯,使用前判定是否为null,这里的执行频率很高,可能会导致大量报错刷屏
        if(this.mesh){
            this.mesh.rotation.x += 0.01;
            this.mesh.rotation.y += 0.01;
        }
        if(this.controls){
            this.controls.update();
        }
    }

}

两个新增的内容应该不用怎么解释了吧,页面效果

在这里插入图片描述
这里vue的dom跑到左边,是因为前面把定位改成了absolute,现在我们把位置调好
给app设定为100vw和100vh的宽高即可

/* app.vue的css部分 */
#app {
  width: 100vw;
  height: 100vh;
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  position: absolute;/*修改app的position*/
  z-index: 10;/*建议大于10*/
}

解决事件覆盖

在常规的3d项目中,ui的部分一般都是全屏,很容易把画布的事件覆盖了,所以我们在最顶层的dom层级中,设定它的事件为无,这样所有它下面的事件都会变成无
pointer-events:none;

这个样式代码,一般是这样,只看父级
你的父组件是none,则子组件也是none,
你的爷爷组件是none,父组件是auto,那么子组件也是auto
如果爷爷组件是auto,父组件是none,那么子组件也是none
所以用这个样式来处理顶级dom后,记得处理子级dom的pointerEvents

/* app.vue中的css的代码 */
#app {
  width: 100vw;
  height: 100vh;
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  position: absolute;/*修改app的position*/
  z-index: 10;/*建议大于10*/
  pointer-events: none;
}
  #app>*{
    pointer-events: auto;
  }

与Vue交互

在这里插入图片描述

//ThreeCore.js追加代码
    changeBoxColor = ()=>{
        //注意,这里必须要引入 Three里面的Color
        //不要引入错了,类型错误的话也会报错
        this.mesh.material.color = new Color(0xffffff * Math.random())
    }
//HelloWrold.vue重写代码
<template>
  <div class="hello">
    <div @click="changeColor">变色</div>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  methods:{
    changeColor(){
      console.log(this);
      window.threeCore.changeBoxColor();
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

//main.js重写代码
import { createApp } from 'vue'
import App from './App.vue'
import ThreeCore from "@/ThreeCore/ThreeCore";

createApp(App).mount('#app')
window.threeCore = new ThreeCore();

render();

function render() {
    window.threeCore.render();
    requestAnimationFrame(render);
}

添加操作代码

我们只需要在ThreeCore下写一个函数即可
这里我们使用最简单的变色操作来演示

绑定ThreeCore到vue原型或window上

在vue2中,可以向原型上绑定一个对象,但是在vue3中,这个方案似乎不是很好用,本人并不是专门开发前端的,所以对vue3并不熟悉,所以绑定到原型的方式,就交给各位前端朋友了

最简单暴力的方式,就是绑定到 window上,虽然有被别人阅读源码和分析结构的风险,但是threejs本身高门槛,哪怕我代码放出来,你反编译了,你没有Threejs的基础也看不懂

window.threeCore = new ThreeCore();

在window上的对象,在你的程序的任何地方都可以直接调用,相当于一个全局形式的函数

在vue中调用函数

由于本人并不了解vue3,所以采用了比较旧的vue2的编码风格

    changeColor(){
      console.log(this);
      window.threeCore.changeBoxColor();//直接全局调用即可
    }

在这里插入图片描述

协同开发

通过上面的方式,其实不难发现,我们已经将vue部分和three部分彻底的拆开了,vue只需要负责搞dom,three只负责渲染画布即可
如果你们有两个以上的人,完全可以参考这样的开发模式,一个人纯写threejs,另一个人纯写vue,这样做完全不会有任何的冲突,写threejs部分的人,只需要提供几个方便调用的函数给另一个人,这样可以大幅提高开发效率和合作能力

全框架可行性说明

从代码中,我们是把threejs的部分的代码,绑定到window上,且主入口也在main.js这种分离模式下,就可以看出,同样的react,也适用于这样的开发模式
基本上只要是基于html + css + js的技术的,其实都可以使用这种方式来开发
本质上,这种开发方式是一种原生开发,而非什么基于vue啊,基于react,也不是必须要webpack,rolllup这种环境开发,只要你能拿到three.module.js,只要你能在入口文件编写自己的代码,就完全可以走这种开发模式

源码文件已上传,还有有不懂的问题可以在下方留言

如有其他不懂的问题,可以在下方留言,也可以加入qq群咨询,本人的群于2024/7/8日正式创建,群号867120877,欢迎大家来群里交流
请添加图片描述

转载请注明出处或者链接地址:https://www.qianduange.cn//article/14813.html
标签
threejs
评论
发布的文章

JQuery的选择器有哪些?

2024-08-14 22:08:43

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!