当前位置: 首页 > article >正文

ThreeJs常用模块封装——加载进度条

一、功能介绍

【模块功能】
* 由于模型、纹理加载在有网络传输的情况下比较慢,所以一般
* 1、通过一个进度条显示加载进度。
* 2、并在完全加载完成后再进行后续业务逻辑。因为load函数一般都是异步的,执行完load马上做模型操作大概率是拿到个空内容。
* 由于是常用功能,所以将模型、纹理、背景统一加载放在此模块中,统一显示进度条,统一完成加载后再返回回调。
* 【输入输出】
* 1、输入:
* 1)加载内容列表,每个元素的有资源路径、资源类型两个属性
* 2)加载成功回调函数,回调函数入参:已加载内容列表,每个元素属性有:资源路径、资源类型、加载后的资源。
* 3)加载进度回调,回调入参:加载进度列表,每个元素属性有:资源路径、资源类型、当前加载数、总资源数
* 4)加载失败回调,回调入参:加载结果列表,每个元素属性有:资源路径、资源类型、加载结果、失败原因
* 2、输出:无,所有结果均在回调中传递给使用方了

二、关键代码

1、加载完成判断

    _one_object_load_finish(resourceStateInfo) {
        let haveFailed = false
        for (let element of this.resourceStateInfoList) {
            if (element.load_state === LoadState.UNKNOWN || element.load_state === LoadState.LOADING) {
                // 还没加载完,先不调用回调
                return
            }
            if (element.load_state === LoadState.FAILED) {
                haveFailed = true
            }
        }
        // 加载完成,隐藏进度条
        this.processBarElement.style.display = 'none';
        if (haveFailed) {
            if (this.onFail) {
                this.onFail(this.resourceStateInfoList)
            }
        } else {
            if (this.onSuccess) {
                this.onSuccess(this.resourceStateInfoList)
            }
        }

2、加载进度计算

    _calc_process_percent() {
        const element_num = this.resourceStateInfoList.length;
        if (element_num === 0) {
            return 1
        }
        let total_percent = 0
        for (let element of this.resourceStateInfoList) {
            let one_obj_percent = 1
            if (element.xhr_total != 0) {
                one_obj_percent = element.xhr_curr_num/element.xhr_total
            }
            total_percent += one_obj_percent/element_num
        }
        return total_percent
    }

3、进度条处理

    _update_process_bar(total_percent, process_tips) {
        // 在this.processBarElement下添加进度条,并且进度条附件文字显示process tips
        if (this.processBarElement) {
            const progressBar = document.createElement('div');
            progressBar.style.width = `${total_percent * 100}%`;
            progressBar.style.height = '10px';
            progressBar.style.backgroundColor = 'blue';

            // 创建文本元素
            const textElement = document.createElement('span');
            textElement.textContent = process_tips;

            // 清空现有内容并添加进度条和文本
            this.processBarElement.innerHTML = '';
            this.processBarElement.appendChild(textElement);
            this.processBarElement.appendChild(progressBar);
            // TODO:设置进度条在父元素中间,且宽度占2/3左右,设置背景为灰色半透明蒙版,方便看清楚字
            const parentElement = this.processBarElement.parentElement;
            if (parentElement) {
                const parentWidth = parentElement.clientWidth;
                this.processBarElement.style.position = 'absolute';
                this.processBarElement.style.top = `50%`;
                this.processBarElement.style.left = `50%`;
                this.processBarElement.style.transform = 'translate(-50%, -50%)';
                this.processBarElement.style.width = `${parentWidth * 2 / 3}px`;
                this.processBarElement.style.border_radius = `8px`;
                this.processBarElement.style.background = 'rgba(128, 128, 128, 0.5)';
                // border-radius: 8px;
                // border: 1px solid #009999;
            }
        }
    }

三、整体封装代码

/*
* 【模块功能】
* 由于模型、纹理加载在有网络传输的情况下比较慢,所以一般
* 1、通过一个进度条显示加载进度。
* 2、并在完全加载完成后再进行后续业务逻辑。因为load函数一般都是异步的,执行完load马上做模型操作大概率是拿到个空内容。
* 由于是常用功能,所以将模型、纹理、背景统一加载放在此模块中,统一显示进度条,统一完成加载后再返回回调。
*
* 【输入输出】
* 1、输入:
* 1)加载内容列表,每个元素的有资源路径、资源类型两个属性
* 2)加载成功回调函数,回调函数入参:已加载内容列表,每个元素属性有:资源路径、资源类型、加载后的资源。
* 3)加载进度回调,回调入参:加载进度列表,每个元素属性有:资源路径、资源类型、当前加载数、总资源数
* 4)加载失败回调,回调入参:加载结果列表,每个元素属性有:资源路径、资源类型、加载结果、失败原因
* 2、输出:无,所有结果均在回调中传递给使用方了
* */

import { GLTFLoader } from "three/addons/loaders/GLTFloader.js"
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import * as THREE from "three"
export const LoadState = {
    UNKNOWN:255,
    LOADING:1,
    SUCCESS:0,
    FAILED:10
}
export const ResourceType = {
    GLB_TYPE: 1,
    // TEXTURE_TYPE: 2,
    RGBE_TYPE: 3 // 对于
}
export class ToLoadResource {
    constructor(resource_path, resource_type) {
        this.resource_path = resource_path
        this.resource_type = resource_type
    }
}
export class ResourceLoadState {
    constructor(resource_path, resource_type) {
        this.resource_path = resource_path;
        this.resource_type = resource_type;
        this.xhr_curr_num = 0
        this.xhr_total = 1
        this.load_state = LoadState.UNKNOWN
        this.reason = "unknown"
        this.resouceObj = undefined
    }
}
export class ResourceLoader {
    constructor() {
        this.resourceStateInfoList = []
        this.loadedCount = 0;
        this.onSuccess = undefined
        this.onProgress = undefined
        this.onFail = undefined
        this.processBarElement = undefined
    }
    __init_state_info_list(toLoadResourceList) {
        for (let toLoadResource of toLoadResourceList) {
            const resouceLoadState = new ResourceLoadState(toLoadResource.resource_path, toLoadResource.resource_type)
            this.resourceStateInfoList.push(resouceLoadState)
        }
    }
    _one_object_load_finish(resourceStateInfo) {
        let haveFailed = false
        for (let element of this.resourceStateInfoList) {
            if (element.load_state === LoadState.UNKNOWN || element.load_state === LoadState.LOADING) {
                // 还没加载完,先不调用回调
                return
            }
            if (element.load_state === LoadState.FAILED) {
                haveFailed = true
            }
        }
        // 加载完成,隐藏进度条
        this.processBarElement.style.display = 'none';
        if (haveFailed) {
            if (this.onFail) {
                this.onFail(this.resourceStateInfoList)
            }
        } else {
            if (this.onSuccess) {
                this.onSuccess(this.resourceStateInfoList)
            }
        }
    }
    _one_object_on_success(resourceStateInfo) {
        resourceStateInfo.load_state = LoadState.SUCCESS
        resourceStateInfo.reason = "load success"
        resourceStateInfo.xhr_curr_num = resourceStateInfo.xhr_total
        this._update_process_bar(1, "完成所有资源加载!")
        this._one_object_load_finish(resourceStateInfo)
    }

    _one_object_on_fail(resourceStateInfo) {
        resourceStateInfo.load_state = LoadState.FAILED
        resourceStateInfo.reason = "load failed ${resourceStateInfo.resource_path}"
        this._update_process_bar(1, "完成所有资源加载, 但是部分资源加载失败!")
        this._one_object_load_finish(resourceStateInfo)
    }
    _calc_process_percent() {
        const element_num = this.resourceStateInfoList.length;
        if (element_num === 0) {
            return 1
        }
        let total_percent = 0
        for (let element of this.resourceStateInfoList) {
            let one_obj_percent = 1
            if (element.xhr_total != 0) {
                one_obj_percent = element.xhr_curr_num/element.xhr_total
            }
            total_percent += one_obj_percent/element_num
        }
        return total_percent
    }
    _update_process_bar(total_percent, process_tips) {
        // 在this.processBarElement下添加进度条,并且进度条附件文字显示process tips
        if (this.processBarElement) {
            const progressBar = document.createElement('div');
            progressBar.style.width = `${total_percent * 100}%`;
            progressBar.style.height = '10px';
            progressBar.style.backgroundColor = 'blue';

            // 创建文本元素
            const textElement = document.createElement('span');
            textElement.textContent = process_tips;

            // 清空现有内容并添加进度条和文本
            this.processBarElement.innerHTML = '';
            this.processBarElement.appendChild(textElement);
            this.processBarElement.appendChild(progressBar);
            // TODO:设置进度条在父元素中间,且宽度占2/3左右,设置背景为灰色半透明蒙版,方便看清楚字
            const parentElement = this.processBarElement.parentElement;
            if (parentElement) {
                const parentWidth = parentElement.clientWidth;
                this.processBarElement.style.position = 'absolute';
                this.processBarElement.style.top = `50%`;
                this.processBarElement.style.left = `50%`;
                this.processBarElement.style.transform = 'translate(-50%, -50%)';
                this.processBarElement.style.width = `${parentWidth * 2 / 3}px`;
                this.processBarElement.style.border_radius = `8px`;
                this.processBarElement.style.background = 'rgba(128, 128, 128, 0.5)';
                // border-radius: 8px;
                // border: 1px solid #009999;
            }
        }
    }
    _one_object_on_process(resourceStateInfo, xhr) {
        if (resourceStateInfo.load_state != LoadState.SUCCESS || resourceStateInfo.load_state != LoadState.FAILED) {
            resourceStateInfo.load_state = LoadState.LOADING
            resourceStateInfo.xhr_curr_num = xhr.loaded
            resourceStateInfo.xhr_total = xhr.total
        }
        const total_percent = this._calc_process_percent()
        const process_tips = `正在加载${resourceStateInfo.resource_path},进度${(xhr.loaded/1024).toFixed(2)}kb/${(xhr.total/1024).toFixed(2)}kb`
        console.log(process_tips)
        console.log(total_percent.toFixed(2))

        this._update_process_bar(total_percent, process_tips)

        this.onProgress(resourceStateInfo)
    }
    _load_glb_model(resourceStateInfo) {
        const glb_loader = new GLTFLoader();
        const self = this
        glb_loader.load(resourceStateInfo.resource_path, glbOnSuccess, glbOnProgress, glbOnFail);
        function glbOnSuccess(gltf) {
            resourceStateInfo.resouceObj = gltf.scene;
            self._one_object_on_success(resourceStateInfo)
        }
        function glbOnFail(gltf) {
            self._one_object_on_fail(resourceStateInfo)
        }
        function glbOnProgress(xhr) {
            self._one_object_on_process(resourceStateInfo, xhr)
        }
    }
    _load_rgbe_texture(resourceStateInfo) {
        const rgbe_loader = new RGBELoader();
        const self = this
        rgbe_loader.load(resourceStateInfo.resource_path, rgbeOnSuccess, undefined, rgbeOnFail)
        function rgbeOnSuccess(texture) {
            texture.mapping = THREE.EquirectangularReflectionMapping;
            resourceStateInfo.resouceObj = texture
            self._one_object_on_success(resourceStateInfo)
        }
        function rgbeOnFail(texture) {
            self._one_object_on_fail(resourceStateInfo)
        }
        function rgbeOnProgress(xhr) {
            self._one_object_on_process(resourceStateInfo, xhr)
        }
    }
    __load_one_resource_process(resourceStateInfo) {
        if (resourceStateInfo.resource_type === ResourceType.GLB_TYPE) {
            this._load_glb_model(resourceStateInfo)

        }
        if (resourceStateInfo.resource_type === ResourceType.RGBE_TYPE) {
            this._load_rgbe_texture(resourceStateInfo)
        }
    }
    __load_resource_list_process() {
        for (let resouceLoadStateInfo of this.resourceStateInfoList) {
            this.__load_one_resource_process(resouceLoadStateInfo)
        }
    }
    /**
     * 批量加载一组资源,并且在指定元素位置显示进度条
     * @param{list[ToLoadResource]} toLoadResourceList-代表需要加载的资源列表。
     * @param{documentElement} processBarElement-代表用于显示进度条的页面元素
     * @param{function} onSuccess-代表加载成功后的回调函数,返回一个数组ResourceLoadState。顺序与传入的一致
     * @param{function} onProgress-代表加载过程中的回调,一般可以不用
     * @param{function} onFailure-代表加载失败的回调,做一些异常处理
     * **/
    loadResource(toLoadResourceList,
                 processBarElement= undefined,
                 onSuccess = undefined,
                 onProgress = undefined,
                 onFailure = undefined) {
        this.__init_state_info_list(toLoadResourceList);
        this.__load_resource_list_process();
        this.onSuccess = onSuccess;
        this.onProgress = onProgress;
        this.onFail = onFailure;
        this.processBarElement = processBarElement
    }
}

四、演示demo代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js GUI复杂案例</title>
    <style>
        body { margin: 0; overflow: hidden; }
        #camera-info {
            position: absolute;
            top: 10px;
            left: 10px;
            background-color: rgba(0, 0, 0, 0.5);
            color: white;
            padding: 10px;
            font-family: Arial, sans-serif;
        }
        #tag {
            width: 70px;
            height: 40px;
            line-height: 32px;
            text-align: center;
            color: #fff;
            font-size: 16px;
            background-image: url(./标签箭头背景.png);
            background-repeat: no-repeat;
            background-size: 100% 100%;
        }
    </style>
</head>
<body>
<div id="camera-info"></div>
<div id="processBar"></div>
<div id="selection-info"></div>
<div id="tag" style="display: none;">损伤1</div>
<script type="importmap">
    {
        "imports": {
            "three": "../../three.js-master/build/three.module.js",
            "three/addons/": "../../three.js-master/examples/jsm/"
        }
    }
</script>
<script type="module">
    import * as THREE from "three"
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
    import { PCDLoader } from "three/addons/loaders/PCDLoader.js"
    import { GLTFLoader } from "three/addons/loaders/GLTFloader.js"
    import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
    // 引入CSS2模型对象CSS2DObject
    import {CSS2DObject} from 'three/addons/renderers/CSS2DRenderer.js';
    import {ResourceType, ToLoadResource, ResourceLoader, ResourceLoadState}  from "./ResourceLoader.js"
    // 1) 创建画布
    const scene = new THREE.Scene();
    scene.background = new THREE.Color( 0xa0a0a0 );
    const renderer = new THREE.WebGLRenderer();
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1;
    document.body.appendChild(renderer.domElement);
    scene.background = new THREE.Color( 0xAAAAAA );
    scene.add( new THREE.DirectionalLight( 0xffffff, 2 ) );
    scene.add(new THREE.AmbientLight(0xffffff, 2))
    // 2) 设置 camera 位置,朝向角度
    const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    camera.position.set(0.5, 0.6, 0.8); // 设置相机位置
    camera.lookAt(scene.position); // 让相机朝向场景中心

    // 设置控制轨道
    const controls = new OrbitControls( camera, renderer.domElement );
    controls.enableDamping = true;
    controls.target.set( 0, 0.1, 0 );
    controls.update();
    controls.minDistance = 0.5;
    controls.maxDistance = 1000;
    controls.maxPolarAngle = 0.5 * Math.PI;

    // 5) 支持动态显示摄像头位置、角度、缩放信息
    const cameraInfo = document.getElementById('camera-info');
    function updateCameraInfo() {
        cameraInfo.innerHTML = `
                摄像头信息:<br>
                位置: (${camera.position.x.toFixed(2)}, ${camera.position.y.toFixed(2)}, ${camera.position.z.toFixed(2)})<br>
                角度: (${camera.rotation.x.toFixed(2)}, ${camera.rotation.y.toFixed(2)}, ${camera.rotation.z.toFixed(2)})<br>
                缩放: ${camera.zoom.toFixed(2)}
            `;
    }
    updateCameraInfo();
    //辅助观察的坐标系
    const axesHelper = new THREE.AxesHelper(100);
    scene.add(axesHelper);
    // 渲染循环
    function animate() {
        requestAnimationFrame(animate);
        updateCameraInfo();
        renderer.render(scene, camera);
    }
    animate();

    const glb_resourceInfo = new ToLoadResource("../../three.js-master/examples/models/gltf/SheenChair.glb", ResourceType.GLB_TYPE)
    const back_textureInfo = new ToLoadResource("../../three.js-master/examples/textures/equirectangular/royal_esplanade_1k.hdr", ResourceType.RGBE_TYPE)
    const processBarElement = document.getElementById("processBar")
    const resLoader = new ResourceLoader()
    resLoader.loadResource([glb_resourceInfo, back_textureInfo], processBarElement, succFunc, processFunc, failFunc)
    function succFunc(resourceStateInfoList) {
        const [modelInfo, textureInfo] = resourceStateInfoList
        scene.add(modelInfo.resouceObj)
        scene.background = textureInfo.resouceObj;
        scene.environment = textureInfo.resouceObj;
        console.log("succ!")
    }
    function processFunc(resourceStateInfoList) {
        console.log("process!")
    }
    function failFunc(resourceStateInfoList) {
        console.log("fail!")
    }
</script>
</body>
</html>


http://www.kler.cn/a/517585.html

相关文章:

  • 二十三种设计模式-享元模式
  • 第二十一周:Mask R-CNN
  • Qt监控系统辅屏预览/可以同时打开4个屏幕预览/支持5x64通道预览/onvif和rtsp接入/性能好
  • 讯飞绘镜(ai生成视频)技术浅析(二):大模型
  • 数据结构——堆(C语言)
  • 案例研究丨浪潮云洲通过DataEase推进多维度数据可视化建设
  • uniapp使用uni.navigateBack返回页面时携带参数到上个页面
  • Tauri2+Leptos开发桌面应用--绘制图形、制作GIF动画和mp4视频
  • Rust 中的方法与关联函数详解
  • MyBatis最佳实践:动态 SQL
  • ANSYS SimAI
  • leetcode刷题记录(八十一)——236. 二叉树的最近公共祖先
  • 为AI聊天工具添加一个知识系统 之68 详细设计 之9 三种中台和时间度量
  • web前端三大主流框架对比,Angular和React和Vue的区别
  • 【Elasticsearch】如何重新启动_reindex任务?
  • Flutter 与 React 前端框架对比:深入分析与实战示例
  • electron打包客户端在rk3588上支持h265硬解
  • AcWing 3585:三角形的边 ← sort() 函数
  • 矩阵的秩在机器学习中具有广泛的应用
  • 解锁C# EF/EF Core:从入门到进阶的技术飞跃
  • AJAX笔记入门篇
  • 免费高效截图软件(snipaste)附下载链接
  • 亚洲加密市场交易量激增,Safe RWA协议助力 Cobo 与 HQ.xyz 处理超 14.9 亿美元交易
  • 人工智能检测中查全率与查准率的权衡分析
  • Fullcalendar @fullcalendar/react 样式错乱丢失问题和导致页面卡顿崩溃问题
  • Android中Service在新进程中的启动流程3