一、功能介绍
【模块功能】
* 由于模型、纹理加载在有网络传输的情况下比较慢,所以一般
* 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>