vue3.x + threeJs 实现3d动画场景
效果图
项目版本
vue3.x
依赖
threejs
//此处为兼容模型组合插件three-js-csg, 使用 0.107.0 版本npm install three@0.107.0 -S
加载.obj和.mtl文件的插件
// 加载外部obj和mtl所需npm install three-obj-mtl-loader -S
渲染器插件
npm install three-css2drender -S
辅助插件
// 可以容易地创建修改代码变量的界面组件npm install dat-gui -S
性能监听插件
// 能够在页面显示帧数,辅助开发npm install three-stats -S
轨迹球插件
// 轨迹球控制器,可以使用鼠标来轻松移动、平移和缩放场景npm install three-trackballcontrols -S
模型组合插件
// threebsp的vue替代方案(threejs 版本推荐107, 高版本不可用)npm install three-js-csg -S
实现
1、页面逻辑部分
<template><div class="container" ref="container"></div></template><script>import * as THREE from "three";import * as ThreeStats from "three-stats";import * as Dat from 'dat-gui'import TrackballControls from 'three-trackballcontrols'import clockTexture from './utils/clock'const canvas = clockTexture()// 因vue3中使用了Proxy对象代理,引入的外部依赖中使用了大量的===造成对比失败。// 解决办法: 对Proxy对象进行拆箱const unwarp = (obj) => obj && (obj.__v_raw || obj.valueOf() || obj);import {onMounted,reactive,toRefs} from 'vue';export default {name: 'create',setup() {const obj = reactive({container: null,renderer: {}, // 渲染器对象camera: {}, // 相机对象scene: {}, // 场景对象axesHelper: {}, // 三维坐标系对象stats: {}, // 性能监控对象light: {spotLight: {}, // 光源对象}, // 光源集geometry: {ball: {}, // 球体对象cube: {}, // 长方体对象plane: {}, // 平面对象}, // 几何对象集controls: {}, // 辅助对象clock: {}, // 时钟工具对象trackballControls: {}, // 控制器对象step: 0, // 跳跃距离texture: { // 文理集clock: '' // 钟表}})const initRender = () => {obj.renderer = new THREE.WebGLRenderer({antialias : true}); // antialias(是否启用抗锯齿)obj.renderer.setSize(obj.container.offsetWidth, obj.container.offsetHeight);// 告诉渲染器需要阴影效果obj.renderer.shadowMap.enabled = trueobj.renderer.shadowMap.type = THREE.PCFSoftShadowMapobj.renderer.setClearColor(0xFFFFFF, 1.0);obj.container.appendChild(obj.renderer.domElement);}const initScene = () => {obj.scene = new THREE.Scene();}const initCamera = () => {// PerspectiveCamera('视角', '指定投影窗口长宽比', '从距离摄像机多远开始渲染', '截止多远停止渲染1000')obj.camera = new THREE.PerspectiveCamera(45, obj.container.offsetWidth/obj.container.offsetHeight, 0.1, 1000);// 相机位置obj.camera.position.set( -30, 40, 30 );// 设置相机指向的位置 默认0,0,0obj.camera.lookAt(obj.scene.position)}const initGui = () => {obj.controls = {rotationSpeed: 0.02,bouncingSpeed: 0.03}let gui = new Dat.GUI()gui.add(obj.controls, 'rotationSpeed', 0, 0.5)gui.add(obj.controls, 'bouncingSpeed', 0, 0.5)}const initClock = () => {obj.clock = new THREE.Clock()}const initHelper = () => {obj.axesHelper = new THREE.AxesHelper(10)obj.scene.add(obj.axesHelper)}const initLight = () => {// 添加光源obj.light.spotLight = new THREE.SpotLight(0xFFFFFF);obj.light.spotLight.position.set(-40, 60, 10);obj.light.spotLight.castShadow = trueobj.scene.add(obj.light.spotLight);}const initObject = () => {//1、 创建几何模型 球let geometry = new THREE.SphereGeometry(4, 20, 20)let material = new THREE.MeshLambertMaterial({ color: 0x7777FF });obj.geometry.ball = new THREE.Mesh(geometry, material);obj.geometry.ball.castShadow = true // 开启阴影obj.geometry.ball.position.set(20, 4, 2); // 设置位置obj.scene.add(obj.geometry.ball); // 添加到场景中去//2、 创建一个立方体 长宽高为4geometry = new THREE.BoxGeometry(4, 4, 4)// material = new THREE.MeshLambertMaterial({ color: 0xFF0000 }); // 创建立方体材质// obj.geometry.cube = new THREE.Mesh(geometry, material);obj.texture.clock = new THREE.Texture(canvas)material = new THREE.MeshBasicMaterial({ map: obj.texture.clock })obj.texture.clock.needsUpdate = true //开启纹理更新obj.geometry.cube = new THREE.Mesh(geometry, material);obj.geometry.cube.castShadow = true // 开启阴影obj.geometry.cube.position.set(-4, 3, 0); // 设置位置obj.scene.add(obj.geometry.cube); // 添加到场景中去//3、 创建一个平面 宽60,高20geometry = new THREE.PlaneGeometry(60, 20)material = new THREE.MeshLambertMaterial({ color: 0xcccccc }); // 创建待颜色的材质obj.geometry.plane = new THREE.Mesh(geometry, material);obj.geometry.plane.receiveShadow = true // 平面开启接收阴影效果obj.geometry.plane.rotation.x = -0.5 * Math.PI // 设置平面角度obj.geometry.plane.position.set(15, 0, 0); // 设置位置obj.scene.add(obj.geometry.plane); // 添加到场景中去}const initStats = () => {obj.stats = new ThreeStats.Stats();obj.container.appendChild(obj.stats.domElement);}const initControls = () => {obj.trackballControls = new TrackballControls(obj.camera, obj.renderer.domElement)obj.trackballControls.rotationSpeed = 1.0obj.trackballControls.zoomSpeed = 1.2obj.trackballControls.panSpeed = 0.8// 是否可以缩放obj.trackballControls.noZoom = falseobj.trackballControls.noPan = falseobj.trackballControls.staticMoving = true// 动态阻尼系数 就是鼠标拖拽旋转灵敏度obj.trackballControls.dynamicDampingFactor = 0.3obj.trackballControls.key = [65, 83, 68]}const animate = () => {obj.trackballControls.update(obj.clock.getDelta())obj.stats.update(); //更新性能插件// obj.stats.setMode(0); //默认的监听fps// obj.stats.setMode(1); //默认的监听画面渲染时间// obj.stats.setMode(2); //默认的监听当前的不知道是啥// 旋转cubeobj.geometry.cube.rotation.y += obj.controls['rotationSpeed'];obj.geometry.cube.rotation.x += obj.controls['rotationSpeed'];obj.geometry.cube.rotation.z += obj.controls['rotationSpeed'];obj.step += obj.controls['bouncingSpeed']obj.geometry.ball.position.x = 20 + (10 * (Math.cos(obj.step)))obj.geometry.ball.position.y = 2 + (10 * Math.abs(Math.sin(obj.step))) // Math.sin(x) ,Math.cos(x) x 的余弦值。返回的是 -1.0 到 1.0 之间的数/*如何得到圆上每个点的坐标?解决思路:根据三角形的正玄、余弦来得值;假设一个圆的圆心坐标是(a,b),半径为r,则圆上每个点的X坐标=a + Math.sin(2*Math.PI / 360) * r; Y坐标=b + Math.cos(2*Math.PI / 360) * r*/requestAnimationFrame(animate);// 渲染场景unwarp(obj.renderer).render(unwarp(obj.scene), obj.camera);}onMounted(() => {initScene() // 初始化场景initCamera() // 初始化相机initHelper() // 初始化三维坐标系initGui() // 初始化辅助UIinitStats() // 添加性能监控, 初始化帧数显示工具initClock() // 初始化时钟工具initObject() // 初始化几何模型initRender() // 初始化渲染器initLight() // 初始化光源initControls() // 初始化控制器animate();window.addEventListener('resize', () => {//更新镜头长宽比obj.camera.aspect = obj.container.offsetWidth/obj.container.offsetHeight;//更新摄像机投影矩阵。在任何参数被改变以后必须被调用。obj.camera.updateProjectionMatrix();//设置渲染器宽长obj.renderer.setSize(obj.container.offsetWidth, obj.container.offsetHeight);})})return {...toRefs(obj)}}}</script><style scoped>.container {position: absolute;width: 100%;height: 100%;overflow: hidden;}</style>
2、钟表图案生成文件(项目根目录/src/utils/clock.js)
/* eslint-disable */export default function clock() {var canvas = document.createElement('canvas');canvas.width = 200;canvas.height = 200;var ctx = canvas.getContext('2d');var timerId;var frameRate = 60;function canvObject(){this.x = 0;this.y = 0;this.rotation = 0;this.borderWidth = 2;this.borderColor = '#000000';this.fill = false;this.fillColor = '#ff0000';this.update = function(){if(!this.ctx)throw new Error('你没有指定ctx对象。');var ctx = this.ctxctx.save();ctx.lineWidth = this.borderWidth;ctx.strokeStyle = this.borderColor;ctx.fillStyle = this.fillColor;ctx.translate(this.x, this.y);if(this.rotation)ctx.rotate(this.rotation * Math.PI/180);if(this.draw)this.draw(ctx);if(this.fill)ctx.fill();ctx.stroke();ctx.restore();}}function Line(){}Line.prototype = new canvObject();Line.prototype.fill = false;Line.prototype.start = [0,0];Line.prototype.end = [5,5];Line.prototype.draw = function(ctx){ctx.beginPath();ctx.moveTo.apply(ctx,this.start);ctx.lineTo.apply(ctx,this.end);ctx.closePath();}function Circle(){}Circle.prototype = new canvObject();Circle.prototype.draw = function(ctx){ctx.beginPath();ctx.arc(0, 0, this.radius, 0, 2 * Math.PI, true);ctx.closePath();}var circle = new Circle();circle.ctx = ctx;circle.x = 100;circle.y = 100;circle.radius = 90;circle.fill = true;circle.borderWidth = 6;circle.fillColor = '#ffffff';var hour = new Line();hour.ctx = ctx;hour.x = 100;hour.y = 100;hour.borderColor = "#000000";hour.borderWidth = 10;hour.rotation = 0;hour.start = [0,20];hour.end = [0,-50];var minute = new Line();minute.ctx = ctx;minute.x = 100;minute.y = 100;minute.borderColor = "#333333";minute.borderWidth = 7;minute.rotation = 0;minute.start = [0,20];minute.end = [0,-70];var seconds = new Line();seconds.ctx = ctx;seconds.x = 100;seconds.y = 100;seconds.borderColor = "#ff0000";seconds.borderWidth = 4;seconds.rotation = 0;seconds.start = [0,20];seconds.end = [0,-80];var center = new Circle();center.ctx = ctx;center.x = 100;center.y = 100;center.radius = 5;center.fill = true;center.borderColor = 'orange';for(var i=0,ls=[],cache;i<12;i++){cache = ls[i] = new Line();cache.ctx = ctx;cache.x = 100;cache.y = 100;cache.borderColor = "orange";cache.borderWidth = 2;cache.rotation = i * 30;cache.start = [0,-70];cache.end = [0,-80];}function start() {// 清除画布ctx.clearRect(0,0,200,200);// 填充背景色ctx.fillStyle = 'orange';ctx.fillRect(0,0,200,200);// 表盘circle.update();// 刻度for(var i=0;cache=ls[i++];)cache.update();// 时针hour.rotation = (new Date()).getHours() * 30;hour.update();// 分针minute.rotation = (new Date()).getMinutes() * 6;minute.update();// 秒针seconds.rotation = (new Date()).getSeconds() * 6;seconds.update();// 中心圆center.update();}timerId = setInterval(function(){start()},(1000/frameRate)|0);start()return canvas}
3、为页面配置路由(此处不详细介绍)
