Skip to content

案例展示

模型本地缓存:基于 IndexedDB 实现,大幅提升模型加载速度。

查看位置:浏览器开发者工具 → Application → Storage → IndexedDB

模型格式

支持 gltfglbfbxobjstljsonziptextassemble 等格式。

模型渲染总时长对比
已缓存:未测试
未缓存:未测试
<template>
    <div class="vp-px2vw">
        <div class="container">
            <div id="TO"></div>
            <div v-if="loading" class="loading-overlay">
                <div class="loading-spinner"></div>
                <span class="loading-text">{{ loadingText }}</span>
            </div>
            <div id="formContainer">
                <ModelRenderTimePanel
                    :cached-time="cachedTime"
                    :uncached-time="uncachedTime"
                    @loadCachedModel="loadCachedModel"
                    @loadUncachedModel="loadUncachedModel" />
            </div>
        </div>
    </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import ModelRenderTimePanel from './ModelRenderTimePanel.vue'
const loading = ref(false)
const cachedTime = ref(null)
const uncachedTime = ref(null)
let TO = null,
    modelDataList = []
const loadingText = ref('加载中...')

let modelList = [
    {
        modelType: 'gltf',
        modelName: 'ABB机器人-1',
        id: 3012,
        position: {
            x: 0,
            y: 0,
            z: 0
        },
        base: {
            modelUrl: 'https://124.70.30.193:8443/model2/loadFileAsId/56201403'
        }
    },
    {
        modelType: 'gltf',
        modelName: '非皮带传送带-1',
        id: 3021,
        position: {
            x: -24.90687943165758,
            y: 0,
            z: 16.922502148523417
        },
        base: {
            modelUrl: 'https://124.70.30.193:8443/model2/loadFileAsId/258642254'
        }
    },
    {
        modelType: 'gltf',
        modelName: '精雕机床-1',
        id: 3016,
        position: {
            x: -6,
            y: 0,
            z: -23
        },
        rotation: {
            x: 0,
            y: 3.14,
            z: 0
        },
        base: {
            modelUrl: 'https://124.70.30.193:8443/model2/loadFileAsId/701730674'
        }
    },
    {
        modelType: 'gltf',
        modelName: '数字机床-1',
        id: 3014,
        position: {
            x: 46,
            y: 0,
            z: -4
        },
        rotation: {
            x: 3.14,
            y: 0,
            z: 3.14
        },
        base: {
            modelUrl: 'https://124.70.30.193:8443/model2/loadFileAsId/878966042'
        }
    }
]
onMounted(async () => {
    initScene()
})

function initScene() {
    //初始化场景
    TO = new ThingOrigin('fileModel', document.getElementById('TO'), {
        camera: {
            lookAt: {
                x: 25.62454680418967,
                y: -2.3106037456621856,
                z: -66.99965038950995
            },
            position: {
                x: 41.87976418982612,
                y: 78.50564268979477,
                z: 117.31701183506453
            }
        }
    })
    // 首次先加载未缓存的模型
    loadUncachedModel()
}

// 清除所有模型
function clearAllModels() {
    modelDataList.forEach((model) => {
        TO.scene.removeModelByName(model.name)
    })
    modelDataList = []
}

// 设置模型的位置和旋转
function setupModelTransform(model, modelInfo) {
    if (modelInfo.position) {
        model.position.set(modelInfo.position.x, modelInfo.position.y, modelInfo.position.z)
    }
    if (modelInfo.rotation) {
        model.rotation.set(modelInfo.rotation.x, modelInfo.rotation.y, modelInfo.rotation.z)
    }
}

// 添加模型到场景
function addModelToScene(model, modelInfo) {
    setupModelTransform(model, modelInfo)
    modelDataList.push(model)
    TO.scene.add(model)
}

// 加载单个模型(已缓存)
async function loadSingleCachedModel(modelInfo) {
    const accessModel = await TO.indexedDB.accessModel(modelInfo)
    if (accessModel.info.saved) {
        const modelUrl = accessModel.modelUrl
        return TO.model.loadModel(accessModel.info, modelUrl).then((model) => {
            addModelToScene(model, modelInfo)
        })
    }
}

// 加载单个模型(未缓存)
async function loadSingleUncachedModel(modelInfo) {
    try {
        const insertModel = await TO.indexedDB.insertModel(modelInfo)
        if (insertModel && insertModel.info && insertModel.info.saved) {
            const modelUrl = insertModel.modelUrl
            return TO.model
                .loadModel(insertModel.info, modelUrl)
                .then((model) => {
                    addModelToScene(model, modelInfo)
                })
                .catch((error) => {
                    console.error(`加载模型 ${modelInfo.modelName} 失败:`, error)
                    // 返回 resolved Promise,确保不影响后续代码
                    return Promise.resolve()
                })
        } else {
            // 写入失败,自动跳过
            console.warn(`模型 ${modelInfo.modelName} 写入失败,已跳过`)
            return Promise.resolve()
        }
    } catch (error) {
        // 捕获所有可能的错误,确保不影响后续代码
        console.error(`加载模型 ${modelInfo.modelName} 时发生错误:`, error)
        return Promise.resolve()
    }
}

// 加载已缓存的模型
async function loadCachedModel() {
    loadingText.value = '二次加载:从本地 IndexedDB 读取(无网络请求)'
    loading.value = true
    clearAllModels()

    const totalStartTime = performance.now()
    const loadPromises = modelList.map((modelInfo) => loadSingleCachedModel(modelInfo))
    await Promise.all(loadPromises)

    const totalEndTime = performance.now()
    cachedTime.value = Math.round(totalEndTime - totalStartTime)
    loading.value = false
}

// 强制加载未缓存模型(先删除缓存再加载)
async function loadUncachedModel() {
    loadingText.value = '首次加载:从网络请求并缓存到本地 IndexedDB'
    loading.value = true
    deleteCachedModel()

    const totalStartTime = performance.now()
    const loadPromises = modelList.map((modelInfo) => loadSingleUncachedModel(modelInfo))
    await Promise.all(loadPromises)

    const totalEndTime = performance.now()
    uncachedTime.value = Math.round(totalEndTime - totalStartTime)
    loading.value = false
}

async function deleteCachedModel() {
    clearAllModels()
    // cachedTime.value = null;
    // uncachedTime.value = null;

    // 删除所有模型的缓存
    const deletePromises = modelList.map(async (modelInfo) => {
        try {
            await TO.indexedDB.deleteModel(modelInfo.id)
        } catch (e) {
            // 忽略删除错误(可能不存在)
        }
    })

    await Promise.all(deletePromises)
}
</script>

<style scoped lang="scss">
.vp-px2vw {
    .container {
        position: relative;
    }

    #TO {
        width: 100%;
        height: 400px;
        position: relative;
    }

    #formContainer {
        position: absolute;
        top: 10px;
        right: 10px;
        font-size: 14px;
    }

    .loading-overlay {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        background: rgba(0, 0, 0, 0.5);
        z-index: 10;
    }

    .loading-spinner {
        width: 32px;
        height: 32px;
        border: 3px solid rgba(255, 255, 255, 0.3);
        border-top-color: #fff;
        border-radius: 50%;
        animation: spin 0.8s linear infinite;
    }

    .loading-text {
        margin-top: 12px;
        color: #fff;
        font-size: 14px;
    }
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}
</style>

API 介绍

indexedDB.accessModel

方法签名返回值描述
accessModel(modelInfo: modelInfoParams)PromiseLike<accessResult>判断是否已缓存此模型,返回是否存储,若已缓存返回模型参数

indexedDB.insertModel

方法签名返回值描述
insertModel(modelInfo: modelInfoParams)Promise<modelInfoParams>向indexedDB数据表中,存储模型

参数说明:

参数名说明类型必填默认值
modelInfo模型参数modelInfoParams

indexedDB.deleteModel

方法签名返回值描述
deleteModel(id: number)Promise<boolean>indexedDB数据表中,删除模型

参数说明:

参数名说明类型必填默认值
id模型 idnumber