在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">&#xe6a9;</text>
      <view v-show="isScreen && percent == 2 && needScreen" id="exitScreen" class="iconfont screenBtnFont safeBottom">
        <text>&#xe6aa;</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. 渲染循环与自动旋转

组件实现了渲染循环和自动旋转功能,当用户没有交互时会自动旋转模型:

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. 轨道控制交互

组件提供了精细的轨道控制配置,包括缩放限制和平移控制:

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. 全屏展示功能

组件实现了完整的全屏/退出全屏逻辑,包括尺寸调整和过渡动画:

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资源,避免内存泄漏:

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

<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">&#xe6a9;</text>
      <view v-show="isScreen && percent == 2 && needScreen" id="exitScreen" class="iconfont screenBtnFont safeBottom">
        <text>&#xe6aa;</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>
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐