<template> <div id="container"> <!-- <img src="/models/yunlog.png" alt /> --> <button @click="dispose('robot')"> 模型切换 </button> <button @click="changeModel(['Head_4'])"> 模型修改 </button> </div> </template> <script> import * as Three from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; import { GUI } from 'three/examples/jsm/libs/dat.gui.module'; import { DoubleSide, GridHelper, Mesh, MeshBasicMaterial, PlaneBufferGeometry, AnimationMixer, Clock, Raycaster, Vector2 } from 'three'; export default { data() { return { // 场景的宽高获取容器宽高 sceneHeight: null, sceneWidth: null, // 场景, 灯光, 摄像机, 控制器, 渲染器 scene: "", light: "", camera: "", controls: "", renderer: "", // 动画 animationMixer: null, clock: null, animationClipList: [], animationActionList: {}, animationClipList2: [], // 点击的元素 intersect: [], // 可改变的变量 // 点击触发的动作 clickAction: '开关柜门', // 能控制显示隐藏的元素 changeModelList: ['网格001_1', '网格023', '网格001' ], // 实验图形控制组件 api: { state: 'Sitting' } }; }, mounted() { this.init(); this.drawPlane(); this.drweSkybox() this.loadGltf(); // this.loadGltf2() // 监测点击事件 this.clickRender() }, methods: { //初始化three.js相关内容 init() { const container = document.getElementById("container"); // console.log("三维盒子", container, container.offsetHeight, container.offsetWidth) // 创建场景对象 this.scene = new Three.Scene(); this.sceneHeight = container.offsetHeight this.sceneWidth = container.offsetWidth // 创建灯光 this.scene.add(new Three.AmbientLight(0x999889)); //环境光 this.light = new Three.DirectionalLight(0xdfebff, 0.5); //从正上方(不是位置)照射过来的平行光,0.45的强度 this.light.position.set(1, 1, 0); // this.light.position.multiplyScalar(0.5); this.scene.add(this.light); // 初始化相机 (视野大小, 长宽比, 近景, 远景) this.camera = new Three.PerspectiveCamera(70, this.sceneWidth / this.sceneHeight, 0.01, 100) this.camera.position.set(0, 5, 8); // 创建渲染器,在创建渲染器时自带一个dom,在后文中将其放在页面上显示 this.renderer = new Three.WebGLRenderer({ alpha: true, // canvas中是否包含透明度 antialias: true // 是否执行抗锯齿 }); //初始化控制器 this.controls = new OrbitControls(this.camera, this.renderer.domElement); // this.controls.target.set(0, 0, 0);//------------------ // this.controls.minDistance = 3; // this.controls.maxDistance = 100; // this.controls.maxPolarAngle = Math.PI / 3; this.controls.update(); //渲染 this.renderer.setPixelRatio(window.devicePixelRatio); //为了兼容高清屏幕 this.renderer.setSize(this.sceneWidth, this.sceneHeight); this.renderer.outputEncoding = Three.sRGBEncoding; // 转换输出的颜色为sRGB颜色空间,使其输出颜色与建模软件上一致 container.appendChild(this.renderer.domElement); window.addEventListener("resize", this.onWindowResize, false); //添加窗口监听事件(resize-onresize即窗口或框架被重新调整大小) }, //窗口监听函数 onWindowResize() { // 模型比例调整 this.camera.aspect = this.sceneWidth / this.sceneHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.sceneWidth, this.sceneHeight); }, // 绘制平面 drawPlane() { // 加载贴图 let textureLoader = new Three.TextureLoader var boxImg = require('../assets/skybox/bottom.jpg') let boxTexture = textureLoader.load(boxImg) const planeBufferGeometry = new PlaneBufferGeometry( 50, 50 ); // 创建50*50的平面 const plane = new Mesh( planeBufferGeometry, new MeshBasicMaterial({ map: boxTexture, side: DoubleSide }) ) plane.name = 'plane' // 平面绕x轴旋转90度(弧度制) plane.rotation.x = -Math.PI / 2 this.scene.add( plane ) // 添加网格 this.scene.add(new GridHelper(50, 50)) }, // 绘制天空盒 drweSkybox() { let skyBox = new Three.BoxGeometry(100, 100, 100) // 创建立方体 let textureLoader = new Three.TextureLoader let right = require('../assets/skybox/right.jpg') let left = require('../assets/skybox/left.jpg') let top = require('../assets/skybox/top.jpg') let bottom = require('../assets/skybox/bottom.jpg') let back = require('../assets/skybox/back.jpg') let front = require('../assets/skybox/front.jpg') let skyBoxMaterialList = [ new Three.MeshBasicMaterial({ map: textureLoader.load(right), side: DoubleSide }), new Three.MeshBasicMaterial({ map: textureLoader.load(left), side: DoubleSide }), new Three.MeshBasicMaterial({ map: textureLoader.load(top), side: DoubleSide }), new Three.MeshBasicMaterial({ map: textureLoader.load(bottom), side: DoubleSide }), new Three.MeshBasicMaterial({ map: textureLoader.load(front), side: DoubleSide }), new Three.MeshBasicMaterial({ map: textureLoader.load(back), side: DoubleSide }) ] let sky = new Three.Mesh(skyBox, skyBoxMaterialList) // 创建网格 this.scene.add(sky) }, //外部模型加载函数 loadGltf() { // 加载模型 var loader = new GLTFLoader() loader.load("static/jigui.glb", (data) => { // console.log(data) let mesh = data.scene; mesh.position.set(0, 0.1, 0); mesh.name = 'robot' this.scene.add(mesh); // 将模型引入three // 加入图形控制组件 // this.createGUI( mesh, data.animations ); this.animationClipList = data.animations console.log(555, data.animations[0]) this.setAnimation() this.render(); }); }, //外部模型2加载函数 loadGltf2() { // 加载模型 var loader = new GLTFLoader() loader.load("static/RobotExpressive.glb", (data) => { // console.log(data) let mesh = data.scene; // mesh.position.set(-4, 1.2, 0); mesh.position.set(-4, 1.2, 0); mesh.name = 'bowl' this.scene.add(mesh); // 将模型引入three this.animationClipList2 = data.animations this.setAnimation() this.render(); }); }, // 图形控制组件 createGUI( model, animations ) { const states = [ 'Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing' ]; const emotes = [ 'Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp' ]; let gui = new GUI(); let api = this.api let mixer = new Three.AnimationMixer( model ); let actions = {}; for ( let i = 0; i < animations.length; i ++ ) { const clip = animations[ i ]; const action = mixer.clipAction( clip ); actions[ clip.name ] = action; if ( emotes.indexOf( clip.name ) >= 0 || states.indexOf( clip.name ) >= 4 ) { action.clampWhenFinished = true; action.loop = Three.LoopOnce; } } // states const statesFolder = gui.addFolder( 'States' ); const clipCtrl = statesFolder.add( api, 'state' ).options( states ); clipCtrl.onChange( () => { this.fadeToAction( api.state, 0.5 ); } ); statesFolder.open(); // emotes const emoteFolder = gui.addFolder( 'Emotes' ); let createEmoteCallback = ( name ) => { api[ name ] = () => { this.fadeToAction( name, 0.2 ); mixer.addEventListener( 'finished', restoreState ); }; emoteFolder.add( api, name ); } let restoreState = () => { mixer.removeEventListener( 'finished', restoreState ); this.fadeToAction( api.state, 0.2 ); } for ( let i = 0; i < emotes.length; i ++ ) { createEmoteCallback( emotes[ i ] ); } emoteFolder.open(); // expressions let face = model.getObjectByName( 'Head_4' ); console.log("face", face) const expressions = Object.keys( face.morphTargetDictionary ); const expressionFolder = gui.addFolder( 'Expressions' ); for ( let i = 0; i < expressions.length; i ++ ) { expressionFolder.add( face.morphTargetInfluences, i, 0, 1, 0.01 ).name( expressions[ i ] ); } let activeAction = actions[ 'Walking' ]; activeAction.play(); expressionFolder.open(); }, fadeToAction( name, duration ) { let actions = this.animationActionList let activeAction = actions[ name ]; let previousAction = activeAction; if ( previousAction !== activeAction ) { previousAction.fadeOut( duration ); } console.log(41864, this.animationActionList) activeAction .reset() .setEffectiveTimeScale( 1 ) .setEffectiveWeight( 1 ) .fadeIn( duration ) .play(); }, // 加载动画 setAnimation() { this.animationMixer = new AnimationMixer(this.scene) this.clock = new Clock() console.log("动画列表", this.animationClipList) }, render() { requestAnimationFrame(() => { this.render() }); // 调用动画 this.animationMixer.update(this.clock.getDelta()) this.renderer.render(this.scene, this.camera); }, // 监测点击事件 clickRender() { this.renderer.domElement.addEventListener('click', event => { // 获取屏幕坐标 let { offsetX, offsetY } = event // 计算画布上的二维坐标 let x = ( offsetX / this.sceneWidth ) * 2 - 1 let y = - ( offsetY / this.sceneHeight ) * 2 + 1 let mousePoint = new Vector2( x, y ) // 获取点击元素 let raycaster = new Raycaster() // 按照this.camera摄像头下,mousePoint位置设置raycaster raycaster.setFromCamera( mousePoint, this.camera ) // 获取点击的元素 let intersect = raycaster.intersectObjects(this.scene.children, true) // 筛选去除点击的网格元素及平面元素 intersect = intersect.filter( intersect => !(intersect.object instanceof GridHelper) && intersect.object.name != "plane" ) console.log("点击元素", intersect) this.intersect = intersect let hasClick = [] // 所有被点击的指定元素及其子元素组成数组,通过数组中有无数据判断是否点击 intersect.forEach((v) => { // let click = this.isClick(v.object, 'robot') // console.log("比较结果", click) if( this.isClick(v.object, 'robot') ){ hasClick.push(v) } }) // console.log("比较结果", hasClick) // 业务测试:点击触发动作 let action = this.getAnimationAction(this.clickAction) // 判断元素被点击,并且该点击动作还未触发,触发动作,若已触发动作停止动作 if( hasClick.length > 0 && action.isRunning () ){ action.stop() } else if( hasClick.length > 0 ) { action.play() } // 业务测试:点击控制眼睛的显示隐藏 this.changeModel(this.changeModelList) // action.play() // action.weight = 0 // console.log("权重", action.getEffectiveWeight () ) // if( hasClick.length > 0 && action.getEffectiveWeight () ){ // action.fadeOut(1) // } else if( hasClick.length > 0 ) { // action.fadeIn(1) // } }) }, // 判断点击的元素是否是指定元素或其子元素 isClick(object, name) { // console.log("点击验证", object, name) if( object.name == name ){ return object }else if( object.parent ){ return this.isClick(object.parent, name) }else { return false } }, // 动画调用方法,当动作已使用混合器生成AnimationAction直接返回储存的AnimationAction,若还未生成,生成一个AnimationAction返回,并储存下次使用 getAnimationAction(name) { let animationActionList = this.animationActionList // 检索animationActionList中是否已储存AnimationAction for( let actionName in animationActionList ) { if( actionName == name ){ return animationActionList[name] } } // 若还未储存AnimationAction let animationClip = this.animationClipList.find(animationClip => animationClip.name == name ) animationActionList[name] = this.animationMixer.clipAction(animationClip) return animationActionList[name] }, // 删除对象 dispose(name) { // console.log('删除测试', this.scene) let childrenList = this.scene.children let group = childrenList.find(v => v.name == name) group.visible = !group.visible }, // 点击触发,修改模型显示部位 changeModel(body) { let sence = this.scene.getObjectByName('robot') let clickList = this.intersect let object // 使用try...catch跳出循环 try{ clickList.forEach((item) => { body.forEach((objItem) => { object = this.isClick( item.object, objItem ) if( object ){ console.log("控制隐藏模块", object) object.visible = !object.visible } }) }) }catch{ console.log("未点击到需隐藏的元素") } } } }; </script> <style scoped> #container { width: 600px; margin: 0 auto; height: 400px; overflow: hidden; } </style>