Vue3与Three.js的完美结合:3D版demo纯前端实现

发表时间: 2023-10-28 10:55

前言

在网上找了很久都没有找到使用Three.js开发3d的免费文章或者免费视频,自己花了一点时间做了一个纯前端的项目demo。

模型都是在网上免费下载的,没有那么精细美观,请见谅。

此文章只用于想学习three.js的小伙伴做学习用途

技术栈都是最新的:vue3+vite+typeScript+Three+antv G2

有地面版本

无地面版本

开发各种框架的版本 开发工具为vscode

  • "vue": "^3.2.47"
  • "@antv/g2plot": "^2.4.29",
  • "typescript": "^5.0.2",
  • "vite": "^4.3.0",
  • "@types/three": "^0.150.2",

一.搭建three场景

引入three,先初始化场景,相机,渲染器,光线,轨道控制器
先打印一下three 看一下有没有输出,然后在搭建场景等…

<template>  <div class="container" id="container"></div></tempalte><script lang="ts" setup>let scene = null as any,//场景camera = null as any,//相机renderer = null as any,//渲染器controls = null as any//轨道控制器import {onMounted, reactive } from 'vue';import * as THREE from 'three';import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'//设置three的方法const render = async () =>{  //1.创建场景  scene = new THREE.Scene();  //2.创建相机  camera = new THREE.PerspectiveCamera(105,window.innerWidth/window.innerHeight,0.1,1000);  //3.设置相机位置  camera.position.set(0,0,4);  scene.add(camera);  //4.建立3个坐标轴  const axesHelper = new THREE.AxesHelper(5);  scene.add(axesHelper);    //6.设置环境光,要不然模型没有颜色  let ambientLight = new THREE.AmbientLight(); //设置环境光  scene.add(ambientLight); //将环境光添加到场景中  let pointLight = new THREE.PointLight();  pointLight.position.set(200, 200, 200); //设置点光源位置  scene.add(pointLight); //将点光源添加至场景  //7.初始化渲染器  //渲染器透明  renderer = new THREE.WebGLRenderer({    alpha:true,//渲染器透明    antialias:true,//抗锯齿    precision:'highp',//着色器开启高精度  });    //开启HiDPI设置  renderer.setPixelRatio(window.devicePixelRatio);  renderer.setSize(window.innerWidth, window.innerHeight);  //设置渲染器尺寸大小  renderer.setClearColor(0x228b22,0.1);  renderer.setSize(window.innerWidth,window.innerHeight);  //将webgl渲染的canvas内容添加到div  let container = document.getElementById('container') as any;  container.appendChild(renderer.domElement);  //使用渲染器 通过相机将场景渲染出来  renderer.render(scene,camera);  controls = new OrbitControls(camera,renderer.domElement);}const animate = () =>{   requestAnimationFrame(animate);   renderer.render(scene,camera);}onMounted(()=>{  render()  animate()})</script><style scoped>.container{  width:100vw;  height: 100vh;  overflow: hidden;}</style> 

现在我们就看到了three坐标轴了,接下来我们开始导入模型和天空图盒子

二.加载gltf模型

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';//5.导入gltf模型  const gltfLoader = new GLTFLoader();    gltfLoader.load('./model/scene.gltf',function(object){    console.log(object)    scene.add(object.scene);  });

三.加载天空盒子

//1.1 创建天空盒子  const textureCubeLoader = new THREE.CubeTextureLoader();  const textureCube = textureCubeLoader.load([    "../public/img/right.jpg",//右    "../public/img/left.jpg",//左    "../public/img/top.jpg",//上    "../public/img/bottom.jpg",//下    "../public/img/front.jpg",//前    "../public/img/back.jpg",//后  ])  scene.background = textureCube;  scene.environment = textureCube;

现在我们可以看到模型和天空盒子了,接下来我们讲如何给three加文字进去以及触发文字事件

四.加贴图文字

这里我们使用canvas写文字然后转成图片 最后使用three的纹理材质导入到three里面
1.写一个canvas文字
2.canvas转成图片
3.three纹理材质导入图片
4.定位到想要显示的地方

文字显示到three后,使用监听鼠标的方法点击了网页哪里的方法触发事件

let canvas = null as any //文字//创建three文字const threeText = () => {  //用canvas生成图片  canvas = document.getElementById('canvas');  canvas.width = 300  canvas.height = 300  let ctx = canvas.getContext('2d')  //制作矩形  ctx.fillStyle = "rgba(6,7,80,0.8)";  ctx.fillRect(0,0,80,20);  //设置文字  ctx.fillStyle = "#fff";  ctx.font = 'normal 10pt "楷体"'  ctx.fillText('东方明珠', 12.5, 15)  //生成图片  let url = canvas.toDataURL('image/png');  //将图片放到纹理中  let geoMetry1 = new THREE.PlaneGeometry(30,30);  let texture = new THREE.TextureLoader().load(url);  let material1 = new THREE.MeshBasicMaterial({    map:texture,    side:THREE.DoubleSide,    opacity:1,    transparent:true  })  let rect = new THREE.Mesh(geoMetry1,material1)  rect.position.set(10,1,-13)  scene.add(rect)}//触发东方明珠点击事件const threeTextClick = () =>{  window.addEventListener('click',(event)=>{    console.log(event.clientX)    if(event.clientX > 855 && event.clientX < 1022){      alert("触发了点击事件")    }else{return}  })}onMounted(()=>{    threeText()  threeTextClick()})

我们接下来做一个three动态光圈出来

五.做一个three动态光圈

1.先创建一个three的圆柱几何体
2.给几何体加载一个合适的纹理
3.然后让他缓慢变大,重复运动

let cylinderGeometry = null as any//光圈//创建光圈const aperture = () =>{  //创建圆柱  let gemetry = new THREE.CylinderGeometry(1,1,0.2,64);  //加载纹理  let texture = new THREE.TextureLoader().load('../public/img/cheng.png');  texture.wrapS = texture.wrapT = THREE.RepeatWrapping;//每个都重复  texture.repeat.set(1,1);  texture.needsUpdate = true;  let material = [    //圆柱侧面材质,使用纹理贴图    new THREE.MeshBasicMaterial({      map:texture,      side:THREE.DoubleSide,      transparent:true    }),    //圆柱顶材质    new THREE.MeshBasicMaterial({      transparent:true,      opacity:0,      side:THREE.DoubleSide    }),    //圆柱顶材质    new THREE.MeshBasicMaterial({      transparent:true,      opacity:0,      side:THREE.DoubleSide    })  ];  cylinderGeometry = new THREE.Mesh(gemetry,material);  cylinderGeometry.position.set(0,-0.2,1);  scene.add(cylinderGeometry);}onMounted(()=>{  aperture()})

让几合体(光圈)动起来
这个动态方法要放在animate方法里面

let cylinderRadius = 0;let cylinderOpacity = 1;//圆柱光圈扩散动画const cylinderAnimate = () => {  cylinderRadius += 0.01;  cylinderOpacity -= 0.003;  if (cylinderRadius > 1.6) {    cylinderRadius = 0;    cylinderOpacity = 1;  }  if (cylinderGeometry) {    cylinderGeometry.scale.set(1 + cylinderRadius, 1, 1 + cylinderRadius); //圆柱半径增大    cylinderGeometry.material[0].opacity = cylinderOpacity; //圆柱可见度减小  }}const animate = () =>{   cylinderAnimate()   requestAnimationFrame(animate);   renderer.render(scene,camera);}

这样光圈就开始动起来了 3d部分就讲完了
接下来的图表和页面样式

六.图标和头部

App.vue

<template>  <div class="container" id="container">    <header class="header">智慧上海驾驶舱</header>    <section class="leftTop"></section>    <section class="leftCenter"></section>    <section class="leftFooter"></section>    <section class="rightTop"></section>    <section class="rightCenter"></section>    <section class="rightFooter"></section> </div></template><style>.container{  width:100vw;  height: 100vh;  overflow: hidden;}.header{  width: 100vw;  height: 80px;  position: fixed;  top: 0;  text-align: center;  font-size: 28px;  letter-spacing: 4px;  line-height: 65px;  color:#fff;  background-image: url("../public/img/23.png");  background-size: 100% 100%;  background-repeat: no-repeat;}.leftTop{  width: 400px;  height: 310px;  position: fixed;  z-index: 9999999;  top: 40px;  left:20px;  border-radius: 10px;  background-color: rgba(6,7,80,0.6);}.leftCenter{  width: 400px;  height: 310px;  position: fixed;  z-index: 9999999;  top: 370px;  left:20px;  border-radius: 10px;  background-color: rgba(6,7,80,0.6);}.leftFooter{  width: 400px;  height: 210px;  position: fixed;  z-index: 9999999;  top: 700px;  left:20px;  border-radius: 10px;  background-color: rgba(6,7,80,0.6);}.rightTop{  width: 400px;  height: 310px;  position: fixed;  z-index: 9999999;  top: 40px;  right:20px;  border-radius: 10px;  background-color: rgba(6,7,80,0.6);}.rightCenter{  width: 400px;  height: 310px;  position: fixed;  z-index: 9999999;  top: 370px;  right:20px;  border-radius: 10px;  background-color: rgba(6,7,80,0.6);}.rightFooter{  width: 400px;  height: 210px;  position: fixed;  z-index: 9999999;  top: 700px;  right:20px;  border-radius: 10px;  background-color: rgba(6,7,80,0.6);}</style>

效果如下

大致结构我们搭建好了 接下来的步骤
1.我们做几个antv的组件 柱状图 条形图 折线图的组件
2.然后引入到我们刚刚创建好的app.vue 的 div 里面去

在views文件夹里面创建如下图的文件夹

leftCenter文件夹->index.vue的代码

<!--  --><template>  <div class="leftTopModule">    <header class="head">        <titleModule :title="state.titleName"></titleModule>    </header>    <section class="main" id="main"></section>  </div></template><script setup>import { onMounted, reactive } from 'vue'import titleModule from '../../components/title/index.vue'import { Bar } from '@antv/g2plot';const state = reactive({    titleName:'各区GDP'})//创建饼图const createBar = () =>{    //1.创建数据源    const data = [        { year: '浦东新区', sales: 15353 },        { year: '徐汇区', sales: 2176 },        { year: '长宁区', sales: 1561 },        { year: '黄埔区', sales: 2616 },        { year: '普陀区', sales: 1226 },    ]    //2.创建bar对象    const barPlot = new Bar('main',{        data,        xField:'sales',        yField:'year',        colorField:'year',        color:(d)=>{            console.log(d)            if(d.year == '浦东新区') return '#5AD8A6';            if(d.year == '徐汇区') return '#F6BD16';            if(d.year == '长宁区') return '#E86452';            if(d.year == '黄埔区') return '#6DC8EC';            if(d.year == '普陀区') return '#945FB9';        },        xAxis:{            label:{                visible:false,                style:{                    fontSize:17,                }            },            tickLine:{                visible:false            }        },        yAxis:{            label:{                style:{                    fontSize:17,                }            },            grid:{                visible:false            }        },    })    //3.渲染    barPlot.render();}onMounted(()=>{    createBar()})</script><style  scoped>.leftTopModule{    width: 100%;    height: 100%;}.head{    width: 100%;    height: 15%;    display: flex;    align-items: center;    justify-content: center;}.main{    width: 95%;    height: 82%;    margin: 0 auto;}</style>

因为六个文件夹的代码太多了 这里我只做一个示例 其他五个文件夹的代码 可以复制leftCenter的

2.然后在app.vue引入组件

<div class="container" id="container">    <header class="header">three学习</header>    <section class="leftTop">      <LeftTop />    </section>    <section class="leftCenter"></section>     <section class="leftFooter"></section>    <section class="rightTop"></section>    <section class="rightCenter"></section>    <section class="rightFooter"></section></div><script lang="ts" setup>import LeftTop from './views/leftTop/index.vue'</script>

七.补充--如何做一个地面出来

素材

图片放在public文件夹


----------------------------手动分割

1.创建一个three的纹理贴图并把草地加载进来

2.然后设置重复次数

3.定位到模型的下面

4.将地板添加到场景中

以下请加在加载gltf模型的前面

 //4.创建地面  const groundTexture = new THREE.TextureLoader().load("./2.png");  groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;  groundTexture.repeat.set(100, 100);  const ground = new THREE.CircleGeometry(500, 100);  const groundMaterial = new THREE.MeshLambertMaterial({    side: THREE.DoubleSide,    map: groundTexture,  });  const groundMesh = new THREE.Mesh(ground, groundMaterial);  groundMesh.name = "地面";  groundMesh.rotateX(-Math.PI / 2);  groundMesh.position.set(0, -0.345, 1);  scene.add(groundMesh);



原文链接:
https://juejin.cn/post/7293463921729372201