网站首页 > 精选文章 正文
# 在Uni-App中实现3D模型展示:基于Three.js的组件开发实践
## 引言
在现代移动应用开发中,3D模型展示已成为提升用户体验的重要手段。本文将基于我开发的`threeCom`组件,详细介绍如何在Uni-App框架中集成Three.js来实现3D模型的加载、展示和交互功能。这个组件支持模型加载进度显示、自动旋转、轨道控制以及全屏展示等功能,是一个完整的3D模型展示解决方案。
## 安装与引入方式:
通过npm安装:npm install three
直接下载源码:从GitHub获取three.js-master文件,放入项目的static文件夹
## 组件架构设计
### 组件结构
`threeCom`组件采用Vue 3的Composition API编写,主要分为三个部分:
1. **模板部分**:负责UI布局和交互元素
2. **脚本部分**:处理业务逻辑和状态管理
3. **RenderJS部分**:实际运行Three.js的代码
<template>
<view :id="'threejsOutStaticDiv' + time" class="threejsOutDiv">
<view :id="'threejsOutDiv' + time" class="threejsOutDiv">
<!-- Three.js渲染容器 -->
<view :id="'threejsContent' + time" :prop="threejsProp" :change:prop="Threejs.create"></view>
<!-- 加载进度条 -->
<view class="progressItemDiv" v-if="percent != 2">
<view class="progressItemOutDiv">
<view class="progressDiv" :style="`width: calc(100% * ${percent})`"></view>
<view class="progressBorder"></view>
</view>
</view>
<!-- 全屏控制按钮 -->
<text id="enterScreen" v-show="!isScreen && percent == 2 && needScreen"
class="iconfont screenBtnFont"></text>
<view v-show="isScreen && percent == 2 && needScreen" id="exitScreen" class="iconfont screenBtnFont safeBottom">
<text></text>
<!-- 适配iOS底部安全区域 -->
<gui-iphone-bottom :customClass="['iphoneBottom']"></gui-iphone-bottom>
</view>
</view>
</view>
</template>
## 核心功能实现
### 1. 模型加载与进度显示
组件使用GLTFLoader来加载glTF格式的3D模型,并实现了加载进度回调:
loader.load(gltfSrc, (gltf) => {
// 模型加载完成回调
this.$ownerInstance.callMethod('modelLoadingProgress', '2');
// 模型自适应大小
const box = new THREE.Box3().setFromObject(gltfScene);
const centerData = box.getCenter(new THREE.Vector3());
const maxDim = Math.max(
box.max.x - box.min.x,
box.max.y - box.min.y,
box.max.z - box.min.z
);
const scale = 600 / maxDim;
gltfScene.scale.set(scale, scale, scale);
gltfScene.position.set(
(0 - centerData.x) * scale,
(0 - centerData.y) * scale,
(0 - centerData.z) * scale
);
scene.add(gltfScene);
}, (xhr) => {
// 加载进度回调
const percent = xhr.loaded / xhr.total;
this.$ownerInstance.callMethod('modelLoadingProgress', percent + '');
});
```
### 2. 渲染循环与自动旋转
组件实现了渲染循环和自动旋转功能,当用户没有交互时会自动旋转模型:
```javascript
const render = () => {
let t1 = new Date();
let t = t1 - t0;
t0 = t1;
animationFrameId = requestAnimationFrame(render);
renderer.render(scene, camera);
// 自动旋转逻辑
if (this.obj4.isRotate && needRotate) {
scene.rotateY(0.0005 * t);
}
}
render();
```
### 3. 轨道控制交互
组件提供了精细的轨道控制配置,包括缩放限制和平移控制:
```javascript
controls = new OrbitControls(camera, renderer.domElement);
controls.maxZoom = 2.5;
controls.minZoom = 1;
controls.zoomSpeed = 0.5;
controls.enablePan = false;
// 交互状态监听
controls.addEventListener('change', () => {
this.obj4.isRotate = false;
});
controls.addEventListener('end', (e) => {
autoRotateSetTimeOut = setTimeout(() => {
this.obj4.isRotate = true;
}, 1500);
});
```
### 4. 全屏展示功能
组件实现了完整的全屏/退出全屏逻辑,包括尺寸调整和过渡动画:
```javascript
async screen(isScreen) {
if (isScreen) {
// 进入全屏逻辑
const threejsOutDiv = document.getElementById(this.compData.threejsOutDivId);
const divTemp = document.createElement('div');
divTemp.classList.add('threejsMengCeng');
// 设置初始尺寸和位置
divTemp.style.height = threejsOutDiv.getBoundingClientRect().height + 'px';
divTemp.style.width = threejsOutDiv.getBoundingClientRect().width + 'px';
divTemp.style.position = 'absolute';
divTemp.style.zIndex = '910';
divTemp.style.top = threejsOutDiv.getBoundingClientRect().top + 'px';
divTemp.style.left = threejsOutDiv.getBoundingClientRect().left + 'px';
divTemp.style.transition = 'all 200ms ease';
document.body.appendChild(divTemp);
divTemp.appendChild(threejsOutDiv);
// 动画过渡到全屏
setTimeout(() => {
divTemp.style.height = document.documentElement.clientHeight + 'px';
divTemp.style.width = document.documentElement.clientWidth + 'px';
divTemp.style.top = '0';
divTemp.style.left = '0';
// 调整渲染器和相机
if (this.rendererComp && this.cameraComp) {
const widthTemp = document.documentElement.clientWidth;
const heightTemp = document.documentElement.clientHeight;
this.rendererComp.setSize(widthTemp, heightTemp);
var k = widthTemp / heightTemp;
var s = 500;
this.cameraComp.left = -s * k;
this.cameraComp.right = s * k;
this.cameraComp.top = s;
this.cameraComp.bottom = -s;
this.cameraComp.updateProjectionMatrix();
}
this.vm.callMethod('setIsScreen', true);
}, 100);
} else {
// 退出全屏逻辑
// ...类似的处理逻辑
}
}
```
## 性能优化实践
### 1. 资源释放管理
组件在卸载时确保释放所有Three.js资源,避免内存泄漏:
```javascript
unmounted() {
if (animationFrameId) cancelAnimationFrame(animationFrameId);
if (autoRotateSetTimeOut) clearTimeout(autoRotateSetTimeOut);
// 释放各种Three.js对象
if (ambient) ambient.dispose();
if (renderer) renderer.dispose();
if (controls) controls.dispose();
// 清空引用
camera = null;
scene = null;
gltfScene = null;
loader = null;
}`
### 2. 跨平台兼容处理
组件针对不同平台做了兼容处理,特别是iOS的安全区域:
<!-- 适配iOS底部安全区域 -->
<!-- #ifdef APP-PLUS -->
<gui-iphone-bottom :customClass="['iphoneBottom']"></gui-iphone-bottom>
<!-- #endif -->
### 3. 渲染性能优化
使用正投影相机(OrthographicCamera)而非透视相机,减少计算量:
camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 2000);`
设置渲染器抗锯齿和透明背景:
renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderer.setClearColor(0x000000, 0); // 透明背景
## 使用示例
在父组件中使用`threeCom`非常简单:
<template>
<view class="container">
<threeCom :src="modelUrl"
:needScreen="true"
:needRotate="true"
:needControl="true"
@modelLoadingProgress="onModelLoadingProgress" />
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import threeCom from '@/components/threeCom.vue'
const modelUrl = ref('https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/Duck/glTF/Duck.gltf')
const progress = ref(0)
function onModelLoadingProgress(e: { detail: { progress: number } }) {
progress.value = Math.floor(e.detail.progress * 100)
}
</script>
```
threeCom.vue
```javascript
<template>
<view :id="'threejsOutStaticDiv' + time" class="threejsOutDiv">
<view :id="'threejsOutDiv' + time" class="threejsOutDiv">
<view :id="'threejsContent' + time" :prop="threejsProp" :change:prop="Threejs.create"></view>
<view class="progressItemDiv" v-if="percent != 2">
<!-- <view class="progressItemDiv"> -->
<view class="progressItemOutDiv">
<view class="progressDiv" :style="`width: calc(100% * ${percent})`"></view>
<view class="progressBorder"></view>
</view>
</view>
<text id="enterScreen" v-show="!isScreen && percent == 2 && needScreen"
class="iconfont screenBtnFont"></text>
<view v-show="isScreen && percent == 2 && needScreen" id="exitScreen" class="iconfont screenBtnFont safeBottom">
<text></text>
<!-- #ifdef APP-PLUS -->
<gui-iphone-bottom :customClass="['iphoneBottom']"></gui-iphone-bottom>
<!-- #endif -->
</view>
</view>
</view>
</template>
<script lang="ts">
import { reactive, ref, toRefs, computed } from "vue";
export default {
props: {
src: {
default: ''
},
needScreen: {
default: false,
},
needRotate: {
default: false,
},
needControl: {
default: true,
},
},
setup(props: any) {
const threejsProp = computed(() => {
return {
src: props.src,
width: data.width,
height: data.height,
threejsContentId: 'threejsContent' + data.time,
threejsOutDivId: 'threejsOutDiv' + data.time,
threejsOutStaticDivId: 'threejsOutStaticDiv' + data.time,
needScreen: props.needScreen,
needRotate: props.needRotate,
needControl: props.needControl,
}
})
const data = reactive({
time: new Date().getTime(),
width: '',
height: '',
percent: 0,
isScreen: false,
});
const modelLoadingProgress = (progress) => {
// console.log('加载进度' + progress);
// if (progress == 2) { // 表示加载完成并开始渲染
// } else {
// }
data.percent = progress
}
const setIsScreen = (val) => {
data.isScreen = val;
}
const pageData = toRefs(data);
return {
...pageData,
threejsProp,
modelLoadingProgress,
setIsScreen,
};
},
mounted() {
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query.select(`#threejsOutDiv${this.time}`).boundingClientRect((res) => {
this.width = res.width
this.height = res.height
}).exec();
});
}
};
</script>
<script module="Threejs" lang="renderjs">
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import Stats from 'three/examples/jsm/libs/stats.module.js';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
let scene = ''
let gui = ''
let autoRotateSetTimeOut = '' // 自开开始旋转计时器
let ambient = '' // 灯光
let camera = '' // 相机
let renderer = ''
let controls = ''
let animationFrameId = ''
let gltfScene = ''
let loader = ''
export default {
data(){
return {
compData: {},
vm: '',
rendererComp: '',
cameraComp: '',
kComp: '',
obj4: {
isRotate: true,
},
}
},
unmounted() {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
if (autoRotateSetTimeOut) {
clearTimeout(autoRotateSetTimeOut)
autoRotateSetTimeOut = null
}
if (ambient) {
ambient.dispose()
ambient = null
}
// if (camera) {
// camera.dispose() // 报错
// }
camera = null
if (renderer) {
renderer.dispose()
renderer = null
}
if (controls) {
controls.dispose()
controls = null
}
// if (scene) {
// scene.dispose() // 报错
// }
scene = null
// if (gltfScene) {
// gltfScene.dispose() // 报错
// }
gltfScene = null
// if (loader) {
// loader.dispose() // 报错
// }
loader = null
},
methods: {
async create(newVal, oldVal, vm) {
this.vm = vm;
let newValue = newVal;
let newObj = '';
if (newValue && typeof(newValue) === 'string') {
if (newValue.indexOf('json://') === 0) {
newValue = newValue.replace('json://', '');
}
// console.log('------', newValue);
try{
newObj = JSON.parse(newValue);
}catch(e){
//TODO handle the exception
console.error('JSONParse Err----catch!!');
}
} else if (newValue) {
newObj = newValue;
}
if (!newObj) {
console.error('renderJsNewObj is null!!');
return;
}
let {
src,
width,
height,
threejsContentId,
threejsOutDivId,
threejsOutStaticDivId,
needRotate,
needControl,
} = newObj;
this.compData = newObj
if (src && width && height) {
if (!gui) {
// gui = new GUI();
// gui.domElement.style.right = '0px';
// gui.domElement.style.top = '50px';
// gui.domElement.style.width = '200px';
}
/**
* 创建场景对象Scene
*/
scene = new THREE.Scene();
// 辅助观察坐标系 坐标轴颜色红R、绿G、蓝B分别对应坐标系的x、y、z轴,对于three.js的3D坐标系默认y轴朝上
// const axesHelper = new THREE.AxesHelper(150);
// scene.add(axesHelper);
/**
* 创建网格模型
*/
//创建一个球体几何对象
// var geometry = new THREE.SphereGeometry(60, 40, 40);
//创建一个立方体几何对象Geometry 长、高、宽
// var geometry = new THREE.BoxGeometry(100, 100, 200);
// const textLoader = new THREE.TextureLoader();
// let tietuSrc = '/static/images/tietu.png'
// const texture = textLoader.load(await this.getfile(tietuSrc))
//材质对象Material
// var material = new THREE.MeshLambertMaterial({
// // color: 0x0000ff, // 贴图时不需要设置颜色
// map: texture,
// });
// 高光材质 模拟镜面反射,产生一个高光效果
// var material = new THREE.MeshPhongMaterial({
// color: 0x0000ff, // 贴图时不需要设置颜色
// shininess: 20, //高光部分的亮度,默认30
// // specular: 0xffd700, //高光部分的颜色
// });
// 材质半透明,不受光照影响
// var material = new THREE.MeshBasicMaterial({
// color: 0x0000ff,
// transparent:true,//开启透明
// opacity:0.5,//设置透明度
// });
//网格模型对象Mesh
// var mesh = new THREE.Mesh(geometry, material);
// 设置模型mesh的xyz坐标
// mesh.position.set(50, 50, 100)
// gui.add(mesh.position, 'x', 0, 180).name('x轴').step(0.06);
// gui.add(mesh.position, 'y', 0, 180).name('y轴').step(0.05);
// gui.add(mesh.position, 'z', 0, 180).name('z轴').step(0.05);
// const obj = {
// color:0x0000ff,
// };
// .addColor()生成颜色值改变的交互界面
// gui.addColor(obj, 'color').name('颜色').onChange((value) => {
// mesh.material.color.set(value);
// });
// 下拉菜单
// const obj2 = {
// scale: 0,
// };
// 参数3数据类型:数组(下拉菜单)
// gui.add(obj2, 'scale', [-100, 0, 100]).name('y坐标').onChange(function (value) {
// mesh.position.y = value;
// });
// const obj3 = {
// scale: 0,
// };
// 参数3数据类型:对象(下拉菜单)
// gui.add(obj3, 'scale', {
// left: -100,
// center: 0,
// right: 100
// // 左: -100,//可以用中文
// // 中: 0,
// // 右: 100
// }).name('位置选择').onChange(function (value) {
// mesh.position.x = value;
// });
// const obj5 = {
// scale: 1,
// }
// gui.add(obj5, 'scale', 0.1, 5).name('缩放模型').step(0.01).onChange((value) => {
// mesh.scale.set(value, value, value)
// });
//网格模型添加到场景中
// scene.add(mesh);
// 创建GLTF加载器对象
loader = new GLTFLoader();
loader.setCrossOrigin('anonymous')
// let gltfSrc = '/static/gltf/untitled.glb'
// let gltfSrc = '/static/gltf/nftboli.gltf'
let gltfSrc = src
// loader.load(await this.getfile(gltfSrc), ( gltf ) => {
loader.load(gltfSrc, ( gltf ) => {
// console.log('loading finish')
this.$ownerInstance.callMethod('modelLoadingProgress', '2');
const enterScreenBtn = document.getElementById('enterScreen');
if (enterScreenBtn) {
enterScreenBtn.addEventListener('click', () => {
this.screen(true)
})
}
const exitScreenBtn = document.getElementById('exitScreen');
if (exitScreenBtn) {
exitScreenBtn.addEventListener('click', () => {
this.screen(false)
})
}
// console.log('控制台查看加载gltf文件返回的对象结构',gltf);
// console.log('gltf对象场景属性',gltf.scene);
gltfScene = gltf.scene
// 模型自适应大小
const box = new THREE.Box3().setFromObject(gltfScene)
// console.log('box-----', box.getCenter(new THREE.Vector3()))
const x = (box.max.x - box.min.x)
const y = (box.max.y - box.min.y)
const z = (box.max.z - box.min.z)
const maxDim = Math.max(x, y, z)
const scale = 600 / maxDim
gltfScene.scale.set(scale,scale, scale)
// 自动设置到中心点
const centerData = box.getCenter(new THREE.Vector3())
// gltfScene.translateX((0 - centerData.x) * scale)
// gltfScene.translateY((0 - centerData.y) * scale)
// gltfScene.translateZ((0 - centerData.z) * scale)
gltfScene.position.set((0 - centerData.x) * scale, (0 - centerData.y) * scale, (0 - centerData.z) * scale)
// console.log('gltfScene--', gltfScene.position)
// gltfScene.traverse( function ( child ) {
// if ( child.isMesh ) {
// child.geometry.center(); // center here
// }
// });
scene.add(gltfScene);
}, (xhr) => {
const percent = xhr.loaded / xhr.total;
// console.log('percent--', percent)
this.$ownerInstance.callMethod('modelLoadingProgress', percent + '');
})
/**
* 光源设置
*/
//点光源, 参数为光色、光照强度
// var point = new THREE.PointLight(0xffffff, 2);
// //点光源位置
// point.position.set(400, 190, 0);
// gui.add(point, 'intensity', 0, 2.0).name('光强')
// //点光源添加到场景中
// scene.add(point);
// const pointLightHelper = new THREE.PointLightHelper(point, 10);
// scene.add(pointLightHelper);
//环境光
ambient = new THREE.AmbientLight(0xffffff);
scene.add(ambient);
/**
* 相机设置
*/
// var width = window.innerWidth; //窗口宽度
// var height = window.innerHeight; //窗口高度
// var width = window.innerWidth; //窗口宽度
// var height = 400; //窗口高度
var k = width / height; //窗口宽高比
var s = 500; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建相机对象
camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 2000); // 正投影相机
// const camera = new THREE.PerspectiveCamera(100, width / height, 1, 2000); // 透视相机。模拟人眼观察
camera.focus = 100
camera.position.set(0, 0, 500); //设置相机位置
// camera.lookAt(mesh.position); //设置相机方向(指向的场景对象)
// gltfScene && camera.lookAt(gltfScene.position); //设置相机方向(指向的场景对象)
camera.lookAt(0, 0, 0); // //指向坐标原点
camera.updateProjectionMatrix();
// 画布跟随窗口变化
window.onresize = () => {
// const widthTemp = window.innerWidth; //canvas画布高度
// const heightTemp = window.innerWidth / k; //canvas画布宽度
// renderer.setSize(widthTemp, heightTemp);
// camera.aspect = widthTemp / heightTemp;
// camera.updateProjectionMatrix();
};
// const obj5 = {
// scale: 1,
// }
// gui.add(obj5, 'scale', 0, 5).name('缩放相机').step(0.01).onChange((value) => {
// camera.zoom = value
// camera.updateProjectionMatrix()
// });
/**
* 创建渲染器对象
*/
renderer = new THREE.WebGLRenderer({
antialias: true, // 锯齿模糊
});
// 获取你屏幕对应的设备像素比.devicePixelRatio告诉threejs,以免渲染模糊问题
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);//设置渲染区域尺寸
// renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
renderer.setClearColor(0x000000, 0); //设置背景颜色
renderer.outputEncoding = THREE.sRGBEncoding;
const threejsContentDom = document.getElementById(threejsContentId)
// const stats = new Stats();
if (threejsContentDom) {
threejsContentDom.appendChild(renderer.domElement); //threejsContent元素中插入canvas对象
// threejsContentDom.appendChild(stats.domElement)
}
//执行渲染操作 指定场景、相机作为参数(---渲染---)
// renderer.render(scene, camera);
// const obj4 = {
// isRotate: true,
// };
// 如果.add()改变属性的对应的数据类型如果是布尔值,那么交互界面就是一个单选框。
// gui.add(obj4, 'isRotate').name('是否旋转')
// 自动循环旋转模型 (---渲染---)
let t0 = new Date(); // 上次时间
const render = () => {
let t1 = new Date() // 本地时间
let t = t1 - t0; // 时间差
t0 = t1 //把本次时间赋值给上次时间
// stats.update();
animationFrameId = requestAnimationFrame(render) // 相当于settimeout 正常是16.7毫秒执行一次,相当于1秒60帧
renderer.render(scene, camera); //执行渲染操作
if (this.obj4.isRotate) {
// mesh.rotateY(0.0005 * t) // //每次绕y轴旋转0.01弧度 注意是围绕Y
// mesh.rotateY(0.0005 * t) // //每次绕y轴旋转0.01弧度 注意是围绕Y
// gltfScene && gltfScene.rotateY(0.0008 * t)
if (needRotate) {
scene.rotateY(0.0005 * t)
}
// scene.rotateY(0.0005 * t)
}
}
render()
// console.log('scene---', scene)
// 鼠标操作模型 (---渲染---)
// const render = () => {
// renderer.render(scene, camera); //执行渲染操作
// }
// render()
if (needControl) {
controls = new OrbitControls(camera, renderer.domElement);//创建控件对象
controls.maxZoom = 2.5
controls.minZoom = 1
controls.zoomSpeed = 0.5
controls.enablePan = false
// controls.maxDistance = 500
// controls.minDistance = 220
if (controls) {
controls.addEventListener('change', () => {
if (autoRotateSetTimeOut) {
clearTimeout(autoRotateSetTimeOut)
}
this.obj4.isRotate = false
});//监听鼠标、键盘事件
controls.addEventListener('start', (e) => {
if (autoRotateSetTimeOut) {
clearTimeout(autoRotateSetTimeOut)
}
// console.log('启动交互---', e)
this.obj4.isRotate = false
})
controls.addEventListener('end', (e) => {
if (autoRotateSetTimeOut) {
clearTimeout(autoRotateSetTimeOut)
}
// console.log('停止交互---')
autoRotateSetTimeOut = setTimeout(() => {
this.obj4.isRotate = true
}, 1500)
})
}
}
this.rendererComp = renderer
this.cameraComp = camera
}
},
getfile(src) {
// #ifdef APP-PLUS
let url = plus.io.convertLocalFileSystemURL(src)
return new Promise((resolve,reject)=>{
plus.io.resolveLocalFileSystemURL(url, entry => {
var reader = null;
entry.file( file => {
reader = new plus.io.FileReader();
reader.onloadend = ( read )=> {
resolve(read.target.result)
};
reader.readAsDataURL( file );
}, function ( error ) {
alert( error.message );
} );
}, err => {
resolve(src)
})
})
// #endif
// #ifndef APP-PLUS
return new Promise((resolve,reject)=>{
resolve(src)
})
// #endif
},
async screen(isScreen) {
if (isScreen) { // 全屏
await this.removeThreejsMengCeng();
const threejsOutDiv = document.getElementById(this.compData.threejsOutDivId);
const divTemp = document.createElement('div');
if (divTemp) {
divTemp.classList.add('threejsMengCeng');
divTemp.style.height = threejsOutDiv.getBoundingClientRect().height + 'px';
divTemp.style.width = threejsOutDiv.getBoundingClientRect().width + 'px';
divTemp.style.position = 'absolute';
divTemp.style.zIndex = '910';
divTemp.style.top = threejsOutDiv.getBoundingClientRect().top + 'px';
divTemp.style.left = threejsOutDiv.getBoundingClientRect().left + 'px';
divTemp.style.transition = 'all 200ms ease';
const pageDiv = document.getElementsByTagName('body')[0];
if (pageDiv) {
pageDiv.appendChild(divTemp);
}
}
if (threejsOutDiv) {
setTimeout(() => {
divTemp.appendChild(threejsOutDiv);
divTemp.style.height = document.documentElement.clientHeight + 'px';
divTemp.style.width = document.documentElement.clientWidth + 'px';
divTemp.style.top = '0';
divTemp.style.left = '0';
setTimeout(() => {
if (this.rendererComp && this.cameraComp) {
const widthTemp = document.documentElement.clientWidth; //canvas画布高度
const heightTemp = document.documentElement.clientHeight; //canvas画布宽度
this.rendererComp.setSize(widthTemp, heightTemp);
var k = widthTemp / heightTemp; //窗口宽高比
var s = 500; //三维场景显示范围控制系数,系数越大,显示的范围越大
// OrthographicCamera(-s * k, s * k, s, -s, 1, 2000); // 正投影相机
this.cameraComp.left = -s * k
this.cameraComp.right = s * k
this.cameraComp.top = s
this.cameraComp.bottom = -s
this.cameraComp.updateProjectionMatrix();
}
}, 0)
this.vm.callMethod('setIsScreen', true);
}, 100)
}
} else { // 退出全屏
const threejsOutStaticDivId = document.getElementById(this.compData.threejsOutStaticDivId);
const threejsOutDivId = document.getElementById(this.compData.threejsOutDivId);
const child = document.getElementsByClassName('threejsMengCeng')[0];
if (child) {
child.style.height = threejsOutStaticDivId.getBoundingClientRect().height + 'px';
child.style.width = threejsOutStaticDivId.getBoundingClientRect().width + 'px';
child.style.top = threejsOutStaticDivId.getBoundingClientRect().top + 'px';
child.style.left = threejsOutStaticDivId.getBoundingClientRect().left + 'px';
}
if (this.rendererComp && this.cameraComp) {
const widthTemp = threejsOutStaticDivId.getBoundingClientRect().width; //canvas画布高度
const heightTemp = threejsOutStaticDivId.getBoundingClientRect().height; //canvas画布宽度
this.rendererComp.setSize(widthTemp, heightTemp);
var k = widthTemp / heightTemp; //窗口宽高比
var s = 500; //三维场景显示范围控制系数,系数越大,显示的范围越大
// OrthographicCamera(-s * k, s * k, s, -s, 1, 2000); // 正投影相机
this.cameraComp.left = -s * k
this.cameraComp.right = s * k
this.cameraComp.top = s
this.cameraComp.bottom = -s
this.cameraComp.updateProjectionMatrix();
}
this.vm.callMethod('setIsScreen', false);
setTimeout(async () => {
if (threejsOutStaticDivId) {
threejsOutStaticDivId.appendChild(threejsOutDivId);
}
setTimeout(() => {
this.removeThreejsMengCeng();
}, 500)
}, 600);
}
},
removeThreejsMengCeng() {
return new Promise((resolve) => {
const bodyEle = document.getElementsByTagName('body')[0];
if (bodyEle) {
const child = document.getElementsByClassName('threejsMengCeng');
if (child) {
if (child.length) {
for (let i = 0; i < child.length; i++) {
bodyEle.removeChild(child[i]);
}
}
}
}
resolve();
});
}
},
}
</script>
<style scoped lang="scss">
.threejsOutDiv {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: #000;
.progressItemDiv {
position: absolute;
z-index: 2;
width: 80%;
height: 8px;
display: flex;
.progressItemOutDiv {
height: 100%;
width: 100%;
display: flex;
overflow: hidden;
border-radius: 20rpx;
margin-top: 2rpx;
position: relative;
}
.progressDiv {
position: absolute;
height: 100%;
background-color: #ffffff;
border-radius: 20rpx;
width: 0;
}
.progressBorder {
flex: 1;
border: 2rpx solid #ffffff;
// width: calc(100%);
// height: calc(24rpx * 0.9 - 2rpx - 4rpx);
border-radius: 20rpx;
}
}
.screenBtnFont {
font-size: 40rpx;
font-weight: bold;
color: #ffffff;
position: absolute;
bottom: 20rpx;
left: 20rpx;
}
.safeBottom {
/* #ifdef H5 */
bottom: calc(20rpx + constant(safe-area-inset-bottom));
bottom: calc(20rpx + env(safe-area-inset-bottom));
/* #endif */
}
}
</style>
猜你喜欢
- 2025-06-12 一文讲透支付宝沙箱的基本应用(支付宝沙箱是干什么的)
- 2025-06-12 管理系统 UI 设计,怎样让操作效率提升 50%?
- 2025-06-12 哎?你的Python代码怎么这么像TypeScript
- 2025-06-12 web前端培训需要多少时间(web前端培训需要多少时间完成)
- 2025-06-12 mongodb/redis/neo4j 如何自己打造一个 web 数据库可视化客户端?
- 2025-06-12 大雨暴雨!考生注意,昆明将迎强降雨,最强时段在→
- 2025-06-12 用Python写了一个上课点名系统(附源码)(自制考勤系统)
- 2025-06-12 细聊single-spa + vue来实现前端微服务项目
- 2025-06-12 springboot+Neo4j:快速搭建自己的知识图谱可视化构建平台
- 2025-06-12 一文带你搞懂Vue3 底层源码(vue3.0源码)
- 最近发表
-
- Vue基础入门,第15节 一键页面换新衣,动态修改样式的3种方法
- uniapp Vue3.x组件库uview-vue3(uniapp用什么组件库)
- Vue3 样式绑定: 内联样式与Class属性的数组语法
- Vue2的样式(class和style)绑定(vue样式scoped)
- 前端开发,在项目中常用的css样式整理
- 前端必看!7 个 Vue3 性能优化实战技巧,让页面飞起来
- 前端也能玩转截图?uni-app + Vue3 实现页面快照功能
- Vue2 升级 Vue3 一文通关(vue-cli2.0升级3.0)
- Vue2的16种传参通信方式(vue有几种传参方式)
- 面试官:聊聊你知道的Vue与React的区别
- 标签列表
-
- 向日葵无法连接服务器 (32)
- git.exe (33)
- vscode更新 (34)
- dev c (33)
- git ignore命令 (32)
- gitlab提交代码步骤 (37)
- java update (36)
- vue debug (34)
- vue blur (32)
- vscode导入vue项目 (33)
- vue chart (32)
- vue cms (32)
- 大雅数据库 (34)
- 技术迭代 (37)
- 同一局域网 (33)
- github拒绝连接 (33)
- vscode php插件 (32)
- vue注释快捷键 (32)
- linux ssr (33)
- 微端服务器 (35)
- 导航猫 (32)
- 获取当前时间年月日 (33)
- stp软件 (33)
- http下载文件 (33)
- linux bt下载 (33)