You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在EGL中后台加载OpenGL纹理?解决启动卡顿问题

后台加载OpenGL纹理解决启动卡顿问题

问题背景

大学作业场景:基于OpenGL实现支持纹理/材质切换的3D场景,切换功能已完成,但启动时因纹理从磁盘加载耗时过长导致卡顿,希望在渲染材质场景的同时后台加载纹理,此前尝试线程加载未成功。

核心原因

OpenGL上下文是线程绑定的,后台线程无法直接调用glGenTextures、glTexImage2D这类GL API——这是你之前线程加载失败的根本原因。必须将「磁盘读取+像素解码」与「GPU纹理创建+上传」拆分为两个独立步骤:后台线程只执行CPU侧的像素加载,GPU侧操作必须在拥有GL上下文的主线程中完成。

解决方案

1. 重构Texture类,分离加载与上传逻辑

修改类结构,将像素加载与GPU上传解耦,新增状态标记跟踪纹理加载进度:

修改后的Texture.h

#ifndef TEXTURE_H
#define TEXTURE_H

#include <iostream>
#include <GL/glew.h>
#include <FreeImage.h>
#include <mutex>

class Texture {
public:
    // 纹理加载状态枚举
    enum class State {
        UNLOADED,          // 未开始加载
        LOADING,           // 后台加载像素中
        READY_TO_UPLOAD,   // 像素已加载,等待GPU上传
        UPLOADED           // 已完成GPU上传
    };

    Texture() : texture(0), state(State::UNLOADED), width(0), height(0), pixels(nullptr) {}
    virtual ~Texture();

    // 启动后台异步加载(仅处理像素读取,不调用GL API)
    void startAsyncLoad(const char* textureFile);
    // 主线程调用:将像素上传至GPU并配置纹理
    void uploadToGPU();
    // 获取纹理ID(仅UPLOADED状态有效)
    unsigned int getTexture();
    // 获取当前加载状态
    State getState() const { return state; }

private:
    // 后台线程执行的像素加载逻辑
    void loadPixelData(const char* textureFile);

    unsigned int texture;
    State state;
    mutable std::mutex mutex; // 线程安全访问成员变量

    // 后台加载的像素数据缓存
    unsigned int width;
    unsigned int height;
    unsigned char* pixels;

    // 静态像素加载方法,避免重复初始化FreeImage
    static unsigned char* loadTexture(const char* textureFile, unsigned int& w, unsigned int& h);
};

#endif /* TEXTURE_H */

修改后的Texture.cpp

#include "Texture.h"
#include <thread>

Texture::~Texture() {
    std::lock_guard<std::mutex> lock(mutex);
    if (pixels) delete[] pixels;
    if (texture != 0) glDeleteTextures(1, &texture);
}

void Texture::startAsyncLoad(const char* textureFile) {
    std::lock_guard<std::mutex> lock(mutex);
    if (state != State::UNLOADED) return;

    state = State::LOADING;
    // 启动后台线程加载像素数据
    std::thread([this, textureFile]() {
        loadPixelData(textureFile);
    }).detach();
}

void Texture::loadPixelData(const char* textureFile) {
    unsigned int w, h;
    unsigned char* pixelData = loadTexture(textureFile, w, h);

    std::lock_guard<std::mutex> lock(mutex);
    if (pixelData) {
        width = w;
        height = h;
        pixels = pixelData;
        state = State::READY_TO_UPLOAD;
    } else {
        state = State::UNLOADED; // 加载失败,重置状态
    }
}

void Texture::uploadToGPU() {
    std::lock_guard<std::mutex> lock(mutex);
    if (state != State::READY_TO_UPLOAD || !pixels) return;

    // 以下GL操作必须在主线程执行
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    glGenerateMipmap(GL_TEXTURE_2D);

    // 纹理参数配置
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    float aniso;
    glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &aniso);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, aniso);

    // 上传完成后释放CPU侧像素内存
    delete[] pixels;
    pixels = nullptr;
    state = State::UPLOADED;
}

unsigned int Texture::getTexture() {
    std::lock_guard<std::mutex> lock(mutex);
    return texture;
}

unsigned char* Texture::loadTexture(const char* textureFile, unsigned int& w, unsigned int& h) {
    // FreeImage仅初始化一次
    static bool freeImageInitialized = false;
    if (!freeImageInitialized) {
        FreeImage_Initialise(TRUE);
        freeImageInitialized = true;
    }

    FREE_IMAGE_FORMAT format = FreeImage_GetFileType(textureFile, 0);
    if (format == FIF_UNKNOWN) format = FreeImage_GetFIFFromFilename(textureFile);
    if ((format == FIF_UNKNOWN) || !FreeImage_FIFSupportsReading(format)) {
        std::cerr << "纹理文件" << textureFile << "格式不支持。" << std::endl;
        return nullptr;
    }
    FIBITMAP* texture = FreeImage_Load(format, textureFile);
    if (!texture) {
        std::cerr << "无法打开文件" << textureFile << "。" << std::endl;
        return nullptr;
    }
    FIBITMAP* temp = texture;
    texture = FreeImage_ConvertTo32Bits(texture);
    FreeImage_Unload(temp);

    w = FreeImage_GetWidth(texture);
    h = FreeImage_GetHeight(texture);
    unsigned char* pixelsBGRA = (unsigned char*)FreeImage_GetBits(texture);
    unsigned char* pixelsRGBA = new unsigned char[4 * w * h];
    for (int j = 0; j < w * h; j++) {
        pixelsRGBA[j * 4 + 0] = pixelsBGRA[j * 4 + 2];
        pixelsRGBA[j * 4 + 1] = pixelsBGRA[j * 4 + 1];
        pixelsRGBA[j * 4 + 2] = pixelsBGRA[j * 4 + 0];
        pixelsRGBA[j * 4 + 3] = pixelsBGRA[j * 4 + 3];
    }

    FreeImage_Unload(texture);
    return pixelsRGBA;
}

2. 主线程集成与渲染逻辑

初始化阶段(主线程)

启动所有纹理的异步加载,同时创建占位纹理避免渲染异常:

#include <vector>
#include <memory>

std::vector<std::unique_ptr<Texture>> textures;
Textures goldTextures;

void init() {
    // 创建占位纹理(灰色纯色,避免渲染时无有效纹理)
    unsigned int placeholderTex;
    glGenTextures(1, &placeholderTex);
    glBindTexture(GL_TEXTURE_2D, placeholderTex);
    unsigned char grayPixel[] = {128,128,128,255};
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1,1,0, GL_RGBA, GL_UNSIGNED_BYTE, grayPixel);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // 初始化gold材质的各个纹理,启动异步加载
    auto diffuseTex = std::make_unique<Texture>();
    diffuseTex->startAsyncLoad("resources/textures/gold/gold_diffuse.png");
    textures.push_back(std::move(diffuseTex));

    auto specularTex = std::make_unique<Texture>();
    specularTex->startAsyncLoad("resources/textures/gold/gold_specular.png");
    textures.push_back(std::move(specularTex));

    // 初始用占位纹理填充Textures结构体
    goldTextures.diffuse = placeholderTex;
    goldTextures.specular = placeholderTex;
    goldTextures.shininess = 32.0f;
    // 其他纹理(emissive/normal)同理
}

渲染循环(主线程)

每帧检查纹理状态,完成GPU上传后更新材质结构体:

void renderLoop() {
    while (!shouldClose()) {
        // 处理窗口事件等前置操作

        // 检查所有纹理,上传已准备好的像素数据
        for (size_t i = 0; i < textures.size(); i++) {
            auto& tex = textures[i];
            if (tex->getState() == Texture::State::READY_TO_UPLOAD) {
                tex->uploadToGPU();
                // 根据索引更新对应材质纹理
                if (i == 0) {
                    goldTextures.diffuse = tex->getTexture();
                } else if (i == 1) {
                    goldTextures.specular = tex->getTexture();
                }
                // 其他纹理类型对应索引判断
            }
        }

        // 正常渲染场景,使用goldTextures中的纹理ID
        renderScene(goldTextures);

        // 交换缓冲区、处理输入等后置操作
    }
}

关键注意事项

  • 线程安全:所有访问Texture成员变量的地方必须加互斥锁,避免数据竞争
  • FreeImage生命周期:仅初始化一次,不要在后台线程中重复调用FreeImage_DeInitialise
  • 占位纹理:必须保证渲染时纹理ID有效,防止OpenGL报错
  • 资源清理:Texture析构函数需正确释放CPU侧像素内存与GPU侧纹理对象

内容的提问来源于stack exchange,提问作者lovethefrogs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 03:12:11