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 = true
obj.renderer.shadowMap.type = THREE.PCFSoftShadowMap
obj.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,0
obj.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 = true
obj.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、 创建一个立方体 长宽高为4
geometry = 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,高20
geometry = 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.0
obj.trackballControls.zoomSpeed = 1.2
obj.trackballControls.panSpeed = 0.8
// 是否可以缩放
obj.trackballControls.noZoom = false
obj.trackballControls.noPan = false
obj.trackballControls.staticMoving = true
// 动态阻尼系数 就是鼠标拖拽旋转灵敏度
obj.trackballControls.dynamicDampingFactor = 0.3
obj.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); //默认的监听当前的不知道是啥
// 旋转cube
obj.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() // 初始化辅助UI
initStats() // 添加性能监控, 初始化帧数显示工具
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.ctx
ctx.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、为页面配置路由(此处不详细介绍)